{"$schema":"https://doc-kit.nodejs.org/schemas/api-doc/1.0.1.json","id":"publishing","path":"/publishing","type":"module","module":"publishing","title":"Publishing your docs","introducedIn":null,"sourceLink":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"`doc-kit generate` writes a static site to one directory: the HTML pages,\ntheir scripts and stylesheets, and every other output you target. To publish\nit, build with your production settings, then upload that directory to any\nstatic host. This guide covers both, with recipes for common hosts.","summary":"`doc-kit generate` writes a static site to one directory: the HTML pages, their scripts and stylesheets, and every other output you target. To publish it, build with your production settings, then upload that directory to any static host. This guide covers both, with recipes for common hosts.","examples":[],"children":[{"kind":"section","id":"build-for-production","name":"Build for production","title":"Build for production","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"In your configuration file, set where the site will live and which version it\ndocuments:\n\n```js displayName=\"doc-kit.config.mjs\"\nexport default {\n  target: ['html', 'orama-db', 'json', 'sitemap', 'llms-txt'],\n\n  global: {\n    project: 'Kittens',\n    version: '1.0.0',\n    input: ['docs/**/*.md'],\n    output: 'out',\n    baseURL: 'https://kittens.example.com',\n    minify: true,\n  },\n};\n```\n\nThen build:\n\n```bash\nnpx doc-kit generate\n```\n\nThree settings matter most for a published site:\n\n* `baseURL` is the URL the site is published at, without a trailing slash.\n  The sitemap, `llms.txt` and absolute links are built from it.\n* `version` is the version your docs describe, shown in page titles. Without\n  it, titles show the version of Node.js running the build.\n* `minify` minifies the pages' HTML, and writes the JSON outputs, the search\n  index included, without whitespace. Scripts and stylesheets are minified\n  either way. The `--minify` flag sets it too.\n\n> \\[!WARNING]\n> Without `baseURL`, `sitemap.xml` and `llms.txt` are still written, but every\n> URL in them starts with a literal `{baseURL}`.\n\nThe [configuration reference](./configuration.html#global-options) lists the\nother global options.","summary":"In your configuration file, set where the site will live and which version it documents:","examples":[{"language":"js","displayName":"doc-kit.config.mjs","code":"export default {\n  target: ['html', 'orama-db', 'json', 'sitemap', 'llms-txt'],\n\n  global: {\n    project: 'Kittens',\n    version: '1.0.0',\n    input: ['docs/**/*.md'],\n    output: 'out',\n    baseURL: 'https://kittens.example.com',\n    minify: true,\n  },\n};"},{"language":"bash","displayName":null,"code":"npx doc-kit generate"}],"children":[]},{"kind":"section","id":"check-it-locally","name":"Check it locally","title":"Check it locally","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"```bash\nnpx doc-kit preview\n```\n\n`preview` serves the output directory (`./out` by default) on\n<http://localhost:3000>. Without options, it serves what is already there; pass\nany `generate` option, such as `--config-file doc-kit.config.mjs`, and it builds\nfirst. `--port` and `--host` change where it listens; the\n[CLI reference](./cli.html) has the details.\n\nAlways open the pages over HTTP, never straight from disk. The pages load\ntheir scripts as JavaScript modules, request their stylesheet with CORS, and\nfetch the search index. Browsers block all three for `file://` pages, which\nthen show up unstyled and inert.\n\nThe preview server is for checking a build. It doesn't compress responses or\nset cache headers, so leave those to your host.","summary":"`preview` serves the output directory (`./out` by default) on http://localhost:3000. Without options, it serves what is already there; pass any `generate` option, such as `--config-file doc-kit.config.mjs`, and it builds first. `--port` and `--host` change where it listens; the CLI reference has the details.","examples":[{"language":"bash","displayName":null,"code":"npx doc-kit preview"}],"children":[]},{"kind":"section","id":"ship-what-the-pages-link-to","name":"Ship what the pages link to","title":"Ship what the pages link to","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"Besides its pages and `assets/`, the site links to files that other generators\nwrite, or that you copy in yourself:\n\n| File            | Used by                                                            | How it gets there              |\n| --------------- | ------------------------------------------------------------------ | ------------------------------ |\n| `orama-db.json` | The search box                                                     | Add `orama-db` to `target`.    |\n| `<page>.json`   | The \"View as JSON\" link on each page                               | Add `json` to `target`.        |\n| `<page>.md`     | The \"View as MD\" link on each page, `llms.txt` and `llms-full.txt` | Copy your Markdown, see below. |\n| `404.html`      | Your host, for URLs that don't exist                               | Written by default.            |\n\nThe search box only appears when the search index is built in the same run as\nthe pages. If you build the index separately, set `html.showSearchBox` to\n`true`.\n\nTo copy files into the output as part of the build, list them in\n`pathsToCopy`, relative to the working directory. A string copies a path under\nits own name (`'public'` lands in `out/public/`), and an object maps each\nsource to a path inside the output. The default list is\n`['assets', 'public', 'static']`, and setting `pathsToCopy` replaces it rather\nthan adding to it. This copies your Markdown next to the pages:\n\n```js displayName=\"doc-kit.config.mjs\"\nexport default {\n  global: {\n    // docs/kittens.md is published as kittens.md, next to kittens.html\n    pathsToCopy: [{ docs: '.' }],\n  },\n};\n```","summary":"Besides its pages and `assets/`, the site links to files that other generators write, or that you copy in yourself:","examples":[{"language":"js","displayName":"doc-kit.config.mjs","code":"export default {\n  global: {\n    // docs/kittens.md is published as kittens.md, next to kittens.html\n    pathsToCopy: [{ docs: '.' }],\n  },\n};"}],"children":[]},{"kind":"section","id":"page-urls","name":"Page URLs","title":"Page URLs","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"Each page is written as `<name>.html`, and the links doc-kit makes between\nyour pages carry that extension, so the site works on any static host without\nconfiguration. Many hosts also serve a page at its extensionless URL\n(`/kittens`), and some redirect `/kittens.html` there, such as Vercel with\n`cleanUrls` and Cloudflare Workers by default. The site follows those\nredirects when it navigates, so either setup works.\n\nType links are the exception: doc-kit uses the paths in your type map as\nwritten. Those that [`@doc-kit/typedoc`](./packages/typedoc.html) writes have no\nextension, so they need a host that serves `/kittens` from `kittens.html`.\n\nIf your host redirects to extensionless URLs, list those in the sitemap, so\nsearch engines don't crawl redirects:\n\n```js displayName=\"doc-kit.config.mjs\"\nexport default {\n  sitemap: {\n    // `{path}` is the page's path without its extension, starting with `/`\n    pageURL: '{baseURL}{path}',\n  },\n};\n```\n\nThe [sitemap](./generators/sitemap.html) and [`llms.txt`](./generators/llms-txt.html)\npages list their URL templates and defaults.","summary":"Each page is written as `<name>.html`, and the links doc-kit makes between your pages carry that extension, so the site works on any static host without configuration. Many hosts also serve a page at its extensionless URL (`/kittens`), and some redirect `/kittens.html` there, such as Vercel with `cleanUrls` and Cloudflare Workers by default. The site follows those redirects when it navigates, so either setup works.","examples":[{"language":"js","displayName":"doc-kit.config.mjs","code":"export default {\n  sitemap: {\n    // `{path}` is the page's path without its extension, starting with `/`\n    pageURL: '{baseURL}{path}',\n  },\n};"}],"children":[{"kind":"section","id":"hosting-under-a-subpath","name":"Hosting under a subpath","title":"Hosting under a subpath","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"Pages reach each other and their assets through relative URLs, so a site\nworks under any path, such as a GitHub Pages project site at\n`https://<owner>.github.io/<repo>/`. Two pages load their assets from the root\nof the domain instead: `404.html`, which hosts serve at any missing URL, and\n`all.html`. Turn on `useAbsoluteURLs`, and they load them from `baseURL`:\n\n```js displayName=\"doc-kit.config.mjs\"\nexport default {\n  global: {\n    baseURL: 'https://owner.github.io/repo',\n  },\n\n  html: {\n    useAbsoluteURLs: true,\n  },\n};\n```\n\nEvery link then points at `baseURL`, so when you preview that build locally,\nfollowing a link takes you to the published site. The logo in the default\nnavigation bar links to the root of the domain either way; to change that,\nreplace `#theme/Navigation` (see [Customizing the site](./customization.html)).","summary":"Pages reach each other and their assets through relative URLs, so a site works under any path, such as a GitHub Pages project site at `https://<owner>.github.io/<repo>/`. Two pages load their assets from the root of the domain instead: `404.html`, which hosts serve at any missing URL, and `all.html`. Turn on `useAbsoluteURLs`, and they load them from `baseURL`:","examples":[{"language":"js","displayName":"doc-kit.config.mjs","code":"export default {\n  global: {\n    baseURL: 'https://owner.github.io/repo',\n  },\n\n  html: {\n    useAbsoluteURLs: true,\n  },\n};"}],"children":[]}]},{"kind":"section","id":"cache-the-assets","name":"Cache the assets","title":"Cache the assets","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"Every file the `html` generator writes to `assets/` (scripts, stylesheets,\nfonts) is named after a hash of its content, so a changed file always gets a\nnew name. Serve that directory with a long-lived, immutable cache, and let\neverything else (the pages, the search index) revalidate:\n\n```text\n/assets/*   Cache-Control: public, max-age=31536000, immutable\n```\n\nMost hosts revalidate every file on every load by default, which costs a\nrequest per asset each time someone opens the site. The Netlify, Vercel and\nCloudflare Workers [host recipes](#host-recipes) below set this header (GitHub\nPages doesn't let you). Within a visit, the site swaps pages in client-side in\nbrowsers that support it, so moving between pages doesn't load the assets\nagain.\n\n> \\[!CAUTION]\n> Keep your own files out of a project-level `assets/` directory. The default\n> `pathsToCopy` copies it into `out/assets/` as is, without hashed names, so a\n> changed file would stay cached for a year.","summary":"Every file the `html` generator writes to `assets/` (scripts, stylesheets, fonts) is named after a hash of its content, so a changed file always gets a new name. Serve that directory with a long-lived, immutable cache, and let everything else (the pages, the search index) revalidate:","examples":[{"language":"text","displayName":null,"code":"/assets/*   Cache-Control: public, max-age=31536000, immutable"}],"children":[]},{"kind":"section","id":"compress-the-pages","name":"Compress the pages","title":"Compress the pages","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"Pages, scripts and the search index are text, and shrink a lot when served\ncompressed (gzip, brotli or zstd). Cloudflare, Vercel, Netlify and GitHub\nPages compress on their own. If you serve the output yourself, turn\ncompression on:\n\n* **nginx:** `gzip on;` compresses HTML; add the other types with\n  `gzip_types text/css text/javascript application/javascript application/json image/svg+xml;`.\n* **Caddy:** `encode zstd gzip`.\n* **Apache:** with `mod_deflate`,\n  `AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json`.\n* **Amazon S3:** serve the bucket through CloudFront, with **Compress objects\n  automatically** on.","summary":"Pages, scripts and the search index are text, and shrink a lot when served compressed (gzip, brotli or zstd). Cloudflare, Vercel, Netlify and GitHub Pages compress on their own. If you serve the output yourself, turn compression on:","examples":[],"children":[]},{"kind":"section","id":"host-recipes","name":"Host recipes","title":"Host recipes","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"Each recipe assumes a `doc-kit.config.mjs` that writes to `out`, with\n`@doc-kit/cli` (and `@doc-kit/generator-react`, for the site) in your\n`devDependencies`.","summary":"Each recipe assumes a `doc-kit.config.mjs` that writes to `out`, with `@doc-kit/cli` (and `@doc-kit/generator-react`, for the site) in your `devDependencies`.","examples":[],"children":[{"kind":"section","id":"github-pages","name":"GitHub Pages","title":"GitHub Pages","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"In your repository's settings, under **Pages**, set the source to **GitHub\nActions**, then add a workflow that builds the site and deploys it:\n\n```yaml displayName=\".github/workflows/docs.yml\"\nname: Publish docs\n\non:\n  push:\n    branches: [main]\n\npermissions:\n  contents: read\n\nconcurrency:\n  group: pages\n  cancel-in-progress: false\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v7\n      - uses: actions/setup-node@v7\n        with:\n          node-version: 24\n      - run: npm ci\n      - run: npx doc-kit generate\n      - uses: actions/upload-pages-artifact@v5\n        with:\n          path: out\n\n  deploy:\n    needs: build\n    runs-on: ubuntu-latest\n    permissions:\n      pages: write\n      id-token: write\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    steps:\n      - id: deployment\n        uses: actions/deploy-pages@v5\n```\n\nGitHub Pages serves `404.html` for missing pages, but doesn't let you set\nresponse headers, so the assets keep its default caching. A project site\nlives under a subpath; see [Hosting under a subpath](#hosting-under-a-subpath).","summary":"In your repository's settings, under **Pages**, set the source to **GitHub Actions**, then add a workflow that builds the site and deploys it:","examples":[{"language":"yaml","displayName":".github/workflows/docs.yml","code":"name: Publish docs\n\non:\n  push:\n    branches: [main]\n\npermissions:\n  contents: read\n\nconcurrency:\n  group: pages\n  cancel-in-progress: false\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v7\n      - uses: actions/setup-node@v7\n        with:\n          node-version: 24\n      - run: npm ci\n      - run: npx doc-kit generate\n      - uses: actions/upload-pages-artifact@v5\n        with:\n          path: out\n\n  deploy:\n    needs: build\n    runs-on: ubuntu-latest\n    permissions:\n      pages: write\n      id-token: write\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    steps:\n      - id: deployment\n        uses: actions/deploy-pages@v5"}],"children":[]},{"kind":"section","id":"netlify","name":"Netlify","title":"Netlify","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"Netlify installs your dependencies, runs the build `command` and publishes the\n`publish` directory:\n\n```toml displayName=\"netlify.toml\"\n[build]\n  command = \"npx doc-kit generate\"\n  publish = \"out\"\n\n[[headers]]\n  for = \"/assets/*\"\n  [headers.values]\n    Cache-Control = \"public, max-age=31536000, immutable\"\n```","summary":"Netlify installs your dependencies, runs the build `command` and publishes the `publish` directory:","examples":[{"language":"toml","displayName":"netlify.toml","code":"[build]\n  command = \"npx doc-kit generate\"\n  publish = \"out\"\n\n[[headers]]\n  for = \"/assets/*\"\n  [headers.values]\n    Cache-Control = \"public, max-age=31536000, immutable\""}],"children":[]},{"kind":"section","id":"vercel","name":"Vercel","title":"Vercel","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"```json displayName=\"vercel.json\"\n{\n  \"buildCommand\": \"npx doc-kit generate\",\n  \"outputDirectory\": \"out\",\n  \"cleanUrls\": true,\n  \"headers\": [\n    {\n      \"source\": \"/assets/(.*)\",\n      \"headers\": [\n        {\n          \"key\": \"Cache-Control\",\n          \"value\": \"public, max-age=31536000, immutable\"\n        }\n      ]\n    }\n  ]\n}\n```\n\n`cleanUrls` serves each page without its `.html` and redirects the `.html` URL\nthere. Drop it to keep the extensions; otherwise, set the sitemap's `pageURL`\nto match (see [Page URLs](#page-urls)).","summary":"`cleanUrls` serves each page without its `.html` and redirects the `.html` URL there. Drop it to keep the extensions; otherwise, set the sitemap's `pageURL` to match (see Page URLs).","examples":[{"language":"json","displayName":"vercel.json","code":"{\n  \"buildCommand\": \"npx doc-kit generate\",\n  \"outputDirectory\": \"out\",\n  \"cleanUrls\": true,\n  \"headers\": [\n    {\n      \"source\": \"/assets/(.*)\",\n      \"headers\": [\n        {\n          \"key\": \"Cache-Control\",\n          \"value\": \"public, max-age=31536000, immutable\"\n        }\n      ]\n    }\n  ]\n}"}],"children":[]},{"kind":"section","id":"cloudflare-workers","name":"Cloudflare Workers","title":"Cloudflare Workers","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"Cloudflare Workers serves the output as static assets. Its response headers\ncome from a `_headers` file at the root of the output, which `pathsToCopy`\ncan put there:\n\n```jsonc displayName=\"wrangler.jsonc\"\n{\n  \"name\": \"kittens-docs\",\n  \"compatibility_date\": \"2026-10-01\",\n  \"assets\": {\n    \"directory\": \"./out\",\n    \"html_handling\": \"auto-trailing-slash\",\n    \"not_found_handling\": \"404-page\",\n  },\n}\n```\n\n```text displayName=\"public/_headers\"\n/assets/*\n  Cache-Control: public, max-age=31536000, immutable\n```\n\n```js displayName=\"doc-kit.config.mjs\"\nexport default {\n  global: {\n    // Replaces the default list: copies public/ (with _headers) to the root of\n    // out/, and docs/ for the .md files the pages link to\n    pathsToCopy: [{ public: '.', docs: '.' }],\n  },\n};\n```\n\n`auto-trailing-slash` serves `/kittens` from `kittens.html` and redirects\n`/kittens.html` there, and `404-page` answers missing URLs with `404.html`.\nAs with Vercel, set the sitemap's `pageURL` to match (see\n[Page URLs](#page-urls)). Build, then deploy with Wrangler:\n\n```bash\nnpx doc-kit generate\nnpx wrangler deploy\n```","summary":"Cloudflare Workers serves the output as static assets. Its response headers come from a `_headers` file at the root of the output, which `pathsToCopy` can put there:","examples":[{"language":"jsonc","displayName":"wrangler.jsonc","code":"{\n  \"name\": \"kittens-docs\",\n  \"compatibility_date\": \"2026-10-01\",\n  \"assets\": {\n    \"directory\": \"./out\",\n    \"html_handling\": \"auto-trailing-slash\",\n    \"not_found_handling\": \"404-page\",\n  },\n}"},{"language":"text","displayName":"public/_headers","code":"/assets/*\n  Cache-Control: public, max-age=31536000, immutable"},{"language":"js","displayName":"doc-kit.config.mjs","code":"export default {\n  global: {\n    // Replaces the default list: copies public/ (with _headers) to the root of\n    // out/, and docs/ for the .md files the pages link to\n    pathsToCopy: [{ public: '.', docs: '.' }],\n  },\n};"},{"language":"bash","displayName":null,"code":"npx doc-kit generate\nnpx wrangler deploy"}],"children":[]}]},{"kind":"section","id":"publishing-several-versions","name":"Publishing several versions","title":"Publishing several versions","scope":"module","overloadOf":null,"stability":null,"added":[],"deprecated":[],"removed":[],"napiVersion":[],"changes":[],"description":"The version selector in the sidebar lists the releases in `global.changelog`\n(or `--changelog`); a site built without one has no selector. Build each\nversion on its own, with its `version` and `baseURL`, and publish it where\n`html.pageURL` says it lives:\n\n```js displayName=\"doc-kit.config.mjs\"\nexport default {\n  global: {\n    version: '3.0.0',\n    baseURL: 'https://kittens.example.com/v3.x',\n    changelog: './CHANGELOG.md',\n  },\n\n  html: {\n    // Where each version's copy of a page lives\n    pageURL: 'https://kittens.example.com/{version}{path}.html',\n  },\n};\n```\n\nIn `pageURL`, `{version}` becomes `v<major>.x`, or `v<major>.<minor>.x` for a\nrelease whose minor version isn't 0 (`v3.x`, `v2.1.x`), and `{path}` the\npage's path.\n\n`changelog` takes a URL or a path to a Markdown file that lists releases one\nper line, in the format of the\n[Node.js changelog](https://github.com/nodejs/node/blob/main/CHANGELOG.md):\n\n```text displayName=\"CHANGELOG.md\"\n* [Node.js 3.0](https://kittens.example.com/v3.x/) Current\n* [Node.js 2.1](https://kittens.example.com/v2.1.x/) **Long Term Support**\n```\n\nEach line needs the `*` bullet, the literal `Node.js` label, whatever your\nproject is called, and some notes after the link. Lines in any other shape are\nskipped without an error. The link's target is ignored, since the selector\nbuilds its links from `html.pageURL`, and notes that mention \"Long Term\nSupport\" mark an LTS release.\n\nIn a JavaScript configuration file, you can list the releases instead, each\nas `{ version, isLts, isCurrent }` with a version from the `semver` package.\n`isCurrent` adds \"(Current)\" to the release's label in the selector, which the\nMarkdown file can't do:\n\n```js displayName=\"doc-kit.config.mjs\"\nimport { coerce } from 'semver';\n\nexport default {\n  global: {\n    changelog: [\n      { version: coerce('3.0.0'), isLts: false, isCurrent: true },\n      { version: coerce('2.1.0'), isLts: true },\n    ],\n  },\n};\n```\n\nDocs built for an older release keep the list they were built with. To update\nit without rebuilding them, point `html.remoteConfigUrl` at a JSON file whose\n`versions` replace the built-in list; the\n[website generator](./generators/html.html#remote-configuration) describes its\nshape.\n\nOnce your site is online, [Creating generators](./creating-generators.html)\nshows how to add outputs doc-kit doesn't ship, such as an RSS feed.","summary":"The version selector in the sidebar lists the releases in `global.changelog` (or `--changelog`); a site built without one has no selector. Build each version on its own, with its `version` and `baseURL`, and publish it where `html.pageURL` says it lives:","examples":[{"language":"js","displayName":"doc-kit.config.mjs","code":"export default {\n  global: {\n    version: '3.0.0',\n    baseURL: 'https://kittens.example.com/v3.x',\n    changelog: './CHANGELOG.md',\n  },\n\n  html: {\n    // Where each version's copy of a page lives\n    pageURL: 'https://kittens.example.com/{version}{path}.html',\n  },\n};"},{"language":"text","displayName":"CHANGELOG.md","code":"* [Node.js 3.0](https://kittens.example.com/v3.x/) Current\n* [Node.js 2.1](https://kittens.example.com/v2.1.x/) **Long Term Support**"},{"language":"js","displayName":"doc-kit.config.mjs","code":"import { coerce } from 'semver';\n\nexport default {\n  global: {\n    changelog: [\n      { version: coerce('3.0.0'), isLts: false, isCurrent: true },\n      { version: coerce('2.1.0'), isLts: true },\n    ],\n  },\n};"}],"children":[]}]}