The html generator turns the pages' JSX into a complete static site: the
server-rendered HTML pages, the client-side JavaScript, CSS, and imported
assets, written to output. Vite is the default bundler adapter, but projects
can supply an adapter for webpack or another bundler. The generator is
output-only and does not return an in-memory copy of its HTML or CSS.
The site is built in pieces that are each as small as they can be. The bundler
builds the component library once and the client assets once. Each page's
program is then compiled — JSX to a plain module — and the worker pool imports,
renders, templates, minifies and writes the pages one at a time, so memory
scales with the largest page rather than with the site. all.html is assembled
from the module pages' compiled content rather than built again from scratch.
stringstring'template.html'.stringglobal.project.string{project},
{version}). Default: '{project} v{version} Documentation'.booleantrue, all internal links use absolute URLs
based on baseURL. Default: false.stringstring'{baseURL}{path}.html'.stringArraystylesheets. Default: [].Object#theme/ aliases to component paths for
customization. See Default imports.Object{}.Objectcomponents. Default: {}.Objectnavigation. Default: {}.booleantrue, writes all.html: every module
page's content on one page, in sidebar order, assembled from the module pages
rather than built again. Chunk pages and the index are left out.
Default: true.WebBundlercreateViteBundler().The head object controls the project-specific markup injected into the
document <head> (rendered into the template's ${head} placeholder).
Each attribute bag is rendered as a tag: a boolean true becomes a valueless
attribute (e.g. crossorigin), and false/null/undefined attributes are
omitted. Using arrays of attribute bags (rather than name → value maps) means
you can emit repeated tags (e.g. two preconnect links) and pick the right
attribute (name vs property) per tag.
The default head is empty — brand the output by supplying your own tags:
// doc-kit.config.mjs export default { html: { head: { meta: [ { name: 'description', content: 'My project documentation' }, { property: 'og:image', content: 'https://example.com/og.png' }, ], links: [ { rel: 'icon', href: 'https://example.com/favicon.ico' }, { rel: 'stylesheet', href: 'https://example.com/fonts.css' }, ], html: ['<meta name="theme-color" content="#000" />'], }, }, };
Structural and theme-bound tags are emitted by the template itself rather than via
head, includingog:title(which mirrors the per-page title) andog:type. The UI stylesheet bundles its fonts locally.
Each entry is a path to a CSS file, bundled into the site's single stylesheet
after the built-in one — so its rules and custom properties win. Relative paths
resolve against the working directory; prefer absolute paths (e.g.
join(import.meta.dirname, 'theme.css')) when the config file can be loaded
from elsewhere.
The built-in accent palette is a project-neutral grey. Rebrand the output by
redefining the nine --color-brand-* custom properties, which the UI components
use for links, focus rings, and active states:
/* theme.css */ :root { --color-brand-100: #edf2eb; --color-brand-200: #c5e5b4; --color-brand-300: #99cc7d; --color-brand-400: #84ba64; --color-brand-500: #5fa04e; --color-brand-600: #417e38; --color-brand-700: #2c682c; --color-brand-800: #2c682c; --color-brand-900: #1a3f1d; }
// doc-kit.config.mjs import { join } from 'node:path'; export default { html: { stylesheets: [join(import.meta.dirname, 'theme.css')], }, };
The navigation object supplies the site's two navigation surfaces. Both keys
are optional; omit either one to keep that component's default.
Sidebar items are { label, link } and may nest through an items array of
their own. A label is plain text, except that backticked spans render as
<code> ('`fs`'), matching how page headings are rendered. A link is a
page path without its extension (/fs, /generators/html): it is resolved
against the page being rendered, so it obeys useAbsoluteURLs and highlights
while it is the current page. Links starting with http:// or https:// are
used as authored.
Navigation bar links are always used as authored, since they typically point
outside the generated site. Give them a target of '_blank' to open in a new
tab and mark them with an external-link icon.
// doc-kit.config.mjs export default { html: { navigation: { sidebar: [ { groupName: 'Guides', items: [{ label: 'Getting started', link: '/getting-started' }], }, { groupName: 'Reference', items: [{ label: '`fs`', link: '/fs' }], }, ], navbar: [ { text: 'Learn', link: 'https://nodejs.org/en/learn' }, { text: 'Download', link: 'https://nodejs.org/en/download' }, ], }, }, };
The sidebar also renders a version <Select> built from changelog. A site
configured without one has no versions to switch between, so the control is
omitted rather than rendered empty.
Functionfile: URL of the built module.FunctionFunctionentry into config.output and
return the assets every page loads.The bundler option accepts a small Doc Kit adapter rather than configuration
for a particular build system.
buildServer receives { entry, virtualImports, outDir, config }. The entry
is the component library's source: re-exports of every component a page may
render, Preact's h and Fragment, and renderToStringAsync. It must be
bundled into one self-contained module written under outDir (a temporary
directory the generator removes afterwards), since the page programs import it
from wherever they are compiled to.
compile(code, fileName) receives one page program: a module that imports the
library and exports the page's content and a default function rendering the
page from its layout props, written in JSX. It must return plain JavaScript. The JSX must compile with the classic
runtime to calls of the _jsx and _Fragment bindings the program imports
(these names are exported as JSX_PRAGMA and JSX_PRAGMA_FRAG from the
generator's constants.mjs), so that the page and the library share one Preact.
buildClient receives { entry, virtualImports, config }. The client entry
is a single program shared by every page. It must be bundled into
config.output and the call must return
{ scripts, preloads, stylesheets }: paths relative to the output root of the
module scripts to load, the chunks they statically import (rendered as
modulepreload hints), and the stylesheets. The generator renders those into
every page, resolved against the page's location.
config is the resolved html configuration. The adapter must compile the
generated Preact JSX and CSS imports and resolve the supplied theme aliases and
virtual modules. The generated #theme/config module exports server as
true for the server build and false for the client build.
A webpack integration can live entirely in project configuration without adding webpack to Doc Kit:
// webpack-bundler.mjs export const createWebpackBundler = webpackOptions => ({ async buildServer({ entry, virtualImports, outDir, config }) { // Materialize or load the in-memory modules, run webpack's Node target // with the entry, write one self-contained module under `outDir`, and // return its `file:` URL. }, async compile(code, fileName) { // Transform the page's JSX (classic runtime, pragma `_jsx`, fragment // pragma `_Fragment`) and return the resulting module source. }, async buildClient({ entry, virtualImports, config }) { // Run webpack's browser target into config.output and return // { scripts, preloads, stylesheets } as output-relative paths. }, });
// doc-kit.config.mjs import { createWebpackBundler } from './webpack-bundler.mjs'; export default { html: { bundler: createWebpackBundler({ // Project-owned webpack configuration. }), }, };
When bundler is omitted, the generator imports and uses
createViteBundler() automatically. To customize Vite, import the adapter
directly and pass Vite's UserConfig to it:
// doc-kit.config.mjs import { createViteBundler } from '@doc-kit/generator-react/html/bundlers/vite'; import myVitePlugin from './my-vite-plugin.mjs'; export default { html: { bundler: createViteBundler({ plugins: [myVitePlugin()], define: { 'process.env.ANALYTICS_ID': JSON.stringify('UA-XXXXX'), }, resolve: { alias: { '@components': './src/components', }, }, css: { lightningcss: { targets: { chrome: 100 << 16, }, }, }, }), }, };
The generator owns the fields required to coordinate its builds: config-file loading, app type and base, virtual inputs, Preact compatibility aliases and automatic JSX runtime, the Lightning CSS transformer, output/write mode, SSR format and output, and SSR dependency bundling. Values supplied for those fields are replaced after configuration is merged. User plugins are registered after the generator's virtual-module plugin; other Vite options are preserved.
Vite builds the client entry as a module, not the pages as HTML entries, so plugins see and can transform every module of the client and server builds but never the HTML pages. Customize the pages through the HTML template instead.
The adapter reads the client asset names from Vite's manifest. A manifest is
written either way; pass build: { manifest: true } (or a file name) to
createViteBundler to keep it in the output for another tool.
The adapter is only ever used on the main thread, so function-valued plugins
and hooks are supported. Worker threads receive the html configuration with
its function values removed.
#theme/LogostringLogo rendered inside the navigation bar. Defaults to the built-inProjectNamecomponent, which rendersprojectas plain text.#theme/NavigationstringTop navigation bar. Defaults to the built-inNavBarcomponent.#theme/SidebarstringSidebar with version selector and page links. Defaults to the built-inSideBarcomponent.#theme/MetabarstringMetadata bar displayed alongside page content. Defaults to the built-inMetaBarcomponent.#theme/FooterstringOptional footer rendered at the bottom of each page. Defaults to the built-inNoOpcomponent, which renders nothing.#theme/LayoutstringOutermost wrapper around the full page. Defaults to the built-inLayoutcomponent.
Override any alias in your config file to swap in a custom component:
// doc-kit.config.mjs export default { html: { imports: { '#theme/Logo': './src/MyLogo.jsx', '#theme/Sidebar': './src/MySidebar.jsx', }, }, };
components registers custom JSX components so they can be used directly in
content (see JSX-in-MDX below). Each entry maps a JSX tag name to
an import descriptor ({ name, source, isDefaultExport? }, the same shape as the
built-in JSX_IMPORTS). A Tag: 'source' string shorthand expands to
{ name: Tag, source } with a default export. Registered components are merged
with the built-ins, and a matching imports alias resolves the source to a
real module path:
// doc-kit.config.mjs export default { html: { components: { // Shorthand — equivalent to { name: 'Hero', source: '#theme/Hero' } Hero: '#theme/Hero', // Full descriptor Stats: { name: 'Stats', source: '#theme/Stats' }, }, imports: { '#theme/Hero': './src/components/Hero.jsx', '#theme/Stats': './src/components/Stats.jsx', }, }, };
By default every input file is parsed as Markdown, where bare < and { are
treated literally (Node.js core docs use <string>-style type annotations). To
author real JSX — <Hero />, {expression} — use an .mdx file, or set
mdx: true in a file's --- frontmatter (frontmatter wins, so mdx: false
opts a .mdx file back out). MDX files are parsed with remark-mdx and skip the
API-doc type/signature parsing; headings, frontmatter, TOC, and sidebar still
work. Reference any component registered via components:
--- title: Welcome --- # Welcome <Hero title="Node.js" /> There are {stats.length} APIs documented.
The built-in components are available without registration. Notably,
<DocumentationIndex /> renders an index of every documented module with its
stability badge and description, sourced from the documentationIndex export
of #theme/config.
The html generator provides a #theme/config virtual module that exposes pre-computed configuration as named exports. Any component (including custom overrides) can import the values it needs, and tree-shaking removes the rest.
import { project, repository, editURL } from '#theme/config';
string'Node.js').stringowner/repo format, or
undefined when none is configured.string'v22.x').Array{ url, label, major },
with labels and URL templates (only {path} remains for per-page use).string{path} remains).Array[name, path] tuples for sidebar navigation.Array<DocumentationIndex /> component — every page with a stability index, each
{ api, name, index, description }.Objectsection-pages generator, keyed by the module's path: each
{ label, items }, where items is the module's section tree — { label, path, items? } entries nested by heading depth, in document order. Empty
unless section-pages ran.Objectnavigation (consumed by the
built-in SideBar and NavBar).booleanstringuseAbsoluteURLs is true).stringremoteConfigUrl (fetched
client-side by RemoteLoadableBanner to load announcement banners).booleanWhen overriding a #theme/* component, import only the config values you need:
// my-custom-sidebar.jsx import { pages, versions, version } from '#theme/config'; export default ({ metadata }) => ( <nav> <p>Current: {version}</p> <ul> {pages.map(([name, path]) => ( <li key={path}> <a href={`${path}.html`}>{name}</a> </li> ))} </ul> </nav> );
ObjectaddedIn, basename, path, and any custom user-defined fields. Pages
produced by the section-pages generator also carry chunk, which
describes the module and section they were split from.Array'5 min read').
Only present when the jsx-ast generator's showReadingTime option is
enabled. all.html has none.ComponentChildrenThe Layout component receives the props above. Custom Layout components can use
any combination of them alongside #theme/config imports.
The HTML template file (set via templatePath) uses JavaScript template literal syntax (${...} placeholders) and is evaluated at build time with full expression support.
string'File system | Node.js v22.x').stringstring<script> and <link> tags loading the client
assets, resolved against this page's location.stringstringstringObjectObjecthtml generator configuration.string<meta>/<link>/raw markup from the head
config.Since the template supports arbitrary JS expressions, you can use conditionals and method calls:
<title>${title}</title> ${assets}
The populated page is the final HTML: it is minified when minify is set and
written as is. Put ${assets} in the <head>, or the page loads no script and
no stylesheet.