Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@node-core/doc-kit",
"type": "module",
"version": "1.3.2",
"version": "1.3.3",
"repository": {
"type": "git",
"url": "git+https://github.com/nodejs/doc-kit.git"
Expand Down
2 changes: 1 addition & 1 deletion src/generators/orama-db/generate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function generate(input) {
description: paragraph
? transformNodeToString(paragraph, true)
: undefined,
href: `${entry.path.slice(1)}.html#${entry.heading.data.slug}`,
href: `${entry.path}.html#${entry.heading.data.slug}`,
siteSection: headings[0].heading.data.name,
};
})
Expand Down
50 changes: 40 additions & 10 deletions src/generators/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ The `web` generator transforms JSX AST entries into complete web bundles, produc

The `web` generator accepts the following configuration options:

| Name | Type | Default | Description |
| ---------------- | -------- | --------------------------------------------- | --------------------------------------------------------------------- |
| `output` | `string` | - | The directory where HTML, JavaScript, and CSS files will be written |
| `templatePath` | `string` | `'template.html'` | Path to the HTML template file |
| `project` | `string` | `'Node.js'` | Project name used in page titles and the version selector |
| `title` | `string` | `'{project} v{version} Documentation'` | Title template for HTML pages (supports `{project}`, `{version}`) |
| `editURL` | `string` | `'${GITHUB_EDIT_URL}/doc/api{path}.md'` | URL template for "edit this page" links |
| `pageURL` | `string` | `'{baseURL}/latest-{version}/api{path}.html'` | URL template for documentation page links |
| `imports` | `object` | See below | Object mapping `#theme/` aliases to component paths for customization |
| `virtualImports` | `object` | `{}` | Additional virtual module mappings merged into the build |
| Name | Type | Default | Description |
| ----------------- | --------- | --------------------------------------------- | --------------------------------------------------------------------- |
| `output` | `string` | - | The directory where HTML, JavaScript, and CSS files will be written |
| `templatePath` | `string` | `'template.html'` | Path to the HTML template file |
| `project` | `string` | `'Node.js'` | Project name used in page titles and the version selector |
| `title` | `string` | `'{project} v{version} Documentation'` | Title template for HTML pages (supports `{project}`, `{version}`) |
| `useAbsoluteURLs` | `boolean` | `false` | When `true`, all internal links use absolute URLs based on `baseURL` |
| `editURL` | `string` | `'${GITHUB_EDIT_URL}/doc/api{path}.md'` | URL template for "edit this page" links |
| `pageURL` | `string` | `'{baseURL}/latest-{version}/api{path}.html'` | URL template for documentation page links |
| `imports` | `object` | See below | Object mapping `#theme/` aliases to component paths for customization |
| `virtualImports` | `object` | `{}` | Additional virtual module mappings merged into the build |

#### Default `imports`

Expand Down Expand Up @@ -60,6 +61,8 @@ All scalar (non-object) configuration values are automatically exported. The def
| `versions` | `Array<{ url, label, major }>` | Pre-computed version entries with labels and URL templates (only `{path}` remains for per-page use) |
| `editURL` | `string` | Partially populated "edit this page" URL template (only `{path}` remains) |
| `pages` | `Array<[string, string]>` | Sorted `[name, path]` tuples for sidebar navigation |
| `useAbsoluteURLs` | `boolean` | Whether internal links use absolute URLs (mirrors config value) |
| `baseURL` | `string` | Base URL for the documentation site (used when `useAbsoluteURLs` is `true`) |
| `languageDisplayNameMap` | `Map<string, string>` | Shiki language alias → display name map for code blocks |

#### Usage in custom components
Expand Down Expand Up @@ -96,3 +99,30 @@ The `Layout` component receives the following props:
| `children` | `ComponentChildren` | Processed page content |

Custom Layout components can use any combination of these props alongside `#theme/config` imports.

### HTML template

The HTML template file (set via `templatePath`) uses JavaScript template literal syntax (`${...}` placeholders) and is evaluated at build time with full expression support.

#### Available template variables

| Variable | Type | Description |
| ------------------ | -------- | ----------------------------------------------------------------- |
| `title` | `string` | Fully resolved page title (e.g. `'File system \| Node.js v22.x'`) |
| `dehydrated` | `string` | Server-rendered HTML for the page content |
| `importMap` | `string` | JSON import map for client-side module resolution |
| `entrypoint` | `string` | Client-side entry point filename with cache-bust query |
| `speculationRules` | `string` | Speculation rules JSON for prefetching |
| `root` | `string` | Relative or absolute path to the site root |
| `metadata` | `object` | Full page metadata (frontmatter, path, heading, etc.) |
| `config` | `object` | The resolved web generator configuration |

Since the template supports arbitrary JS expressions, you can use conditionals and method calls:

```html
<title>${title}</title>
<link rel="stylesheet" href="${root}styles.css" />
<script type="importmap">
${importMap}
</script>
```
1 change: 1 addition & 0 deletions src/generators/web/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default createLazyGenerator({
templatePath: join(import.meta.dirname, 'template.html'),
project: 'Node.js',
title: '{project} v{version} Documentation',
useAbsoluteURLs: false,
editURL: `${GITHUB_EDIT_URL}/doc/api{path}.md`,
pageURL: '{baseURL}/latest-{version}/api{path}.html',
imports: {
Expand Down
14 changes: 7 additions & 7 deletions src/generators/web/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://nodejs.org/static/images/favicons/favicon.png"/>
<title>{title}</title>
<title>${title}</title>
<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.">
<link rel="stylesheet" href="{root}styles.css" />
<meta property="og:title" content="{title}">
<link rel="stylesheet" href="${root}styles.css" />
<meta property="og:title" content="${title}">
<meta 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.">
<meta property="og:image" content="https://nodejs.org/en/next-data/og/announcement/Node.js%20%E2%80%94%20Run%20JavaScript%20Everywhere" />
<meta property="og:type" content="website">
Expand All @@ -20,12 +20,12 @@

<!-- Apply theme before paint to avoid Flash of Unstyled Content -->
<script>document.documentElement.setAttribute("data-theme", document.documentElement.style.colorScheme = localStorage.getItem("theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"));</script>
<script type="importmap">{importMap}</script>
<script type="speculationrules">{speculationRules}</script>
<script type="importmap">${importMap}</script>
<script type="speculationrules">${speculationRules}</script>
</head>

<body>
<div id="root">{dehydrated}</div>
<script type="module" src="{root}{entrypoint}"></script>
<div id="root">${dehydrated}</div>
<script type="module" src="${root}${entrypoint}"></script>
</body>
</html>
1 change: 1 addition & 0 deletions src/generators/web/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { JSXContent } from '../jsx-ast/utils/buildContent.mjs';
export type Configuration = {
templatePath: string;
title: string;
useAbsoluteURLs: boolean;
imports: Record<string, string>;
virtualImports: Record<string, string>;
};
Expand Down
4 changes: 2 additions & 2 deletions src/generators/web/ui/components/SearchBox/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import SearchResults from '@node-core/ui-components/Common/Search/Results';
import SearchHit from '@node-core/ui-components/Common/Search/Results/Hit';

import styles from './index.module.css';
import { relative } from '../../../../../utils/url.mjs';
import useOrama from '../../hooks/useOrama.mjs';
import { relativeOrAbsolute } from '../../utils/relativeOrAbsolute.mjs';

const SearchBox = ({ pathname }) => {
const client = useOrama(pathname);
Expand All @@ -23,7 +23,7 @@ const SearchBox = ({ pathname }) => {
<SearchHit
document={{
...hit.document,
href: relative(hit.document.href, pathname),
href: relativeOrAbsolute(hit.document.href, pathname),
}}
/>
)}
Expand Down
4 changes: 2 additions & 2 deletions src/generators/web/ui/components/SideBar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Select from '@node-core/ui-components/Common/Select';
import SideBar from '@node-core/ui-components/Containers/Sidebar';

import styles from './index.module.css';
import { relative } from '../../../../../utils/url.mjs';
import { relativeOrAbsolute } from '../../utils/relativeOrAbsolute.mjs';

import { project, version, versions, pages } from '#theme/config';

Expand Down Expand Up @@ -41,7 +41,7 @@ export default ({ metadata }) => {
link:
metadata.path === path
? `${metadata.basename}.html`
: `${relative(path, metadata.path)}.html`,
: `${relativeOrAbsolute(path, metadata.path)}.html`,
}));

return (
Expand Down
4 changes: 2 additions & 2 deletions src/generators/web/ui/hooks/useOrama.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { create, search, load } from '@orama/orama';
import { useState, useEffect } from 'react';

import { relative } from '../../../../utils/url.mjs';
import { relativeOrAbsolute } from '../utils/relativeOrAbsolute.mjs';

/**
* Hook for initializing and managing Orama search database
Expand All @@ -26,7 +26,7 @@ export default pathname => {
setClient(db);

// Load the search data
fetch(relative('/orama-db.json', pathname))
fetch(relativeOrAbsolute('/orama-db.json', pathname))
.then(response => response.ok && response.json())
.then(data => load(db, data));
}, []);
Expand Down
1 change: 1 addition & 0 deletions src/generators/web/ui/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ declare module '#theme/config' {
// From web configuration
export const templatePath: Configuration['templatePath'];
export const title: Configuration['title'];
export const useAbsoluteURLs: Configuration['useAbsoluteURLs'];

// From config generation
export const version: string;
Expand Down
16 changes: 16 additions & 0 deletions src/generators/web/ui/utils/relativeOrAbsolute.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { relative } from '../../../../utils/url.mjs';

import { useAbsoluteURLs, baseURL } from '#theme/config';

/**
* Returns an absolute URL (based on baseURL) or a relative URL,
* depending on the useAbsoluteURLs configuration option.
*
* @param {string} to - Target path (e.g., '/fs', '/orama-db.json')
* @param {string} from - Current page path (e.g., '/api/fs')
* @returns {string}
*/
export const relativeOrAbsolute = (to, from) =>
useAbsoluteURLs
? new URL(`.${to}`, baseURL.replace(/\/?$/, '/')).href
: relative(to, from);
63 changes: 63 additions & 0 deletions src/generators/web/utils/__tests__/processing.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

import { populateWithEvaluation } from '../processing.mjs';

describe('populateWithEvaluation', () => {
it('substitutes simple ${variable} placeholders', () => {
const result = populateWithEvaluation('Hello ${name}!', { name: 'World' });
assert.strictEqual(result, 'Hello World!');
});

it('supports multiple variables', () => {
const result = populateWithEvaluation('${greeting} ${name}!', {
greeting: 'Hi',
name: 'Node',
});
assert.strictEqual(result, 'Hi Node!');
});

it('supports JavaScript expressions', () => {
const result = populateWithEvaluation('${value > 5 ? "big" : "small"}', {
value: 10,
});
assert.strictEqual(result, 'big');
});

it('supports ternary expressions for conditional content', () => {
const result = populateWithEvaluation(
'${showExtra ? "extra content" : ""}',
{ showExtra: false }
);
assert.strictEqual(result, '');
});

it('handles JSON.stringify for objects', () => {
const obj = { key: 'value' };
const result = populateWithEvaluation('${JSON.stringify(data)}', {
data: obj,
});
assert.strictEqual(result, '{"key":"value"}');
});

it('preserves surrounding HTML content', () => {
const result = populateWithEvaluation(
'<title>${title}</title><link href="${root}styles.css" />',
{ title: 'Test Page', root: '../' }
);
assert.strictEqual(
result,
'<title>Test Page</title><link href="../styles.css" />'
);
});

it('handles empty string values', () => {
const result = populateWithEvaluation('[${content}]', { content: '' });
assert.strictEqual(result, '[]');
});

it('handles numeric values', () => {
const result = populateWithEvaluation('count: ${count}', { count: 42 });
assert.strictEqual(result, 'count: 42');
});
});
66 changes: 66 additions & 0 deletions src/generators/web/utils/__tests__/relativeOrAbsolute.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import assert from 'node:assert/strict';
import { beforeEach, describe, it } from 'node:test';

import {
setConfig,
default as getConfig,
} from '../../../../utils/configuration/index.mjs';
import { relativeOrAbsolute } from '../relativeOrAbsolute.mjs';

await setConfig({
version: 'v22.0.0',
changelog: [],
generators: {
web: {
useAbsoluteURLs: false,
baseURL: 'https://nodejs.org/docs',
},
},
});

describe('relativeOrAbsolute (relative mode)', () => {
beforeEach(() => {
getConfig('web').useAbsoluteURLs = false;
});

it('returns a relative path from a nested page to root', () => {
const result = relativeOrAbsolute('/', '/api/fs');
assert.strictEqual(result, '..');
});

it('returns a relative path between sibling pages', () => {
const result = relativeOrAbsolute('/http', '/fs');
assert.strictEqual(result, 'http');
});

it('returns a relative path for a deeper target', () => {
const result = relativeOrAbsolute('/orama-db.json', '/api/fs');
assert.strictEqual(result, '../orama-db.json');
});

it('returns "." when source and target resolve to the same path', () => {
const result = relativeOrAbsolute('/', '/');
assert.strictEqual(result, '.');
});
});

describe('relativeOrAbsolute (absolute mode)', () => {
beforeEach(() => {
getConfig('web').useAbsoluteURLs = true;
});

it('returns an absolute URL to root', () => {
const result = relativeOrAbsolute('/', '/api/fs');
assert.strictEqual(result, 'https://nodejs.org/docs/');
});

it('returns an absolute URL for a page path', () => {
const result = relativeOrAbsolute('/http', '/fs');
assert.strictEqual(result, 'https://nodejs.org/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');
});
});
Loading
Loading