Scribe

Configuration

Every scribe.config.ts option: project settings, content types, field markers, and locale routing.

View as Markdown

Everything 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(...) */ ],
});
OptionDefaultDescription
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/.
assetsDirStatic 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.
localeRoutingpath-prefix, unprefixed defaultHow 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.
localeFallbackstrueDerive 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.
localePresetsNamed locale groups for scribe translate --preset name.
translateProject-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 */ },
})
OptionDescription
idUnique id. Becomes the typed accessor (scribe.blog) and the default contentDir.
schemaZod object schema for the frontmatter, with field markers (below).
pathURL template with exactly one {slug} (e.g. /blog/{slug}). Omit for reference-only types (authors, categories) that have no pages of their own.
contentDirFolder 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).
indexFallbackWhat resolve() returns when a locale has no translation: "en" serves the English document (with actualLocale: "en"), "none" returns nothing.
orderByDefault sort for list(): "slug", "publishedAt", "-publishedAt", "updatedAt", "-updatedAt", or a comparator (a, b) => number over fully-typed documents.
crossValidateExtra validation run by scribe validate, receiving the parsed (typed) frontmatter. Return { field, message, level }[].
translatePer-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?)

OptionDefaultDescription
multiplefalseThe field is an array of slugs.
optionalfalseThe field may be omitted. related() then returns null (single) or skips missing items (multiple).
min / maxItem 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"];
Configuration · Scribe