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
26 changes: 21 additions & 5 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,26 @@
to = "/home/stable/release-notes"
status = 301

# The bundle file names are not content-hashed, so a stale asset can be
# served until max-age expires. One hour keeps that window small; with
# content-hashed file names this could be immutable.
# The css and js file names carry a content hash, so they can be cached
# forever; fonts and images keep stable names and get moderate lifetimes.
# The rules are deliberately non-overlapping: netlify applies every matching
# header rule, and overlapping rules would emit duplicate Cache-Control.
[[headers]]
for = "/_/*"
for = "/_/css/*"
[headers.values]
Cache-Control = "public, max-age=3600"
Cache-Control = "public, max-age=31536000, immutable"

[[headers]]
for = "/_/js/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"

[[headers]]
for = "/_/font/*"
[headers.values]
Cache-Control = "public, max-age=604800"

[[headers]]
for = "/_/img/*"
[headers.values]
Cache-Control = "public, max-age=86400"
11 changes: 11 additions & 0 deletions package-lock.json

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

8 changes: 5 additions & 3 deletions ui/NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ third-party components. Build-time minification strips embedded license
header comments; this file preserves the attributions and is included
in the published bundle.

- Font Awesome Free 6.2.1 (https://fontawesome.com)
The inline svg icon path data is derived from Font Awesome Free icons
(CC BY 4.0). https://fontawesome.com/license/free
- Font Awesome Free (https://fontawesome.com)
The icon sprite (img/icons.svg) is generated at build time from the
@fortawesome/fontawesome-free package (see ui/package.json for the
version); the icon path data is CC BY 4.0.
https://fontawesome.com/license/free
- highlight.js (https://highlightjs.org)
BSD 3-Clause License, (c) 2006 Ivan Sagalaev and contributors
- Asciidoctor Tabs (https://github.com/asciidoctor/asciidoctor-tabs)
Expand Down
13 changes: 13 additions & 0 deletions ui/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ Inside the `src` directory are:

The js entries are `js/site.js` (the numbered scripts under `src/js`, concatenated in order) and one `js/vendor/<name>.js` per `src/js/vendor/<name>.bundle.js` with its imports bundled in.

=== Dependency special cases

* `mermaid`: ships its own prebuilt es-module dist and breaks at runtime when re-bundled; the build copies the entry and its transitive chunk imports verbatim and loads it on demand (see `partials/mermaid-script.hbs`).
* `pagefind`: lives entirely outside this UI bundle. It is a devDependency of the repository root, indexes the finished site after the Antora build (`make build-search-index`) and self-serves its assets under `/pagefind/`. The asset hashing below does not cover it.
* `@asciidoctor/tabs`: doubles as a build-time Asciidoctor extension (playbook) and a runtime asset. The browser js and the css are imported by explicit path (`dist/js/tabs.js`, `dist/css/tabs.css`) because Vite honors neither the `browser` nor the `style` package field.
* `highlight.js`: bundled from its modular source with a curated language list in `src/js/vendor/highlight.bundle.js`.
* `@fortawesome/fontawesome-free`: the icon sprite `img/icons.svg` is generated at build time from the package's svgs (CC BY 4.0, attributed in NOTICE and in the sprite itself).
* `@fontsource/*`: the font files are pulled out of the packages via the `~@fontsource/` url alias in `src/css/fonts.css`.

=== Asset caching

The css and js entry files carry a content hash in their names and are served with immutable cache headers (see `netlify.toml`). The hbs templates reference them by their unhashed names; the build rewrites the references while staging the bundle. Fonts and images keep stable names with moderate cache lifetimes.

=== Building the final documentation

The build is wired into the Playbooks of this repository:
Expand Down
79 changes: 73 additions & 6 deletions ui/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import {
mkdirSync,
readdirSync,
readFileSync,
rmSync
rmSync,
writeFileSync
} from 'node:fs';
import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
import { resolve, basename, dirname } from 'node:path';
import { ZipArchive } from 'archiver';
Expand Down Expand Up @@ -68,8 +70,14 @@ function config(overrides) {

rmSync(resolve(root, 'build'), { recursive: true, force: true });

// css and js entry files carry a content hash in their name so they can be
// cached as immutable; the partials reference them by their unhashed name and
// are rewritten from this manifest when they are staged. Fonts and images
// keep stable names (they are referenced from css/content and rarely change).
const hashedNames = new Map();

for (const entry of jsEntries) {
await build(
const result = await build(
config({
build: {
outDir: staged,
Expand All @@ -78,15 +86,17 @@ for (const entry of jsEntries) {
input: entry.input,
output: {
format: 'iife',
entryFileNames: `js/${entry.name}.js`
entryFileNames: `js/${entry.name}-[hash].js`
}
}
}
})
);
const chunk = result.output.find((file) => file.type === 'chunk' && file.isEntry);
hashedNames.set(`js/${entry.name}.js`, chunk.fileName);
}

await build(
const cssResult = await build(
config({
build: {
outDir: staged,
Expand All @@ -97,7 +107,7 @@ await build(
output: {
assetFileNames: (asset) => {
const name = asset.names[0] ?? '';
if (name === 'site.css') return 'css/site.css';
if (name === 'site.css') return 'css/site-[hash][extname]';
if (/\.(woff2?|ttf)$/.test(name)) return 'font/[name][extname]';
if (/\.(svg|png|gif|ico|jpg)$/.test(name)) return 'img/[name][extname]';
return 'css/[name][extname]';
Expand All @@ -107,6 +117,8 @@ await build(
}
})
);
const siteCss = cssResult.output.find((file) => file.fileName.startsWith('css/site-'));
hashedNames.set('css/site.css', siteCss.fileName);

// mermaid is imported on demand by partials/mermaid-script.hbs. It ships its
// own prebuilt es-module dist and breaks at runtime when re-bundled, so the
Expand All @@ -121,16 +133,71 @@ while (mermaidQueue.length > 0) {
if (mermaidSeen.has(file)) continue;
mermaidSeen.add(file);
const content = readFileSync(resolve(mermaidDist, file), 'utf8');
copyFileSync(resolve(mermaidDist, file), resolve(mermaidOut, file));
if (file === 'mermaid.esm.min.mjs') {
// the chunks already carry upstream content hashes; the entry does not,
// so it gets one here to be safely cacheable as immutable like the rest
const hash = createHash('sha256').update(content).digest('hex').slice(0, 8);
const hashedEntry = `mermaid.esm.min-${hash}.mjs`;
copyFileSync(resolve(mermaidDist, file), resolve(mermaidOut, hashedEntry));
hashedNames.set('js/vendor/mermaid/mermaid.esm.min.mjs', `js/vendor/mermaid/${hashedEntry}`);
} else {
copyFileSync(resolve(mermaidDist, file), resolve(mermaidOut, file));
}
for (const match of content.matchAll(/(?:from\s*|import\s*\(?\s*)"\.\/([^"]+)"/g)) {
mermaidQueue.push(match[1]);
}
}
console.log(`mermaid: copied ${mermaidSeen.size} dist files`);

// The icon sprite is generated from the Font Awesome package so the icons are
// version-managed like every other dependency (CC BY 4.0, see NOTICE).
const faDir = dirname(require.resolve('@fortawesome/fontawesome-free/package.json'));
const faVersion = JSON.parse(readFileSync(resolve(faDir, 'package.json'), 'utf8')).version;
const icons = {
'external-link': 'solid/arrow-up-right-from-square',
search: 'solid/magnifying-glass',
xing: 'brands/xing',
linkedin: 'brands/linkedin',
github: 'brands/github',
twitter: 'brands/twitter',
link: 'solid/link'
};
const symbols = Object.entries(icons).map(([name, faIcon]) => {
const svg = readFileSync(resolve(faDir, 'svgs', `${faIcon}.svg`), 'utf8');
const viewBox = svg.match(/viewBox="([^"]+)"/)[1];
const path = svg.match(/<path d="([^"]+)"/)[1];
return (
` <!-- Font Awesome Free ${faVersion}: ${faIcon} -->\n` +
` <symbol id="icon-${name}" viewBox="${viewBox}"><path d="${path}"/></symbol>`
);
});
const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<!--
Icon path data from Font Awesome Free ${faVersion} (https://fontawesome.com),
licensed under CC BY 4.0 (https://fontawesome.com/license/free).
See NOTICE in this bundle for all third-party attributions.
-->
${symbols.join('\n')}
</svg>
`;

// hbs templates reference the hashed assets by their unhashed names and are
// rewritten from the manifest while being staged
for (const dir of ['helpers', 'layouts', 'partials', 'img']) {
cpSync(resolve(src, dir), resolve(staged, dir), { recursive: true });
}
for (const dir of ['layouts', 'partials']) {
for (const name of readdirSync(resolve(staged, dir))) {
if (!name.endsWith('.hbs')) continue;
const file = resolve(staged, dir, name);
let content = readFileSync(file, 'utf8');
for (const [plain, hashed] of hashedNames) {
content = content.replaceAll(plain, hashed);
}
writeFileSync(file, content);
}
}
writeFileSync(resolve(staged, 'img', 'icons.svg'), sprite);
for (const file of ['NOTICE', 'LICENSE']) {
copyFileSync(resolve(root, file), resolve(staged, file));
}
Expand Down
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@fontsource/ibm-plex-mono": "^5.0.14",
"@fontsource/noto-sans": "^5.0.22",
"@fontsource/noto-sans-mono": "^5.0.20",
"@fortawesome/fontawesome-free": "^6.7.2",
"archiver": "^8.0.0",
"eslint": "^10.7.0",
"globals": "^17.7.0",
Expand Down
22 changes: 0 additions & 22 deletions ui/src/img/icons.svg

This file was deleted.

1 change: 1 addition & 0 deletions ui/src/partials/head-styles.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<link rel="preload" href="{{{uiRootPath}}}/font/noto-sans-latin-400-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="{{{uiRootPath}}}/font/ibm-plex-mono-latin-600-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="{{{uiRootPath}}}/font/noto-sans-latin-700-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="{{{uiRootPath}}}/css/site.css">
<link rel="stylesheet" href="/pagefind/pagefind-modular-ui.css">
Loading