On this page

The web generator transforms JSX AST entries into complete web bundles. Its bundler adapter builds server-rendered HTML and client-side JavaScript, CSS, and imported assets, then writes the complete static site to output. Vite is the default 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 web generator accepts the following configuration options:

NameTypeDefaultDescription
outputstringRequiredThe directory where HTML and bundled client output are written
templatePathstring'template.html'Path to the HTML template file
projectstring'Node.js'Project name used in page titles and the version selector
titlestring'{project} v{version} Documentation'Title template for HTML pages (supports {project}, {version})
useAbsoluteURLsbooleanfalseWhen true, all internal links use absolute URLs based on baseURL
editURLstring'${GITHUB_EDIT_URL}/doc/api{path}.md'URL template for "edit this page" links
pageURLstring'{baseURL}/latest-{version}/api{path}.html'URL template for documentation page links
remoteConfigUrlstring'https://nodejs.org/site.json'URL fetched client-side at runtime for remote site config (currently used to power the announcement banner)
headobjectSee belowConfigurable <meta>, <link>, and raw markup for the document head
importsobjectSee belowObject mapping #theme/ aliases to component paths for customization
virtualImportsobject{}Additional virtual module mappings supplied to the server and client builds
componentsobject{}Maps JSX tag names to component imports, enabling JSX-in-MDX (see below)
bundlerWebBundlerVite adapterAdapter that renders server entries and writes the client and HTML output (see below)

The head object controls the project-specific markup injected into the document <head> (rendered into the template's ${head} placeholder). It has three keys:

KeyTypeDescription
metaarray<meta> tags. Each entry is an attribute bag, e.g. { name: 'description', content: '…' }
linksarray<link> tags. Each entry is an attribute bag, e.g. { rel: 'icon', href: '…' }
htmlarrayRaw HTML strings appended verbatim — an escape hatch for anything not expressible above

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 defaults are Node.js-branded — override head entirely to brand the output for any project:

// doc-kit.config.mjs
export default {
  web: {
    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, including og:title (which mirrors the per-page title) and og:type. The UI stylesheet bundles its fonts locally.

The bundler option accepts a small Doc Kit adapter rather than configuration for a particular build system:

MethodResponsibility
getEntryIdReturn the module identifier placed in the populated HTML for an API name
renderBundle and execute the server entries, returning a Map of API name to rendered HTML
buildBundle the client entries, process the populated pages, and write the complete output

Both render and build receive { entries, virtualImports, config }; build also receives pages. Entry maps use ${api}.jsx keys, rendered server results use api keys, and page maps use output-relative HTML file names. config is the resolved web 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 => ({
  getEntryId: api => `virtual:doc-kit/client/${api}.jsx`,

  async render({ entries, virtualImports, config }) {
    // Materialize or load the in-memory modules, run webpack's server target,
    // execute each emitted entry, and return Map<api, renderedHtml>.
  },

  async build({ entries, virtualImports, pages, config }) {
    // Run webpack's browser target, inject its emitted assets into `pages`,
    // and write the HTML and assets to config.output.
  },
});

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 '@node-core/doc-kit/src/generators/web/bundlers/vite.mjs';
import myVitePlugin from './my-vite-plugin.mjs';

export default {
  web: {
    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 temporary 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 manifests are optional. Pass build: { manifest: true } or a manifest file name to createViteBundler when another tool needs one. The generated HTML already references the correct hashed scripts, stylesheets, imported assets, and module preloads.

Function-valued plugins and hooks are supported because the web generator runs on the main thread and does not serialize the bundler to a worker.

AliasDefaultDescription
#theme/Logo@node-core/ui-components/Common/NodejsLogoLogo rendered inside the navigation bar
#theme/NavigationBuilt-in NavBar componentTop navigation bar
#theme/SidebarBuilt-in SideBar componentSidebar with version selector and page links
#theme/MetabarBuilt-in MetaBar componentMetadata bar displayed alongside page content
#theme/FooterBuilt-in NoOp component (renders nothing)Optional footer rendered at the bottom of each page
#theme/LayoutBuilt-in Layout componentOutermost wrapper around the full page

Override any alias in your config file to swap in a custom component:

// doc-kit.config.mjs
export default {
  web: {
    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 {
  web: {
    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 web 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.

All scalar (non-object) configuration values are automatically exported. The defaults include:

ExportTypeDescription
projectstringProject name (e.g. 'Node.js')
repositorystringGitHub repository in owner/repo format
versionstringCurrent version label (e.g. 'v22.x')
versionsArray<{ url, label, major }>Pre-computed version entries with labels and URL templates (only {path} remains for per-page use)
editURLstringPartially populated "edit this page" URL template (only {path} remains)
pagesArray<[string, string]>Sorted [name, path] tuples for sidebar navigation
useAbsoluteURLsbooleanWhether internal links use absolute URLs (mirrors config value)
baseURLstringBase URL for the documentation site (used when useAbsoluteURLs is true)
languageDisplayNameMapMap<string, string>Shiki language alias → display name map for code blocks
remoteConfigUrlstringMirrors the configured remoteConfigUrl (fetched client-side by RemoteLoadableBanner to load announcement banners)
serverbooleanWhether the current bundle is the server build

When 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>
);

The Layout component receives the following props:

PropTypeDescription
metadataobjectSerialized page metadata — all YAML frontmatter properties plus addedIn, basename, path, and any custom user-defined fields
headingsArrayPre-computed table of contents heading entries
readingTimestringEstimated reading time (e.g. '5 min read')
childrenComponentChildrenProcessed page content

Custom Layout components can use any combination of these props 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.

VariableTypeDescription
titlestringFully resolved page title (e.g. 'File system | Node.js v22.x')
dehydratedstringServer-rendered HTML for the page content
entrypointstringAdapter-provided module identifier for this page's hydration
speculationRulesstringSpeculation rules JSON for prefetching
themeScriptstringInline script that applies the saved theme before paint
rootstringRelative or absolute path to the site root
metadataobjectFull page metadata (frontmatter, path, heading, etc.)
configobjectThe resolved web generator configuration
headstringPre-rendered <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>
<script type="module" src="${entrypoint}"></script>

The configured adapter processes each populated page. It must replace or resolve entrypoint, include that page's scripts and stylesheets, and write the final HTML.