zfb
GitHub repository

Type to search...

to open search from anywhere

defineConfig

Created Jun 24, 2026Takeshi Takatsudo

Define a zfb project configuration with full type inference.

Signature

defineConfig(config: ZfbConfig): ZfbConfig

defineConfig is exported from zfb/config. The helper is identity-typed: it returns its argument unchanged. Its only job is to give your editor IntelliSense and type-checking against the ZfbConfig shape. The actual schema is enforced by Rust serde at config-load time, so the same rules apply whether you author your config in TypeScript or JSON.

Typing the zfb/config import

zfb.config.ts imports defineConfig from the bare specifier zfb/config. At config-load time zfb aliases that import to a runtime-only stub (so your config parses without the @takazudo/zfb package installed), and no real zfb/config module exists in node_modules — the published types live under the scoped subpath @takazudo/zfb/config. So a plain tsc --noEmit (what zfb check runs) has nothing to type the bare import against and reports Cannot find module 'zfb/config'.

Bridge the two with a one-line ambient declaration that re-exports the scoped package's types. Add a zfb-shim.d.ts at your project root (any .d.ts your tsconfig includes works):

// zfb-shim.d.ts — types the bare `zfb/config` specifier.
declare module "zfb/config" {
  export * from "@takazudo/zfb/config";
}

Re-export — do not hand-copy the ZfbConfig shape into the shim. A hand-mirrored field list silently lags the engine: every new config field has to be re-added by hand, or zfb check rejects a perfectly valid field with TS2353: Object literal may only specify known properties. export * tracks the published @takazudo/zfb/config automatically, so the shim can never fall behind.

Config shape

All keys are camelCase. The shape is enforced by the Rust serde deserializer (crates/zfb/src/config.rs) — the TypeScript type in packages/zfb/src/config.ts mirrors it for editor IntelliSense.

  • outDir?: string — output directory. Default: "dist".

  • publicDir?: string — static assets directory, copied verbatim. Default: "public".

  • host?: string — dev/preview server bind host.

  • port?: number — dev/preview server port.

  • allowedHosts?: string[] — Host header values the dev/preview server accepts when bound to a non-localhost interface (mirrors Vite's server.allowedHosts). Only consulted for non-loopback binds; localhost, the explicitly bound host, and any IP-literal Host (127.0.0.1, [::1], the LAN URLs the startup banner prints) are always allowed — DNS rebinding needs a DNS name, so raw IPs are safe. Entries match exactly (case-insensitive, request port stripped); a leading-dot entry like ".example.com" also matches subdomains.

  • framework?: "preact" | "react" — JSX framework runtime. Default: "preact".

  • collections?: CollectionDef[] — content collections. Each entry has:

    • name: string — identifier used in getCollection calls.

    • path: string — directory relative to the project root.

    • schema?: Record<string, unknown> — optional JSON Schema for frontmatter validation (enforced by zfb check; the build does not validate frontmatter against it).

    • include?: string[] — glob patterns (globset dialect, relative to path); when set, only matching entries are kept.

    • exclude?: string[] — glob patterns; matching entries are dropped (evaluated after include).

    • idStripSuffix?: string — suffix stripped from each entry's slug and module specifier (e.g. ".en" for multi-locale layouts).

  • tailwind?: { enabled?: boolean } — Tailwind options.

  • prefetch?: { disabled?: boolean } — prefetch options. When disabled: true, the runtime's prefetch wiring is skipped entirely via a build-time meta tag.

  • bundle?: { exclude?: string[]; mainFields?: string[]; external?: string[] } — esbuild escape hatches for the --platform=neutral page/SSR pass. exclude lists project-relative globs of source files to keep out of the esbuild graph; mainFields overrides the main-fields list for deps without an exports map; external marks bare specifiers as external so esbuild leaves them unbundled. Useful for CJS-only deps that fail under the neutral platform (see issues #680/#676).

  • plugins?: PluginConfig[] — user-supplied plugins. Each entry has name (npm specifier or ./-relative path) and optional options (an arbitrary JSON object passed to the plugin's hooks). See Plugins for the full hook contract — setup, preBuild, postBuild, devMiddleware — including virtual modules, import aliases, and dev-only injected routes.

  • presets?: Partial<ZfbConfig>[] — config presets to merge before validation. Each preset is a partial ZfbConfig-shaped object, typically the return value of a preset package's factory function. Array fields (plugins, collections, extraWatchPaths, allowedHosts) are prepended from presets so the main config's entries retain their position. Scalar fields are filled in by presets only when the main config leaves them at their default — the main config is always authoritative. Nested presets inside a preset are not recursively expanded. Preset authors should use definePreset so that relative-path plugins resolve correctly.

  • adapter?: string — deploy-target adapter package name. Omit for a pure static build. A package like "@takazudo/zfb-adapter-cloudflare" wraps the SSR bundle into a deploy-ready entry (e.g. dist/_worker.js for Cloudflare Pages).

  • output?: "static" | "hybrid" | "auto" — project output mode. "static" errors at build start if any route exports prerender = false; "hybrid" always enables V8/SSR even if no SSR routes currently exist; "auto" (default) detects from the route set.

  • site?: string — canonical origin URL (e.g. "https://example.com"). When set, exposes globalThis.__zfb.site so layouts can build canonical <link> tags, OpenGraph meta, sitemap absolute hrefs, and hreflang alternates. Must be an absolute HTTP/HTTPS URL; omit for builds that do not need server-side canonical URL construction. Distinct from base — see below.

  • base?: string — public URL prefix for asset URLs. Use when the site is deployed under a sub-path (e.g. "/pj/my-site/"). Distinct from site: base prefixes asset URLs; site is the full canonical origin used in metadata.

  • copyPublicWithBase?: boolean — whether public/ assets are copied under the base sub-path segment (default true) or flat to the dist/ root (false). Set false when the deploy pipeline relocates the entire dist/ tree into the base path (e.g. cp -a dist/. deploy-root/pj/site/) to avoid a double-nested dist/<base>/<base>/... path. See Static Assets — flat copy for deploy-relocation pipelines.

  • stripMdExt?: boolean — strip .md/.mdx extensions from internal link hrefs during MDX compilation and append a trailing /. Default: false.

  • trailingSlash?: boolean — append a trailing / to extensionless absolute hrefs when rewriting base paths. Default: false.

  • markdown?: MarkdownConfig — Markdown/MDX parsing options (e.g. GFM toggle). See the Markdown Features section.

  • resolveMarkdownLinks?: ResolveMarkdownLinksConfig — markdown link resolver settings. Enable to rewrite [label](./other.mdx) links to their rendered route URLs. Extensionless (./other) and directory-style (other/) targets resolve too, probing {name}.mdx{name}.md{name}/index.mdx{name}/index.md. Relative targets resolve from the source file's directory; a directory-style link written from a non-index page against its rendered URL (which sits one directory deeper — ../sibling/ from section/article.mdx) resolves via a URL-space fallback when every file-space candidate misses.

  • emitRoutesManifest?: boolean — whether zfb build writes the post-build route manifest to <outDir>/__zfb/routes.json. Default: true (emit). Set false to suppress.

  • extraWatchPaths?: string[] — extra absolute filesystem paths the dev watcher follows in addition to the in-project source roots. See Watching paths outside the project root.

The following keys are part of the same ZfbConfig type — defineConfig and zfb check type-check them like any other field. They configure the Rust engine's code-rendering and plugin behaviour:

  • codeHighlight — syntect syntax-highlight theme options. See the syntax-highlighting guide. Fields:

    • theme?: string — single-theme mode. A syntect built-in or user-loaded theme name (e.g. "InspiredGitHub", "Solarized (dark)"); defaults to "base16-ocean.dark". Tokens get an inline color:. Mutually exclusive with themeLight / themeDark. These are syntect theme names, not Shiki names like "dracula".

    • themesDir?: string — directory of .tmTheme files, relative to the project root. Each file becomes available by its declared name to theme, themeLight, or themeDark. Applies to both single- and dual-theme mode. Must be relative and must not escape the root via ..; a missing directory errors at build start.

    • themeLight?: string — light-mode syntect theme name for dual-theme highlighting. Must be set together with themeDark — setting only one is a build error. Mutually exclusive with theme. When the pair is set, each block is highlighted twice and tokens carry --shiki-light / --shiki-dark custom properties instead of an inline color:, the <pre> gets class="syntect-dual" plus --shiki-light-bg / --shiki-dark-bg, and the consumer resolves the active colour with a light-dark() CSS rule.

    • themeDark?: string — dark-mode syntect theme name for dual-theme highlighting. Must be set together with themeLight; mutually exclusive with theme. See themeLight for the full dual-mode contract.

  • pluginHookTimeoutSecs — timeout (seconds) for plugin hook invocations.

Watching paths outside the project root

extraWatchPaths lets zfb dev live-reload when files outside the project tree change — useful when a project reads content from a sibling repo, a file: dep that ships content alongside code, or a shared filesystem directory.

import { defineConfig } from "zfb/config";

export default defineConfig({
  extraWatchPaths: [
    "/home/me/knowledge-base",
    "/srv/shared-content",
  ],
});

Semantics:

  • Absolute paths only. Each entry must be an absolute path. Relative paths are rejected at config-load with an extraWatchPaths[N]: ... must be an absolute path error — the dev watcher registers each entry verbatim, outside the project root, so it has no anchor to resolve a relative path against.

  • Canonicalisation. Each entry is canonicalised (Path::canonicalize) once when zfb dev boots. Symlinks are resolved; downstream events reach the rebuild logic with the canonical form, so the path the watcher emits matches the form you'd see by running realpath on the configured value.

  • Missing-at-boot. If a configured path does not exist at the moment zfb dev starts, it is skipped with a warning. The watcher does not poll for the path to appear later — if you create the directory after the dev server is already running, restart zfb dev to pick it up.

  • Recursive. Each entry is watched recursively. Sub-directories created after boot are picked up automatically by the OS-level recursive watch.

  • Rebuild scope. Events from these paths fall outside the dependency graph's coverage (the graph only tracks in-tree edges), so they conservatively trigger broader rebuilds than equivalent in-tree edits. The trade-off is intentional: correctness over precision for out-of-root sources.

Security note. Opt-in only — do not point this at unbounded directories like $HOME or /. On Linux the recursive watcher registers every subdirectory and can quickly hit the inotify max_user_watches ceiling (default ~8192 on many distributions) on a large tree. If you need to watch a sprawling source, watch the narrowest sub-tree that contains the files you actually edit.

This is a dev-mode feature. Production builds (zfb build) snapshot the filesystem once and do not rely on watcher events, so extraWatchPaths has no effect on shipped output.

Examples

// zfb.config.ts — the recommended form
import { defineConfig } from "zfb/config";

export default defineConfig({
  outDir: "dist",
  framework: "preact",
  collections: [
    {
      name: "blog",
      path: "content/blog",
    },
  ],
  tailwind: { enabled: true },
});

The loader accepts zfb.config.ts (preferred) and zfb.config.json (legacy fallback). When only zfb.config.json is present it is read via serde_json; plugin paths declared as ./..., ../..., or absolute paths are resolved relative to the config file (npm specifiers like "@takazudo/some-plugin" work in both forms).

// zfb.config.json — legacy form, still supported
{
  "outDir": "dist",
  "framework": "preact",
  "collections": [
    {
      "name": "blog",
      "path": "content/blog"
    }
  ],
  "tailwind": { "enabled": true }
}

Validation

The loader enforces the following rules and reports errors with the file path plus line:column for JSON parse failures:

  • Collection names must be unique.

  • path cannot be an absolute path.

  • path cannot contain .. segments that escape the project root.

Revision History

Takeshi TakatsudoCreated: 2026-06-25T05:17:25+09:00Updated: 2026-06-25T05:17:25+09:00

AI Assistant

Ask a question about the documentation.