// The product's DOCS SITE — `website/`, an Astro + Starlight package on the // shared theme (docs-theme/), with the five-section top level every Norsk // manual uses: Start here, Guides, How-to, Concepts, Reference. // // Emitted for every shape, not as a feature, because it is not optional in the // way an operator screen is: every product has readers. What the generator // writes is a SKELETON — five near-empty pages saying what belongs in each // section — because a site whose sections a product must invent is a site the // product will instead paste from somewhere else. // // Two things make this different from every other workspace here: // // 1. It is NOT in the root `workspaces` array. A docs site is a STANDALONE // package with its own install, deliberately: Astro's dependency tree has // no business in the product's own install, and the theme is copied rather // than imported for exactly that reason (docs-theme/sync.ts has the bun // error that proves it). So `bun install` at the repo root does not install // it — `bun install --cwd website` does, and the generated README says so. // 2. Its theme files are NOT written from here. `createProduct` lays them // down with the theme's own writer (`syncTheme`), so there is one copier // and one gate (`bun run check:drift`, via check-drift's docsThemeProblems) // rather than a second rendering of the same bytes that could drift. // // It does NOT replace the illustrated manual (conventions/docs.md): that bundle // is still what the image serves at `/docs` and what a public product publishes // nightly. The two channels and the choice between them are set out in // conventions/docs-site.md. import type { GeneratedFile, ShapeContext } from "./create-product.ts"; /** The docs site's directory, and the name check-drift's gate looks for. */ export const SITE_DIR = "website"; /** * What a site needs installed to build. Dictated by the theme, not by taste: * `starlight.ts` imports `@astrojs/starlight`, `@astrojs/markdown-remark` and * `rehype-mermaid`, and `content.config.ts` imports `astro:content`. Kept on the * same versions as the two sites in the norsk-ctl monorepo so one theme is only * ever exercised against one Astro major; docs-site.test.ts pins the set to the * theme's own imports so a theme that grows a dependency cannot ship a site that * cannot build. */ export const SITE_DEPENDENCIES: Record = { // An OPTIONAL peer of both astro and starlight, so nothing installs it for // you: `starlight.ts` builds the markdown pipeline out of it (`unified()`), // and a site that leaves it to hoisting is one lockfile away from a build // that cannot resolve it. "@astrojs/markdown-remark": "^7.3.0", "@astrojs/starlight": "^0.41.3", astro: "^7.1.0", "rehype-mermaid": "^3.0.0", // Astro's image pipeline; an optional dep of astro, declared so a build never // depends on whether it was hoisted. sharp: "^0.33.5", }; /** rehype-mermaid renders a diagram by driving a real browser, so a page with a * mermaid fence needs one. Nothing else in the site does. */ export const SITE_DEV_DEPENDENCIES: Record = { playwright: "^1.59.0" }; function packageJson(ctx: ShapeContext): string { return `${JSON.stringify( { name: `${ctx.name}-docs`, version: "0.0.1", private: true, type: "module", scripts: { dev: "astro dev", // --force clears Astro's content cache: the collection is fed by // symlinks into ../docs, and a cached entry outlives the file it came // from. build: "astro build --force", preview: "astro preview", }, dependencies: SITE_DEPENDENCIES, devDependencies: SITE_DEV_DEPENDENCIES, }, null, 2, )}\n`; } const TSCONFIG = `{ "extends": "astro/tsconfigs/strict" } `; function siteTs(ctx: ShapeContext): string { return `/** The canonical public origin of this product's docs. * * ONE value: Astro's \`site\` (which drives the sitemap and the canonical * links) and anything that checks what was published read it from here rather * than repeating a literal. * * The domain is NOT live until somebody creates the Cloudflare Pages project * and the DNS record for it — see conventions/docs-site.md, "Publishing". The * value belongs here from the start so the sitemap and the canonical links are * right the day it is. */ export const PUBLIC_SITE_ORIGIN = "https://${ctx.dashboardKey}.norsk.video"; `; } const VERSIONS_TS = `/** * The version this docs build was built against, rendered in the sidebar * footer, so a reader can tell what the page describes. * * Read with node fs at build time rather than imported: the site is a * standalone package outside the repo's workspaces, so it cannot import from * them. \`shared/src/version.ts\` carries the same version as the runtime's own * copy; the repo's package.json is the one a docs build can read. */ import { readFileSync } from "node:fs"; import { join } from "node:path"; export function readProductVersion(repoRoot: string): string { const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")) as { version?: string }; if (!pkg.version) throw new Error("docs versions: the repo's package.json has no version"); return pkg.version; } `; function astroConfig(ctx: ShapeContext): string { return `import { fileURLToPath } from "node:url"; import { defineConfig } from "astro/config"; // The shared Norsk docs theme. \`src/starlight.ts\`, \`src/custom.css\`, // \`src/content.config.ts\`, \`src/components/\`, \`src/fonts/\` and // \`public/favicon.png\` are COPIES of @norskvideo/ctl-dev-kit's docs-theme, // rewritten by \`bun run sync:drift\` and gated by \`bun run check:drift\` — // never hand-edited. The dev-kit's docs-theme/README.md says why a copy and not // a dependency (this package is standalone: its install is its own, and a file: // dep on the dev-kit cannot resolve the dev-kit's workspace deps). import { norskDocsSite } from "./src/starlight.ts"; import { PUBLIC_SITE_ORIGIN } from "./src/lib/site.ts"; import { readProductVersion } from "./src/lib/versions.ts"; // The repo root, one level up — the docs state the version they were built // against, read from the product's own files. const repoRoot = fileURLToPath(new URL("..", import.meta.url)); export default defineConfig( norskDocsSite({ origin: PUBLIC_SITE_ORIGIN, // The BROWSER TAB suffix ("Install | Norsk") — the product FAMILY, as on // every other Norsk manual, so several of them read as one family in a tab // strip. The masthead below is deliberately different and names the product // in hand. tabTitle: "Norsk", wordmark: "${ctx.name}", buildVersions: [{ label: "${ctx.name}", value: readProductVersion(repoRoot) }], // FIVE SECTIONS, the same top level as every other Norsk manual, so a // reader who knows one can navigate this one. Keep the shape; replace the // placeholder pages. What belongs in each is on the page itself, and the // reader each serves is in the dev-kit's conventions/personas.md. sidebar: [ { label: "Start here", items: [{ label: "${ctx.name}", slug: "" }] }, { label: "Guides", items: [{ label: "Overview", slug: "guides" }] }, { label: "How-to", items: [{ label: "Overview", slug: "how-to" }] }, { label: "Concepts", items: [{ label: "Overview", slug: "concepts" }] }, { label: "Reference", items: [{ label: "Overview", slug: "reference" }] }, ], }), ); `; } /** A placeholder page: real front matter, and prose that says what the section is * for and who reads it rather than lorem ipsum a product might ship by accident. */ function page(title: string, description: string, body: string): string { return `--- title: "${title}" description: "${description}" --- ${body}`; } function indexMdx(ctx: ShapeContext): string { return page( ctx.name, `What ${ctx.name} does and where to start.`, `**${ctx.name}** — one paragraph saying what this product does, for someone who has just arrived and does not yet know. Lead with the capability, not the architecture. This page is the **Evaluator's** page: it answers "does this do what I need?" in a couple of minutes, and hands off to the rest of the manual. ## Where to start - **See it working:** a link into [Guides](/docs/guides/), once there is a walkthrough to link to. - **Run it somewhere real:** a link into [How-to](/docs/how-to/). - **Understand the model:** [Concepts](/docs/concepts/) is the vocabulary the rest of the manual uses. The four readers this manual serves — Evaluator, Builder, Integrator, Administrator — are defined in the dev-kit's \`node_modules/@norskvideo/ctl-dev-kit/conventions/personas.md\`, and which of them each section is for is on that section's own page. `, ); } const GUIDES_MDX = page( "Guides", "Walkthroughs: one task, start to finish, in the order a reader does it.", `A **guide** walks one task from start to finish, in the order a reader does it, and does not stop to explain alternatives. Its reader is the **Builder** — the person standing the product up for the first time. One page per task. Where the same task can be done more than one way (a CLI, a web UI, an HTTP API), each way is its own page under one navigation entry, because a reader has already chosen before they arrive. Guides are the pages worth **capturing rather than illustrating**: a walkthrough generated by a test that really ran cannot describe a screen that does not exist. The dev-kit's \`conventions/docs.md\` covers the capture toolchain, and the \`capturedAgainst\` front matter the shared theme renders at the foot of such a page. _Replace this page with the first guide, and put it in the sidebar in \`astro.config.mjs\`._ `, ); const HOW_TO_MDX = page( "How-to", "Operator tasks: install, upgrade, back up, and the contracts a host must meet.", `A **how-to** answers a question an operator arrives with — install, upgrade, back up, restore, what a host must provide — and it assumes they already know why they are doing it. Its reader is the **Administrator**. The difference from Guides is the reader's state, not the format: a guide is read once, front to back, by someone learning the product; a how-to is looked up, repeatedly, by someone running it. _Replace this page with the operator runbook, and put it in the sidebar in \`astro.config.mjs\`._ `, ); const CONCEPTS_MDX = page( "Concepts", "The vocabulary the rest of this manual uses.", `**Concepts** is the vocabulary — the nouns this product deals in, what each one is, and how they relate. It is not architecture, and it is not a tour of the implementation: a reader comes here because a word in a guide meant nothing to them. Keep it to terms that appear elsewhere in the manual. A concept nothing else references is a sign the section is documenting the code rather than the product. _Replace this page with the model, and put its pages in the sidebar in \`astro.config.mjs\`._ `, ); const REFERENCE_MDX = page( "Reference", "Exhaustive, look-up-able: the API, the configuration, the limits.", `**Reference** is the exhaustive, look-up-able material — the HTTP API, every configuration field, the limits. Its reader is the **Integrator**, who is writing something against this product and needs the whole surface rather than a path through it. Reference pages are the ones worth generating from the source of truth (an OpenAPI document, a schema) rather than writing by hand, because a hand-written reference is wrong the first time the surface changes. _Replace this page with the real reference, and put it in the sidebar in \`astro.config.mjs\`._ `, ); function readmeMd(ctx: ShapeContext): string { return `# ${ctx.name} docs site The public manual for ${ctx.name}, an [Astro](https://astro.build) + [Starlight](https://starlight.astro.build) site on the **shared Norsk docs theme**. \`https://${ctx.dashboardKey}.norsk.video\` once it is published; until then it builds and is read locally. ## Working on it This is a **standalone package** — its own install, outside the repo's workspaces, because Astro's dependency tree has no business in the product's install. So the repo-root \`bun install\` does not cover it: \`\`\` bun install --cwd website # once bun run docs:dev # dev server, hot reload bun run docs:site # build (base /docs, into website/dist-embedded) bun run docs:site:public # build for the public domain (into website/dist-public) \`\`\` ## What is this site's, and what is the theme's This site owns four things, all in \`website/astro.config.mjs\` and \`website/src/lib/\`: its origin, its two names (the browser-tab suffix and the masthead wordmark), its sidebar, and the version rows in the sidebar footer. Its content is \`website/src/content/docs/\`. Everything else — the palette, the fonts, the component overrides, the content schema, the two-build shape — is the **shared theme**, and reaches this repo as COPIES: \`src/starlight.ts\`, \`src/custom.css\`, \`src/content.config.ts\`, \`src/components/\`, \`src/fonts/\` and \`public/favicon.png\`. Do not hand-edit them: \`bun run check:drift\` fails when they drift from the installed \`@norskvideo/ctl-dev-kit\`, and \`bun run sync:drift\` rewrites them. A styling change belongs in the dev-kit, where it reaches every product's manual at once. \`node_modules/@norskvideo/ctl-dev-kit/conventions/docs-site.md\` is the full contract — the five sections, the two builds, publishing, and how this site relates to the illustrated manual the image serves at \`/docs\`. `; } export function docsSiteFiles(ctx: ShapeContext): GeneratedFile[] { return [ { path: `${SITE_DIR}/package.json`, content: packageJson(ctx) }, { path: `${SITE_DIR}/tsconfig.json`, content: TSCONFIG }, { path: `${SITE_DIR}/astro.config.mjs`, content: astroConfig(ctx) }, { path: `${SITE_DIR}/README.md`, content: readmeMd(ctx) }, { path: `${SITE_DIR}/src/lib/site.ts`, content: siteTs(ctx) }, { path: `${SITE_DIR}/src/lib/versions.ts`, content: VERSIONS_TS }, { path: `${SITE_DIR}/src/content/docs/index.mdx`, content: indexMdx(ctx) }, { path: `${SITE_DIR}/src/content/docs/guides/index.mdx`, content: GUIDES_MDX }, { path: `${SITE_DIR}/src/content/docs/how-to/index.mdx`, content: HOW_TO_MDX }, { path: `${SITE_DIR}/src/content/docs/concepts/index.mdx`, content: CONCEPTS_MDX }, { path: `${SITE_DIR}/src/content/docs/reference/index.mdx`, content: REFERENCE_MDX }, ]; } /** Astro's build output and cache — gitignored, since the repo-root core block * only knows about `dist/`. */ export const DOCS_SITE_GITIGNORE = `# The docs site (website/) is a standalone Astro package: its own install, and # two builds (base /docs for a bundle a binary can embed, / for the public site). ${SITE_DIR}/dist-embedded/ ${SITE_DIR}/dist-public/ ${SITE_DIR}/.astro/ `; /** Before either build: a placeholder page for every recorded guide the * site symlinks from docs/generated whose capture has not run (dev-kit * docs/guide-stubs.ts), so the site builds on a checkout with no captures -- * as ctl's and the Manager's do. A site that declares no recorded guides is * a no-op. The publish workflow refuses to deploy a placeholder. */ const STUB_GUIDES = `bun run node_modules/@norskvideo/ctl-dev-kit/docs/guide-stubs.ts stub ${SITE_DIR}/src/content/docs/guides`; /** The root package.json scripts that drive it. Named the same in every repo * that has a site, including the two in the norsk-ctl monorepo. */ export const DOCS_SITE_SCRIPTS: Record = { "docs:dev": `bun run --cwd ${SITE_DIR} dev`, "docs:site": `${STUB_GUIDES} && bun run --cwd ${SITE_DIR} build`, "docs:site:public": `${STUB_GUIDES} && ASTRO_BUILD=public bun run --cwd ${SITE_DIR} build`, };