zudo-doc
GitHub repository

Type to search...

to open search from anywhere

Configuration

Created Mar 13, 2026Updated Oct 9, 2026Takeshi Takatsudo

The single zudoDoc() config reference — every field and its default.

A zudo-doc project is configured through one file: the root zfb.config.ts. It calls zudoDoc() with the fields you want to change; every other setting falls back to a documented default.

zfb.config.ts
import { defineConfig } from "zfb/config";
import { zudoDoc } from "@takazudo/zudo-doc/config";

export default defineConfig(
  zudoDoc({
    siteName: "My Docs",
    // …only the fields you chose; everything has a documented @default.
  }),
);

zudoDoc() merges your fields over the package defaults per field (you win), supplies the package data defaults (frontmatter schema, directive vocabulary, translations, color schemes, tag vocabulary) unless you override them, and returns a complete ZfbConfig — you spread nothing.

Note

The ZudoDocConfig type is the single source of truth for this reference. Every field carries a @default JSDoc annotation that shows on IDE hover; the tables below mirror those defaults verbatim. When this page and the type disagree, the type wins.

Info

The defaults here are the package defaults a fresh create-zudo-doc project inherits. This showcase enables far more (most features on, i18n, a full header nav) by passing those fields explicitly — do not read the showcase's rich config as the default baseline.

Home page

home

Homepage layout and optional Markdown introduction, applied to / and locale-prefixed homes such as /ja/.

Default: { wide: false, introMarkdown: "", sitemapHeading: "" }

FieldTypePurpose
widebooleanWiden the outer page container for a larger category grid; defaults to false.
introMarkdownstringOptional prose below the logo, title, short tagline and navigation.
sitemapHeadingstringPlain-text heading above the existing documentation tree. Blank uses the translated default.

Use siteDescription for the short tagline and existing metadata uses; locales.<code>.description overrides the tagline. Use introMarkdown for longer prose. It does not replace metadata or the tagline. Keep homeExtras / extras for inline navigation additions in the slash-separated links row; they are not a prose slot.

Markdown-first example

Edit zfb.config.ts; the scaffold's root page can keep its one-line package re-export. The introduction and heading are serializable strings, so a configuration editor can store them without callbacks or executable JavaScript. Fresh scaffolds leave the introduction empty; this showcase's example content is not copied into new projects.

zfb.config.ts
import { defineConfig } from "zfb/config";
import { zudoDoc } from "@takazudo/zudo-doc/config";

export default defineConfig(
  zudoDoc({
    siteName: "My Docs",
    siteDescription: "A short guide to our project.",
    home: {
      wide: false,
      introMarkdown: `## Start here

Learn the **essentials**, then explore the reference.

- [Getting started](docs/getting-started/)
- [Configuration](docs/guides/configuration/)

> [!TIP]
> Keep the introduction focused on your readers' first steps.`,
      sitemapHeading: "Browse the guides",
    },
    locales: {
      ja: {
        label: "JA",
        dir: "src/content/docs-ja",
        description: "プロジェクトの使い方をまとめたガイド。",
        introMarkdown: `## はじめに

**基本的な使い方**を学んでから、リファレンスへ進めます。

- [はじめに](docs/getting-started/)
- [設定](docs/guides/configuration/)

> [!TIP]
> 読者が最初に必要とする情報に絞って紹介しましょう。`,
        sitemapHeading: "ガイドを探す",
      },
    },
  }),
);

Locale precedence and no-description example

Each defined locales.<code>.introMarkdown or sitemapHeading overrides its corresponding home field. Omit a locale field to inherit the home value. An explicit empty introduction disables fallback; whitespace-only content also renders nothing. These rules are independent of the locale's short description.

A blank resolved sitemapHeading restores the translated default: “Explore the documentation” in English and “ドキュメントを探す” in Japanese. It does not hide the heading. A blank locale heading restores that locale's default even when the global heading is customized.

For a home without long description prose, use this configuration. The logo, title, tagline and navigation remain; there is no prose wrapper or upper separator, and exactly one separator appears before the independent sitemap H2. To hide the short tagline too, clear siteDescription and any locale description separately.

home: { introMarkdown: "", sitemapHeading: "" },
locales: {
  ja: {
    label: "JA",
    dir: "src/content/docs-ja",
    introMarkdown: "",
    sitemapHeading: "",
  },
},

Supported Markdown and safe restrictions

The introduction uses the standard Markdown/GFM pipeline and shared non-heading typography, with the following field-specific limits. See Markdown features for detail-page examples; executable MDX components shown there are not accepted in this string.

ContentHomepage introduction behavior
Paragraphs, emphasis, bold, deletion and line breaksSupported.
Ordered, unordered, nested and task listsSupported; task checkboxes retain their checked/disabled state.
Links, reference links and imagesSupported with safe URLs; images are responsive. No filesystem dimension lookup or asset-viewer remapping.
Quotes and GitHub alertsSupported; use > [!TIP] and other GitHub alert markers.
Inline and fenced codeSupported with production semantic highlighting; code examples stay inert and wide fences scroll internally.
Tables and thematic breaksSupported; table alignment is retained and wide tables scroll internally.
RubyThe non-executable {base}^{reading} syntax is supported.
MermaidDiagram fences use the existing Mermaid initialization when mermaid: true; otherwise they remain ordinary code fences.
MathDollar notation stays literal, including with math: true; JSX MathBlock is unavailable here.
Directives, transclusion and filesystem includesRejected even when enabled for document pages; the introduction has no document source path.
Raw HTML, JSX, MDX expressions, import/export and YAML frontmatterRejected with diagnostics, never executed. Inline code and fences may display them as examples; ordinary non-MDX braces stay inert text. Initial paragraph import/export statements are reserved.
Code tabs, advanced fence enrichment, heading markers and reading-time/TOC exportsNot enabled; ordinary fences are supported without promising component parity for advanced metadata.

Invalid source, unsafe URLs or unsupported generated markup fail the build with a home.introMarkdown diagnostic. Nonblank introductions require the @takazudo/zfb-md-wasm peer used by the server/build renderer; blank introductions do not load it.

Author homepage URLs, not source-file links: unlike links inside MDX documents, .md and .mdx extensions are not rewritten. Relative URLs resolve against the active localized homepage with a trailing slash. For example, with base /manual/ on the Japanese homepage, docs/getting-started/ resolves to /manual/ja/docs/getting-started/, while /docs/getting-started/ resolves to /manual/docs/getting-started/. Root-relative URLs receive the base once and preserve explicit locale prefixes. Use /img/logo.svg for a shared root asset; img/logo.svg targets the active locale directory.

HTTP and HTTPS links/images are allowed, and links also allow mailto: and tel:. Fragments stay fragments, spaces in paths are encoded, and .. follows normal URL resolution and can leave the base. Protocol-relative URLs, data:, javascript:, vbscript:, control characters and backslashes are rejected.

Compact layout and headings

Identity and prose share a centered, fixed 60rem maximum width (960 CSS px at the standard 16px root size), shrinking to the available space and respecting text-size changes. There is no introduction width option. The logo stays 320 CSS px with a 1200:630 aspect ratio and a 100% maximum width. Desktop places the logo left of left-aligned identity text; mobile stacks and centers the identity. Prose stays left-aligned, and long titles, taglines and links wrap.

With an introduction, full content-width separators surround the padded prose, followed by a separate sitemap H2 and the existing documentation tree. When tags are enabled, the same separator precedes the Tags heading. The separators follow the outer page container, not the 60rem inner cap. home.wide: true still widens that outer container and category grid; its behavior is unchanged.

Compared with detail-page headings, compact headings are plain bold text without borders, gradients or hash decorations. H1 Markdown becomes H2 to retain one page H1, and introduction headings never enter the documentation TOC. H2 shares the home section-heading style used by the sitemap and Tags headings — text-title (1.4rem by default) at line-height 1.25, with any theme-pack H2 rules, counters or bars removed; H3 uses 1.125rem, H4 1rem, and H5/H6 0.9375rem; body text uses 1rem/1.75. Normal flow gaps are 1rem and heading-adjacent gaps 0.5rem, with section gaps of 1.75rem for H2, 1.5rem for H3, and 1.25rem below that. The first child has no top gap.

siteTreeNavIgnore

Top-level category slugs to hide from the package-owned home-page category grid on / and locale-prefixed home pages such as /ja, and from the <SiteTreeNav /> / <SiteTreeNavDemo /> MDX tags. This does not affect header nav, sidebar, search, or sitemap.

zfb.config.ts
export default defineConfig(
  zudoDoc({
    siteName: "My Docs",
    siteTreeNavIgnore: ["inbox", "develop"],
  }),
);

The package previously hard-coded ["inbox", "develop"] for this showcase's own need. The package default is now []; set this field when your site needs to hide categories.

Type: string[] · Default: []

siteTreeNavSecondary

Top-level category slugs to move out of the package-owned home-page category grid on / and locale-prefixed home pages such as /ja, and into a compact secondary row directly under it. Each listed category renders as a small chevron link with its label, and the row follows the array order rather than the grid's category order. Use it for system-level categories that should stay reachable from the home page without competing with the primary reading paths.

zfb.config.ts
export default defineConfig(
  zudoDoc({
    siteName: "My Docs",
    siteTreeNavSecondary: ["changelog", "claude", "codex"],
  }),
);
  • A slug that matches no top-level category is skipped.

  • A slug that is also listed in siteTreeNavIgnore is hidden entirely — siteTreeNavIgnore wins.

  • A category without its own page (category_no_page: true) has no link target, so it stays in the grid and a development warning is logged.

  • An empty list renders no secondary row.

This affects only the home page. The <SiteTreeNav /> / <SiteTreeNavDemo /> MDX tags ignore it, and header nav, sidebar, search, and sitemap are unchanged.

Type: string[] · Default: []

Identity and URLs

siteName

Site name shown in the header and metadata. Page titles render as {page title} | {siteName}. The one field you almost always set.

Default: "Docs"

siteDescription

Site description used in metadata (<meta name="description">, llms.txt).

Default: ""

Home-hero logo. "auto" (the default) renders a generated deterministic SVG seeded by siteName — a framed "decorated plate" mark that adapts to light/dark automatically, so a fresh project gets a presentable hero with no asset work. Set a path string (e.g. "/img/logo.svg") to render your own asset as a theme-adaptive CSS mask, or false to hide the logo block entirely.

Run npx zudo-doc eject logo to materialize the generated mark as a real file: it writes public/img/logo.svg and flips this field to "/img/logo.svg", so you own an editable asset — tweak it, replace it, or reuse it elsewhere (e.g. for OGP) — instead of the request-time generated default. See Customizing → Rung 5 for the CLI flags.

logo: "" throws a TypeError at config resolution instead of silently rendering a CSS mask of an empty path — pass false to hide the logo, or omit the field for the default.

Default: "auto"

favicon

The <link rel="icon"> set emitted into every page's <head>. Omit it and you get the four-file convention a fresh create-zudo-doc project ships in public/: favicon.svg, favicon.ico (sizes="any"), favicon-32x32.png, favicon-16x16.png, in that order.

Type: string | FaviconConfig | false · Default: undefined (the four-link set above)

ValueWhat it emits
omittedThe four-link default
"auto"One inline SVG data: URL icon generated from siteName — no asset files needed
any other stringOne link to that path, with type inferred from the file extension
a FaviconConfig objectOnly the slots you supply, always in svg → ico → png32 → png16 order
falseNo favicon links at all
""Throws a TypeError at config resolution — see below

The empty string is rejected, not emitted. Per the HTML spec an empty href resolves to the current document, so favicon: "" would silently make every page fetch its own HTML as a "favicon" — a wasted request and a garbage tab icon. zudoDoc({ favicon: "" }) throws a TypeError naming the field instead; pass false for no favicon links, or omit the field for the default four-link set. The same rejection applies to an empty-string FaviconConfig slot (e.g. favicon: { ico: "" }) — the error names the specific slot (favicon.ico). An empty object (favicon: {}, no slots at all) is unaffected and still validly emits nothing. Only the exact empty string is rejected — a whitespace-only value like " " is passed through as-is.

The default is a convention, not a check. It emits all four links whether or not the files exist — so a project whose public/ has no favicon.ico still ships <link rel="icon" href="/favicon.ico" sizes="any"> on every page, and every visitor's browser console logs a 404 for it. Supplying only the slots you actually have is the fix:

zfb.config.ts
export default defineConfig(
  zudoDoc({
    siteName: "My Docs",
    // No favicon.ico in public/, so no ico link — and no console 404.
    favicon: {
      svg: "/favicon.svg",
      png32: "/favicon-32x32.png",
      png16: "/favicon-16x16.png",
    },
  }),
);

FaviconConfig is { svg?: string; png32?: string; png16?: string; ico?: string }. Every key is optional, the order you write them in does not matter (emission order is fixed), and {} emits nothing at all — the same result as false.

A plain string collapses the set to a single link and infers type from the extension: .svg, .png, .ico, .jpg / .jpeg, .gif, .webp, .avif. Anything else omits type and lets the browser sniff. A ?query or #hash suffix is ignored when inferring, so "/icon.svg?v=2" still gets type="image/svg+xml".

favicon: "/icon.png", // one link, type="image/png"
favicon: false,       // no favicon links at all

favicon: "auto" needs no asset files at all: it emits one SVG favicon as an inline data: URL, generated deterministically from siteName with the same seeded glyph as logo: "auto", so the hero logo and the browser-tab icon show the same mark. The favicon variant is a square, opaque, fixed light-palette plate — a tab icon cannot follow the page's color mode the way the masked hero logo does.

Note

SVG favicon support is still uneven across browsers (Safari most notably). "auto" degrades to the browser's default blank icon rather than a broken one, but a project that wants maximal coverage should ship real .ico / .png files and use the object form.

Href values starting with / are prefixed with the configured base, so "/favicon.svg" resolves to /pj/my-site/favicon.svg on a sub-path deployment. Anything else — an absolute https://… URL, a data: URL — is emitted verbatim.

Warning

head.alternateLinks cannot replace this set. Those entries render after the favicon block, so a { rel: "icon" } entry there only appends one more link — it can never remove or override one of these. favicon is the only way to change what the set contains.

siteUrl

Canonical site origin (e.g. "https://example.com") for sitemap / canonical / og:url. Empty = not set.

Default: ""

base

Public URL sub-path prefix mounted in front of every absolute asset URL (e.g. "/pj/my-site/"). "/" = root-mounted. All internal links (sidebar, nav, prev/next, search) are prefixed automatically.

Default: "/"

Note

Inline markdown links in MDX (e.g. [text](/docs/some-page)) are not rewritten for a non-root base — use relative links in content instead.

trailingSlash

Append a trailing / to extensionless internal hrefs.

Default: false

See Trailing-Slash Policy for how the rule splits between build time and the deployment host.

minifyHtml

Minify production HTML output from zfb build. Set false for readable output while debugging.

Default: true

githubUrl

GitHub repository URL shown in the header (via the github-link header item), or false to omit.

Type: string | false · Default: false

editUrl

"Edit this page" link base, or false to omit. The full URL is editUrl + contentDir + "/" + entryId.

Type: string | false · Default: false

noindex

Add noindex,nofollow to every page (for internal docs).

Default: false

See Avoid Robots Indexing.

head

Site-wide custom <head> extras (preconnect / preload / stylesheets / meta / alternateLinks). Omit to emit nothing extra.

Type: SiteHeadConfig · Default: undefined

metaTags

<meta> / OpenGraph / Twitter-card emission toggles.

Default: { description: true, keywords: false, ogImage: false, ogSiteName: true, twitterCard: false }

PropertyTypeDescription
descriptionbooleanEmit <meta name="description">
keywordsstring | false<meta name="keywords"> value, or omit
ogImagestring | falsePath for og:image / twitter:image, or omit
ogSiteNamebooleanEmit og:site_name
twitterCard"summary" | "summary_large_image" | falseTwitter Card type, or omit the block
twitterSitestring (optional)twitter:site handle
twitterCreatorstring (optional)twitter:creator handle

sitemap

Emit a sitemap route.

Default: false

Set sitemap: true if you want one. At the default false, no /sitemap.xml route is emitted at all — the URL 404s. Setting siteUrl is not enough on its own: configuring siteUrl only for canonical / og:url is a perfectly normal setup, so nothing is inferred from it and no build-time warning nudges you. This reference is where that contract lives.

Serving nothing is deliberate. An empty <urlset> at a well-known URL is not a neutral placeholder — it is a positive assertion to crawlers that the site has no indexable URLs, which is worse than the URL simply not existing. The generated robots.txt already drops its Sitemap: line in this state, so the two artifacts now agree instead of contradicting each other. Earlier versions did emit the empty <urlset>; they no longer do.

The gate lives in the package's route injection (packageOwnedRoutes, on by default). A project that hand-wires its own pages/sitemap.xml.tsx sits outside it and keeps whatever that page returns — and if that page re-exports the package entrypoint while sitemap is false, the build warns and still writes the empty <urlset>.

See also the SEO guide for how metaTags and sitemap combine into a complete social-share + search-indexing setup.

What to do when a .md/.mdx link cannot be resolved: "warn" logs and continues, "error" fails the build, "ignore" is silent.

Type: "warn" | "error" | "ignore" · Default: "warn"

This setting also governs the post-build existence check for raw <img src> attributes in rendered HTML. "error" reports problems and fails the build, "warn" reports and continues, and "ignore" skips the scan entirely.

The scan checks site-absolute paths beginning with /. It strips queries and fragments, decodes URL escapes, and checks for a file in the build output. With base: "/manual/", /manual/img/logo.svg maps to dist/img/logo.svg. Paths outside that base, missing files, and paths escaping the output directory are reported as broken.

External URLs, protocol-relative // URLs, schemes such as data:, and relative paths are excluded. Comments, script strings, and escaped code examples are not treated as images. srcset is not checked.

Color

colorScheme

Active color scheme name (must exist in colorSchemes). The two bundled schemes are Default Light and Default Dark.

Default: "Default Dark"

colorMode

Light/dark mode wiring, or false for a single fixed scheme.

Default: { defaultMode: "dark", lightScheme: "Default Light", darkScheme: "Default Dark", respectPrefersColorScheme: true }

PropertyTypeDescription
defaultMode"light" | "dark"Light/dark fallback when system following is disabled and no choice is saved
lightSchemestringScheme used in light mode
darkSchemestringScheme used in dark mode
respectPrefersColorSchemebooleanUse System as the first-visit choice when no preference is saved

With the default respectPrefersColorScheme: true, a first visit with no saved choice starts in System and follows the device's light/dark preference. The compact header appearance menu offers Light, Dark, and System; its icon shows the selected preference (sun for Light, moon for Dark, monitor for System), not the next action. The preference is saved in localStorage under zudo-doc-theme and remains selected after reload.

defaultMode remains a two-value light/dark fallback. If you set respectPrefersColorScheme: false, a first visit with no saved choice uses defaultMode; a visitor can still explicitly select System, which continues to follow the device. colorMode: false keeps a single fixed scheme and has no appearance menu.

colorMode: false, // single fixed scheme (colorScheme only)

See the Color reference for the ramp-native model and adding a custom scheme (via the colorSchemes escape hatch below).

Theme packs

Theme packs are installable design bundles layered on top of the color-scheme system above — every pack defines both its light and dark values, so the appearance menu keeps working on any pack. See the Theme Packs reference for the switcher UI, the theme CLI, and pack authoring.

themePack

Active theme pack slug. "default" is the stock zudo-doc look (no pack stylesheet loaded). Must be a member of the resolved themePacks list; an unknown slug fails the build loudly.

Default: "default"

themePackSwitcher

Mount the bottom-right theme-pack switcher flyout (and its browse-all dialog) on every page.

Default: false

themePacks

Enabled pack slugs, in switcher order — the list order is the switcher's Prev/Next cycle order and the browse-all grid order. undefined enables all bundled packs: "default" first, then the rest alphabetically. An explicit list is authoritative — it may omit "default" and reorder freely; duplicates or unknown slugs fail the build loudly.

Default: undefined

themePacks: ["default", "foundry"],

Content and i18n

docsDir

Directory (project-root-relative) holding the default-locale docs.

Default: "src/content/docs"

entryDocSlug

Route slug of the doc page the versions page links to as the "latest docs" entry point (and each past version's docs link). No leading or trailing slashes — e.g. "getting-started" or "overview/getting-started". Not validated at runtime; an invalid slug simply 404s.

Default: "getting-started"

dateFormat

Date-display format. "locale" keeps the built-in Intl rendering; anything else is a token pattern.

Type: DateFormatSetting · Default: "locale"

zudo-doc renders dates in five distinct shapes, so the setting is keyed by shape — called a role — rather than by call site. Nobody wants the pager date to differ from the header date, but a compact card stamp genuinely differs from a full date line.

RoleDefault (en)Default (ja)Where it appears
fullAug 22, 20262026年8月22日H1 date line, git metainfo, pager, asset page, revision history, note-tray list and timeline, ungrouped site-tree rows
monthDayAug 228月22日Note-tray card grid
year20262026年Note-tray card grid, year-group headings
yearMonth2026 August2026年8月Note-tray, sidebar and site-tree month-group headings
numericMonthDay08-2208-22Sidebar tray rows, site-tree grouped rows

numericMonthDay is deliberately locale-insensitive at its default: it is a compact sort-aligned stamp, not prose, so it renders 08-22 in every locale. Give it a pattern and it formats like any other role.

The `year` role has two surfaces that differ at the default

Year-group headings in the sidebar and the site tree print the grouping key itself — 2026 in every locale — because a year heading has never gone through a formatter, and routing it through one would silently turn existing Japanese sites into 2026年. The note-tray card grid does format its year, so a Japanese page shows 2026年 there. Set year to a pattern and both surfaces follow it; headings apply the pattern against January 1 of that year, so month or day tokens in a year pattern render that boundary date.

A bare string sets only full

This is the setting's one real surprise. A bare pattern is shorthand for { full: "…" }, so it changes the H1 date line, the pager and the other full surfaces — and leaves card stamps, month headings and sidebar rows exactly as they were:

dateFormat: "YYYY/MM/DD",

To change those too, name their roles.

Roles and per-locale overrides

The object form takes the five role keys plus an optional locales map keyed by locale code, whose values take the same five keys:

dateFormat: {
  full: "MM/DD/YYYY",
  yearMonth: "MM/YYYY",
  locales: {
    ja: { full: "YYYY年M月D日" },
  },
},

Resolution precedence, evaluated per role:

  1. locales[locale][role] — the per-locale override

  2. [role] — the top-level role

  3. "locale" — the built-in Intl rendering

Per-locale overrides are the point of the object form on a multilingual site: Aug 22, 2026 and 2026年8月22日 are both correct for their locale, and no single pattern serves both. See Internationalization.

Supported tokens

TokenMeaning2026-08-05 (en)
YYYY4-digit year2026
YY2-digit year26
MMMMFull month name (locale-aware)August
MMMShort month name (locale-aware)Aug
MMZero-padded month08
MMonth8
DDZero-padded day05
DDay5
[…]Literal escape — [at] YYYYat 2026

Matching rules worth knowing:

  • Anything that is not a token passes through literally, so /, -, ., spaces and 年 月 日 need no escaping.

  • The longest token wins at each position: YYYY beats YY, and MMMM beats MMM, MM and M.

  • A substituted month name is never rescanned, so March and May keep their M.

  • An unrecognized run degrades instead of failing the build: YYYYY renders the year followed by a literal Y, and dddd stays dddd.

  • Brackets do not nest — the first ] closes the escape — and an unmatched [ is emitted literally while the rest of the pattern still resolves.

  • MMM and MMMM produce localized month names for en, ja and de only; every other locale falls back to English names. See Date formats per locale.

A supported subset, not Day.js

The token spellings are borrowed from Day.js because they are familiar, but this is a bounded vocabulary, not Day.js compatibility. The eight tokens above and […] escapes are the whole set: there are no weekday, time, quarter, or era tokens and no plugin surface. Everything else is emitted literally.

Examples

A bare pattern — the full role in every locale. The H1 date line and pager read 2026/08/22; card stamps, month headings and sidebar rows are untouched:

zfb.config.ts
export default defineConfig(
  zudoDoc({
    dateFormat: "YYYY/MM/DD",
  }),
);

Two roles, every locale. full reads 08/22/2026 and yearMonth reads 08/2026; monthDay, year and numericMonthDay stay at "locale":

zfb.config.ts
export default defineConfig(
  zudoDoc({
    dateFormat: {
      full: "MM/DD/YYYY",
      yearMonth: "MM/YYYY",
    },
  }),
);

Per-locale. English pages read Aug 22, 2026 and Japanese pages 2026年8月22日; yearMonth is overridden on ja only, so English keeps its default 2026 August while Japanese reads 2026年8月:

zfb.config.ts
export default defineConfig(
  zudoDoc({
    dateFormat: {
      full: "MMM D, YYYY",
      locales: {
        ja: { full: "YYYY年M月D日", yearMonth: "YYYY年M月" },
      },
    },
  }),
);

An object replaces the default wholesale

zudoDoc() merges your fields over the package defaults at the top level only, so a supplied dateFormat object replaces the default "locale" outright rather than being deep-merged into it. That costs nothing here: the resolver fills every role you leave out with "locale" anyway, which is exactly what the default meant.

What it does not affect

dateFormat is a display setting. It does not touch the YYYY-MM-DD frontmatter authoring format, the machine-readable datetime attributes on <time> elements, the sitemap's lastmod values, or the grouping keys behind year and month headings — all of those stay ISO.

defaultLocale

Default locale code (unprefixed routes).

Default: "en"

locales

Additional locales: code → { label, dir, description?, introMarkdown?, sitemapHeading? }. Each becomes a /<code>/docs/… route tree and a docs-<code> collection. The map is ordered: the language switcher renders every configured label in that order, so labels are not hard-coded to JP or JA.

Default: {}

locales: {
  ja: {
    label: "JA",
    dir: "src/content/docs-ja",
    description: "プロジェクトの使い方をまとめたドキュメント。",
  },
  de: { label: "DE", dir: "src/content/docs-de" },
},

description is an optional string for that locale’s home page hero. When omitted, it falls back to settings.siteDescription; an empty string ("") intentionally leaves the hero description blank. settings.locales contains only non-default locales, so the default locale always uses settings.siteDescription and has no locale-specific override.

Current limitations: this field affects only the home page hero description. llms.txt still uses the global siteDescription for every locale, and home pages still emit no <meta name="description">.

Locale codes are used as URL segments and directory suffixes. Keep them lowercase and path-safe; duplicate codes, the primary defaultLocale, path separators, and traversal segments are rejected by create-zudo-doc before it writes files. The generated ja tree starts with Japanese prose; other arbitrary locale trees start with English placeholder prose for translation.

defaultLocaleOnlyPrefixes

Route prefixes served only in the default locale (never locale-prefixed). The language switcher omits them on affected pages.

Default: []

See Default-locale-only prefixes.

versions

Docs versions, or false for a single (unversioned) doc set. Each versioned section is served at /v/{slug}/docs/.

Type: VersionConfig[] | false · Default: false

PropertyTypeDescription
slugstringVersion identifier in the URL path
labelstringVersion-switcher display label
docsDirstringContent directory for this version's default-locale docs
localesRecord (optional)Per-locale content dirs for this version
banner"unmaintained" | "unreleased" | false (optional)Banner shown on this version's pages

See the Versioning guide.

mermaid

Enable mermaid diagram rendering in markdown.

Default: true

transclude

Enable :::include{file="…"} transclusion. zudo-doc passes the enabled boolean to zfb as the object-typed markdown.features.transclude: {} setting; configure it here rather than replacing the package-owned Markdown feature block.

zfb.config.ts
export default defineConfig(
  zudoDoc({
    transclude: true,
  }),
);

Type: boolean · Default: false

See Transclude for the directive syntax, filesystem restrictions, and build-fatal failure behavior.

math

Enable KaTeX math rendering ($…$, $$…$$, fenced math).

Default: false

math: true requires installing the optional peer dependency katex (pnpm add katex) — create-zudo-doc does not add it for you, and it is absent from a fresh scaffold's package.json. katex is an optional peer of @takazudo/zudo-doc: the build stays green without it, but rendering a <MathBlock> throws at request time. See Math Equations for the component. docHistory's Compare view has the matching contract with diff — see docHistory below.

cjkFriendly

Enable zfb's CJK-friendly line-break / emphasis handling.

Default: false

Tags

tagVocabulary and tagGovernance are orthogonal — one is the runtime gate, the other the enforcement level. The vocabulary entries are supplied separately via the tagVocabularyEntries escape hatch.

docTags

Enable the /docs/tags + /docs/tags/[tag] tag index routes.

Default: false

tagPlacement

Where per-page tags render relative to the content.

Type: "after-title" | "before-pager" · Default: "after-title"

tagGovernance

Tag-governance enforcement level when the vocabulary is consulted ("off" / "warn" / "strict").

Default: "off"

tagVocabulary

Whether the tag vocabulary is consulted at runtime (exact-id recognition and grouped footer). Orthogonal to tagGovernance. This is the boolean gate — the entries themselves come from tagVocabularyEntries.

Default: false

See Tag governance.

Table of contents and headings

tocMinDepth

Minimum heading depth included in the TOC (2–4).

Default: 2

tocMaxDepth

Maximum heading depth included in the TOC (2–4).

Default: 4

searchMaxBodyLength

Maximum number of characters of each page's body indexed for search matching.

Type: number · Default: 3000

This controls how deep into a page a search query can match, not how much of a result is displayed — results always show a compact snippet centered on the match; see Site Search. Raising the cap grows search-index.json accordingly: for example, raising it from the previous 300-character default to 3,000 grew this showcase's index about 3.4× (roughly 410 KB to 1.4 MB). Invalid values (non-integer or <= 0) throw at config time.

Site Settings

designTokenPanel

Enable the interactive Design Token Panel (zdtp) for live editing of spacing, font, size, and color tokens.

Default: false

The panel works with zero extra config in a project scaffolded with the feature on — that is what installs the optional @takazudo/zdtp peer and adds its @import "@takazudo/zdtp/styles.css"; line. Turning this on in a project scaffolded without it needs both of those steps by hand; otherwise the panel's lazy import("@takazudo/zdtp") fails at runtime and only logs a warning. To fully customize it, point designTokenPanelConfigModule at a host module — see Host Chrome Bindings.

sidebarResizer

Enable the draggable sidebar resizer.

Default: false

sidebarToggle

Enable the desktop sidebar collapse toggle.

Default: false

tocToggle

Enable the desktop table-of-contents collapse toggle — a chevron button pinned to the right edge of the viewport on xl screens (1280px and up).

Default: false

Collapsing the TOC hands its width to the content column, so the same page can be read at a wider measure. That is the point of the feature: wide tables and long code lines stop wrapping or scrolling sideways once the TOC is out of the way. The reader's choice is remembered, so it holds across pages and across visits.

The toggle drives the package default TOC only. If you replace the Toc slot through chromeBindingsModule, your own component renders unchanged and no toggle appears — see Host Chrome Bindings.

tocToggle vs. hide_toc

They solve different problems and can be used together. The hide_toc frontmatter is an author decision baked in at build time: the page ships without a TOC, and no reader can bring it back. tocToggle is a reader control: wherever the default TOC is rendered, the reader decides whether to see it right now. It changes nothing on a page that already opted out with hide_toc.

imageEnlarge

Enable click-to-enlarge for content images.

Default: false

assetViewer

Generate a default-locale viewer page for every included file under public/<assetViewerDir>/ and enable manifest-backed asset links, cards, code excerpts, and image-caption links in MDX.

Default: false

See Asset Viewer for the directory layout, viewer behavior, and author entry points.

assetViewerDir

Set the directory below public/ that contains viewer-managed files. The same value becomes the raw public URL prefix.

Default: "assets"

The value must be a safe relative URL path and must differ from assetViewerRoutePrefix.

assetViewerRoutePrefix

Set the URL prefix for generated viewer pages.

Default: "files"

With the defaults, public/assets/guide.pdf is raw at /assets/guide.pdf and has a viewer at /files/guide.pdf/.

assetViewerExclude

Exclude asset-relative glob paths from viewer generation while leaving the underlying files in public/.

Type: string[] · Default: []

For example, ["**/*.map", "drafts/**"] skips source maps and everything below the asset directory's drafts/ folder.

assetViewerIndex

Generate an asset listing page at /<assetViewerRoutePrefix>/ when the asset viewer is enabled.

Default: false

See Asset Viewer for the listing behavior and header-navigation setup.

assetViewerIndexing

Independently opt asset viewer pages into the search index, llms.txt, and the sitemap. assetViewer: true is required; indexing subkeys have no effect when the viewer is disabled.

Type: false | { search?: boolean; llmsTxt?: boolean; sitemap?: boolean } · Default: false

Each subkey is off unless it is explicitly true, including when the setting is a partial object. search adds asset pages to search-index.json with asset:-prefixed IDs. llmsTxt adds a ## Files section to llms.txt and appends asset pages to llms-full.txt; text-like assets include up to 8 KB of body text and then an explicit truncation marker, while binary assets are one-line stubs with no body. sitemap adds the /files/... routes.

See Asset Viewer for a configuration example. This showcase enables all three subkeys.

findInPage

Mount the FindInPageInit island (Cmd/Ctrl+F find bar) in the body-end islands.

Default: false

Auto-enabled by create-zudo-doc's tauri feature for generated Tauri scaffolds (the generator emits findInPage: true when the tauri feature is selected; it ships with the next create-zudo-doc release). Self-gates on window.__TAURI_INTERNALS__, so it is a safe no-op outside a Tauri shell even when true. A host that overrides the BodyEndIslands chrome-bindings slot (part of ChromeHostBindings — see Host Chrome Bindings for the general seam) must mount the island itself — the package default only applies when the slot is left unset.

dynamicPageTransition

Enable the dynamic (view-transition) page-loading overlay.

Default: false

frontmatterPreview

Frontmatter-preview config (ignore-key overrides), or false to disable the preview panel.

Type: FrontmatterPreviewConfig | false · Default: false

PropertyTypeDescription
ignoreKeysstring[] (optional)Replaces the default ignore list
extraIgnoreKeysstring[] (optional)Adds to the defaults (ignored if ignoreKeys is set)

See the Frontmatter Preview reference.

docHistory

Enable per-page git history (Created/Updated + dropdown). In dev, history is served by @takazudo/zudo-doc-history-server on port 4322; in CI a generator emits static JSON.

Default: false

Note

Fresh scaffolds wire the real scanner-reachable DocHistory island into every generated document-route shape when this feature is selected. No chromeBindingsModule or route edit is required for the history button itself.

The history dropdown's Compare view requires the optional peer dependency diff; the build itself does not. create-zudo-doc adds diff to package.json automatically whenever docHistory is selected — directly, or indirectly through bodyFootUtilArea, which forces docHistory on. diff is an optional peer of @takazudo/zudo-doc, so a project that enables docHistory by hand in zfb.config.ts (without going through the generator) must pnpm add diff itself. See math above for the matching contract with katex.

See the Document History guide.

docHistoryUi

Controls the History button and DocHistory island, postBuild history JSON generation, and dev history proxy. Setting it to false keeps the preBuild metadata manifest when docHistory: true. The view-source link is retained too.

Type: boolean · Default: true

Set this by hand in zfb.config.ts; there is no new CLI flag. See the Document History guide for environment-variable interactions and CI job cleanup.

docMetainfo

Show the Created/Updated/Author meta block on doc pages (extracted from git history at build).

Default: false

Metadata data requires a host binding

The docHistoryMeta slot defaults to {}. Point chromeBindingsModule at a module that binds it to the build-time history manifest. Fresh route stubs already consume that module; do not edit them. See Custom Components and Host Chrome Bindings.

The binding module must provide docHistoryMeta; with the default {} data slot, there is no metadata block to render. Follow the Document History guide for the paired docHistory setup.

docMetainfoFields

Select the fields shown in the metadata block. Requires docMetainfo: true and a docHistoryMeta binding that supplies data.

Type: Array<"created" | "updated" | "author"> · Default: ["created", "updated", "author"]

Omitting this enables all fields. Display order stays Created, Updated, Author regardless of array order. When Created is also displayed, Updated is suppressed when its formatted display value equals Created (the configured date format can include a time). With ["updated"], Updated appears even when the dates match. An empty array [] renders no metadata block.

docMetainfo: true,
docMetainfoFields: ["updated"],

Set this by hand in zfb.config.ts; there is no new CLI flag. Selecting fewer fields does not stop history data generation.

bodyFootUtilArea

Body-foot utility area (doc-history / view-source), or false to disable.

Type: BodyFootUtilAreaConfig | false · Default: false

PropertyTypeDescription
docHistoryboolean (optional)Show the History button (requires docHistory: true and docHistoryUi: true)
viewSourceLinkboolean (optional)Show a raw-source link (requires githubUrl)

htmlPreview

Global HTML-preview sandbox config, or undefined to disable global injection into every <HtmlPreview> iframe.

Type: HtmlPreviewConfig | undefined · Default: undefined

PropertyTypeDescription
headstring (optional)Raw HTML injected into <head>
cssstring (optional)CSS injected as a <style> block
jsstring (optional)JS injected before </body>

Footer config (link columns / copyright / taglist), or false for no footer.

Type: FooterConfig | false · Default: false

PropertyTypeDescription
linksFooterLinkColumn[]Link columns
copyrightstring (optional)Copyright text (HTML allowed)

Each FooterLinkColumn has title, items ({ label, href }[]), and optional per-locale overrides.

headerNav

Primary header navigation items.

Default: []

PropertyTypeDescription
labelstringDisplay text
labelKeystring (optional)i18n translation key (overrides label)
pathstringTarget path
categoryMatchstring (optional)Links this tab to a sidebar category
versionedboolean (optional)Whether this item's link carries the active /v/{version} prefix. Default true — see Header Navigation
headerNav: [
  { label: "Guides", path: "/docs/guides", categoryMatch: "guides" },
  { label: "Reference", path: "/docs/reference", categoryMatch: "reference" },
],

See How to Structure Navigations.

headerRightItems

Header right-hand-side items (toggles, switchers, links), in order.

Default: [{ type: "component", component: "theme-toggle" }]

typeExtra fieldsDescription
"component"component — "theme-toggle" (the Light / Dark / System appearance menu), "language-switcher", "version-switcher", "github-link", "search"Built-in header component
"trigger"trigger — "design-token-panel", "ai-chat"Opens a panel (requires the feature)
"link"href, label?, ariaLabel?, icon?Custom link
"html"htmlRaw HTML

AI assistant

aiAssistant

Enable the AI-chat assistant route (/api/ai-chat, SSR).

Default: false

Running a live assistant requires a deploy adapter, per-IP KV, an Anthropic secret, and the AI_CHAT_DAILY_SPEND_CAP Durable Object migration when the exact cap is enabled — see Customizing → Deploy path and the AI Assistant API. These are host-owned pieces: the package/scaffold supplies a safe placeholder route, not the showcase's live handler, Worker entry, or Durable Object class.

aiChatDemoMode

Short-circuit /api/ai-chat with a fixed "disabled" reply (no API key, KV, Durable Object, rate limiter, or provider fetch touched). Set false to enable live Claude-backed chat.

Default: false

aiChatAllowedOrigins

Allowed CORS origins for /api/ai-chat when not in demo mode. Empty = block all cross-origin browser requests.

Default: []

aiChatGlobalDailyLimit

Exact Anthropic fetch-admission limit per UTC day across all IPs, or false to skip the exact cap. Provider/network failure does not refund an admitted slot.

Type: number | false · Default: false

Generation and integrations

llmsTxt

Emit an llms.txt route summarising the docs.

Default: false

See llms.txt.

agentExport

Publish a versioned, read-only static corpus at <base>/agent/v1/ for documentation clients. It includes a manifest, a complete agent search index, fetchable item JSON, and per-page Markdown. This works on any static host.

Default: false

See Agent-readable export.

mcp

Add the read-only search and fetch MCP endpoint at <base>/mcp. Requires agentExport: true and a request-time deployment adapter. The initializer's mcpDeploy: "cloudflare" option generates the Cloudflare adapter and deployment files; mcpDeploy is not a zudoDoc setting.

Default: false

See MCP server.

changelogs

Changelog generation config(s), or false to disable.

Default: false

For a multi-package project, add one entry per package. Each sourceDir must be the package-specific directory that contains non-index, per-release MDX files—not the top-level changelog landing directory:

changelogs: [
  {
    sourceDir: "src/content/docs/changelog/core",
    outputFile: "packages/core/CHANGELOG.md",
    packageName: "@acme/core",
  },
  {
    sourceDir: "src/content/docs/changelog/cli",
    outputFile: "packages/cli/CHANGELOG.md",
    packageName: "@acme/cli",
  },
],

See the Changelog guide.

claudeResources

Claude-resources ingestion config, or false to disable.

Type: { claudeDir; projectRoot?; scanRoot? } | false · Default: false

claudeResources: { claudeDir: ".claude" },

See the Claude Resources guide.

codexResources

Codex-resources ingestion config, or false to disable.

Type: { codexDir; projectRoot?; scanRoot? } | false · Default: false

codexResources: { codexDir: ".codex" },

See the Codex Resources guide.

Routing seams

packageOwnedRoutes

Build-time package-owned route injection. When true, doc routes are injected from the package (no project-shipped pages/*.tsx layout stubs needed). Set false only if your project ships its own doc-route stubs.

Default: true

chromeBindingsModule

Project-root-relative path to a host module exporting a chromeBindings object built with defineChromeBindings (from @takazudo/zudo-doc/chrome-bindings; only consumed when packageOwnedRoutes is on). Omit to keep the injected chrome shim at its package-default stubs.

Default: undefined

See Host Chrome Bindings.

Escape-hatch fields

These override non-serializable / data defaults. They travel the import graph rather than being JSON-serialized, so they can carry functions, Zod types, and component maps.

buildDocsSchema

Replace the default docs-frontmatter Zod schema builder entirely. When omitted, zudoDoc() builds the package default (@takazudo/zudo-doc/docs-schema), governance-aware from tagGovernance + tagVocabularyEntries.

Type: () => ZodType · Default: undefined (package default used)

This is how you add custom frontmatter keys — see Customizing → Rung 1.

colorSchemes

Override the color-scheme palette map. When omitted, the two shipped schemes (Default Light / Default Dark) are used.

Type: Record<string, ColorScheme> · Default: undefined

translations

Override the UI-string translation table. When omitted, the shipped en/ja/de defaults are used.

Type: PresetTranslations · Default: undefined

directives

Override the directive → JSX-component-name map. When omitted, the canonical seven are used.

Type: DirectiveVocabulary · Default: undefined

Warning

directives replaces the vocabulary wholesale. Preserve the canonical directives when adding one, or existing :::note, :::warning, and other built-ins stop resolving.

zfb.config.ts
import { defaultDirectiveVocabulary } from "@takazudo/zudo-doc/directive-vocabulary-defaults";

export default defineConfig(
  zudoDoc({
    directives: {
      ...defaultDirectiveVocabulary,
      callout: "Callout",
    },
  }),
);

See the Directives Registry for the complete component-registration recipe.

tagVocabularyEntries

The tag-vocabulary entries array (distinct from the boolean tagVocabulary gate). Threaded into the route-context virtual module and consulted by the governance-aware default schema builder.

Type: readonly PresetTagVocabularyEntry[] · Default: []

Warning

Do not conflate tagVocabulary (a boolean gate) with tagVocabularyEntries (the entries array). They are separate fields — the gate decides whether the vocabulary is consulted; the entries are what it consults.

Shell passthrough fields

These are host-owned ZfbConfig shell fields, not part of Settings.

port

Dev/preview server port.

Default: 4321

adapter

Deploy-target adapter package name (e.g. "@takazudo/zfb-adapter-cloudflare"). Omit for a pure static build.

Default: undefined (pure static build)

bundle

zfb bundler options (exclude / mainFields / external), passed through verbatim when set.

Default: undefined

wind

Top-level WindConfig | false override, passed to zfb rather than added to serialized Settings. Default: undefined (inherit the package preset).

The preset registers var-backed semantic tokens, reset: "owned-v1", dark: false, the public @takazudo/zudo-doc/wind.json manifest, and the sm/lg/xl pixel breakpoints. It omits spacingUnit. zfb deep-merges an explicit override over those defaults; wind: false disables generation. Use ordinary :root declarations to change an existing token value; use wind.tokens when introducing a new utility name. See Design System for examples and audit coverage limits.

An extended project as reference

This showcase's own zfb.config.ts is the canonical "extended project" example — it spreads a typed settings object and passes genuinely showcase-specific data blocks (tag vocabulary, custom translations) through the escape-hatch fields, then adds the shell fields (port, adapter, bundle):

zfb.config.ts (this showcase)
export default defineConfig(
  zudoDoc({
    ...settings,
    tagVocabularyEntries: tagVocabulary,
    translations,
    chromeBindingsModule: "./src/chrome-bindings.tsx",
    port: 4321,
    adapter: "@takazudo/zfb-adapter-cloudflare",
    bundle: { exclude: ["e2e/fixtures/**"] },
  }),
);

A minimal project needs none of that — just the handful of fields it changes.

Revision History

Takeshi TakatsudoCreated: 2026-03-14T08:14:41+09:00Updated: 2026-10-09T16:26:27Z

AI Assistant

Ask a question about the documentation.

Preview theme

Loading theme previews…