Building a theme
Schema
Collections, taxonomies, globals, navigation and forms — the shape of a site, as YAML.
Schema is where you say what content exists. A collection is a YAML file; the entries in it are rows whose fields are checked against a blueprint.
This is the part that makes Nibble different from a CMS where you click a new field into existence: the shape of your content is a file, reviewed like any other change, and identical on every environment because it arrives with the code.
The three layers
Schema is read in order, and the last to define a handle wins:
- Nibble’s — a small baseline: pages, posts, a couple of fieldsets.
- The active theme’s —
themes/<name>/schema/. - The site’s own —
schema/, which beats both.
An override replaces the whole file; it does not merge into it. If you want your own posts collection,
write the file you want rather than a fragment, and Nibble’s is out of the picture.
That layering is what lets one theme serve several sites: the theme carries what it needs to render, and each
site adjusts in its own schema/.
Where files go
schema/
├── collections/guides.yml
├── taxonomies/regions.yml
├── blueprints/collections/guides/guide.yml
├── fieldsets/seo.yml
├── globals/contact.yml
├── navigation/main.yml
└── forms/enquiry.yml
Generate them rather than copying ours — each command writes a valid stub in the right place and refreshes the theme’s generated types:
bin/rails nibble:generate:collection guides
bin/rails nibble:generate:taxonomy regions
bin/rails nibble:generate:blueprint guides/gallery
bin/rails nibble:generate:fieldset seo
bin/rails nibble:generate:global contact
bin/rails nibble:generate:navigation main
bin/rails nibble:generate:form enquiry
Then check your work:
bin/rails nibble:check
It reads every layer, reports anything that will not load, and — importantly — reports content the schema no longer covers, which is how you find out that removing a field would have orphaned data.
A collection
schema: 1
title: Guides
route: /guides/{slug}
blueprints: [guide]
template: guides/show
sort: published_at:desc
dated: true
| Key | What it does |
|---|---|
title |
what the control panel calls it |
route |
the URL pattern; {slug}, {parent_uri} and date parts are available |
blueprints |
which shapes an entry may take; the first is the default |
template |
the view that renders one |
sort |
the listing’s default order, like published_at:desc |
dated |
entries have a publish date, and cannot be published without one |
structure |
entries may sit under one another, with max_depth |
taxonomies |
which taxonomies entries may relate to |
search |
whether entries are indexed for site search |
source |
the pages are written as files instead |
A template can be overridden per blueprint, and per entry, so an unusual page does not force an unusual collection.
Taxonomies
Terms that entries relate to — topics, regions, authors. A taxonomy has a route of its own, so /topics/design
is a real page listing everything with that term, without you building it.
Globals
One set of fields for the whole site: contact details, social links, the address mail comes from. Not a page, not a list — just fields, edited in one place and available to every view.
Navigation
A tree of links to entries or URLs, arranged by editors in the control panel. Declare which ones exist and the theme draws them; see Navigation for the editor’s side.
Forms
Fields, validation, and what happens on submission. Submissions are stored, can be emailed, and can be exported. See Forms.
Changing a schema that already has content
This is the part to take slowly, because content already exists in the old shape.
bin/rails nibble:check refuses a change that would orphan stored data and tells you which entries are
affected. When a change really does need data moved, write a content migration:
# schema/migrations/2026_09_21_rename_intro.yml
operations:
- rename_field:
collection: guides
from: intro
to: summary
The operations available are rename_field, set_default, change_blueprint and move_to_taxonomy. Drafts
waiting to be published are migrated too, so publishing one later cannot bring the old field back.
They are idempotent, so running one twice is safe, and they run as part of bin/rails nibble:upgrade on deploy.
The rule of thumb: adding a field is free, renaming needs a migration, removing needs a decision.