A scaffolded zudo-doc project is intentionally minimal: one config file, your content, a stylesheet, and two thin route stubs (see [Installation](https://zudo-doc.takazudomodular.com/docs/getting-started/installation#project-structure)). Everything else — layout, chrome, schema, tokens — ships from `@takazudo/zudo-doc` and is consumed from `node_modules`.

That "one config file" ideal covers the common cases. This page is the **escalation ladder** for when it does not: each rung is more invasive than the last, and you should climb only as far as the change actually requires. The rungs are ordered by how soon a real project tends to hit them.

## The ladder

| Rung | Reach for it when | Cost |
| --- | --- | --- |
| **1. `buildDocsSchema` override** | You need a custom, validated frontmatter key | Replace one function; own the schema |
| **2. Config fields** | Toggling a feature or tuning behavior | None — it is just a field |
| **3. Theme packs** | You want a whole prebuilt look, not a hand-tune | None — one field or one CLI command |
| **4. Token overrides** | Re-theming colors, spacing, typography | A `:root` block in `global.css` |
| **5. `zudo-doc eject`** | A content-layer component you re-reference must change | You own the ejected copy and its imports |
| **6. `chromeBindingsModule`** | Replacing primary chrome or injecting host callables, including [custom MDX components](https://zudo-doc.takazudomodular.com/docs/guides/custom-components#global-registration-mdxextras) | One host module |
| **7. Your own `pages/*.tsx`** | A whole route must be yours | You own that route |
| **8. Deploy path** | Going live (especially with SSR features) | Adapter + `wrangler.toml` + secrets |
| **9. Restoring pre-push / HTML checks** | You want the showcase's validation gates | Copy a script + add deps |

## Rung 1 — Custom frontmatter keys (`buildDocsSchema`)

This is the **first place the one-config ideal breaks**, so it leads the ladder.

Every other feature is a field on `zudoDoc()`. Frontmatter validation is not: the docs collection is validated by a Zod schema, and there is no config field that says "also allow a `tier` key." The schema is a **function you replace** — the `buildDocsSchema` escape hatch.

By default `zudoDoc()` builds the package schema for you (governance-aware, derived from `tagGovernance` + `tagVocabularyEntries`). To add your own key, import the default builder, extend its result, and hand the new builder back:

```ts title="zfb.config.ts"
import { defineConfig } from "zfb/config";
import { zudoDoc } from "@takazudo/zudo-doc/config";
import { buildDocsSchema } from "@takazudo/zudo-doc/docs-schema";
import { z } from "zod";

export default defineConfig(
  zudoDoc({
    siteName: "My Docs",
    // Replace the schema builder entirely. Start from the package default,
    // then extend it with your own validated frontmatter keys.
    buildDocsSchema: () =>
      buildDocsSchema({ tagGovernance: "off" }).extend({
        tier: z.enum(["core", "opt-in"]).optional(),
        reviewed_by: z.string().optional(),
      }),
  }),
);
```

Now pages can declare `tier: core` in frontmatter and it validates at build time. Combine this with [`docContentHeaderExtras`](https://zudo-doc.takazudomodular.com/docs/reference/host-chrome-bindings) (rung 6) to actually render a badge from that key.

[Dynamic content unavailable in text export]

`buildDocsSchema` is one of the non-serializable **escape-hatch fields** on `ZudoDocConfig` — alongside `colorSchemes`, `translations`, `directives`, and `tagVocabularyEntries`. They travel the import graph (never JSON-serialized), which is exactly why they can carry functions and Zod types. See [Configuration](https://zudo-doc.takazudomodular.com/docs/guides/configuration#escape-hatch-fields).



## Rung 2 — Config fields

Almost everything else **is** a field. `zudoDoc()` defaults every setting, so your `zfb.config.ts` lists only what differs:

```ts title="zfb.config.ts"
export default defineConfig(
  zudoDoc({
    siteName: "My Docs",
    docHistory: true,
    sidebarToggle: true,
    tocToggle: true,
    tocMaxDepth: 3,
    headerNav: [
      { label: "Guides", path: "/docs/guides", categoryMatch: "guides" },
    ],
    footer: { copyright: "© 2026 Me" },
  }),
);
```

**Reach for it when:** you are turning a feature on/off or tuning behavior (TOC depth, navigation, color mode, locales).

**Limit:** config fields control *what exists and how it behaves*, not the pixel-level rendering of a component. For that, climb to rungs 3–5.

The full field list — with the default for every one — is the [Configuration](https://zudo-doc.takazudomodular.com/docs/guides/configuration) reference, which mirrors the `ZudoDocConfig` type's own JSDoc.

## Rung 3 — Theme packs

Before hand-tuning tokens, check whether a bundled **theme pack** already gets you the look. A pack is a prebuilt design bundle — tokens, fonts, component details — applied with one config field or one CLI command:

```ts title="zfb.config.ts"
zudoDoc({
  siteName: "My Docs",
  themePack: "foundry",
});
```

```sh
pnpm exec zudo-doc theme apply foundry
```

Every pack defines both its light and dark values, so readers can keep choosing Light, Dark, or System from the appearance menu on any pack. System follows the device independently of the active pack. Add `themePackSwitcher: true` to let readers switch packs live from a bottom-right flyout.

**Reach for it when:** you want a different overall look and a bundled pack matches it.

**Limit:** you pick from installed packs as-is; adjusting individual tokens on top is the next rung. See [Theme Packs](https://zudo-doc.takazudomodular.com/docs/reference/theme-packs).

## Rung 4 — Token overrides

The scaffold's `src/styles/global.css` is a short `@import` chain that pulls the package's `theme.css` (all the `:root` design tokens), `content.css`, and `features.css` — followed by an empty `:root { … }` block reserved for you. Because it comes **after** the package imports, anything you redefine there wins the cascade:

```css title="src/styles/global.css"
/* ...package @imports above... */

:root {
  --color-accent: oklch(0.6 0.2 250);
  --font-sans: "Inter", system-ui, sans-serif;
  --z-index-modal: 200; /* override a single default z-index tier */
}
```

For per-component rebranding without touching every element that reads a global token, the package also exposes `--zdc-*` component tokens (heading font, prose font, card radius, TOC width, …). Set them once in `:root`.

**Reach for it when:** the change is a re-theme — colors, spacing, typography, radii.

**Limit:** tokens restyle; they do not restructure markup. See [Design System](https://zudo-doc.takazudomodular.com/docs/reference/design-system), [Color](https://zudo-doc.takazudomodular.com/docs/reference/color), and [Component Tokens](https://zudo-doc.takazudomodular.com/docs/reference/component-tokens).

If your project also merges a second design system’s `:root`, read [Coexisting with another `@theme`](https://zudo-doc.takazudomodular.com/docs/reference/design-system#coexisting-with-another-theme).

## Rung 5 — Ejecting a component

When no token can reach the change — the markup itself is wrong for you — eject a **content-layer** component that your project re-references (for example through `mdxExtras`). Ejection copies its source out of the package into your project and rewrites matching host imports to the local copy:

```sh
# eject a component (run via your package manager's bin runner)
pnpm exec zudo-doc eject details
# → copies source into your project and rewrites imports to resolve locally

# see the full list
pnpm exec zudo-doc --help
```

The 18 ejectable components include two different scopes:

- **Content-layer:** `tab-item`, `content-admonition`, `code-group`, and `details`. Re-reference these from project-owned bindings or MDX component registration when you need the local copy to render.
- **Primary chrome:** `header`, `footer`, `breadcrumb`, `toc`, `sidebar`, and `doc-pager`. Bind the local copy to the matching `chromeBindings.Header`, `Footer`, `Breadcrumb`, `Toc`, `Sidebar`, or `DocPager` slot.
- **Nested chrome:** `theme-toggle`, `page-loading`, `sidebar-tree-island`, `sidebar-toggle-island`, `desktop-sidebar-toggle-island`, `image-enlarge`, `doc-history`, and `site-tree-nav-island`. These need the exact owner-level binding named by the CLI warning; copying a nested file alone does not replace its package-owned parent.

For content-layer components, wire the local import into the host code that renders it, then edit the local copy. The [custom components guide](https://zudo-doc.takazudomodular.com/docs/guides/custom-components#global-registration-mdxextras) shows the bindings and route-stub setup required for `mdxExtras`.

[Dynamic content unavailable in text export]

Eject writes provenance into a `.zudo-doc.json` file recording which components you own. That file is **lazy-created on the first eject** — a fresh scaffold does not ship it, so an un-ejected project never carries an unexplained config file. To revert an eject, first restore every rewritten or manually added local import to its original `@takazudo/zudo-doc/...` package import; then remove the component directory and its `.zudo-doc.json` entry. Removing the directory first breaks those imports.



**Limit:** an ejected copy no longer receives package updates for that component — you take on its maintenance. The CLI classifies each eject statically: primary and nested chrome copies warn loudly until a supported binding is present, while content-layer copies keep quiet manual `mdxExtras` guidance. Ejection copies source; `chromeBindingsModule` is the supported seam that makes a primary or owner-level replacement render.

**A different kind of eject — `zudo-doc eject logo`.** This subcommand shares the `eject` name but is not a component-source copy like the targets above: it never touches `.zudo-doc.json` provenance or a component directory. It is an **asset materialization** — it renders the generated auto-logo to a real SVG file and rewires the [`logo`](https://zudo-doc.takazudomodular.com/docs/guides/configuration#identity-and-urls-logo) config field to point at it, so you end up owning an editable file instead of the request-time generated default.

```sh
pnpm exec zudo-doc eject logo
```

This writes `public/img/logo.svg` — the mark is expressed as an internal SVG luminance mask, so it stays theme-adaptive through the same CSS-mask hero path as any custom `logo` asset — and rewrites `logo` to `"/img/logo.svg"` in `zfb.config.ts`.

- `--seed <name>` — override the `siteName` seed used to render the mark. Required when `zfb.config.ts` is not in the canonical scaffold shape (for example `zudoDoc({ ...settings })`), since the seed cannot be read statically in that case.
- `--force` — overwrite an existing `public/img/logo.svg`. Without it, re-running the command **refuses cleanly** rather than silently overwriting a file you may have hand-edited.

[Dynamic content unavailable in text export]

`--seed` only supplies the seed — it does not make the config rewrite possible. On a non-canonical config the command still writes the SVG, then reports that it could not update `zfb.config.ts`, prints the line to add, and **exits nonzero**. Finish it by hand:

```ts title="zfb.config.ts"
export default zudoDoc({
  ...settings,
  logo: "/img/logo.svg",
});
```

The CLI only rewrites a literal, hand-editable field list, so it refuses rather than guess where the field belongs in a spread-controlled object.



## Rung 6 — `chromeBindingsModule` (host-callables)

Under `packageOwnedRoutes: true` (the default) the doc chrome is wired inside the package, so there is no host file to render project-specific content into. `chromeBindingsModule` is the seam that gives it back: point it at a host module that exports a `chromeBindings` object, and the injected routes pick up your slots — a content-header renderer, home-hero extras, custom frontmatter-preview renderers, a footer tag loader, and more.

```ts title="zfb.config.ts"
export default defineConfig(
  zudoDoc({
    siteName: "My Docs",
    chromeBindingsModule: "./src/chrome-bindings.tsx",
  }),
);
```

```tsx title="src/chrome-bindings.tsx"
import { defineChromeBindings } from "@takazudo/zudo-doc/chrome-bindings";

export const chromeBindings = defineChromeBindings({
  docContentHeaderExtras: ({ entry }) =>
    entry.data.tier === "core" ? <span class="...">Core</span> : null,
});
```

Fresh scaffold document-route stubs already import `virtual:zudo-doc-chrome-bindings` and pass it to `createChrome`, including the locale and doc-history shapes. Add the config field and module; do not fork the route stub. Hand-authored routes must pass the same object as `createChrome(routeCtx, chromeBindings)`'s second argument. The [custom components guide](https://zudo-doc.takazudomodular.com/docs/guides/custom-components#global-registration-mdxextras) shows the complete pattern.

The Design Token Panel has a parallel channel, `designTokenPanelConfigModule`: the package-default panel works with zero config (just `designTokenPanel: true`), and to fully customize it you point that setting at a host module exporting `buildDesignTokenPanelConfig(mode)`. Both channels are documented in full — including their loud missing-file behavior — on [Host Chrome Bindings](https://zudo-doc.takazudomodular.com/docs/reference/host-chrome-bindings).

## Rung 7 — Your own `pages/*.tsx`

If a whole route needs to be yours, add the `.tsx` file to `pages/` at the matching path. **A project-owned page always wins over the package-injected route for the same path** — the host file shadows the injection. This is how you add an entirely custom landing route or take over a specific injected doc path.

The scaffold stubs are special. `pages/index.tsx` shadows nothing: `/` is never injected, so it is a one-line re-export that provides the package home page. `pages/docs/[[...slug]].tsx` does shadow the injected doc route, but it remains an explicit host-owned seam rather than an optional customization example. zfb 2.13.1 renders injected dynamic routes in both dev and build; the kept stub preserves host ownership and customization. See [Routing Conventions](https://zudo-doc.takazudomodular.com/docs/develop/routing-conventions#package-route-injection-and-shadowing) for the complete injection and shadowing model.

## Rung 8 — The deploy path (an explicit extend step)

The minimal scaffold builds a **pure static site** — `pnpm build` emits `dist/`, which you can drop on any static host. It ships **no adapter and no `wrangler.toml`** by design. Going beyond a static export is a deliberate extend step:

1. **Add a deploy adapter.** For Cloudflare Workers, set the `adapter` shell field:

   ```ts title="zfb.config.ts"
   zudoDoc({
     siteName: "My Docs",
     adapter: "@takazudo/zfb-adapter-cloudflare",
   });
   ```

   An adapter is **required** for any route that opts out of prerendering (`prerender = false`) — which is exactly what the AI assistant endpoint does.

2. **Add `wrangler.toml`.** Declare the Worker name, the custom domain route, and any bindings. This file does not exist in a fresh scaffold — you author it when you decide to deploy.

3. **Wire the AI assistant, if you enable it.** `aiAssistant: true` mounts a package-owned SSR `/api/ai-chat` seam. It returns the fixed demo response while demo mode is enabled and HTTP 501 if live mode is selected without a host implementation. The package/scaffold does **not** ship the showcase's full handler, `worker-entry.ts`, or `AiChatDailySpendCap`. For a real, Claude-backed assistant you must first implement a host-owned `/api/ai-chat` handler and source Worker entry/Durable Object equivalent to the showcase, then:
   - set `aiChatDemoMode: false` after that host handler is installed;
   - create a `RATE_LIMIT` KV namespace (`wrangler kv namespace create RATE_LIMIT`) and bind it in `wrangler.toml`;
   - bind `AI_CHAT_DAILY_SPEND_CAP` to `AiChatDailySpendCap` and add migration tag
     `v1-ai-chat-daily-spend-cap` with `new_sqlite_classes = ["AiChatDailySpendCap"]`;
   - add the `ANTHROPIC_API_KEY` secret (`wrangler secret put ANTHROPIC_API_KEY`);
   - set `aiChatAllowedOrigins` to your site origin.

See the showcase [Deployment guide](https://zudo-doc.takazudomodular.com/docs/guides/deployment) and [AI Assistant API reference](https://zudo-doc.takazudomodular.com/docs/reference/ai-assistant-api) as a host-implementation reference (secrets, soft per-IP KV, exact UTC-day admission, CORS, and preview limitations). Binding the named class without supplying its source is not sufficient.

## Rung 9 — Restoring pre-push and HTML validation

The minimal scaffold **drops the validation gates** the showcase runs — they were dead weight for a small doc set. If your project grows to want them back, add them explicitly:

- **Pre-push validation (`b4push`).** The scaffold no longer ships `scripts/run-b4push.sh` or a `b4push` package script. To restore a before-push suite, add your own script (format → typecheck → build → link check) and wire it as a `package.json` script. The showcase's `scripts/run-b4push.sh` is a reference implementation.
- **HTML validation.** The generated `package.json` no longer includes the `check:html` / `html-validate` step (nor `.htmlvalidate.json`). To validate built HTML, add `html-validate` as a dev dependency, restore a `.htmlvalidate.json`, and add a `check:html` script that runs it over `dist/`.

[Dynamic content unavailable in text export]

The `gen:z-index` / `check:z-index` codegen was likewise removed from generated projects — the 13 default z-index tiers now ship unconditionally from `@takazudo/zudo-doc/theme.css`, so a project only re-adds that codegen if it maintains a *custom* tier set. Overriding a single tier is a one-line `:root` change (rung 4), not a codegen concern.



## See also

- [Configuration](https://zudo-doc.takazudomodular.com/docs/guides/configuration) — every `zudoDoc()` field and its default
- [Theme Packs](https://zudo-doc.takazudomodular.com/docs/reference/theme-packs) — the bundled packs, the switcher UI, and the theme CLI
- [Custom Components](https://zudo-doc.takazudomodular.com/docs/guides/custom-components) — register project-owned MDX components through the shared bindings module
- [Host Chrome Bindings](https://zudo-doc.takazudomodular.com/docs/reference/host-chrome-bindings) — `chromeBindingsModule` and `designTokenPanelConfigModule` in depth
- [create-zudo-doc CLI](https://zudo-doc.takazudomodular.com/docs/reference/create-zudo-doc) — what the scaffold emits