/** * 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), }, }, }; }