Configuration
Every scribe.config.ts option: project settings, content types, field markers, and locale routing.
View as MarkdownEverything lives in one file, scribe.config.ts, exporting defineConfig({...}). Both the runtime (createScribe) and the CLI (which discovers scribe.config.ts in the working directory) read the same file.
Project options
export default defineConfig({
rootDir: ".",
contentDir: "content", // default
store: ".scribe/store.sqlite", // default
assetsDir: "public", // optional
locales: ["en", "fr", "de"],
defaultLocale: "en", // default
localeRouting: { strategy: "path-prefix", prefixDefaultLocale: false },
localeFallbacks: true, // default
localePresets: { active: ["fr"] },
translate: { /* see the translation page */ },
types: [ /* defineContentType(...) */ ],
});
| Option | Default | Description |
|---|---|---|
rootDir | — (required) | Project root. Keep it relative (usually "."): the CLI resolves it against the config file's directory, createScribe against process.cwd(). Don't build it from import.meta.url: bundlers inline that path at build time, which breaks on serverless hosts like Vercel. |
contentDir | "content" | Directory containing one folder per content type. |
store | ".scribe/store.sqlite" | SQLite translation store. Commit it; do not gitignore .scribe/. |
assetsDir | — | Static assets root (e.g. "public"). When set, scribe validate warns about image paths in frontmatter or bodies that don't exist on disk. |
locales | — (required) | All locales, including the default one. |
defaultLocale | "en" | Canonical source locale. Must appear in locales. |
localeRouting | path-prefix, unprefixed default | How locale markers appear in generated URLs. path-prefix prepends the locale (omitting the default locale unless prefixDefaultLocale is true). search-param appends ?param=locale for non-default locales. |
localeFallbacks | true | Derive locale fallback chains from locale tags: a regional variant with no translation of a document is served the closest base-language translation (e.g. pt-BR falls back to pt) before the default-locale fallback. Set false to disable. See Locale fallback chains. |
localePresets | — | Named locale groups for scribe translate --preset name. |
translate | — | Project-wide translation defaults (translation). |
types | — (required) | Content type definitions. |
Configs are validated up front: an unknown defaultLocale, duplicate type ids, or a malformed path template throw immediately with a clear message.
Content type options
defineContentType({
id: "blog",
schema: blogSchema,
path: "/blog/{slug}",
contentDir: "blog", // default: the id
slugStrategy: "localized", // default: "fixed"
indexFallback: "en", // default: "en" if path is set, else "none"
orderBy: "-publishedAt", // default: "slug"
label: "Blog", // default: capitalized id (studio UI only)
crossValidate: (data, ctx) => [],
translate: { /* per-type overrides */ },
})
| Option | Description |
|---|---|
id | Unique id. Becomes the typed accessor (scribe.blog) and the default contentDir. |
schema | Zod object schema for the frontmatter, with field markers (below). |
path | URL template with exactly one {slug} (e.g. /blog/{slug}). Omit for reference-only types (authors, categories) that have no pages of their own. |
contentDir | Folder under the project contentDir holding this type's .mdx files. |
slugStrategy | "fixed": every locale uses the English slug. "localized": the translator produces a per-locale slug (/fr/blog/bonjour-le-monde). |
indexFallback | What resolve() returns when a locale has no translation: "en" serves the English document (with actualLocale: "en"), "none" returns nothing. |
orderBy | Default sort for list(): "slug", "publishedAt", "-publishedAt", "updatedAt", "-updatedAt", or a comparator (a, b) => number over fully-typed documents. |
crossValidate | Extra validation run by scribe validate, receiving the parsed (typed) frontmatter. Return { field, message, level }[]. |
translate | Per-type translation prompt/rules/model (translation). |
Field markers
Every schema field is one of three kinds. Unmarked fields default to structural.
import { field } from "scribe-cms";
const schema = z.object({
// Sent to the translator, stored per locale:
title: field.translatable(z.string().min(1)),
// English-only; copied from the EN document into every locale:
heroImage: field.structural(z.string().optional()),
// English slug reference(s) to another content type:
author: field.relation("author"),
relatedPosts: field.relation("blog", { multiple: true, max: 4, optional: true }),
});
Translatable markers also work on fields nested inside plain arrays or objects (e.g. sections.*.title); just don't wrap the containing array in field.structural, or the whole subtree becomes English-only.
field.relation(typeId, options?)
| Option | Default | Description |
|---|---|---|
multiple | false | The field is an array of slugs. |
optional | false | The field may be omitted. related() then returns null (single) or skips missing items (multiple). |
min / max | — | Item count bounds (multiple: true only). |
Constraints go in the options object: not chained Zod methods. Chaining (.max(8), .optional()) clones the schema and would strip the relation metadata.
Relations always store English slugs, are checked by scribe validate (a dangling required relation is a build-blocking error; a dangling optional one is a warning), and are dereferenced at runtime with related(); see the runtime API.
Locale fallback chains
By default, a regional locale with no translation of a document is served its base language before the default locale: each locale tries the successively shorter prefixes of its own tag that are also in locales, so fr-CA falls back to fr, and zh-Hant-TW tries zh-Hant, then zh. Set localeFallbacks: false to disable. The default locale is never part of a chain; it stays the final fallback, governed by the type's indexFallback.
Fallbacks apply to resolve() (the served locale is reported in actualLocale, and slug redirects use the fallback locale's slug) and staticParams() (prerendered slugs match what resolve() serves). They deliberately do not apply to get(), list(), translation(), alternates(), or the sitemap: those stay exact-match.
Typed client types
For app-side type aliases, derive everything from the config:
import type { ScribeClient, ScribeDocs } from "scribe-cms/runtime";
import config from "./scribe.config";
type MyScribe = ScribeClient<typeof config>;
type BlogDoc = ScribeDocs<typeof config>["blog"];