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.
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
| Key | Default | Purpose |
|---|---|---|
root | process.cwd() | Base for relative configuration paths. |
collections | required | Content directories, schemas, locales, and slugs. |
mdx | see below | MDX syntax, math, and JSX runtime options. |
highlight | Shiki enabled | Syntax highlighting policy. Use false to disable. |
media | rewriting enabled | Markdown media import policy. Use false to disable. |
derived | all disabled | Reading time and last-modified metadata. |
manifests | {} | Named JSON projections. |
cache | enabled | Persistent compiled records. Use false to disable. |
generatedDirectory | .amamo-mdx | Registry, 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
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'] },
},
}| Key | Default | Behavior |
|---|---|---|
directory | required | Resolved from root; the directory must exist before build(). |
extensions | ['.mdx'] | Included suffixes. Every value must begin with .. |
locales | none | Optional filename-based locale mapping. |
schema | required | Zod 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:
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
mdx: {
extensions: {
footnotes: true,
headingIds: false,
taskLists: true,
},
gfm: true,
hardBreaks: false,
jsxImportSource: 'react',
math: false,
providerImportSource: '',
}| Key | Default | Behavior |
|---|---|---|
extensions | see below | Optional Markdown syntax. |
gfm | true | Enables the GFM bundle; footnotes and task lists can override it. |
hardBreaks | false | Converts newlines inside text nodes to <br> elements. |
jsxImportSource | 'react' | JSX runtime package used by compiled modules. |
math | disabled | Build-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:
| Key | Default | Behavior |
|---|---|---|
footnotes | inherits GFM | Reference[^id] and [^id]: Definition. |
headingIds | false | Derives IDs from static heading text. |
taskLists | inherits 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
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
highlight: {
provider: 'shiki',
engine: 'oniguruma',
languages: 'auto',
themes: { light: 'vitesse-light', dark: 'vitesse-dark' },
unknownLanguage: 'error',
colorReplacements: {},
}| Key | Default | Behavior |
|---|---|---|
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. |
themes | Vitesse light/dark | Bundled 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
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.
missing | Result |
|---|---|
'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
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
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' },
},
},
}| Key | Default | Behavior |
|---|---|---|
output | required | JSON path resolved from root, independently of generatedDirectory. |
collections | all collections | Collection names included in this manifest. |
fields | required | Output field to source projection map. |
sort | [] | Ordered projected-field sort keys; direction defaults to asc. |
key | none | When 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, andslugfrontmatter.<path>derived.<path>
A field value can be a path string or { from, transform?, default? }.
| Transform | Result |
|---|---|
| none | Copy 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
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
generatedDirectory: '.amamo-mdx'The compiler always writes exactly three shared outputs here:
collections.mjscollections.d.tsindex.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.