On this page

The web generator transforms JSX AST entries into complete web bundles, producing server-side rendered HTML pages, client-side JavaScript with code splitting, and bundled CSS styles.

The web generator accepts the following configuration options:

NameTypeDefaultDescription
outputstring-The directory where HTML, JavaScript, and CSS files will be 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
lightningcssobject{}Options spread into LightningCSS while bundling CSS (see below)
importsobjectSee belowObject mapping #theme/ aliases to component paths for customization
virtualImportsobject{}Additional virtual module mappings merged into the build
componentsobject{}Maps JSX tag names to component imports, enabling JSX-in-MDX (see below)
rolldownobject{}Options merged into the Rolldown build — extra plugins, etc. (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: og:title (mirrors the per-page title), og:type, and the font preconnects/stylesheet the bundled UI components rely on.

The lightningcss object is spread directly into LightningCSS while CSS is bundled, so any of its options — visitor (custom plugins), customAtRules, targets, drafts, and so on — are supported. The generator manages filename, code, cssModules, and resolver, so those are ignored.

// doc-kit.config.mjs
export default {
  web: {
    lightningcss: {
      customAtRules: {
        mixin: { prelude: '<custom-ident>', body: 'style-block' },
      },
      visitor: {
        Color(color) {
          // e.g. transform every color through a design-token map
          return color;
        },
      },
    },
  },
};

To apply more than one visitor, compose them with LightningCSS's composeVisitors helper and pass the result as visitor.

The rolldown object is merged into the Rolldown build call used for both the client and server bundles, so you can register extra plugins, inject compile-time constants, add module aliases, or set any other Rolldown option. The same config is applied to both builds — branch inside a plugin (or read the SERVER/CLIENT defines) if you need per-target behavior.

The merge follows these rules:

  • plugins are registered after the built-in virtual-module plugin (so they see the in-memory entry modules) and before the CSS loader.
  • transform.define and resolve.alias are merged key-by-key, so you can add entries without dropping the generator's. The built-in SERVER/ CLIENT defines and the react/react-dompreact/compat aliases (plus anything from imports) always win.
  • All other options (e.g. output.minify, treeshake, external, platform, logLevel) override the generator's defaults.
  • input and the built-in plugins are managed by the generator and cannot be overridden.

Functions (plugins, hooks, alias resolvers, etc.) are allowed here because the web generator runs entirely on the main thread. Unlike the chunked generators, its config is never serialized to a worker, so values that can't be structured-cloned are safe. Keep this in mind if web ever gains parallel processing: function-valued options would then need a serializable form.

// doc-kit.config.mjs
import myRolldownPlugin from './my-rolldown-plugin.mjs';

export default {
  web: {
    rolldown: {
      // Register additional plugins (run after the built-in ones).
      plugins: [myRolldownPlugin()],

      // Inject extra compile-time constants (merged with SERVER/CLIENT).
      transform: {
        define: {
          'process.env.ANALYTICS_ID': JSON.stringify('UA-XXXXX'),
        },
      },

      // Extend module resolution with custom aliases.
      resolve: {
        alias: {
          '@components': './src/components',
        },
      },
    },
  },
};
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)

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
importMapstringJSON import map for client-side module resolution
entrypointstringClient-side entry point filename with cache-bust query
speculationRulesstringSpeculation rules JSON for prefetching
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>
<link rel="stylesheet" href="${root}styles.css" />
<script type="importmap">
  ${importMap}
</script>