From c5118db4b0d0c99903a7695898661ca77ae716d9 Mon Sep 17 00:00:00 2001 From: bmuenzenmeyer Date: Fri, 10 Jul 2026 16:08:19 -0500 Subject: [PATCH 1/5] setup CICD --- .github/workflows/docs.yml | 48 ++++++++++++++++++++ .gitignore | 3 ++ package.json | 1 + scripts/build-docs-content.mjs | 82 ++++++++++++++++++++++++++++++++++ scripts/vercel-docs-build.sh | 9 ++++ www/doc-kit.config.mjs | 75 +++++++++++++++++++++++++++++++ 6 files changed, 218 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 scripts/build-docs-content.mjs create mode 100755 scripts/vercel-docs-build.sh create mode 100644 www/doc-kit.config.mjs diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..c4ed667e --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,48 @@ +name: Documentation Site + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Build docs site + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - name: Git Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build site + run: npm run docs:build + + - name: Upload site artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: docs-site + path: www/out diff --git a/.gitignore b/.gitignore index ac08a4ba..ef3c932b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ npm-debug.log out base +# Docs site content, assembled by scripts/build-docs-content.mjs +www/content + # Tests coverage junit.xml diff --git a/package.json b/package.json index cefc6454..00da24d2 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "prepare": "husky || exit 0", "run": "node bin/cli.mjs", "watch": "node --watch bin/cli.mjs", + "docs:build": "bash scripts/vercel-docs-build.sh", "changeset": "changeset", "changeset:version": "changeset version", "release": "changeset publish" diff --git a/scripts/build-docs-content.mjs b/scripts/build-docs-content.mjs new file mode 100644 index 00000000..74e72582 --- /dev/null +++ b/scripts/build-docs-content.mjs @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +// Assembles `www/content/` — the input tree for the doc-kit documentation +// site — from three sources that live elsewhere in the repo: +// +// www/pages/*.md authored narrative pages, copied verbatim +// docs/*.md the existing reference docs +// src/generators/*/README.md per-generator config reference +// +// `www/content/` is a build artifact and is gitignored. Run this before +// invoking the `web` generator against it. + +import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const CONTENT = join(ROOT, 'www', 'content'); + +/** + * Collects the `{ name, markdown }` pages to write into `www/content/`. + * + * @returns {Promise>} + */ +const collectPages = async () => { + const pages = []; + + const pagesDir = join(ROOT, 'www', 'pages'); + for (const file of await readdir(pagesDir)) { + if (file.endsWith('.md')) { + pages.push({ + name: file, + markdown: await readFile(join(pagesDir, file), 'utf-8'), + }); + } + } + + const docsDir = join(ROOT, 'docs'); + for (const file of await readdir(docsDir)) { + if (file.endsWith('.md')) { + pages.push({ + name: file, + markdown: await readFile(join(docsDir, file), 'utf-8'), + }); + } + } + + const generatorsDir = join(ROOT, 'src', 'generators'); + for (const generator of await readdir(generatorsDir, { + withFileTypes: true, + })) { + if (!generator.isDirectory()) { + continue; + } + + const readme = join(generatorsDir, generator.name, 'README.md'); + + try { + pages.push({ + name: `generator-${generator.name}.md`, + markdown: await readFile(readme, 'utf-8'), + }); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + } + + return pages; +}; + +const pages = await collectPages(); + +await rm(CONTENT, { recursive: true, force: true }); +await mkdir(CONTENT, { recursive: true }); + +await Promise.all( + pages.map(({ name, markdown }) => writeFile(join(CONTENT, name), markdown)) +); + +console.log(`Wrote ${pages.length} pages to www/content/`); diff --git a/scripts/vercel-docs-build.sh b/scripts/vercel-docs-build.sh new file mode 100755 index 00000000..5fa7b11d --- /dev/null +++ b/scripts/vercel-docs-build.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# Build the doc-kit documentation site into `www/out/`. + +node scripts/build-docs-content.mjs + +node bin/cli.mjs generate \ + --config-file ./www/doc-kit.config.mjs \ + --log-level info diff --git a/www/doc-kit.config.mjs b/www/doc-kit.config.mjs new file mode 100644 index 00000000..70e277dd --- /dev/null +++ b/www/doc-kit.config.mjs @@ -0,0 +1,75 @@ +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = dirname(fileURLToPath(import.meta.url)); + +const { version } = JSON.parse( + readFileSync(join(ROOT, '..', 'package.json'), 'utf-8') +); + +const REPOSITORY = 'nodejs/doc-kit'; +const BASE_URL = 'https://doc-kit-docs.vercel.app'; + +const DESCRIPTION = + 'doc-kit is the documentation toolchain behind the Node.js API reference — ' + + 'a pipeline that turns API-shaped Markdown into HTML, JSON, man pages and more.'; + +/** @type {import('../src/utils/configuration/types').Configuration} */ +export default { + target: ['orama-db', 'web'], + + global: { + // `www/content/` is assembled by `scripts/build-docs-content.mjs`. + input: [join(ROOT, 'content', '*.md')], + output: join(ROOT, 'out'), + + version, + repository: REPOSITORY, + ref: 'main', + baseURL: BASE_URL, + minify: true, + + // Both default to fetching from nodejs/node over the network. This site has + // no Node.js release matrix and no `index.md`, and an array short-circuits + // the parse step, so pass empty ones rather than paying for the request. + changelog: [], + index: [], + }, + + 'jsx-ast': { + // `jsx-ast` otherwise synthesizes an `index.html` holding the Node.js API + // stability overview, and it silently overrides an authored `index.md`. + // This site has no stability metadata, so that page would render empty. + generateIndexPage: false, + }, + + web: { + project: 'doc-kit', + title: '{project} documentation', + + // The default is `{baseURL}/latest-{version}/api{path}.html`, which encodes + // Node.js's versioned-docs layout. This site publishes a single flat tree. + pageURL: `${BASE_URL}{path}.html`, + + // Pages are assembled into `www/content/` at build time, so there is no + // single source file a `{path}` template could point at. Link to the repo + // instead; a per-page link would need a `#theme/Metabar` override that maps + // each slug back to its true origin. + editURL: `https://github.com/${REPOSITORY}`, + + imports: { + // Sidebar order and grouping are not configurable; see the component. + '#theme/Sidebar': join(ROOT, 'theme', 'SideBar.jsx'), + }, + + head: { + meta: [ + { name: 'description', content: DESCRIPTION }, + { property: 'og:description', content: DESCRIPTION }, + ], + links: [], + html: [], + }, + }, +}; From 139fcb4c498c7dae1428f521821d49b7a4847038 Mon Sep 17 00:00:00 2001 From: bmuenzenmeyer Date: Fri, 10 Jul 2026 16:08:40 -0500 Subject: [PATCH 2/5] adds doc kit docs --- www/pages/getting-started.md | 84 ++++++++++++++++++++++++++++ www/pages/index.md | 57 +++++++++++++++++++ www/theme/SideBar.jsx | 103 +++++++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 www/pages/getting-started.md create mode 100644 www/pages/index.md create mode 100644 www/theme/SideBar.jsx diff --git a/www/pages/getting-started.md b/www/pages/getting-started.md new file mode 100644 index 00000000..c02bfe1b --- /dev/null +++ b/www/pages/getting-started.md @@ -0,0 +1,84 @@ +# Getting started + +This page takes you from an empty directory to a rendered documentation page. + +## Install + +```bash +npm install --save-dev @node-core/doc-kit +``` + +## Write a valid input document + +Create `docs/hello.md`: + +```markdown +# hello + +A one-line description of the module. + +## `hello.greet(name)` + +- `name` {string} The name to greet. +- Returns: {string} + +Greets `name`. +``` + +> If you omit the `#` heading, `doc-kit` emits no page for the file and still +> exits successfully. An empty output directory almost always means a missing +> level-one heading. + +## Render it + +The `legacy-html-all` target has no additional dependencies and is the quickest +way to see output: + +```bash +npx doc-kit generate \ + -t legacy-html-all \ + -i "docs/*.md" \ + -o out +``` + +Open `out/all.html`. + +## Render the modern site + +The `web` target produces the server-rendered, client-hydrated site that +nodejs.org uses — and that this site is built with: + +```bash +npx doc-kit generate \ + -t web \ + -i "docs/*.md" \ + -o out +``` + +Pair it with `orama-db` to add search: + +```bash +npx doc-kit generate -t web -t orama-db -i "docs/*.md" -o out +``` + +## Preview it locally + +The `web` output uses import maps and client-side hydration, so it must be +served over HTTP — opening the files directly with `file://` will not work. Any +static server does; for example: + +```bash +npx serve out +``` + +Then open the printed URL (usually ). The +`legacy-html-all` output from earlier has no such requirement — `out/all.html` +opens straight from disk. + +## Next steps + +- [Configuration](./configuration.html) — move these flags into + `doc-kit.config.mjs`. +- [The input specification](./specification.html) — the full Markdown contract. +- [`web` generator](./generator-web.html) — theming, `head`, and custom + components. diff --git a/www/pages/index.md b/www/pages/index.md new file mode 100644 index 00000000..82a25e80 --- /dev/null +++ b/www/pages/index.md @@ -0,0 +1,57 @@ +# doc-kit + +`@node-core/doc-kit` is the documentation toolchain behind the Node.js API +reference. It reads API-shaped Markdown and emits HTML, JSON, man pages, +search indexes, and `llms.txt`. + +This site is built by doc-kit, from doc-kit's own repository. The pages you are +reading were produced by the `web` generator. + +## It is a pipeline, not a Markdown converter + +doc-kit is not a general-purpose Markdown-to-HTML tool. Input is parsed once +into a structured model of an API, and from that single model many output +generators fan out — you run any subset of them in one command: + +``` +Raw Markdown Files + │ + [ast] parse to MDAST + │ + [metadata] extract structured API metadata + │ + ├─► [jsx-ast] ─► [web] server-rendered site + ├─► [legacy-html] ─► …-all classic HTML + ├─► [legacy-json] ─► …-all JSON + ├─► [json-simple] simplified JSON + ├─► [llms-txt] llms.txt + ├─► [man-page] man pages + ├─► [orama-db] search index + └─► [sitemap] sitemap.xml +``` + +Only some of these are things you ask for by name. `ast`, `metadata`, and +`jsx-ast` are internal stages — they run because something downstream depends on +them, and they are not valid `-t` targets. Everything in the fan-out below +`metadata` is a target you can pass to `-t`, and passing several at once reuses +the one shared parse rather than repeating it. The full list is in the +[generators reference](./generator-web.html). + +## The input contract + +Because `metadata` is looking for an API document, the shape of your Markdown +matters more than it would in a typical static-site generator. The most +important rule: + +> **Every page must begin with a level-one heading.** The first `#` becomes the +> page's identity — its sidebar label and its output filename. A file without +> one produces no page at all, and the build still exits `0`. + +See [the specification](./specification.html) for the full input format. + +## Start here + +- [Getting started](./getting-started.html) — render your first document. +- [Commands](./commands.html) — the `doc-kit` CLI surface. +- [Configuration](./configuration.html) — `doc-kit.config.mjs` reference. +- [Creating generators](./generators.html) — extend the pipeline. diff --git a/www/theme/SideBar.jsx b/www/theme/SideBar.jsx new file mode 100644 index 00000000..ee828a17 --- /dev/null +++ b/www/theme/SideBar.jsx @@ -0,0 +1,103 @@ +import SideBar from '@node-core/ui-components/Containers/Sidebar'; + +import { relativeOrAbsolute } from '../../src/generators/web/ui/utils/relativeOrAbsolute.mjs'; + +import { pages } from '#theme/config'; + +// `jsx-ast` sorts pages by a hardcoded list of Node.js-specific slugs and then +// alphabetically by heading text, which buries the narrative pages beneath the +// sixteen `` `x` Generator `` reference pages (a backtick sorts before letters). +// There is no configuration hook for this, so the ordering lives here instead. +// +// Pages absent from this list fall to the end of the group, alphabetically. +const GUIDE_ORDER = [ + '/index', + '/getting-started', + '/configuration', + '/commands', + '/generators', + '/specification', + '/comparators', +]; + +const isGenerator = ([, path]) => path.startsWith('/generator-'); + +/** + * Page labels are raw heading text, so a heading like `` ## `web` Generator `` + * arrives with its backticks intact. The sidebar renders the label verbatim, + * showing the literal backticks. Split on backtick pairs and wrap the enclosed + * spans in `` so they render as inline code; labels without backticks pass + * through as a plain string. + * + * @param {string} label + * @returns {import('react').ReactNode} + */ +const renderLabel = label => { + const segments = label.split('`'); + + if (segments.length === 1) { + return label; + } + + // Odd-indexed segments sat between a pair of backticks. + return segments.map((segment, index) => + index % 2 ? ( + {segment} + ) : ( + {segment} + ) + ); +}; + +/** + * Orders the narrative pages by `GUIDE_ORDER`, leaving anything unlisted at the + * end in the alphabetical order `jsx-ast` already produced. + * + * @param {Array<[string, string]>} guides + * @returns {Array<[string, string]>} + */ +const orderGuides = guides => + guides.toSorted(([, a], [, b]) => { + const ai = GUIDE_ORDER.indexOf(a); + const bi = GUIDE_ORDER.indexOf(b); + + if (ai !== -1 && bi !== -1) { + return ai - bi; + } + + return ai !== -1 ? -1 : bi !== -1 ? 1 : 0; + }); + +/** + * Sidebar with the narrative guides split from the generator reference. + * + * The built-in sidebar also renders a version `