Skip to content

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:

  1. Nibble’s — a small baseline: pages, posts, a couple of fieldsets.
  2. The active theme’sthemes/<name>/schema/.
  3. The site’s ownschema/, 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.

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.