Scribe

Inline tokens

Embed links, asset URLs, and untranslatable strings in MDX prose with tokens that update every locale without retranslation.

View as Markdown

MDX bodies often need a value Scribe already knows: a link to another entry, the public URL of an asset, a literal that must never be translated, or a small per-document string reused in several places. Hard-coding these makes bodies brittle (a renamed slug or moved asset silently breaks links) and pollutes translation (a URL should never be "translated").

Inline tokens solve this. You author a token once in the English body; it resolves per locale at read time, while translation treats it as an opaque, immutable placeholder.

Syntax

A token is ${{<kind>:<args>}}. There are four kinds:

TokenResolves to
${{static:"text"}}The verbatim literal text (a JSON string, so quotes and escapes are well defined). Never translated, identical in every locale.
${{relation:<typeId>:<enSlug>:href}}A link path to the target entry (see relation modes). The mode suffix is required.
${{relation:<typeId>:<enSlug>:slug}}The target's English slug string, a stable identifier for MDX components that load Scribe content themselves.
${{asset:/web/path.webp}}The public asset URL (assets.publicPath joined to the path). See assets.
${{var:key}}frontmatter.vars[key] of the same document.

Slugs cannot contain :, so relation parsing is unambiguous. A bare ${{relation:<typeId>:<enSlug>}} with no mode suffix is a validation error.

Relation modes

Every relation token must end with :href or :slug.

ModePurpose
:hrefNavigable link. The resolved shape depends on the consumer (below).
:slugIdentity: always the target's English slug string.

:href resolves differently depending on who reads the document:

Consumer:href resolves to
createScribe() (app runtime)Locale-free pathname with the localized slug, e.g. /for/vestidos; pass straight to your router's Link.
Static .md exportFull localized public path with file extension, e.g. /es/for/vestidos.md, matching the export file layout.

Escape hatch

${{ (a backslash between the dollar sign and the braces) renders a literal ${{ and is never treated as a token. Use it to show token syntax in documentation content; this page does exactly that.

The vars frontmatter key

vars is an optional Record<string, string> any entry may declare without adding it to the type schema:

---
title: Spring sale
vars:
  cta: Shop the sale
---

Ready? ${{var:cta}}. Limited time only.

vars is a reserved key: it is pulled out before schema validation (so a strict schema never rejects it), it is never treated as a translatable field, and it lives only on the English document. Translated documents read ${{var:key}} from their English parent's vars map, so there is a single source of truth.

Hashing and translation

Tokens are extracted before hashing. Each token is replaced by an inert numbered marker (%%1%%, %%2%%, …, in order of appearance), and that placeholder body is what gets hashed and sent to the translator. Consequences:

  • Changing a token's value (the static text, the relation target, the asset path, or a var value) does not change the English hash, so no locale goes stale for a value-only edit.
  • Adding, removing, or reordering tokens does change the hash, so translations restage as expected.
  • A body with zero tokens hashes byte-for-byte as before, so adopting tokens causes no mass re-staleness.

The translator is instructed that %%n%% markers are immutable: reproduce each exactly once, never translate or renumber them, and move them within a sentence only when grammar requires it. After a translation is received, a post-receive check verifies every marker appears exactly once; a mismatch fails the row so it retries. Stored translated bodies keep the markers and fill them at read time.

Read-time substitution

Substitution is a read-path concern, gated like asset resolution. createScribe() enables it for the app runtime; static exports enable it for the .md layout. The CLI, scribe validate, and the studio keep raw token syntax so they can introspect and re-hash source bodies.

  • English documents have their tokens substituted in place, resolved for the default locale.
  • Translated documents have their %%n%% markers filled using the token list extracted from the current English body, resolved for the document's locale; so a relation link always points at the live localized slug even when the translation itself is older.

Two edge cases resolve to an empty string at runtime (and are flagged by scribe validate): a relation whose target type has no path in :href mode, and a missing var key.

Validation

scribe validate reports these as entry-level issues:

  • Malformed token syntax (bad JSON string, wrong arity, unknown kind): error.
  • relation: unknown typeId, unknown enSlug, missing mode, or an :href relation targeting a non-routable type: error.
  • asset: the file is missing on disk: error.
  • var: the key is absent from the document's vars map, or vars is not a string-to-string record: error.

Raw tokens are masked to inert text before MDX body validation, so a valid token never produces a false MDX parse error.

Body references and deletion

The studio "Used by" panel and asset browser scan bodies: ${{relation:...}} tokens appear as back-references (field label body), and ${{asset:...}} tokens register as declared asset references. Deleting an entry referenced only from another entry's body never cascades, detaches, or blocks; see entry deletion. The deletion plan lists such references under a warn-only "body references" section: they will dangle and become validation errors after the deletion.

Inline tokens · Scribe