/**
* The Astro + Starlight configuration every Norsk product docs site is built
* from. A site's own `astro.config.mjs` is reduced to what genuinely differs:
* its origin, its two names, its sidebar, and the versions it was built
* against.
*
* export default defineConfig(
* norskDocsSite({
* origin: PUBLIC_SITE_ORIGIN,
* tabTitle: "Norsk",
* wordmark: "norsk-ctl",
* buildVersions: [...],
* sidebar: [...],
* }),
* );
*
* Everything here is the part that must NOT differ between sites: the theme,
* the component overrides, the mermaid pipeline, the favicon convention, and
* the two-build (public / embedded) shape described on `publicBuild` below.
*/
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { unified } from "@astrojs/markdown-remark";
import starlight from "@astrojs/starlight";
import rehypeMermaid from "rehype-mermaid";
/** One row of the sidebar's "Built against" footer (components/Sidebar.astro). */
export interface BuildVersion {
/** Row label, e.g. "norsk-ctl". */
label: string;
/** Row value, read at build time from the product's own files. */
value: string;
/** Break a `repo:tag` image pin after the last colon so it wraps sensibly. */
splitOnLastColon?: boolean;
}
export interface NorskDocsSiteOptions {
/** Canonical public origin. Drives the sitemap and . */
origin: string;
/** BROWSER TAB suffix only ("Quickstart | Norsk"), kept short — tabs truncate. */
tabTitle: string;
/** Masthead wordmark. Deliberately different from `tabTitle`; see SiteTitle.astro. */
wordmark: string;
/** Starlight's sidebar. The one part of the nav a site fully owns. */
sidebar: unknown[];
/** Rows for the sidebar's "Built against" footer. */
buildVersions: BuildVersion[];
/**
* Dev-preview only: machine names `astro dev --host` may be reached by on the
* LAN (Vite 403s unknown Host headers otherwise). No effect on builds.
*/
devAllowedHosts?: string[];
}
/**
* Two builds from one content collection, selected by ASTRO_BUILD:
* `dist-embedded` (base `/docs`) is the bundle baked into a product binary and
* served by the daemon, `dist-public` (base `/`) is the standalone site on the
* product's own domain. Exported because a site's sidebar legitimately differs
* between the two — a section that only exists once it is published.
*/
export function isPublicBuild(): boolean {
return process.env.ASTRO_BUILD === "public";
}
/**
* Content (and any vendored HTML) is authored with absolute /docs/... links —
* correct for the daemon-embedded build (served under /docs), but a 404 in the
* public build (served at /). Astro doesn't base-prefix absolute links in
* MDX/components/static files, so for the public build a post-build pass strips
* the /docs prefix from href/src attributes across the emitted HTML.
* Attribute-scoped so it never touches /docs literals in body text. No-op for
* the embedded build.
*/
function stripDocsBaseForPublic() {
const rewriteHtml = (html: string) =>
html.replace(/((?:href|src)=")\/docs(\/|")/g, (_m, attr, tail) => (tail === "/" ? `${attr}/` : `${attr}/"`));
const walk = (root: string, fn: (p: string) => void) => {
for (const name of readdirSync(root)) {
const p = join(root, name);
if (statSync(p).isDirectory()) walk(p, fn);
else if (p.endsWith(".html")) fn(p);
}
};
return {
name: "strip-docs-base-public",
hooks: {
"astro:build:done": ({ dir }: { dir: URL }) => {
if (!isPublicBuild()) return;
walk(fileURLToPath(dir), (p) => {
writeFileSync(p, rewriteHtml(readFileSync(p, "utf-8")));
});
},
},
};
}
export function norskDocsSite(options: NorskDocsSiteOptions) {
const isPublic = isPublicBuild();
const mermaidLaunchOptions = process.env.BROWSER_FOR_TESTING
? { executablePath: process.env.BROWSER_FOR_TESTING }
: undefined;
return {
// The public site lives at the domain root; the embedded bundle is served
// by the daemon under /docs. `site` is the canonical origin: it drives the
// sitemap and , and without it @astrojs/sitemap
// (pulled in by Starlight) skips and warns. The embedded build reuses the
// same origin — its sitemap is inert since it is never crawled, but setting
// `site` keeps the build warning-free.
site: options.origin,
base: isPublic ? "/" : "/docs",
outDir: isPublic ? "./dist-public" : "./dist-embedded",
markdown: {
syntaxHighlight: { excludeLangs: ["mermaid"] },
// Astro 6 deprecated top-level `rehypePlugins`; the pipeline now lives in
// a `unified()` processor. `syntaxHighlight` stays at the markdown level.
processor: unified({
rehypePlugins: [
[
rehypeMermaid,
{ strategy: "img-svg", dark: true, mermaidConfig: { theme: "dark" }, launchOptions: mermaidLaunchOptions },
],
],
}),
},
integrations: [
stripDocsBaseForPublic(),
starlight({
title: options.tabTitle,
customCss: ["./src/custom.css"],
components: {
// Appends the "Built against" footer below the nav.
Sidebar: "./src/components/Sidebar.astro",
// Decouples the masthead wordmark from `title` above.
SiteTitle: "./src/components/SiteTitle.astro",
// Adds the "captured against" line to a generated guide (front
// matter `capturedAgainst`, see content.config.ts).
Footer: "./src/components/Footer.astro",
},
// Starlight base-prefixes this (emits a `/docs/favicon.png`
// shortcut-icon link for the embedded build); a raw head would
// not, and 404s the embedded link-check.
favicon: "/favicon.png",
sidebar: options.sidebar,
}),
],
vite: {
server: { allowedHosts: options.devAllowedHosts ?? [] },
// Injected rather than imported: a component resolving its own path at
// runtime finds dist-embedded/.prerender/chunks/ instead of the source
// tree and fails the build, and the site's version readers want the
// product's files, not the site's. Both values are build-time constants,
// so a define is the cheapest correct channel.
define: {
__BUILD_VERSIONS__: JSON.stringify(options.buildVersions),
__SITE_WORDMARK__: JSON.stringify(options.wordmark),
},
},
};
}