diff --git a/.changeset/node-preset.md b/.changeset/node-preset.md new file mode 100644 index 00000000..664f1319 --- /dev/null +++ b/.changeset/node-preset.md @@ -0,0 +1,5 @@ +--- +'@node-core/doc-kit': minor +--- + +Add the `@node-core/doc-kit/config` preset diff --git a/.changeset/plain-defaults.md b/.changeset/plain-defaults.md new file mode 100644 index 00000000..56ae327b --- /dev/null +++ b/.changeset/plain-defaults.md @@ -0,0 +1,6 @@ +--- +'@nodejs/doc-kit': patch +'@nodejs/doc-kit-generator-react': minor +--- + +Defaults are now project-neutral instead of Node.js-specific diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4702774f..65aee295 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -140,8 +140,8 @@ jobs: sparse-checkout: doc/api/assert.md path: node - - name: Build docs - run: npx doc-kit generate -t web -i "./node/doc/api/assert.md" -o out + - name: Build Node.js API docs + run: npx doc-kit generate -t web -i "./node/doc/api/assert.md" -o out --config-file ./e2e/doc-kit.config.mjs - name: Install Playwright browsers run: npx playwright install --with-deps diff --git a/.vercelignore b/.vercelignore index 7b1c7347..3627399c 100644 --- a/.vercelignore +++ b/.vercelignore @@ -1,2 +1,2 @@ # Ignored the cloned `node` folder -node +/node diff --git a/beta/doc-kit.config.mjs b/beta/doc-kit.config.mjs index 4d6b6c06..2a1f3776 100644 --- a/beta/doc-kit.config.mjs +++ b/beta/doc-kit.config.mjs @@ -1,4 +1,6 @@ export default { + extends: '@node-core/doc-kit/config', + html: { remoteConfigUrl: 'https://raw.githubusercontent.com/nodejs/doc-kit/main/beta/site.json', diff --git a/docs/configuration.md b/docs/configuration.md index a05f917f..983bef22 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,35 +1,99 @@ # Configuration -`doc-kit` discovers configuration with -[`cosmiconfig`](https://github.com/cosmiconfig/cosmiconfig): run the CLI -from your project directory and it looks for a `doc-kit.config.mjs` (or -`.js`/`.cjs`/`.ts`), a `.doc-kitrc` file (JSON or YAML, with any of the -usual extensions), or a `doc-kit` property in `package.json`. Use -`--config-file ` to load a specific file instead of searching. - -```mjs displayName="doc-kit.config.mjs" -/** @type {import('@nodejs/doc-kit/utils/configuration/types').Configuration} */ -export default { - // Which generators to run. Built-in names, or import specifiers - // resolving to custom generator modules. - target: ['html', 'orama-db'], +`doc-kit` uses [`cosmiconfig`](https://github.com/cosmiconfig/cosmiconfig) to +discover and load configuration. Run the CLI from your project directory and it +will automatically look for a `doc-kit` property in `package.json`, rc files +such as `.doc-kitrc.yml`, and module files such as `doc-kit.config.mjs`. + +Use `--config-file ` to load a specific file instead of searching. + +## Configuration File Format + +Configuration files can be either: + +- **JavaScript** (`.js`, `.mjs`, `.cjs`) +- **TypeScript** (`.ts`, when `typescript` is installed in the project) +- **JSON** (`.json`) +- **YAML** (`.yaml`, `.yml`, or an extensionless rc file) + +JavaScript and TypeScript configuration files export the configuration object. +JSON and YAML files contain the object directly. A `package.json` configuration +uses the `doc-kit` property: + +```json +{ + "doc-kit": { + "target": ["json-simple"], + "global": { + "input": "doc/api/*.md", + "output": "out" + } + } +} +``` +### Basic Example + +```javascript +export default { + // Targets, alternatively supplied by command line flags. Each entry is + // either a built-in shorthand name or an import specifier resolving to a + // generator module (e.g. '@my-scope/my-package/my-generator'). + target: ['orama-db', 'html'], global: { - input: ['docs/**/*.md'], - output: 'out', + project: 'My Project', version: '1.2.0', - baseURL: 'https://example.com/docs', - changelog: [], + input: 'docs/**/*.md', + output: 'dist/', + ignore: ['node_modules/', 'test/'], + baseURL: 'https://example.com/docs/', }, - // Generator-specific sections, keyed by generator name + threads: 4, + chunkSize: 10, + + // Generator-specific configurations html: { - project: 'My Project', + title: '{project} Documentation', + }, + + metadata: { + typeMap: { + MyThing: 'https://example.com/docs/my-thing.html', + }, }, }; ``` -## How values merge +## Extending Presets + +A configuration file may declare `extends`: one or more presets whose values +are merged underneath its own. Each entry is either an import specifier of a +module whose default export is a configuration object, or a path relative to +the configuration file: + +```mjs +export default { + // Build the docs the way nodejs.org does — branding, URL layouts, + // and release history included + extends: '@node-core/doc-kit/config', + + html: { + // Your own values still win over the preset + title: '{project} {version} API Reference', + }, +}; +``` + +`extends` also accepts an array; later presets take precedence over earlier +ones, and the configuration file itself wins over all of them. + +The built-in defaults are deliberately project-neutral: no repository, +site URL, release history, or branding is assumed. The +[`@node-core/doc-kit/config`](https://github.com/nodejs/doc-kit/tree/main/packages/node) +preset opts back into everything Node.js-specific. + +## Configuration Structure Three sources, in order of precedence: @@ -37,25 +101,23 @@ Three sources, in order of precedence: 2. **the configuration file**, which overrides 3. **built-in defaults**. -Each generator's section starts from its own defaults, then inherits every -`global` value it doesn't override. So `global.minify` applies to all -targets, while `'legacy-json': { minify: false }` exempts one. +| Property | Type | Description | Default | +| ------------ | ------------------------ | ----------------------------------------------------------------------------------------- | --------------------------------- | +| `project` | `string` | Name of the project being documented, used in titles, logos, and templated text | The `name` in your `package.json` | +| `version` | `string \| SemVer` | Documentation version | `process.version` | +| `minify` | `boolean` | Whether to minify output | `true` | +| `repository` | `string` | GitHub repository in `owner/repo` format; without one, repository UI is omitted | - | +| `ref` | `string` | Git reference (branch, tag, or commit SHA) | `'HEAD'` | +| `baseURL` | `string \| URL` | Base URL of the published site, used wherever absolute URLs are needed | - | +| `input` | `string[]` | Input directory path | - | +| `output` | `string` | Output directory path | - | +| `ignore` | `string[]` | Patterns to ignore | `[]` | +| `changelog` | `string \| URL \| Array` | Release history used for version selectors; a URL or path to parse, or a pre-parsed array | `[]` (single-version output) | +| `index` | `string \| URL \| Array` | Index URL | - | ## Global options -| Property | Type | Description | Default | -| ------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | -| `input` | `string \| string[]` | Glob patterns for the source Markdown files. Required (with `target`) to run. | — | -| `output` | `string` | The directory generated files are written to. | — | -| `ignore` | `string \| string[]` | Glob patterns excluded from `input`. | — | -| `version` | `string` | The version of the project being documented (coerced to semver). | `process.version` | -| `changelog` | `string \| URL \| Array` | Release history used to build version selectors. A URL or path to a `CHANGELOG.md` to parse, or a pre-parsed array — `[]` disables versioning (and the network fetch). | The Node.js `CHANGELOG.md` | -| `index` | `string \| URL \| Array` | An `index.md` listing section titles, or a pre-parsed array. | — | -| `baseURL` | `string \| URL` | The public URL of the published site; used wherever absolute links are needed (sitemaps, `llms.txt`, social metadata). | `'https://nodejs.org/docs'` | -| `repository` | `string` | GitHub repository in `owner/repo` form, used for source and edit links. | `'nodejs/node'` | -| `ref` | `string` | Git ref (branch, tag, or SHA) used in source links. | `'HEAD'` | -| `minify` | `boolean` | Minify the output, in whatever form it takes. | `true` | -| `pathsToCopy` | `Array` | Extra files or directories copied into the output. A string copies to `output/`; a `{ source: destination }` object controls the target path. Missing paths are skipped. | `['assets', 'public', 'static']` | +Each generator (e.g., `html`, `legacy-json`) can have its own configuration that overrides global settings: ## Execution options @@ -73,8 +135,9 @@ Each generator documents its own options on its reference page — see the ```js export default { - html: { - project: 'My Project', + global: { + version: '1.2.0', + minify: true, }, metadata: { @@ -84,3 +147,32 @@ export default { }, }; ``` + +## Configuration Merging + +Configurations are merged in the following order (higher sources take +precedence): + +1. **CLI options** (command-line arguments) +2. **Configuration file** (discovered or selected with `--config-file`) +3. **Presets** (listed in the configuration file's `extends`) +4. **Default values** (built-in defaults) + +## CLI Options Mapping + +CLI options map to configuration properties: + +| CLI Option | Config Property | Example | +| ---------------------- | ------------------ | ------------------------- | +| `--input ` | `global.input` | `--input src/` | +| `--output ` | `global.output` | `--output dist/` | +| `--ignore ` | `global.ignore[]` | `--ignore test/` | +| `--minify` | `global.minify` | `--minify` | +| `--git-ref ` | `global.ref` | `--git-ref v20.0.0` | +| `--version ` | `global.version` | `--version 20.0.0` | +| `--changelog ` | `global.changelog` | `--changelog https://...` | +| `--index ` | `global.index` | `--index file://...` | +| `--type-map ` | `metadata.typeMap` | `--type-map file://...` | +| `--target ` | `target` | `--target json` | +| `--threads ` | `threads` | `--threads 4` | +| `--chunk-size ` | `chunkSize` | `--chunk-size 10` | diff --git a/e2e/doc-kit.config.mjs b/e2e/doc-kit.config.mjs new file mode 100644 index 00000000..4dc5b37b --- /dev/null +++ b/e2e/doc-kit.config.mjs @@ -0,0 +1,7 @@ +export default { + html: { + // The announcement banner under test is opt-in: point it at the URL the + // spec intercepts (no real request is made). + remoteConfigUrl: 'https://nodejs.org/site.json', + }, +}; diff --git a/packages/core/src/utils/configuration/__tests__/index.test.mjs b/packages/core/src/utils/configuration/__tests__/index.test.mjs index a2cc957f..da73eab4 100644 --- a/packages/core/src/utils/configuration/__tests__/index.test.mjs +++ b/packages/core/src/utils/configuration/__tests__/index.test.mjs @@ -1,4 +1,7 @@ import assert from 'node:assert'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { describe, it, mock, beforeEach } from 'node:test'; // Mock dependencies @@ -96,6 +99,47 @@ describe('config.mjs', () => { ); assert.strictEqual(mockConfigSearch.mock.calls.length, 0); }); + + it('should merge extends presets underneath the config file', async () => { + const dir = mkdtempSync(join(tmpdir(), 'doc-kit-config-')); + + writeFileSync( + join(dir, 'base.mjs'), + 'export default { global: { project: "Base", ref: "base" }, html: { a: 1 } };' + ); + writeFileSync( + join(dir, 'other.mjs'), + 'export default { global: { project: "Other" }, html: { b: 2 } };' + ); + + mockConfigLoad.mock.mockImplementationOnce(async () => ({ + config: { + extends: ['./base.mjs', './other.mjs'], + global: { ref: 'own' }, + }, + filepath: join(dir, 'doc-kit.config.mjs'), + })); + + const result = await loadConfigFile('any'); + + // Later presets win over earlier ones; the file itself wins over all + assert.deepStrictEqual(result, { + global: { project: 'Other', ref: 'own' }, + html: { a: 1, b: 2 }, + }); + }); + + it('should resolve extends package specifiers from the config file', async () => { + mockConfigLoad.mock.mockImplementationOnce(async () => ({ + config: { extends: '@node-core/doc-kit/config' }, + filepath: join(process.cwd(), 'doc-kit.config.mjs'), + })); + + const result = await loadConfigFile('any'); + + assert.strictEqual(result.global.project, 'Node.js'); + assert.strictEqual(result.global.repository, 'nodejs/node'); + }); }); describe('createConfigFromCLIOptions', () => { @@ -250,6 +294,17 @@ describe('config.mjs', () => { assert.strictEqual(mockConfigSearch.mock.calls.length, 1); }); + it('should default to project-neutral values', async () => { + const config = await createRunConfiguration({}); + + // No repository, site, or release history is assumed; presets such as + // @node-core/doc-kit/config opt back into the Node.js values + assert.strictEqual(config.global.repository, undefined); + assert.strictEqual(config.global.baseURL, undefined); + assert.deepStrictEqual(config.global.changelog, []); + assert.strictEqual(typeof config.global.project, 'string'); + }); + it('should handle generator-specific overrides', async () => { mockConfigLoad.mock.mockImplementationOnce(async () => ({ config: createMockConfig({ diff --git a/packages/core/src/utils/configuration/index.mjs b/packages/core/src/utils/configuration/index.mjs index 965cfab5..5d00ee53 100644 --- a/packages/core/src/utils/configuration/index.mjs +++ b/packages/core/src/utils/configuration/index.mjs @@ -1,10 +1,13 @@ +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; import { cpus } from 'node:os'; +import { dirname, isAbsolute, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; import { isMainThread } from 'node:worker_threads'; import { cosmiconfig } from 'cosmiconfig'; import { coerce } from 'semver'; -import { CHANGELOG_URL, populate } from './templates.mjs'; import { loadGenerators, resolveGeneratorSpecifier, @@ -17,6 +20,20 @@ import { deepMerge } from '../misc.mjs'; const configExplorer = cosmiconfig('doc-kit'); +/** + * The name of the project being documented, from the manifest in the working + * directory. Generators use it for titles, logos, and templated text. + * + * @returns {string | undefined} + */ +const detectProject = () => { + try { + return JSON.parse(readFileSync('package.json', 'utf-8')).name; + } catch { + return undefined; + } +}; + /** * Get's the default configuration for the loaded generators * @@ -37,15 +54,13 @@ export const getDefaultConfig = (generators, config) => }, /** @type {import('./types').Configuration} */ ({ global: { + project: detectProject() ?? 'API Docs', version: process.version, minify: true, - repository: 'nodejs/node', ref: 'HEAD', - baseURL: 'https://nodejs.org/docs', - changelog: populate(CHANGELOG_URL, { - repository: 'nodejs/node', - ref: 'HEAD', - }), + // Without release history there is nothing to build a version picker + // from, so generators render single-version output. + changelog: [], pathsToCopy: ['assets', 'public', 'static'], }, @@ -58,6 +73,23 @@ export const getDefaultConfig = (generators, config) => }) ); +/** + * Resolves an `extends` entry of a configuration file into an importable + * URL: relative paths resolve against the configuration file, anything else + * resolves as a package import specifier (e.g. `@node-core/doc-kit/config`). + * + * @param {string} specifier - The `extends` entry + * @param {string} configFilePath - The configuration file it appears in + * @returns {string} A `file:` URL to import + */ +const resolveConfigExtends = (specifier, configFilePath) => { + if (specifier.startsWith('.') || isAbsolute(specifier)) { + return pathToFileURL(resolve(dirname(configFilePath), specifier)).href; + } + + return pathToFileURL(createRequire(configFilePath).resolve(specifier)).href; +}; + /** * Loads an explicit configuration file or searches for one using cosmiconfig. * @@ -69,7 +101,19 @@ export const loadConfigFile = async filePath => { ? await configExplorer.load(filePath) : await configExplorer.search(); - return result?.config ?? {}; + if (!result) { + return {}; + } + + let { extends: presets, ...config } = result.config ?? {}; + + for (const preset of enforceArray(presets ?? []).toReversed()) { + const module = await import(resolveConfigExtends(preset, result.filepath)); + + config = deepMerge(module.default ?? module, config); + } + + return config; }; /** diff --git a/packages/core/src/utils/configuration/types.d.ts b/packages/core/src/utils/configuration/types.d.ts index 3536ba05..d3b0c402 100644 --- a/packages/core/src/utils/configuration/types.d.ts +++ b/packages/core/src/utils/configuration/types.d.ts @@ -21,8 +21,14 @@ export type Configuration = { }; export type GlobalConfiguration = { - // The repository - repository: string; + // The name of the project being documented, used for titles, logos, and + // templated text (defaults to the `name` in the working directory's + // `package.json`) + project: string; + + // The repository (`owner/name`), used for source and edit links; when + // omitted, repository-specific UI (e.g. the GitHub link) is omitted + repository?: string; // The path to the input source files. This parameter accepts globs and can // be a glob when passed to a generator. @@ -49,8 +55,9 @@ export type GlobalConfiguration = { // A list of all the titles of all the documentation files index: Array<{ section: string; api: string }>; - // The base URL - baseURL: string | URL; + // The base URL of the published site; templates referencing `{baseURL}` + // (e.g. sitemap and llms-txt page URLs) need it to produce absolute URLs + baseURL?: string | URL; // Git ref (i.e. HEAD) ref: string; diff --git a/packages/node/package.json b/packages/node/package.json index 052e9cc4..3f7ec767 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -11,6 +11,7 @@ "exports": { "./addon-verify": "./src/addon-verify/index.mjs", "./api-links": "./src/api-links/index.mjs", + "./config": "./src/config/index.mjs", "./man-page": "./src/man-page/index.mjs", "./package.json": "./package.json" }, diff --git a/packages/node/src/config/__tests__/index.test.mjs b/packages/node/src/config/__tests__/index.test.mjs new file mode 100644 index 00000000..8b5875fb --- /dev/null +++ b/packages/node/src/config/__tests__/index.test.mjs @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import { existsSync } from 'node:fs'; +import { describe, it } from 'node:test'; + +import preset from '../index.mjs'; + +describe('Node.js preset', () => { + it('should brand the generators for nodejs.org', () => { + assert.equal(preset.global.project, 'Node.js'); + assert.equal(preset.global.repository, 'nodejs/node'); + assert.equal(preset.global.baseURL, 'https://nodejs.org/docs'); + assert.match(preset.global.changelog, /nodejs\/node/); + + assert.equal(preset.html.remoteConfigUrl, 'https://nodejs.org/site.json'); + assert.equal( + preset.html.imports['#theme/Logo'], + '@node-core/ui-components/Common/NodejsLogo' + ); + assert.ok(preset.html.editURL.includes('/doc/api{path}.md')); + }); + + it('should ship the Node.js llms.txt template', () => { + assert.ok(existsSync(preset['llms-txt'].templatePath)); + }); +}); diff --git a/packages/node/src/config/index.mjs b/packages/node/src/config/index.mjs new file mode 100644 index 00000000..62f6f038 --- /dev/null +++ b/packages/node/src/config/index.mjs @@ -0,0 +1,86 @@ +'use strict'; + +import { join } from 'node:path'; + +import { + CHANGELOG_URL, + GITHUB_EDIT_URL, + populate, +} from '@nodejs/doc-kit/utils/configuration/templates.mjs'; + +const NODE_REPOSITORY = { repository: 'nodejs/node', ref: 'HEAD' }; + +/** + * The Node.js preset: configures the project-neutral doc-kit generators the + * way nodejs.org builds its API documentation — branding, URL layouts, and + * release history included. + * + * Use it from a configuration file: + * + * ```mjs + * export default { + * extends: '@node-core/doc-kit/config', + * }; + * ``` + * + * @type {Partial} + */ +export default { + global: { + project: 'Node.js', + ...NODE_REPOSITORY, + baseURL: 'https://nodejs.org/docs', + changelog: populate(CHANGELOG_URL, NODE_REPOSITORY), + }, + + html: { + editURL: `${GITHUB_EDIT_URL}/doc/api{path}.md`, + pageURL: '{baseURL}/latest-{version}/api{path}.html', + remoteConfigUrl: 'https://nodejs.org/site.json', + + head: { + meta: [ + { + name: 'description', + content: + 'Node.js® is a free, open-source, cross-platform JavaScript ' + + 'runtime environment that lets developers create servers, web ' + + 'apps, command line tools and scripts.', + }, + { + property: 'og:description', + content: + 'Node.js® is a free, open-source, cross-platform JavaScript ' + + 'runtime environment that lets developers create servers, web ' + + 'apps, command line tools and scripts.', + }, + { + property: 'og:image', + content: + 'https://nodejs.org/en/next-data/og/announcement/Node.js%20%E2%80%94%20Run%20JavaScript%20Everywhere', + }, + ], + links: [ + { + rel: 'icon', + href: 'https://nodejs.org/static/images/favicons/favicon.png', + }, + ], + }, + + stylesheets: [join(import.meta.dirname, 'theme.css')], + + imports: { + '#theme/Logo': '@node-core/ui-components/Common/NodejsLogo', + }, + }, + + 'llms-txt': { + templatePath: join(import.meta.dirname, 'llms-template.txt'), + pageURL: '{baseURL}/latest/api{path}.md', + }, + + sitemap: { + indexURL: '{baseURL}/latest/api/', + }, +}; diff --git a/packages/node/src/config/llms-template.txt b/packages/node/src/config/llms-template.txt new file mode 100644 index 00000000..04da10b9 --- /dev/null +++ b/packages/node/src/config/llms-template.txt @@ -0,0 +1,7 @@ +# Node.js Documentation + +> Node.js is an open-source, cross-platform JavaScript runtime environment that executes JavaScript code outside a web browser. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient for building scalable network applications. + +Below are the sections of the API documentation. Look out especially towards the links that point towards guidance/introduction to the structure of this documentation. + +## API Documentations diff --git a/packages/node/src/config/theme.css b/packages/node/src/config/theme.css new file mode 100644 index 00000000..c2f2f8ed --- /dev/null +++ b/packages/node/src/config/theme.css @@ -0,0 +1,11 @@ +: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; +} diff --git a/packages/react/src/html/README.md b/packages/react/src/html/README.md index 7f875f7a..ff6b95db 100644 --- a/packages/react/src/html/README.md +++ b/packages/react/src/html/README.md @@ -14,20 +14,22 @@ its HTML or CSS. - `templatePath` {string} Path to the HTML template file. **Default:** `'template.html'`. - `project` {string} Project name used in page titles and the version selector. - **Default:** `'Node.js'`. + **Default:** inherited from `global.project`. - `title` {string} Title template for HTML pages (supports `{project}`, `{version}`). **Default:** `'{project} v{version} Documentation'`. - `useAbsoluteURLs` {boolean} When `true`, all internal links use absolute URLs based on `baseURL`. **Default:** `false`. - `editURL` {string} URL template for "edit this page" links. - **Default:** `'${GITHUB_EDIT_URL}/doc/api{path}.md'`. + **Default:** none — the "edit this page" link is omitted. - `pageURL` {string} URL template for documentation page links. - **Default:** `'{baseURL}/latest-{version}/api{path}.html'`. + **Default:** `'{baseURL}{path}.html'`. - `remoteConfigUrl` {string} URL fetched client-side at runtime for remote site config (currently used to power the announcement banner). - **Default:** `'https://nodejs.org/site.json'`. + **Default:** none — no runtime fetch, no banner. - `head` {Object} Configurable ``, ``, and raw markup for the document head. See [`head`](#head). +- `stylesheets` {Array} Paths to extra stylesheets bundled after the built-in + one. See [`stylesheets`](#stylesheets). **Default:** `[]`. - `imports` {Object} Object mapping `#theme/` aliases to component paths for customization. See [Default `imports`](#default-imports). - `virtualImports` {Object} Additional virtual module mappings supplied to the @@ -58,8 +60,7 @@ omitted. Using arrays of attribute bags (rather than `name → value` maps) mean 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: +The default `head` is empty — brand the output by supplying your own tags: ```js // doc-kit.config.mjs @@ -84,6 +85,44 @@ export default { > via `head`, including `og:title` (which mirrors the per-page title) and > `og:type`. The UI stylesheet bundles its fonts locally. +### `stylesheets` + +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: + +```css +/* 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; +} +``` + +```js +// doc-kit.config.mjs +import { join } from 'node:path'; + +export default { + html: { + stylesheets: [join(import.meta.dirname, 'theme.css')], + }, +}; +``` + ### `navigation` - `sidebar` {Array} Sidebar groups, each `{ groupName, items }`. Defaults to one @@ -242,8 +281,8 @@ runs on the main thread and does not serialize the bundler to a worker. ### Default `imports` -- `#theme/Logo` {string} Logo rendered inside the navigation bar. - **Default:** `'@node-core/ui-components/Common/NodejsLogo'`. +- `#theme/Logo` {string} Logo rendered inside the navigation bar. Defaults to + the built-in `ProjectName` component, which renders `project` as plain text. - `#theme/Navigation` {string} Top navigation bar. Defaults to the built-in `NavBar` component. - `#theme/Sidebar` {string} Sidebar with version selector and page links. @@ -330,7 +369,8 @@ import { project, repository, editURL } from '#theme/config'; ### Available exports - `project` {string} Project name (e.g. `'Node.js'`). -- `repository` {string} GitHub repository in `owner/repo` format. +- `repository` {string} GitHub repository in `owner/repo` format, or + `undefined` when none is configured. - `version` {string} Current version label (e.g. `'v22.x'`). - `versions` {Array} Pre-computed version entries, each `{ url, label, major }`, with labels and URL templates (only `{path}` remains for per-page use). diff --git a/packages/react/src/html/index.mjs b/packages/react/src/html/index.mjs index b061ffa6..461e6387 100644 --- a/packages/react/src/html/index.mjs +++ b/packages/react/src/html/index.mjs @@ -2,8 +2,6 @@ import { join } from 'node:path'; -import { GITHUB_EDIT_URL } from '@nodejs/doc-kit/utils/configuration/templates.mjs'; - import { generate } from './generate.mjs'; /** @@ -37,12 +35,9 @@ export default { */ defaultConfiguration: config => ({ templatePath: join(import.meta.dirname, 'template.html'), - project: 'Node.js', title: '{project} {version} Documentation', useAbsoluteURLs: false, - editURL: `${GITHUB_EDIT_URL}/doc/api{path}.md`, - pageURL: '{baseURL}/latest-{version}/api{path}.html', - remoteConfigUrl: 'https://nodejs.org/site.json', + pageURL: '{baseURL}{path}.html', // By default, the search box is only shown when we are _also_ building // search data. `target` holds resolved import specifiers, so match on the // subpath rather than an exact name. @@ -56,38 +51,16 @@ export default { // hatch. Structural/theme tags such as `og:type` are hardcoded in the // template instead. head: { - meta: [ - { - name: 'description', - content: - 'Node.js® is a free, open-source, cross-platform JavaScript ' + - 'runtime environment that lets developers create servers, web ' + - 'apps, command line tools and scripts.', - }, - { - property: 'og:description', - content: - 'Node.js® is a free, open-source, cross-platform JavaScript ' + - 'runtime environment that lets developers create servers, web ' + - 'apps, command line tools and scripts.', - }, - { - property: 'og:image', - content: - 'https://nodejs.org/en/next-data/og/announcement/Node.js%20%E2%80%94%20Run%20JavaScript%20Everywhere', - }, - ], - links: [ - { - rel: 'icon', - href: 'https://nodejs.org/static/images/favicons/favicon.png', - }, - ], + meta: [], + links: [], html: [], }, + // Extra stylesheets + stylesheets: [], + imports: { - '#theme/Logo': '@node-core/ui-components/Common/NodejsLogo', + '#theme/Logo': join(import.meta.dirname, './ui/components/ProjectName'), '#theme/Navigation': join(import.meta.dirname, './ui/components/NavBar'), '#theme/Sidebar': join(import.meta.dirname, './ui/components/SideBar'), '#theme/Metabar': join(import.meta.dirname, './ui/components/MetaBar'), diff --git a/packages/react/src/html/types.d.ts b/packages/react/src/html/types.d.ts index b6df9d78..c0ac4cfd 100644 --- a/packages/react/src/html/types.d.ts +++ b/packages/react/src/html/types.d.ts @@ -62,6 +62,8 @@ export type Configuration = { title: string; useAbsoluteURLs: boolean; head: HeadConfig; + // Paths to extra stylesheets + stylesheets: Array; imports: Record; virtualImports: Record; // Maps a JSX tag name to its import, enabling JSX-in-MDX. The string shorthand diff --git a/packages/react/src/html/ui/components/MetaBar/index.jsx b/packages/react/src/html/ui/components/MetaBar/index.jsx index 261498ea..5f705efd 100644 --- a/packages/react/src/html/ui/components/MetaBar/index.jsx +++ b/packages/react/src/html/ui/components/MetaBar/index.jsx @@ -52,7 +52,7 @@ const HeadingValue = ({ value, stability }) => { * @param {{ metadata: import('../../types').SerializedMetadata, headings: Array, readingTime: string }} props */ export default ({ metadata, headings = [], readingTime }) => { - const editThisPage = editURL.replace('{path}', metadata.path); + const editThisPage = editURL?.replace('{path}', metadata.path); const viewAs = [ ['JSON', `${metadata.basename}.json`], @@ -88,7 +88,7 @@ export default ({ metadata, headings = [], readingTime }) => { })} ), - Contribute: !metadata.synthetic && ( + Contribute: !metadata.synthetic && editThisPage && ( <> diff --git a/packages/react/src/html/ui/components/NavBar.jsx b/packages/react/src/html/ui/components/NavBar.jsx index c190a37e..a3a77f31 100644 --- a/packages/react/src/html/ui/components/NavBar.jsx +++ b/packages/react/src/html/ui/components/NavBar.jsx @@ -22,12 +22,14 @@ export default ({ metadata }) => ( > {showSearchBox && } - - - + {repository && ( + + + + )} ); diff --git a/packages/react/src/html/ui/components/ProjectName.jsx b/packages/react/src/html/ui/components/ProjectName.jsx new file mode 100644 index 00000000..e91a57b2 --- /dev/null +++ b/packages/react/src/html/ui/components/ProjectName.jsx @@ -0,0 +1,7 @@ +import { project } from '#theme/config'; + +/** + * Plain-text stand-in for `#theme/Logo`, used when a project has not + * configured a logo component of its own. + */ +export default props => {project}; diff --git a/packages/react/src/html/ui/index.css b/packages/react/src/html/ui/index.css index 9b21667c..14237df8 100644 --- a/packages/react/src/html/ui/index.css +++ b/packages/react/src/html/ui/index.css @@ -4,10 +4,20 @@ @import '@node-core/ui-components/styles/index.css'; @import '@node-core/rehype-shiki/index.css'; -/* Fonts */ +/* Variables */ :root { --font-open-sans: 'Open Sans Variable', sans-serif; --font-ibm-plex-mono: 'IBM Plex Mono', monospace; + + --color-brand-100: #f4f4f5; + --color-brand-200: #e4e4e7; + --color-brand-300: #c9c9ce; + --color-brand-400: #a8a8af; + --color-brand-500: #83838c; + --color-brand-600: #62626b; + --color-brand-700: #4b4b53; + --color-brand-800: #3a3a41; + --color-brand-900: #27272b; } is-land, diff --git a/packages/react/src/html/utils/__tests__/processing.test.mjs b/packages/react/src/html/utils/__tests__/processing.test.mjs index 9980c9ad..7312f530 100644 --- a/packages/react/src/html/utils/__tests__/processing.test.mjs +++ b/packages/react/src/html/utils/__tests__/processing.test.mjs @@ -99,12 +99,13 @@ describe('resolvePageRoot', () => { it('uses the configured base URL for synthetic pages with absolute URLs', async () => { getConfig('html').useAbsoluteURLs = true; + getConfig('html').baseURL = 'https://example.com/docs'; const result = resolvePageRoot({ path: '/404', synthetic: true, }); - assert.strictEqual(result, 'https://nodejs.org/docs/'); + assert.strictEqual(result, 'https://example.com/docs/'); getConfig('html').useAbsoluteURLs = false; }); diff --git a/packages/react/src/html/utils/__tests__/relativeOrAbsolute.test.mjs b/packages/react/src/html/utils/__tests__/relativeOrAbsolute.test.mjs index bb90e6e1..af9d1e6e 100644 --- a/packages/react/src/html/utils/__tests__/relativeOrAbsolute.test.mjs +++ b/packages/react/src/html/utils/__tests__/relativeOrAbsolute.test.mjs @@ -49,20 +49,21 @@ describe('relativeOrAbsolute (relative mode)', () => { describe('relativeOrAbsolute (absolute mode)', () => { beforeEach(() => { getConfig('html').useAbsoluteURLs = true; + getConfig('html').baseURL = 'https://example.com/docs'; }); it('returns an absolute URL to root', () => { const result = relativeOrAbsolute('/', '/api/fs'); - assert.strictEqual(result, 'https://nodejs.org/docs/'); + assert.strictEqual(result, 'https://example.com/docs/'); }); it('returns an absolute URL for a page path', () => { const result = relativeOrAbsolute('/http', '/fs'); - assert.strictEqual(result, 'https://nodejs.org/docs/http'); + assert.strictEqual(result, 'https://example.com/docs/http'); }); it('returns an absolute URL for a resource', () => { const result = relativeOrAbsolute('/orama-db.json', '/api/fs'); - assert.strictEqual(result, 'https://nodejs.org/docs/orama-db.json'); + assert.strictEqual(result, 'https://example.com/docs/orama-db.json'); }); }); diff --git a/packages/react/src/html/utils/config.mjs b/packages/react/src/html/utils/config.mjs index d2427d38..f69592ba 100644 --- a/packages/react/src/html/utils/config.mjs +++ b/packages/react/src/html/utils/config.mjs @@ -76,13 +76,18 @@ export function buildLanguageDisplayNameMap() { export default function createConfigSource(input, server = false) { const { version: configVersion, ...config } = getConfig('html'); - const editURL = populate(config.editURL, { - ...config, - version: `v${configVersion.version}`, - }); + const editURL = + config.editURL && + populate(config.editURL, { + ...config, + version: `v${configVersion.version}`, + }); const pageURL = populate(config.pageURL, config); const exports = { + repository: undefined, + baseURL: undefined, + remoteConfigUrl: undefined, ...omitKeys( config, // These are large or build-time-only keys, or may contain functions, so @@ -92,6 +97,7 @@ export default function createConfigSource(input, server = false) { 'index', 'imports', 'virtualImports', + 'stylesheets', 'components', 'head', 'bundler', diff --git a/packages/react/src/html/utils/generate.mjs b/packages/react/src/html/utils/generate.mjs index d5f0b51c..4663d314 100644 --- a/packages/react/src/html/utils/generate.mjs +++ b/packages/react/src/html/utils/generate.mjs @@ -51,7 +51,7 @@ export const createImportDeclaration = ( */ export default () => { // User-configured components (for JSX-in-MDX), merged with the built-ins. - const { components } = getConfig('html'); + const { components, stylesheets } = getConfig('html'); const componentImports = [ ...Object.values(JSX_IMPORTS), @@ -99,6 +99,12 @@ export default () => { const clientProgram = [ createImportDeclaration(null, resolve(ROOT, './ui/index.css')), + // Project stylesheets are bundled after the built-in one, so their rules + // and custom properties (e.g. `--color-brand-*`) win. + ...stylesheets.map(stylesheet => + createImportDeclaration(null, resolve(stylesheet)) + ), + createImportDeclaration( 'registerIslands', resolve(ROOT, './ui/islands/runtime.mjs'), diff --git a/packages/react/src/llms-txt/generate.mjs b/packages/react/src/llms-txt/generate.mjs index c55f39ce..dac8aaf7 100644 --- a/packages/react/src/llms-txt/generate.mjs +++ b/packages/react/src/llms-txt/generate.mjs @@ -4,6 +4,7 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import getConfig from '@nodejs/doc-kit/utils/configuration/index.mjs'; +import { populate } from '@nodejs/doc-kit/utils/configuration/templates.mjs'; import { writeFile } from '@nodejs/doc-kit/utils/file.mjs'; import { buildApiDocLink } from './utils/buildApiDocLink.mjs'; @@ -23,7 +24,7 @@ export async function generate(input) { .map(entry => `- ${buildApiDocLink(entry, config)}`) .join('\n'); - const filledTemplate = `${template}${apiDocsLinks}`; + const filledTemplate = `${populate(template, config)}${apiDocsLinks}`; if (config.output) { await writeFile(join(config.output, 'llms.txt'), filledTemplate); diff --git a/packages/react/src/llms-txt/index.mjs b/packages/react/src/llms-txt/index.mjs index 9cffc62b..cd6d5d73 100644 --- a/packages/react/src/llms-txt/index.mjs +++ b/packages/react/src/llms-txt/index.mjs @@ -20,7 +20,7 @@ export default { defaultConfiguration: { templatePath: join(import.meta.dirname, 'template.txt'), - pageURL: '{baseURL}/latest/api{path}.md', + pageURL: '{baseURL}{path}.md', }, generate, diff --git a/packages/react/src/llms-txt/template.txt b/packages/react/src/llms-txt/template.txt index 04da10b9..adeb27a8 100644 --- a/packages/react/src/llms-txt/template.txt +++ b/packages/react/src/llms-txt/template.txt @@ -1,6 +1,6 @@ -# Node.js Documentation +# {project} Documentation -> Node.js is an open-source, cross-platform JavaScript runtime environment that executes JavaScript code outside a web browser. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient for building scalable network applications. +> API documentation for {project}. Below are the sections of the API documentation. Look out especially towards the links that point towards guidance/introduction to the structure of this documentation. diff --git a/packages/react/src/sitemap/index.mjs b/packages/react/src/sitemap/index.mjs index 3abda40a..a9d777f1 100644 --- a/packages/react/src/sitemap/index.mjs +++ b/packages/react/src/sitemap/index.mjs @@ -15,7 +15,7 @@ export default { dependsOn: '@nodejs/doc-kit/metadata', defaultConfiguration: { - indexURL: '{baseURL}/latest/api/', + indexURL: '{baseURL}/', pageURL: '{indexURL}{path}.html', }, diff --git a/www/doc-kit.config.mjs b/www/doc-kit.config.mjs index 627bff1d..14e3164e 100644 --- a/www/doc-kit.config.mjs +++ b/www/doc-kit.config.mjs @@ -49,17 +49,12 @@ export default { input: [join(ROOT, 'content', '**', '*.md')], output: join(ROOT, 'out'), + project: 'doc-kit', 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': { @@ -70,13 +65,8 @@ export default { }, html: { - 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 diff --git a/www/pages/getting-started.md b/www/pages/getting-started.md index 468ee9fd..02996e4b 100644 --- a/www/pages/getting-started.md +++ b/www/pages/getting-started.md @@ -37,7 +37,68 @@ serve them over HTTP rather than opening the files from disk; any static server works: ```bash -npx serve out +npx doc-kit generate -t html -t orama-db -i "docs/*.md" -o out +``` + +## Preview it locally + +The `html` 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 will do the trick; for example: + +```bash +npx serve out -p 3000 +``` + +Then open the printed URL (usually ). The +`legacy-html-all` output from earlier has no such requirement — `out/all.html` +opens straight from disk. + +## Customize the `html` generator output + +The power of the `html` generator comes from its customization hooks. Let's walk +through a couple quick changes. + +Create a `doc-kit.config.mjs` file at the root of the project. + +```mjs +import { join } from 'node:path'; + +/** @type {import('@nodejs/doc-kit/src/utils/configuration/types').Configuration} */ +export default { + global: { + project: 'My Project', // Project name used in titles, the logo, and templates + }, + + html: { + head: { + html: [ + // re-write the brand color for effect + ``, + ], + }, + // use a custom logo component instead of the plain project name + // our logo.jsx file like this, just for the demo + // export default Logo = () => + // ; + imports: { + '#theme/Logo': join(import.meta.dirname, './logo.jsx'), + // You can also change things such as `#theme/Layout`, + // and more! + }, + }, +}; ``` Open the printed URL (usually ) and your documentation is