Project config
The .norcube/cms/ directory — config.yml, collections, blocks, inputs, the preview block, and the identity sidecar.
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.
Field types
Every field type and the YAML it produces — the fields: list referenced throughout this page.
Media library
The storages: block and how image/file fields route uploads.
Live preview
Deep-linking the preview to the page you're editing — the preview block and previewPath on a collection.
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| Entry | What it holds |
|---|---|
config.yml | Root 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| Key | Type | Effect |
|---|---|---|
schemaVersion | integer | Schema-language version. Absent defaults to 1; a version this build doesn't support is rejected at load. |
siteUrl | string | Canonical site URL. Carried verbatim into the merged config. |
pagesDir | string | Root directory of the default pages collection, e.g. src/content/pages. |
publicDir | string | Where the deployed site serves static files from. Used as the leading-slash asset-resolver root. Defaults to public. |
defaultStorage | string | Storage name used when an image/file field omits storage:. See Media library. |
inputsFromGlob | string or string[] | Extra <dir>/*.yml globs to load input snippets from, in addition to inputs/. Glob entries win on name collision. |
storages | object | Named upload destinations. See Media library. |
collections | object (inline) | Inline collections, keyed by name. Prefer one file per collection under collections/. |
blocks | object (inline) | Inline blocks, keyed by name. Prefer one file per block under blocks/. |
inputs | object (inline) | Inline input snippets, keyed by name. Prefer one file per input under inputs/. |
preview | object | Site-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| Key | Effect |
|---|---|
label | Human-facing collection name, shown in the editor nav. |
path | Repo-relative directory this collection's files live in (e.g. src/content/works). Drives directory-based collection resolution. |
match | Glob (relative to path) selecting which files belong to the collection. Defaults to **/*.md. |
hasBody | When true, pages carry a Markdown body after the frontmatter, surfaced as a body editor. |
extraKeysPolicy | What to do with frontmatter keys not in the schema — show, hide, or strip. See below. |
preview | How a page renders as a summary row in the file browser. See preview resolvers. |
previewPath | URL template for deep-linking the live preview. See Preview deep-linking. |
localeFrom | Where a file's locale comes from, for {locale} in previewPath. See Preview deep-linking. |
fields | Ordered list of frontmatter fields. See Field types. |
schemaVersion | Per-file schema version. Absent defaults to 1; unsupported versions are rejected. |
extraKeysPolicy controls stray frontmatter keys the schema doesn't
declare:
| Value | Effect |
|---|---|
show | Default. Surface them in the "Out-of-schema content" panel so authors can clean up or migrate the schema. |
hide | Keep them in the YAML but don't render the panel — useful when a build pipeline writes its own keys. |
strip | Purge 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: uploadsReference 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-textThe 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-titleBlocks 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| Key | Where | Effect |
|---|---|---|
defaultLocale | site | Locale used for locale-less files and as the fallback. |
locales | site | Declared locales. A path segment is only treated as a locale when it's in this list. |
prefixDefaultLocale | site | When false, the default locale is unprefixed ({locale} expands to empty for it). |
previewPath | collection | URL template. Tokens: {locale}, {slug}, {fields.<key>}. Absent ⇒ preview opens the site root. |
localeFrom | collection | path (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| Slot | Shape | Renders as |
|---|---|---|
title | array of resolvers | Primary line of the row and the breadcrumb crumb. |
description | array of resolvers | Secondary line under the title. |
image | array of resolvers | Thumbnail to the left of the row. |
badges | array of resolver arrays | Zero 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:
| Layout | Where | Status |
|---|---|---|
| Directory-based | .norcube/cms/ | Preferred. One file per collection / block / input; the CMS writes per-file through the schema endpoints. |
| Single-file | norcube-cms.config.yml | The 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.