Scribe

The git-based CMS for multilingual MDX sites

English content lives in MDX files in your repo. AI translations live in a SQLite file next to them. Zod validates both, and a typed runtime reads everything at build time. No CMS server, no network at request time.

pnpm add scribe-cms zod better-sqlite3

How it works

  1. 1

    Define a schema

    Content types are Zod schemas. Mark which fields get translated, which stay English-only, and how documents reference each other.

    // scribe.config.ts
    defineContentType({
      id: "blog",
      path: "/blog/{slug}",
      slugStrategy: "localized",
      schema: z.object({
        title: field.translatable(z.string().min(1)),
        description: field.translatable(z.string().min(50)),
        author: field.relation("author"),
        heroImage: field.structural(z.string().optional()),
      }),
    });
  2. 2

    Write content

    One MDX file per document, committed to your repo. The file name is the English slug; frontmatter is validated against the schema.

    ---
    title: "Hello, world"
    description: "A first post that says hello to the world."
    author: jane
    publishedAt: "2026-01-15"
    ---
    
    The body is MDX. **Markdown** and <Components /> both work.
  3. 3

    Translate what changed

    Translatable content is hashed, so only missing or stale pages are sent to Gemini, through the Batch API at half the token price. The prompt is tuned for transcreation, so copy reads like it was written in the target language. Output is parsed as MDX before it is stored; a bad response fails the run, not your site.

    export GEMINI_API_KEY=...
    
    npx scribe translate --locale fr de --dry-run   # show the worklist
    npx scribe translate --locale fr de             # translate, validate, store
    git add .scribe/store.sqlite                    # translations live in git
  4. 4

    Read it, typed

    The runtime infers document types from your config. Lists, lookups, relations, hreflang alternates, and sitemap entries all come back fully typed.

    const scribe = createScribe(config);
    
    const posts = scribe.blog.list("fr");                  // BlogDoc[]
    const { document } = scribe.blog.resolve(slug, "fr");  // EN fallback built in
    const author = scribe.blog.related(document!, "author"); // AuthorDoc
    const hreflang = scribe.blog.alternates(document!);

What you get

Incremental translation

Translations are keyed to a hash of their English source. Edit one paragraph and only that document is retranslated; a dry run shows the cost first.

Reads like a native wrote it

The prompt is built for transcreation: a native copywriter's voice that recreates idioms instead of translating word for word.

Validated before stored

Every translation is parsed as MDX and re-checked against your Zod schema before it is stored. Model mistakes fail the command, not production.

Typed end to end

Zod schemas, inferred document types, no codegen. Relations come back dereferenced and fully typed.

Inline tokens

Links, assets, and untranslatable strings embedded right in prose. Edit a token and every locale updates instantly, no retranslation.

SEO built in

hreflang alternates, sitemaps with x-default, canonicals, noindex, JSON redirect rules, and locale fallback chains.

Everything in git

MDX files plus one committed SQLite store. Snapshots record what each translation was made from; scribe history shows the timeline.

No server

Everything is read from disk at build time; works with Next.js, Astro, or any Node stack. A local studio adds browsing, search, and coverage at a glance.

AI-first by design

Agents read and edit MDX with the same tools they use on code, and every change is a diff you review. No API between the model and the words.

Built in the open

scribe-cms is MIT-licensed and developed on GitHub. This site runs on it. Every page here is scribe content, with a committed SQLite store behind ten locales. Read the source, open an issue, or send a pull request.

The git-based CMS for multilingual MDX sites