zudo-doc
GitHub repository

Type to search...

to open search from anywhere

Customizing zudo-doc

Created Jun 29, 2026Updated Jul 11, 2026Takeshi Takatsudo

The minimal→extend escalation ladder — from a one-line config field to full source control, in the order you reach for them.

A scaffolded zudo-doc project is intentionally minimal: one config file, your content, a stylesheet, and two thin route stubs (see Installation). 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

RungReach for it whenCost
1. buildDocsSchema overrideYou need a custom, validated frontmatter keyReplace one function; own the schema
2. Config fieldsToggling a feature or tuning behaviorNone — it is just a field
3. Token overridesRe-theming colors, spacing, typographyA @theme block in global.css
4. zudo-doc ejectA component's markup itself must changeYou own the ejected copy
5. chromeBindingsModuleInjecting host content/callables into injected routesOne host module
6. Your own pages/*.tsxA whole route must be yoursYou own that route
7. Deploy pathGoing live (especially with SSR features)Adapter + wrangler.toml + secrets
8. Restoring pre-push / HTML checksYou want the showcase's validation gatesCopy 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:

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 (rung 5) to actually render a badge from that key.

Note

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.


Rung 2 — Config fields

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

zfb.config.ts
export default defineConfig(
  zudoDoc({
    siteName: "My Docs",
    docHistory: true,
    sidebarToggle: 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 rung 3 or 4.

The full field list — with the default for every one — is the Configuration reference, which mirrors the ZudoDocConfig type's own JSDoc.


Rung 3 — Token overrides

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

src/styles/global.css
/* ...package @imports above... */

@theme {
  --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, Color, and Component Tokens.


Rung 4 — Ejecting a component

When no token can reach the change — the markup itself is wrong for you — eject the component. Ejection copies its source out of the package into your project so you own the file directly:

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

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

The 18 ejectable components:

header, footer, breadcrumb, toc, sidebar, theme-toggle, page-loading, tab-item, doc-pager, content-admonition, code-group, details, sidebar-tree-island, sidebar-toggle-island, desktop-sidebar-toggle-island, image-enlarge, doc-history, site-tree-nav-island

After ejecting, edit the local copy — changes take effect immediately under pnpm dev.

Note

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. Remove a component's directory and its .zudo-doc.json entry to go back to the package version.

Limit: an ejected copy no longer receives package updates for that component — you take on its maintenance.


Rung 5 — 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.

zfb.config.ts
export default defineConfig(
  zudoDoc({
    siteName: "My Docs",
    chromeBindingsModule: "./src/chrome-bindings.tsx",
  }),
);
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,
});

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.


Rung 6 — 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 replace the home page, add an entirely custom landing route, or take over a specific doc path.

The two scaffold stubs (pages/index.tsx, pages/docs/[[...slug]].tsx) are themselves ordinary host pages using this precedence; you can expand them or add siblings. See Routing Conventions for how routes are enumerated and shadowed.


Rung 7 — The deploy path (an explicit extend step)

The minimal scaffold builds a pure static sitepnpm 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:

    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 an SSR /api/ai-chat route. It ships in demo mode by default (aiChatDemoMode defaults to false in config, but the scaffold's chat is a static demo until you flip it to a live backend). For a real, Claude-backed assistant you must:

    • set aiChatDemoMode: false;

    • create a RATE_LIMIT KV namespace (wrangler kv namespace create RATE_LIMIT) and bind it in wrangler.toml;

    • add the ANTHROPIC_API_KEY secret (wrangler secret put ANTHROPIC_API_KEY);

    • set aiChatAllowedOrigins to your site origin.

See the Deployment guide and the AI Assistant API reference for the full runbook (secrets, KV, CORS, rate limiting).


Rung 8 — 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/.

Note

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 @theme change (rung 3), not a codegen concern.


See also

Revision History

Takeshi TakatsudoCreated: 2026-06-30T06:02:03+09:00Updated: 2026-07-11T19:10:50Z

AI Assistant

Ask a question about the documentation.