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'.stringwebsiteBanners power the announcement banner, and its
versions (same shape as the versions export below) replace the build-time
version selector entries, so docs built for an older release still list the
releases that came after it.
Default: none — no runtime fetch, no banner.Arraystylesheets. 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, fonts }: paths relative to the output root
of the module scripts to load, the chunks they statically import (rendered as
modulepreload hints), the stylesheets, and optionally the fonts to preload.
The generator renders those into every page, resolved against the page's
location. Name every file after its content (a hash), so hosts can cache them
indefinitely (see Publishing).
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 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.
CSS is transformed and minified by Lightning CSS. On platforms without its
native bindings, such as ppc64 and s390x, Vite's PostCSS pipeline transforms
the CSS instead and the output is not minified; css.lightningcss options have
no effect there.
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, including the
hashed names of the fonts to preload. 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 the useRemoteConfig hook for the banner and version selector).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.stringstringstringstringObjectObjecthtml 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.
The site moves between its own pages client-side, through the Navigation API: following a link to another page fetches that page and swaps it into the current document instead of loading a new one. Scripts, stylesheets and fonts stay loaded, the search index and the remote config are fetched once per visit, and the sidebar keeps its scroll position. Back and forward, scroll restoration and focus behave as they do for full loads.
Pages are prefetched into memory when a link is hovered (unless the browser asks to save data) or pressed, so most navigations do not wait on the network.
Everything else is a regular navigation: links outside the site (including other versions of the docs), files that are not pages (such as the JSON and Markdown renderings), and every navigation in a browser without the Navigation API. So is a page that loads scripts or stylesheets the current one did not, such as one from a newer deployment.
When customizing the site, keep in mind that:
- The whole
<body>is replaced, and scripts in it do not run. Of the<head>, only the<title>and the page-specific tags follow the page:<meta>tags, and<link>tags other than stylesheets and preloads (such ascanonical). - Islands are unmounted and hydrated again on every page. Anything shared across pages belongs in module scope, which lasts for the whole visit. An island that scrolls keeps its position when the next page renders it too.
- Once the document has navigated client-side, the
<html>element has adata-navigatedattribute, for styles that only belong on the first page, such as entry animations. - Analytics can count client-side page views through the Navigation API's
navigatesuccessevent.