Schema reference

Project config

The .norcube/cms/ directory — config.yml, collections, blocks, inputs, the preview block, and the identity sidecar.

Not verified yet

Everything the CMS knows about a site lives in one directory in your repo: .norcube/cms/. The root config.yml holds site-wide settings; one file per collection, block, and reusable input lives in sub-directories; and a meta/ sidecar stores block identity out of your content. This page is the reference for all of it.

For the field-level knobs (the fields: list inside a collection or block) see Field types. For the broader picture see Schema reference.

The .norcube/cms/ directory

.norcube/cms/
├── config.yml              # root: site-wide settings + the preview block
├── collections/            # one file per content type
│   ├── pages.yml
│   └── works.yml
├── blocks/                 # one file per reusable nestable block
│   ├── hero.yml
│   └── media-text.yml
├── inputs/                 # one file per single-field snippet
│   └── seo-title.yml
└── meta/                   # the identity sidecar (CMS-written)
    └── src/content/pages/about.yml
EntryWhat it holds
config.ymlRoot settings — schemaVersion, dirs, storages, the preview block.
collections/One YAML file per content type. Basename is the collection key (pages.yml → pages).
blocks/One YAML file per nestable block. Basename is the $ref target.
inputs/One YAML file per reusable single-field snippet. Also $ref-able.
meta/The sidecar — stable block/array-item _ids, written and read by the CMS only.

Each sub-directory is optional. A site with only collections/ and no blocks/, inputs/, or meta/ is valid. Files are loaded in alphabetical order by basename, so the editor's left-rail list is stable across loads. Only .yml / .yaml files are read; a file that fails to parse is skipped with a warning rather than breaking the whole site.

Root config.yml

config.yml carries the site-wide keys. Per-file collections, blocks, and inputs are merged on top of any inline collections: / blocks: / inputs: you declare here, with the per-file definitions winning on a name collision.

# .norcube/cms/config.yml
schemaVersion: 1 # optional; defaults to 1, unknown versions rejected
siteUrl: https://example.com # canonical site URL
pagesDir: src/content/pages # root dir for the default `pages` collection
publicDir: public # leading-slash asset-resolver root
defaultStorage: uploads # fallback for fields with no `storage:`
inputsFromGlob: # extra dirs to load input snippets from
  - shared/inputs/*.yml
storages: {} # see Media library
preview: # site-level i18n facts for deep-linking
  defaultLocale: cs
  locales: [cs, en, it]
  prefixDefaultLocale: false
KeyTypeEffect
schemaVersionintegerSchema-language version. Absent defaults to 1; a version this build doesn't support is rejected at load.
siteUrlstringCanonical site URL. Carried verbatim into the merged config.
pagesDirstringRoot directory of the default pages collection, e.g. src/content/pages.
publicDirstringWhere the deployed site serves static files from. Used as the leading-slash asset-resolver root. Defaults to public.
defaultStoragestringStorage name used when an image/file field omits storage:. See Media library.
inputsFromGlobstring or string[]Extra <dir>/*.yml globs to load input snippets from, in addition to inputs/. Glob entries win on name collision.
storagesobjectNamed upload destinations. See Media library.
collectionsobject (inline)Inline collections, keyed by name. Prefer one file per collection under collections/.
blocksobject (inline)Inline blocks, keyed by name. Prefer one file per block under blocks/.
inputsobject (inline)Inline input snippets, keyed by name. Prefer one file per input under inputs/.
previewobjectSite-level i18n facts for live-preview deep-linking. See Preview deep-linking.

inputsFromGlob accepts either a single string or a list. Only <dir>/*.yml and <dir>/*.yaml patterns are supported — broader globs like **/*.yml are ignored with a warning. A malformed value is not fatal.

The preview block is covered under Preview deep-linking.

Collections

One YAML file per collection under .norcube/cms/collections/<name>.yml. The basename is the collection key. Every page whose path matches the collection is edited through its schema.

# .norcube/cms/collections/works.yml
label: Works
path: src/content/works
match: '**/*.md'
hasBody: true
extraKeysPolicy: show
preview:
  title:
    - { key: title }
    - Untitled work
  description:
    - { key: summary }
  image:
    - { key: cover }
  badges:
    - - { key: category }
previewPath: /{locale}/works/{slug}/
localeFrom: path
fields:
  - key: title
    type: text
    label: Title
  - key: summary
    type: paragraph
    label: Summary
KeyEffect
labelHuman-facing collection name, shown in the editor nav.
pathRepo-relative directory this collection's files live in (e.g. src/content/works). Drives directory-based collection resolution.
matchGlob (relative to path) selecting which files belong to the collection. Defaults to **/*.md.
hasBodyWhen true, pages carry a Markdown body after the frontmatter, surfaced as a body editor.
extraKeysPolicyWhat to do with frontmatter keys not in the schema — show, hide, or strip. See below.
previewHow a page renders as a summary row in the file browser. See preview resolvers.
previewPathURL template for deep-linking the live preview. See Preview deep-linking.
localeFromWhere a file's locale comes from, for {locale} in previewPath. See Preview deep-linking.
fieldsOrdered list of frontmatter fields. See Field types.
schemaVersionPer-file schema version. Absent defaults to 1; unsupported versions are rejected.

extraKeysPolicy controls stray frontmatter keys the schema doesn't declare:

ValueEffect
showDefault. Surface them in the "Out-of-schema content" panel so authors can clean up or migrate the schema.
hideKeep them in the YAML but don't render the panel — useful when a build pipeline writes its own keys.
stripPurge them on save. Destructive; pairs with a schema you treat as authoritative.

The default pages collection can be addressed through pagesDir alone — path is optional for single-collection sites. Directory-based sites with several content types give each collection its own file with an explicit path.

Blocks and $ref

A block is a reusable, nestable group of fields — a hero, a card, a media-text row. One YAML file per block lives under .norcube/cms/blocks/<name>.yml, and the basename is what $ref targets.

# .norcube/cms/blocks/media-text.yml
label: Media + text
preview:
  title:
    - { key: heading }
    - Media + text
fields:
  - key: heading
    type: text
    label: Heading
  - key: body
    type: richtext
    label: Body
  - key: image
    type: image
    label: Image
    storage: uploads

Reference a block from a field tree — almost always inside an array field's items: — with $ref: <basename>:

# .norcube/cms/collections/pages.yml (excerpt)
- key: blocks
  type: array
  label: Content blocks
  items:
    - $ref: hero
    - $ref: media-text

The CMS resolves $ref server-side: at load it inlines the referenced block's content in place, so the editor never sees $ref — it works on the fully inlined field tree. A block file omits its own type: (blocks are groups), so the resolver injects type: group and a key: <basename> discriminator when inlining into an items[] slot. References resolve through a shared registry of all blocks and inputs; a $ref to a name in neither is a hard error, and a reference cycle deeper than ten levels is rejected.

Inputs

An input is a single reusable field snippet — one FieldDef — that you $ref into any schema the same way you reference a block. Input files live under .norcube/cms/inputs/<name>.yml, and inputsFromGlob: at the root adds extra directories to load them from.

# .norcube/cms/inputs/seo-title.yml
key: meta_title
type: text
label: SEO title
recommendedLength: { from: 50, to: 60 }
# referenced the same way a block is
- $ref: seo-title

Blocks and inputs share one $ref registry, and inputs win on a name collision — defining an input with the same name as a block is treated as an explicit override. Note inputs have no dedicated editor UI today: they resolve at load like blocks, but there is no left-rail entry for editing an input snippet on its own.

The identity sidecar

The CMS addresses every block and array item by a stable _id so that reorders, AI proposals, and per-field history survive content churn. Those _ids are not written into your content. They live in a sidecar file that mirrors the content tree:

# content at src/content/pages/about.md
# sidecar at
.norcube/cms/meta/src/content/pages/about.yml
# .norcube/cms/meta/src/content/pages/about.yml
schemaVersion: 1
page: 6f1e... # the page's own _id, mirrored as a rename anchor
arrays:
  blocks: # dotted path to the array in the content
    - id: 9a2c... # stable id for blocks[0]
      hashes: [b3d1...] # recent content hashes for drift recovery
    - id: 41ff...
      hashes: [77ab...]

Block and array-item _ids are stamped here, not inline, so your build never sees stray CMS bookkeeping keys. That matters because strict schema validation on the site side — a Zod content schema, for instance — rejects unknown keys and fails the build the moment the CMS writes an inline _id. The sidecar keeps your content YAML clean.

The one exception is the page-level _id, which stays inline in the page's frontmatter. It's the rename anchor: if a developer moves or renames the content file outside the CMS, the sidecar's page: field is matched against that inline _id to recover the orphaned sidecar. Identity survives reorders through the recorded content hashes — a block that keeps its content matches its old _id even after it moves.

Preview deep-linking

Live preview can open the exact URL of the page you're editing rather than the site root. The CMS edits content files and can't infer a file's rendered URL, so the site declares the mapping: a previewPath template (plus a localeFrom rule) on each collection, and the site-level preview: block supplying the i18n facts.

# .norcube/cms/config.yml
preview:
  defaultLocale: cs
  locales: [cs, en, it]
  prefixDefaultLocale: false
# .norcube/cms/collections/pages.yml (excerpt)
previewPath: /{locale}/{slug}/
localeFrom: path
KeyWhereEffect
defaultLocalesiteLocale used for locale-less files and as the fallback.
localessiteDeclared locales. A path segment is only treated as a locale when it's in this list.
prefixDefaultLocalesiteWhen false, the default locale is unprefixed ({locale} expands to empty for it).
previewPathcollectionURL template. Tokens: {locale}, {slug}, {fields.<key>}. Absent ⇒ preview opens the site root.
localeFromcollectionpath (a folder segment under path), none (locale-less, use the default), or field:<key> (a frontmatter field holds the locale).

The {slug} token reads the file's slug frontmatter — a plain string used as-is (empty means home), or a localized map (slug: {cs, en, it}) indexed by the resolved locale, falling back to the filename when absent. {fields.<key>} reads any frontmatter key, dotted paths included. The resolver collapses duplicate slashes and always returns a leading-slash path.

Preview resolvers

The preview: block on a collection or block drives how an entry renders as a summary row — its title line, description, thumbnail, and chips. Each slot is an array of resolvers tried in order; the first one that produces a non-empty value wins. A resolver is either a literal string or a { key: <field> } reference (a dotted key like seo.title descends into a group).

preview:
  title:
    - { key: heading } # read the heading field
    - Untitled # literal fallback
  description:
    - { key: summary }
  image:
    - { key: cover }
  badges:
    - - { key: category } # one chip: its own resolver chain
    - - { key: status } # a second chip
SlotShapeRenders as
titlearray of resolversPrimary line of the row and the breadcrumb crumb.
descriptionarray of resolversSecondary line under the title.
imagearray of resolversThumbnail to the left of the row.
badgesarray of resolver arraysZero or more chips above the title; each inner array is one chip's own chain.

A collection with title-, description-, or image-shaped fields but no preview falls back to the raw filename in the file browser, so give each content type a preview with at least a title slot.

Two layouts

There are two on-disk layouts, and the directory-based one is preferred:

LayoutWhereStatus
Directory-based.norcube/cms/Preferred. One file per collection / block / input; the CMS writes per-file through the schema endpoints.
Single-filenorcube-cms.config.ymlThe original layout, kept working as a fallback.

When both exist the directory-based config wins — the loader assembles .norcube/cms/ and only falls back to the single file when .norcube/cms/config.yml is absent. Initialising the directory layout seeds it from the single-file config when one is present, so you don't lose anything by migrating. On a directory-based site, writes go through the per-file schema endpoints (the merged document has no single SHA to write back), and the CMS will not create a new single-file norcube-cms.config.yml for a site that's already directory-based.

On this page