Scribe

Content model

Files, frontmatter, built-in fields, redirects, and what scribe validate checks.

View as Markdown

Files

One document = one .mdx (or .md) file in the type's folder:

content/
  blog/
    hello-world.mdx        → slug "hello-world"
    _redirects.json        → redirect rules for this type (optional)
  authors/
    jane.mdx               → slug "jane"
  • The file name is the English slug. Slugs are lowercase kebab-case.
  • Only English files exist on disk; locale versions live in the SQLite store.
  • Files whose name starts with _ or an uppercase letter (e.g. PUBLISHING.md) are ignored, so you can keep notes next to your content.

Frontmatter

Frontmatter is YAML, validated against the type's Zod schema:

---
title: "Hello, world"
description: "A long enough description for the schema."
author: jane
publishedAt: "2026-01-15"
updatedAt: "2026-02-01"
---

Body is MDX.

Schema validation follows your Zod schema: with a plain z.object unknown keys are silently stripped; use .strict() if you want typos in field names to fail validation.

Built-in frontmatter fields

These are available on every content type without declaring them in the schema; they are extracted from the frontmatter before your schema runs, so declaring them in your Zod schema has no effect:

FieldTypeDescription
publishedAtISO datePublication date.
updatedAtISO dateLast significant update. Defaults to publishedAt. Drives sitemap lastModified.
noindexbooleanExcluded from the sitemap; expose it as a robots meta tag in your pages.
canonicalPathstringManually override the canonical URL path.

Locale documents inherit publishedAt, updatedAt, noindex, and canonicalPath from their English parent; translators can't change them.

Every loaded document also carries slug, enSlug (the English parent slug; equal to slug for English documents), locale, frontmatter, and content.

Bodyless types

Reference-only collections (a model or category type whose schema is entirely structural) often have no MDX body at all. Declare body: false on the type to make that explicit:

defineContentType({
  id: "model",
  contentDir: "models",
  schema: modelSchema,
  body: false, // entries are frontmatter-only
});

The default is body: true, so this is fully backwards compatible. On a body: false type, any non-whitespace body content is a validation error, the loader skips MDX parsing, and static exports emit no body. A bodyless type with zero translatable fields also drops out of every translation workflow (no missing/stale noise); a bodyless type that still has a translatable field stays in the todos with a frontmatter-only payload. See translation for the translatability rule.

Beyond frontmatter

MDX bodies can do more than plain prose:

  • Inline tokens embed links, asset URLs, and untranslatable literals directly in the body, resolved per locale at read time.
  • Assets declared with field.asset() are validated on disk and resolved to public URLs at load time.

Redirects: _redirects.json

Per content-type folder, add an optional _redirects.json file to declare slug migrations and retired documents. Redirects survive after you delete the source MDX; translated source slugs are expanded automatically from SQLite.

{
  "redirects": [
    { "from": "hello-world", "toSlug": "hello-scribe" },
    { "from": ["old-a", "old-b"], "toSlug": "hello-scribe" },
    { "from": "moved-post", "toType": "glossary", "toSlug": "virtual-try-on" },
    { "from": "retired-page", "toUrl": "/pricing" },
    { "from": "retired-ext", "toUrl": "https://example.com/app" }
  ]
}

Three redirect kinds (exactly one target per entry):

KindFieldsDescription
Same-typetoSlugTarget EN slug in the same content type. URL built from this type's path, localized per locale.
Cross-typetoType + toSlugTarget EN slug in another routable content type (must have a path).
AnywheretoUrlRoot-relative same-site path or absolute external URL, identical for every locale.
  • from: EN slug(s) only. Translated source slugs are resolved from SQLite.
  • Optional permanent (default true).

To retire or rename a document: add an entry to _redirects.json, delete (or rename) the source MDX, then run scribe validate and rebuild. Redirect rules are emitted by buildAllContentRedirects() for your proxy or framework config.

Validation

scribe validate

Checks, per English file: schema parse, built-in field shapes, your crossValidate hook, and MDX compilation of the body; a body that doesn't parse as MDX is a build-blocking error, for English sources and stored translations alike.

Across the project it also checks: relation integrity (dangling required relation = error, dangling optional relation = warning), _redirects.json rules (valid targets, no duplicate sources, no redirect chains), localized-slug suffix rules, declared asset fields (file existence, formats, maxKB), inline body tokens (malformed spans, dangling relations, :href to non-routable types, missing vars keys, missing asset files), body: false types that still carry body content, missing image assets when the assets directory is set, and that the translation store exists.

Exit code is non-zero when any error is found; run it before your build:

{
  "scripts": {
    "build": "scribe validate && <your framework build>"
  }
}
Content model · Scribe