@amamo/mdx
On This Page

Configuration

defineConfig returns its argument unchanged. normalizeConfig is the runtime boundary: it converts Zod collection schemas to JSON Schema, rejects other non-plain data, applies defaults, resolves paths, and returns the IAmamoMDXConfig used by every compiler and adapter.

TypeScript
import { defineConfig, z } from '@amamo/mdx'

export default defineConfig({
  collections: {
    posts: {
      directory: 'content/posts',
      schema: z.object({}),
    },
  },
})

At least one collection is required.

Top-level keys

KeyDefaultPurpose
rootprocess.cwd()Base for relative configuration paths.
collectionsrequiredContent directories, schemas, locales, and slugs.
mdxsee belowMDX syntax, math, and JSX runtime options.
highlightShiki enabledSyntax highlighting policy. Use false to disable.
mediarewriting enabledMarkdown media import policy. Use false to disable.
derivedall disabledReading time and last-modified metadata.
manifests{}Named JSON projections.
cacheenabledPersistent compiled records. Use false to disable.
generatedDirectory.amamo-mdxRegistry, declaration, and private Next index directory.

Apart from collection Zod schemas, functions, symbols, accessors, class instances, cycles, undefined, bigint, and non-finite numbers are rejected. This check runs after your configuration module itself has been imported; it does not sandbox that module.

Collections

TypeScript
import { z } from '@amamo/mdx'

collections: {
  posts: {
    directory: 'content/posts',
    extensions: ['.mdx'],
    locales: { default: 'en', names: ['en', 'zh-CN'] },
    schema: z.object({
      title: z.string(),
      draft: z.boolean().default(false),
    }),
    slug: { indexNames: ['index', 'page'] },
  },
}
KeyDefaultBehavior
directoryrequiredResolved from root; the directory must exist before build().
extensions['.mdx']Included suffixes. Every value must begin with ..
localesnoneOptional filename-based locale mapping.
schemarequiredZod object schema for YAML frontmatter.
slug.indexNames['index', 'page']Basenames omitted from the derived slug.

The package keeps Zod internal and exposes its compatible z builder. The config boundary is the structural IFrontmatterSchema interface, so another compatible object schema can also be passed. The object schema is converted to JSON Schema Draft 2020-12; raw JSON Schema objects are rejected. Rust applies .default() values before validation, but it does not run Zod parsing, coercion, transforms, or custom refinements. Use JSON-Schema-representable types and built-in checks. Validation diagnostics may contain submitted values; do not expose raw build errors to untrusted users.

When locales are configured, an unsuffixed file uses the default locale. A recognized suffix such as page.zh-CN.mdx selects that locale. The default locale must appear in names. Document identity is derived as follows:

text
posts/guide.mdx          -> slug "guide", key "en:guide", locale "en"
posts/guide.zh-CN.mdx    -> slug "guide", key "zh-CN:guide", locale "zh-CN"
posts/index.mdx          -> slug "/",     key "en:/",     locale "en"

Without locale config, the key equals the slug. A full build rejects duplicate keys within a collection.

The Next adapter currently registers only *.mdx. Custom collection extensions can be handled by the direct compiler and Vite plugin, but not by the built-in Next loader.

MDX

TypeScript
mdx: {
  extensions: {
    footnotes: true,
    headingIds: false,
    taskLists: true,
  },
  gfm: true,
  hardBreaks: false,
  jsxImportSource: 'react',
  math: false,
  providerImportSource: '',
}
KeyDefaultBehavior
extensionssee belowOptional Markdown syntax.
gfmtrueEnables the GFM bundle; footnotes and task lists can override it.
hardBreaksfalseConverts newlines inside text nodes to <br> elements.
jsxImportSource'react'JSX runtime package used by compiled modules.
mathdisabledBuild-time math parsing and rendering. Use {} to enable.
providerImportSource''Optional MDX component-provider import; empty means no provider import.

Authored ESM and MDX JSX remain part of the compiled module. providerImportSource controls MDX component-provider wiring; it is not a switch for preserving or stripping JSX.

Markdown extensions

Each supported extension is independently optional:

KeyDefaultBehavior
footnotesinherits GFMReference[^id] and [^id]: Definition.
headingIdsfalseDerives IDs from static heading text.
taskListsinherits GFM- [ ] Todo and - [x] Done.

footnotes and taskLists use their explicit value when present; otherwise they follow gfm. This allows either feature to be enabled while the rest of GFM is off, or disabled while tables, autolinks, and other GFM syntax remain on.

Generated footnote anchors are namespaced by document, so rendering multiple compiled documents on one page does not duplicate their IDs. Automatic heading IDs lowercase text, preserve Unicode letters and numbers, replace other runs with hyphens, and append -2, -3, and so on for duplicates. Use an authored MDX heading such as <h2 id="custom-id">Heading</h2> when the ID must be fixed manually.

No shorthand is added for marks, subscripts, superscripts, or definition lists. Standard MDX elements such as <mark>, <sub>, <sup>, and <dl> remain available without configuration.

Math

TypeScript
mdx: {
  math: {
    singleDollar: true,
    macros: {
      '\\RR': '\\mathbb{R}',
    },
  },
},

Math syntax is opt-in. Set mdx.math: {} to render inline $x$ and display $$ expressions, or mdx.math: false to keep dollar signs as ordinary text. singleDollar: false disables the $x$ inline form, which is useful for prose containing currency. With the default enabled, write a literal currency sign as \$.

macros maps one TeX control sequence to a fixed expansion. Each expansion is limited to 1 KiB, and names plus expansions are limited to 16 KiB in total. Parameter placeholders and dynamic definition commands such as \\def and \\newcommand are rejected. The map is copied and applied independently to each expression, so definitions cannot leak between documents.

RaTeX parses, lays out, and renders each expression inside the Rust compiler. The generated module contains self-contained SVG output with no browser math runtime, stylesheet, webfont, raw HTML, or dangerouslySetInnerHTML. Invalid TeX reports AMAMO_MATH_PARSE; unsafe, external, or non-embedded SVG output reports AMAMO_MATH_RENDER. A glyph that cannot be embedded fails the build instead of falling back to an external font. Non-KaTeX Unicode glyphs can use an installed system font, so their exact outlines may differ between build machines. Only publish those glyphs when the build machine's fallback-font license permits embedding and redistribution.

Each expression is limited to 64 KiB of TeX, 100,000 expanded tokens, an 8 MiB or 100,000-node parsed AST, 100,000 expanded array cells in total, 10,000 display items, 10,000 em in either dimension, a 16 MiB SVG, and 100,000 SVG nodes. Parse and structural limit failures report AMAMO_MATH_PARSE; layout or SVG limit failures report AMAMO_MATH_RENDER.

Inline SVG uses RaTeX's depth for baseline alignment and inherits currentColor; explicit TeX colors remain explicit. The wrapper exposes role="math" and the source expression as an accessible label. RaTeX does not currently emit MathML, so formula text is not selectable and assistive technology receives the TeX label rather than a MathML tree.

Highlighting

TypeScript
highlight: {
  provider: 'shiki',
  engine: 'oniguruma',
  languages: 'auto',
  themes: { light: 'vitesse-light', dark: 'vitesse-dark' },
  unknownLanguage: 'error',
  colorReplacements: {},
}
KeyDefaultBehavior
provider'shiki'Required literal; no other provider is shipped.
engine'oniguruma'Shiki oniguruma or javascript engine.
languages'auto'Load on demand, or preload the listed bundled languages.
themesVitesse light/darkBundled Shiki theme names.
unknownLanguage'error'Reject an unknown language or render it as 'plain'.
colorReplacements{}Shiki color-to-color replacement map.

An explicit language list preloads those grammars; it does not prevent later lazy loading. Shiki memoizes language loading in memory. Highlighted output is stored inside the document cache record; there is no separate highlighter cache on disk.

Set highlight: false to skip code-block highlighting.

Media

TypeScript
media: {
  attributes: {
    audio: ['src'],
    embed: ['src'],
    img: ['src', 'srcset'],
    object: ['data'],
    source: ['src', 'srcset'],
    track: ['src'],
    video: ['src', 'poster'],
  },
  missing: 'error',
}

The table above is the default attribute map. Supplying attributes replaces the whole map; include every tag you still want rewritten.

Relative URLs produced by Markdown elements are resolved from the source file and become static imports. Absolute paths, fragments, protocol-relative URLs, data URLs, and URLs with a scheme pass through. Authored MDX JSX such as <img src="./manual.png" /> is not rewritten.

missingResult
'error'Reject the fresh compilation.
'warn'Keep the original URL and add AMAMO_MEDIA_MISSING to record.diagnostics.

Warnings are not logged automatically by the direct compiler or Vite adapter. Set media: false to disable rewriting.

Derived fields

TypeScript
derived: {
  lastModified: false,
  readingTime: false,
}

readingTime excludes code and counts each contiguous ASCII word or CJK/Japanese/Korean character as one unit. Minutes are ceil(units / 300), with a minimum of one minute.

lastModified uses the latest Git commit timestamp for the file when available, then falls back to the filesystem modification time. Enabled values appear under record.derived and as named exports from the compiled module.

Manifests

TypeScript
manifests: {
  public: {
    output: '.amamo-mdx/public.json',
    collections: ['posts'],
    sort: [{ field: 'publishedAt', direction: 'desc' }],
    fields: {
      key: 'key',
      title: 'title',
      publishedAt: { from: 'publishedAt', default: null },
    },
  },
  server: {
    output: '.amamo-mdx/server.json',
    key: 'key',
    fields: {
      key: 'key',
      tokenPresent: { from: 'accessToken', transform: 'exists' },
      tokenFingerprint: { from: 'accessToken', transform: 'sha256' },
    },
  },
}
KeyDefaultBehavior
outputrequiredJSON path resolved from root, independently of generatedDirectory.
collectionsall collectionsCollection names included in this manifest.
fieldsrequiredOutput field to source projection map.
sort[]Ordered projected-field sort keys; direction defaults to asc.
keynoneWhen absent, output an array; when present, output an object keyed by this projected field.

If key is configured, that name must exist in the projected record and resolve to a unique string, number, or boolean. Missing, null, object, array, and duplicate values fail the build. Missing or null sort values remain last in both directions; the document key breaks remaining ties.

Projection paths can read top-level frontmatter or these reserved values:

  • collection, file, key, locale, and slug
  • frontmatter.<path>
  • derived.<path>

A field value can be a path string or { from, transform?, default? }.

TransformResult
noneCopy the source value, or default, or null.
'exists'Boolean indicating that the source exists and is not null.
'sha256'Lowercase SHA-256 hex of a string or serialized JSON value; missing uses default or null.
'mediaUrl'Pass through absolute/external URLs or map a relative string to a root-relative URL.

mediaUrl performs a path projection; it does not check that the file exists or create a static import.

Cache

TypeScript
cache: {
  directory: '.amamo-mdx/cache',
}

Cache keys are BLAKE3 digests over the cache format, native crate version, OS, architecture, target mode, normalized config plus runtime package versions, source path and bytes, and enabled lastModified value. A cache hit also requires every previously recorded media dependency to still exist.

Corrupt records are removed and recompiled with an AMAMO_CACHE_CORRUPT warning. A successful full build prunes records no longer referenced by the discovered content set.

Set cache: false to disable persistent records. The direct API and Vite can still compile, but the Next loader cannot operate without cache files.

Generated output

TypeScript
generatedDirectory: '.amamo-mdx'

The compiler always writes exactly three shared outputs here:

  • collections.mjs
  • collections.d.ts
  • index.json

Cache and manifest locations are independent. Relative collection, cache, generated, and manifest paths resolve from root, which defaults to process.cwd(); Markdown media paths resolve from the containing source document. Existing roots and collection directories are canonicalized during normalization. Set root explicitly whenever the config may be loaded from another working directory.

Copyright © 2026 白熱.