---
description: Exact defaults and behavior for collections, MDX, math, highlighting, media, derived fields, manifests, cache, and generated output.
---

# 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.

```ts
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

```ts
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:

```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

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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`, and `slug`
- `frontmatter.<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

```ts
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

```ts
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.
