Skip to content

Building a theme

Content written as files

Turn a folder of Markdown into a collection, the way these pages work.

Most content belongs in the control panel, where somebody can write it without asking a developer. Some content does not: documentation, a changelog, reference pages that change in the same pull request as the code they describe.

For those, a collection can be written as Markdown files in your repository. Nibble reads the folder and makes the collection match it — the pages are ordinary entries afterwards, with URLs, search, caching and a navigation tree, but the files are the source and the control panel will not let anyone edit them.

The pages you are reading now are exactly this.

Setting it up

Three things: a folder, a blueprint, and a collection that points at them.

1. Put the files in content/. That directory is reserved for content kept as files, and it is the only place a collection may read from.

content/
└── docs/
    ├── index.md
    ├── guides/
    │   ├── index.md
    │   └── testing.md
    └── images/
        └── diagram.png

Note

A folder’s index.md is the page its files sit under, and a folder without one is an error rather than a guess. The root of the folder is the exception: name that page something other than index.md.

2. Write a blueprint with a field for the body and whatever the frontmatter carries:

# schema/blueprints/collections/docs/doc.yml
schema: 1
title: Doc
tabs:
  main:
    sections:
      - fields:
          - handle: title
            field: { type: text, required: true }
          - handle: description
            field: { type: text }
          - handle: body
            field: { type: markdown }

3. Point a collection at the folder:

# schema/collections/docs.yml
schema: 1
title: Docs
route: "{parent_uri}/{slug}"
structure:
  max_depth: 3
blueprints: [doc]
template: docs/show
source:
  markdown: docs          # the folder inside content/
  field: body             # the field the Markdown lands in
  navigation: docs        # optional: build this navigation from the folders

markdown: docs and markdown: content/docs mean the same thing. Anything outside content/ is refused, so a collection cannot be pointed at the rest of the application.

4. Bring the files in:

bin/rails nibble:content:markdown

What a file becomes

---
title: Testing
description: How we test, and why it is worth it.
order: 2
---

# Testing

Start with [the guides](/docs/themes), and see the [diagram](../images/diagram.png).
In the file Becomes
its path where the page sits — guides/testing.md is testing, under guides
title, description fields on the entry, by the same name
order where it sits among its siblings in the navigation
everything after the frontmatter the Markdown field named by field

A folder’s index.md is the page its other files sit under. So guides/index.md is the “Guides” page and guides/testing.md is a page beneath it. A folder without an index.md is an error rather than a guess — its files would have nowhere to live.

Frontmatter keys must be fields of the blueprint, or columns Nibble already understands such as published_at. A key that is neither is reported by name, so a typo is caught rather than silently dropped.

A link to another .md file becomes a link to that page. It is stored as a reference, not as a URL, and resolved when the page renders — so if either page moves later, the link still lands. A link to a page that does not exist renders as a dead link rather than something strange.

An image beside the pages becomes an asset. It is filed in a folder mirroring where it was written, and uploaded again only when the file itself changes, so running the command repeatedly does not fill your bucket with copies.

External links, anchors and everything else are left exactly as written.

The navigation

Name a navigation in source and the folders become its tree: a folder’s page is the branch, the files inside it are the children, and order in the frontmatter decides what comes first. Pages without an order come last, alphabetically.

It is available to your theme as an ordinary navigation, which is where a documentation sidebar comes from:

<nav>
  <a v-for="item in site.navigation.docs" :key="item.id" :href="item.url">{{ item.title }}</a>
</nav>

See Views for what a theme receives.

Running it

bin/rails nibble:content:markdown              # every collection written as files
bin/rails nibble:content:markdown docs         # just this one
bin/rails nibble:content:markdown --dry-run    # report what would change, write nothing

It prints what it did:

docs: created 12, updated 3, trashed 1

The folder decides what exists. A page whose file has been deleted is trashed, not left live — that is the difference between this and importing a content package, and it is why the collection must be the folder’s alone.

Running it again changes nothing. That is what makes it safe to run on every deploy, which is where it belongs: merge a change to a file, deploy, and the site has it.

In the control panel

Pages written as files are read-only. The listing and the page open normally, but the fields are locked and saving is refused — not just hidden, refused, because an edit that survived the afternoon and vanished at the next deploy would be worse than no edit at all.

Each page says which file it is written in and what to run after changing it, so somebody who finds a typo knows where to go.

When to use it, and when not

Use it when the content belongs with the code: documentation, reference material, anything reviewed in a pull request alongside what it describes.

Do not use it for content that people who do not use git need to change. That is what the control panel is for, and a locked collection is a frustrating answer to “can you fix this sentence”.