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..4f68c425 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,7 +141,7 @@ jobs: path: node - name: Build docs - run: npx doc-kit generate -t web -i "./node/doc/api/assert.md" -o out + 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 5edaaa29..13d2b824 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 214d2857..f8863c71 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -23,7 +23,7 @@ uses the `doc-kit` property: ```json { "doc-kit": { - "target": ["json"], + "target": ["json-simple"], "global": { "input": "doc/api/*.md", "output": "out" @@ -41,70 +41,86 @@ export default { // generator module (e.g. '@my-scope/my-package/my-generator'). target: ['orama-db', 'html'], global: { - version: '20.0.0', - minify: true, - repository: 'nodejs/node', - ref: 'main', - baseURL: 'https://nodejs.org/docs/', - input: 'src/', + project: 'My Project', + version: '1.2.0', + input: 'docs/**/*.md', output: 'dist/', ignore: ['node_modules/', 'test/'], - changelog: - 'https://raw.githubusercontent.com/nodejs/node/main/CHANGELOG.md', - index: - 'https://raw.githubusercontent.com/nodejs/node/main/doc/api/index.md', + baseURL: 'https://example.com/docs/', }, threads: 4, chunkSize: 10, // Generator-specific configurations - json: { - format: 'json', - minify: false, // Override global setting - }, - html: { - format: 'html', + title: '{project} Documentation', }, metadata: { typeMap: { - String: 'string', - Number: 'number', - Boolean: 'boolean', + MyThing: 'https://example.com/docs/my-thing.html', }, }, }; ``` +## 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 ### Global Configuration The `global` object contains settings that apply to all generators unless overridden: -| Property | Type | Description | Default | -| ------------ | ------------------ | ------------------------------------------ | -------------------------------------------------- | -| `version` | `string \| SemVer` | Documentation version | `process.version` | -| `minify` | `boolean` | Whether to minify output | `true` | -| `repository` | `string` | GitHub repository in `owner/repo` format | `'nodejs/node'` | -| `ref` | `string` | Git reference (branch, tag, or commit SHA) | `'HEAD'` | -| `baseURL` | `string \| URL` | Base URL for documentation | `'https://nodejs.org/docs'` | -| `input` | `string[]` | Input directory path | - | -| `output` | `string` | Output directory path | - | -| `ignore` | `string[]` | Patterns to ignore | `[]` | -| `changelog` | `string \| URL` | Changelog URL | Auto-generated URL based on `ref` and `repository` | -| `index` | `string \| URL` | Index URL | - | +| 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 | - | ### Generator-Specific Configuration -Each generator (e.g., `json`, `html`, `markdown`) can have its own configuration that overrides global settings: +Each generator (e.g., `html`, `legacy-json`) can have its own configuration that overrides global settings: ```javascript export default { global: { - version: '20.0.0', + version: '1.2.0', minify: true, }, @@ -121,7 +137,8 @@ precedence): 1. **CLI options** (command-line arguments) 2. **Configuration file** (discovered or selected with `--config-file`) -3. **Default values** (built-in defaults) +3. **Presets** (listed in the configuration file's `extends`) +4. **Default values** (built-in defaults) ## CLI Options Mapping 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 1dfb8cd6..38c7142d 100644 --- a/www/doc-kit.config.mjs +++ b/www/doc-kit.config.mjs @@ -35,17 +35,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': { @@ -56,13 +51,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 61193a34..bd94877d 100644 --- a/www/pages/getting-started.md +++ b/www/pages/getting-started.md @@ -95,9 +95,11 @@ 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: { - project: 'My Project', // Project name used in page titles and the version selector - remoteConfigUrl: '', // Suppress the Node.js default that sets the top banner based on Node.js news. head: { html: [ // re-write the brand color for effect @@ -115,7 +117,7 @@ export default { `, ], }, - // use a custom logo instead of the Node.js logo + // use a custom logo component instead of the plain project name // our logo.jsx file like this, just for the demo // export default Logo = () => // ;