@norskvideo/ctl-dev-kit 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/conventions/check-drift.ts +39 -0
- package/conventions/docs-site.md +185 -0
- package/conventions/sync-drift.ts +23 -0
- package/create-product/create-product.ts +10 -1
- package/create-product/docs-site.ts +316 -0
- package/create-product/turnkey.ts +51 -5
- package/docs-theme/README.md +106 -0
- package/docs-theme/components/Footer.astro +40 -0
- package/docs-theme/components/Sidebar.astro +73 -0
- package/docs-theme/components/SiteTitle.astro +52 -0
- package/docs-theme/content.config.ts +25 -0
- package/docs-theme/custom.css +108 -0
- package/docs-theme/fonts/Geist-LICENSE.txt +92 -0
- package/docs-theme/fonts/Geist.woff2 +0 -0
- package/docs-theme/fonts/GeistMono.woff2 +0 -0
- package/docs-theme/public/favicon.png +0 -0
- package/docs-theme/starlight.ts +162 -0
- package/docs-theme/sync.ts +163 -0
- package/package.json +2 -1
|
@@ -21,6 +21,7 @@ import { execFileSync } from "node:child_process";
|
|
|
21
21
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
22
22
|
import { join } from "node:path";
|
|
23
23
|
import { parseManifestSeed } from "@norskvideo/ctl-sdk/manifest-seed";
|
|
24
|
+
import { checkTheme } from "../docs-theme/sync.ts";
|
|
24
25
|
|
|
25
26
|
export interface DriftReport {
|
|
26
27
|
ok: boolean;
|
|
@@ -366,6 +367,34 @@ function verbatimProblem(
|
|
|
366
367
|
// absent ⇒ `studio`, so existing repos are unaffected.
|
|
367
368
|
export type ProductShape = "studio" | "sdk-app";
|
|
368
369
|
|
|
370
|
+
/** The docs site, when a repo has one: the directory holding `astro.config.mjs`.
|
|
371
|
+
* One name across the fleet (norsk-ctl's and the Manager's both), so the gate
|
|
372
|
+
* can find a site without being told. */
|
|
373
|
+
export const DOCS_SITE_DIR = "website";
|
|
374
|
+
|
|
375
|
+
/** `<repo>/website/astro.config.mjs` — the site's existence test. A docs site is
|
|
376
|
+
* a NESTED standalone package a repo may or may not have, which is why the
|
|
377
|
+
* theme is gated conditionally while every other convention file is demanded
|
|
378
|
+
* outright. */
|
|
379
|
+
export function hasDocsSite(repoRoot: string): boolean {
|
|
380
|
+
return existsSync(join(repoRoot, DOCS_SITE_DIR, "astro.config.mjs"));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** The shared docs theme's copies inside a repo's docs site, as gate problems.
|
|
384
|
+
* The theme has its own writer/checker pair (docs-theme/sync.ts) because it
|
|
385
|
+
* predates this clause and a site in THIS monorepo runs it directly; routing it
|
|
386
|
+
* through here as well is what makes `bun run check:drift` sufficient for a
|
|
387
|
+
* product repo, which has no second suite to hang it off. */
|
|
388
|
+
export function docsThemeProblems(repoRoot: string, themeCanonical?: string): string[] {
|
|
389
|
+
if (!hasDocsSite(repoRoot)) return [];
|
|
390
|
+
const site = join(repoRoot, DOCS_SITE_DIR);
|
|
391
|
+
const { problems } = themeCanonical === undefined ? checkTheme(site) : checkTheme(site, themeCanonical);
|
|
392
|
+
// Paths are reported site-relative by the theme's own checker, so prefix them
|
|
393
|
+
// — a product repo reads them from its root — and name the repo script that
|
|
394
|
+
// fixes them, which the theme's message cannot know.
|
|
395
|
+
return problems.map((p) => `${DOCS_SITE_DIR}/${p} (\`bun run sync:drift\` rewrites it.)`);
|
|
396
|
+
}
|
|
397
|
+
|
|
369
398
|
export interface CheckDriftOptions {
|
|
370
399
|
/** The GitHub repo name (`norsk-ctl-product-playout`), which decides whether the
|
|
371
400
|
* public docs variant may be carried. The CLI resolves it ({@link resolveRepoName});
|
|
@@ -373,6 +402,9 @@ export interface CheckDriftOptions {
|
|
|
373
402
|
repoName?: string;
|
|
374
403
|
/** The product shape ({@link resolveShape}); a fixture passes it. Absent ⇒ `studio`. */
|
|
375
404
|
shape?: ProductShape;
|
|
405
|
+
/** The canonical docs-theme directory. Defaults to this package's own, which
|
|
406
|
+
* is what a product repo wants; a fixture passes one. */
|
|
407
|
+
themeCanonical?: string;
|
|
376
408
|
}
|
|
377
409
|
|
|
378
410
|
/** A product's shape from `package.json` `ctlProduct.shape`; `studio` when the
|
|
@@ -697,6 +729,13 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: Ch
|
|
|
697
729
|
}
|
|
698
730
|
}
|
|
699
731
|
|
|
732
|
+
// The docs site's theme (docs-theme/): gated only when the repo HAS a site,
|
|
733
|
+
// since that is a nested standalone package rather than a repo-root file. A
|
|
734
|
+
// site in this monorepo also runs the theme gate from its own test suite; a
|
|
735
|
+
// product repo has no such suite, so check:drift is where the copies get
|
|
736
|
+
// checked, and one clause covers both.
|
|
737
|
+
for (const p of docsThemeProblems(repoRoot, opts.themeCanonical)) push(p);
|
|
738
|
+
|
|
700
739
|
for (const p of demoProblems(repoRoot, canonical.demoShim)) push(p);
|
|
701
740
|
|
|
702
741
|
return { ok: problems.length === 0, problems };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# The product docs site (shared across all Norsk ctl product repos)
|
|
2
|
+
|
|
3
|
+
What a product repo needs in order to have a **docs site** — the public manual at
|
|
4
|
+
`<product>.norsk.video`, an Astro + Starlight package on the shared Norsk theme.
|
|
5
|
+
This is the shared half; the per-product half is the content.
|
|
6
|
+
|
|
7
|
+
Like [`docs.md`](./docs.md) and [`personas.md`](./personas.md) this is a **shipped
|
|
8
|
+
reference**, not a drift-gated copy: read it from
|
|
9
|
+
`node_modules/@norskvideo/ctl-dev-kit/conventions/docs-site.md`. What IS gated is
|
|
10
|
+
the theme itself — see "The theme is a copy, and the gate is what makes that
|
|
11
|
+
safe" below.
|
|
12
|
+
|
|
13
|
+
**`create-product` renders all of it.** A repo generated by
|
|
14
|
+
`bunx @norskvideo/ctl-dev-kit create-product` comes out with a `website/` that
|
|
15
|
+
builds, so the rest of this document is for a repo that predates the generator,
|
|
16
|
+
or for understanding what you have.
|
|
17
|
+
|
|
18
|
+
## Two documentation channels, and which is which
|
|
19
|
+
|
|
20
|
+
A product has two, on purpose, and they are not competing drafts:
|
|
21
|
+
|
|
22
|
+
| | **The illustrated manual** ([`docs.md`](./docs.md)) | **The docs site** (this document) |
|
|
23
|
+
| -------------- | --------------------------------------------------------- | ----------------------------------------------------- |
|
|
24
|
+
| What it is | one hash-routed HTML page + a bundle of markdown/captures | a multi-page Starlight site |
|
|
25
|
+
| Where it lives | `docs/generated/bundle/`, baked into the product image | `website/`, published to `<product>.norsk.video` |
|
|
26
|
+
| How it's read | at `/docs` on the running container, through the daemon | on the open internet, without the product in hand |
|
|
27
|
+
| Version | exactly the version running — that is its whole point | the tip; it says which version it was built against |
|
|
28
|
+
| Cadence | per image | per commit to main |
|
|
29
|
+
| Reader | an operator who already has it | an evaluator, and anyone without a deployment to hand |
|
|
30
|
+
|
|
31
|
+
The bundle is the **version-true** channel and the only channel a turnkey has
|
|
32
|
+
(ADR-0012). The site is the **discoverable** channel. A product with a public
|
|
33
|
+
audience wants both; a turnkey wants only the bundle and should not carry a site.
|
|
34
|
+
|
|
35
|
+
## What the repo owns, and what the theme owns
|
|
36
|
+
|
|
37
|
+
A site is deliberately thin. It owns **four** things:
|
|
38
|
+
|
|
39
|
+
| The site's own | Where |
|
|
40
|
+
| --------------------------------- | ----------------------------- |
|
|
41
|
+
| its canonical origin | `website/src/lib/site.ts` |
|
|
42
|
+
| its two names + sidebar | `website/astro.config.mjs` |
|
|
43
|
+
| the versions it was built against | `website/src/lib/versions.ts` |
|
|
44
|
+
| its content | `website/src/content/docs/` |
|
|
45
|
+
|
|
46
|
+
Everything else comes from the shared theme (`docs-theme/`): the palette, the
|
|
47
|
+
self-hosted Geist faces, the three Starlight component overrides, the content
|
|
48
|
+
schema, the favicon, and `norskDocsSite()` — the whole Astro config bar those
|
|
49
|
+
four. So `astro.config.mjs` is a sidebar plus five lines:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { defineConfig } from "astro/config";
|
|
53
|
+
import { norskDocsSite } from "./src/starlight.ts";
|
|
54
|
+
import { PUBLIC_SITE_ORIGIN } from "./src/lib/site.ts";
|
|
55
|
+
|
|
56
|
+
export default defineConfig(
|
|
57
|
+
norskDocsSite({
|
|
58
|
+
origin: PUBLIC_SITE_ORIGIN,
|
|
59
|
+
tabTitle: "Norsk", // browser-tab suffix: the FAMILY
|
|
60
|
+
wordmark: "norsk-acme", // masthead: the product in hand
|
|
61
|
+
buildVersions: [{ label: "norsk-acme", value: version }],
|
|
62
|
+
sidebar: [...],
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**The two names are different on purpose.** Several Norsk manuals open in one tab
|
|
68
|
+
strip, and tabs truncate, so the tab suffix is the family; the masthead names the
|
|
69
|
+
binary or product the reader is holding.
|
|
70
|
+
|
|
71
|
+
### The five sections
|
|
72
|
+
|
|
73
|
+
Start here · Guides · How-to · Concepts · Reference
|
|
74
|
+
|
|
75
|
+
Every Norsk manual uses this top level — norsk-ctl's, the Manager's, and every
|
|
76
|
+
generated product's — so a reader who has learned one can navigate the next. Who
|
|
77
|
+
each section serves, in the vocabulary of [`personas.md`](./personas.md):
|
|
78
|
+
|
|
79
|
+
- **Start here** — the Evaluator. What this does, and where to go next.
|
|
80
|
+
- **Guides** — the Builder. One task, start to finish, in the order it is done.
|
|
81
|
+
These are the pages worth **capturing** rather than illustrating; see
|
|
82
|
+
[`docs.md`](./docs.md) for the toolchain and the `capturedAgainst` front matter
|
|
83
|
+
the theme renders at the foot of such a page.
|
|
84
|
+
- **How-to** — the Administrator. Looked up, repeatedly, while running it.
|
|
85
|
+
- **Concepts** — the vocabulary the rest of the manual uses. Not architecture.
|
|
86
|
+
- **Reference** — the Integrator. Exhaustive and generated from the source of
|
|
87
|
+
truth where possible.
|
|
88
|
+
|
|
89
|
+
Add sections below these if a product genuinely needs them. Do not rename or
|
|
90
|
+
reorder these five: the shape is the shared part.
|
|
91
|
+
|
|
92
|
+
## The theme is a copy, and the gate is what makes that safe
|
|
93
|
+
|
|
94
|
+
The theme reaches a site as **files copied into it**, not as an import, and
|
|
95
|
+
`docs-theme/README.md` has the measurement behind that: a docs site is a
|
|
96
|
+
standalone package with its own install, so it cannot depend on this one — a
|
|
97
|
+
`file:` dep fails on this package's own `workspace:*` deps.
|
|
98
|
+
|
|
99
|
+
website/src/starlight.ts website/src/components/*.astro
|
|
100
|
+
website/src/custom.css website/src/fonts/*.woff2
|
|
101
|
+
website/src/content.config.ts website/public/favicon.png
|
|
102
|
+
|
|
103
|
+
**Never hand-edit one.** A change to any of them belongs in the dev-kit, where it
|
|
104
|
+
reaches every product's manual at once.
|
|
105
|
+
|
|
106
|
+
- `bun run check:drift` fails when a copy has drifted, naming the file and the
|
|
107
|
+
line. It is the same gate, and the same CI job, that holds `biome.json` and the
|
|
108
|
+
shared workflows — the clause is conditional on `website/astro.config.mjs`
|
|
109
|
+
existing, so a repo without a site is not asked for one.
|
|
110
|
+
- `bun run sync:drift` rewrites the copies from the installed dev-kit. The
|
|
111
|
+
nightly `sync-dev-kit.yml` runs it after bumping the pin, so a theme change
|
|
112
|
+
rolls out hands-free and only a change that genuinely breaks the site goes red.
|
|
113
|
+
|
|
114
|
+
**Keeping the copies at exactly those paths is load-bearing**, not tidiness:
|
|
115
|
+
Astro derives a component's scoped-style class from its path, so moving
|
|
116
|
+
`Footer.astro` rewrites a class attribute on every page of the site.
|
|
117
|
+
|
|
118
|
+
## Adding a site to a repo that has none
|
|
119
|
+
|
|
120
|
+
`create-product` does all of this; by hand it is:
|
|
121
|
+
|
|
122
|
+
1. **`website/package.json`** — a standalone package (`<name>-docs`), NOT a member
|
|
123
|
+
of the root `workspaces` array. Its dependencies are what the theme imports:
|
|
124
|
+
`astro`, `@astrojs/starlight`, `@astrojs/markdown-remark` (an _optional_ peer
|
|
125
|
+
of both, so nothing installs it for you) and `sharp`, plus `playwright` as a
|
|
126
|
+
dev dependency for pages that render a mermaid diagram.
|
|
127
|
+
2. **`website/tsconfig.json`** — `{"extends": "astro/tsconfigs/strict"}`.
|
|
128
|
+
3. **`website/astro.config.mjs`**, `website/src/lib/{site,versions}.ts` — the four
|
|
129
|
+
things above.
|
|
130
|
+
4. **`bun run sync:drift`** to lay the theme copies down.
|
|
131
|
+
5. **`website/src/content/docs/`** — at least an `index.mdx`, plus a page per
|
|
132
|
+
section.
|
|
133
|
+
6. **Root `package.json` scripts** — `docs:dev`, `docs:site`, `docs:site:public`,
|
|
134
|
+
each `bun run --cwd website …`.
|
|
135
|
+
7. **`.gitignore`** — `website/dist-embedded/`, `website/dist-public/`,
|
|
136
|
+
`website/.astro/`. The shared core block only knows about `dist/`.
|
|
137
|
+
|
|
138
|
+
Then `bun install --cwd website` (the repo-root install does NOT cover a
|
|
139
|
+
standalone package) and `bun run docs:site`.
|
|
140
|
+
|
|
141
|
+
## The two builds
|
|
142
|
+
|
|
143
|
+
One content collection, two outputs, selected by `ASTRO_BUILD`:
|
|
144
|
+
|
|
145
|
+
| Build | `ASTRO_BUILD` | base | outDir | What it is |
|
|
146
|
+
| -------- | ------------- | ------- | --------------- | --------------------------------------------- |
|
|
147
|
+
| embedded | unset | `/docs` | `dist-embedded` | a bundle a product binary can embed and serve |
|
|
148
|
+
| public | `public` | `/` | `dist-public` | the site on its own domain |
|
|
149
|
+
|
|
150
|
+
Content is authored with absolute `/docs/...` links — right for the embedded
|
|
151
|
+
build; the public build strips the prefix in a post-build pass, because Astro does
|
|
152
|
+
not base-prefix absolute links in MDX. So **write `/docs/...` links** and let the
|
|
153
|
+
theme handle the other case.
|
|
154
|
+
|
|
155
|
+
A product only needs the public build. The embedded one costs nothing to keep
|
|
156
|
+
(the theme produces it either way) and is there for a binary that wants to serve
|
|
157
|
+
its own manual offline, as `norsk-ctl` and `norsk-mgr` both do — for about 1.2 MB
|
|
158
|
+
of compressed docs on a ~68 MB binary.
|
|
159
|
+
|
|
160
|
+
## Publishing
|
|
161
|
+
|
|
162
|
+
Per site, and none of it is needed to build or review one locally:
|
|
163
|
+
|
|
164
|
+
1. **A Cloudflare Pages project**, e.g. `norsk-acme-docs`.
|
|
165
|
+
2. **A custom domain** on it — `<product>.norsk.video` — and the DNS record.
|
|
166
|
+
3. **API token scope.** The existing `CLOUDFLARE_API_TOKEN` is deliberately narrow
|
|
167
|
+
(Pages:Edit). It must cover the new project: widen it to all Pages projects in
|
|
168
|
+
the account, or issue a second token.
|
|
169
|
+
4. **A publish step** that runs the public build and `wrangler pages deploy`. Take
|
|
170
|
+
the project name from a repo variable rather than a literal, and skip the
|
|
171
|
+
deploy with a message when it is unset — so main is green before Cloudflare
|
|
172
|
+
exists and publishes the day the variable is set.
|
|
173
|
+
|
|
174
|
+
There is no shared `publish-docs-site.yml` in this package yet: the two existing
|
|
175
|
+
sites publish from norsk-ctl's own monorepo workflow. The first product repo to
|
|
176
|
+
publish one should bring the workflow back here and drift-gate it, the way
|
|
177
|
+
`publish-docs.yml` (the bundle's) already is.
|
|
178
|
+
|
|
179
|
+
## Search does not cross products
|
|
180
|
+
|
|
181
|
+
Each site indexes itself (Pagefind), so a reader searching one product's manual
|
|
182
|
+
does not find another's. That is accepted rather than solved: the alternative is
|
|
183
|
+
one corpus and one navigation carrying six products. A hub page listing the
|
|
184
|
+
products is the cheap mitigation; cross-site search is possible later and nothing
|
|
185
|
+
here forecloses it.
|
|
@@ -20,15 +20,18 @@
|
|
|
20
20
|
// product surfaces as a red lint/typecheck/test — not as a silent bad copy.
|
|
21
21
|
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
23
|
+
import { checkTheme, syncTheme } from "../docs-theme/sync.ts";
|
|
23
24
|
import {
|
|
24
25
|
BEGIN,
|
|
25
26
|
type CanonicalBytes,
|
|
26
27
|
CTL_HASH_LINE,
|
|
27
28
|
CTL_VERSION_LINE,
|
|
28
29
|
carriesIntegrationMarker,
|
|
30
|
+
DOCS_SITE_DIR,
|
|
29
31
|
END,
|
|
30
32
|
GITIGNORE_BEGIN,
|
|
31
33
|
GITIGNORE_END,
|
|
34
|
+
hasDocsSite,
|
|
32
35
|
PRODUCT_LINE,
|
|
33
36
|
PRODUCT_SENTINEL,
|
|
34
37
|
type ProductShape,
|
|
@@ -260,6 +263,25 @@ function syncWithDisplay(repoRoot: string, canonical: string, r: SyncReport): vo
|
|
|
260
263
|
chmodSync(path, 0o755);
|
|
261
264
|
}
|
|
262
265
|
|
|
266
|
+
// The docs site's theme copies — the writer behind check-drift's docsThemeProblems.
|
|
267
|
+
// Only for a repo that HAS a site: sync never creates one (a site is a nested
|
|
268
|
+
// standalone package with its own install, not a file this writer can conjure),
|
|
269
|
+
// it only keeps an existing one's copies in step. That is what makes a theme
|
|
270
|
+
// change roll out through sync-dev-kit.yml like any other convention.
|
|
271
|
+
function syncDocsTheme(repoRoot: string, r: SyncReport): void {
|
|
272
|
+
if (!hasDocsSite(repoRoot)) {
|
|
273
|
+
r.skipped.push(`${DOCS_SITE_DIR}/ docs theme (no docs site)`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const site = join(repoRoot, DOCS_SITE_DIR);
|
|
277
|
+
// The report names what CHANGED, and syncTheme rewrites every copy
|
|
278
|
+
// unconditionally — so ask the gate first which ones are actually adrift. Its
|
|
279
|
+
// problems are `<site-relative path>: <reason>`.
|
|
280
|
+
const adrift = checkTheme(site).problems.map((p) => p.slice(0, p.indexOf(":")));
|
|
281
|
+
syncTheme(site);
|
|
282
|
+
for (const rel of adrift) r.written.push(`${DOCS_SITE_DIR}/${rel}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
263
285
|
export function syncDrift(repoRoot: string, canonical: CanonicalBytes, shape: ProductShape = "studio"): SyncReport {
|
|
264
286
|
const r: SyncReport = { written: [], skipped: [] };
|
|
265
287
|
// Shape-selected canonicals: an sdk-app's flake + checks are its own variants
|
|
@@ -310,6 +332,7 @@ export function syncDrift(repoRoot: string, canonical: CanonicalBytes, shape: Pr
|
|
|
310
332
|
syncDprint(repoRoot, canonical.dprint, r);
|
|
311
333
|
syncDemoShim(repoRoot, canonical.demoShim, r);
|
|
312
334
|
syncWithDisplay(repoRoot, canonical.withDisplay, r);
|
|
335
|
+
syncDocsTheme(repoRoot, r);
|
|
313
336
|
|
|
314
337
|
return r;
|
|
315
338
|
}
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
// The convention layer is shared verbatim across every shape.
|
|
13
13
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
|
+
import { syncTheme } from "../docs-theme/sync.ts";
|
|
15
16
|
import { type Canon, loadAsset, loadCanon, PRODUCT_SENTINEL } from "./canon.ts";
|
|
17
|
+
import { SITE_DIR as DOCS_SITE_DIR } from "./docs-site.ts";
|
|
16
18
|
import { turnkey } from "./turnkey.ts";
|
|
17
19
|
|
|
18
20
|
// A SHAPE is a preset over FEATURES, not a separate skeleton — the four
|
|
@@ -289,5 +291,12 @@ export function createProduct(opts: CreateProductOptions): { files: string[] } {
|
|
|
289
291
|
mkdirSync(dirname(path), { recursive: true });
|
|
290
292
|
writeFileSync(path, file.content, { mode: file.executable ? 0o755 : 0o644 });
|
|
291
293
|
}
|
|
292
|
-
|
|
294
|
+
// The docs site's theme files are written by the THEME'S OWN writer, not
|
|
295
|
+
// rendered here: they include two woff2 faces (a plan entry is a string), and
|
|
296
|
+
// more to the point one copier and one gate is the whole arrangement — the
|
|
297
|
+
// same `syncTheme` the drift gate's writer half calls, against the same
|
|
298
|
+
// canonical bytes `check-drift` compares. A second rendering of those bytes
|
|
299
|
+
// here is exactly the drift this package exists to stop.
|
|
300
|
+
const themeFiles = syncTheme(join(opts.dir, DOCS_SITE_DIR)).map((rel) => `${DOCS_SITE_DIR}/${rel}`);
|
|
301
|
+
return { files: [...plan.map((f) => f.path), ...themeFiles] };
|
|
293
302
|
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// The product's DOCS SITE — `website/`, an Astro + Starlight package on the
|
|
2
|
+
// shared theme (docs-theme/), with the five-section top level every Norsk
|
|
3
|
+
// manual uses: Start here, Guides, How-to, Concepts, Reference.
|
|
4
|
+
//
|
|
5
|
+
// Emitted for every shape, not as a feature, because it is not optional in the
|
|
6
|
+
// way an operator screen is: every product has readers. What the generator
|
|
7
|
+
// writes is a SKELETON — five near-empty pages saying what belongs in each
|
|
8
|
+
// section — because a site whose sections a product must invent is a site the
|
|
9
|
+
// product will instead paste from somewhere else.
|
|
10
|
+
//
|
|
11
|
+
// Two things make this different from every other workspace here:
|
|
12
|
+
//
|
|
13
|
+
// 1. It is NOT in the root `workspaces` array. A docs site is a STANDALONE
|
|
14
|
+
// package with its own install, deliberately: Astro's dependency tree has
|
|
15
|
+
// no business in the product's own install, and the theme is copied rather
|
|
16
|
+
// than imported for exactly that reason (docs-theme/sync.ts has the bun
|
|
17
|
+
// error that proves it). So `bun install` at the repo root does not install
|
|
18
|
+
// it — `bun install --cwd website` does, and the generated README says so.
|
|
19
|
+
// 2. Its theme files are NOT written from here. `createProduct` lays them
|
|
20
|
+
// down with the theme's own writer (`syncTheme`), so there is one copier
|
|
21
|
+
// and one gate (`bun run check:drift`, via check-drift's docsThemeProblems)
|
|
22
|
+
// rather than a second rendering of the same bytes that could drift.
|
|
23
|
+
//
|
|
24
|
+
// It does NOT replace the illustrated manual (conventions/docs.md): that bundle
|
|
25
|
+
// is still what the image serves at `/docs` and what a public product publishes
|
|
26
|
+
// nightly. The two channels and the choice between them are set out in
|
|
27
|
+
// conventions/docs-site.md.
|
|
28
|
+
import type { GeneratedFile, ShapeContext } from "./create-product.ts";
|
|
29
|
+
|
|
30
|
+
/** The docs site's directory, and the name check-drift's gate looks for. */
|
|
31
|
+
export const SITE_DIR = "website";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What a site needs installed to build. Dictated by the theme, not by taste:
|
|
35
|
+
* `starlight.ts` imports `@astrojs/starlight`, `@astrojs/markdown-remark` and
|
|
36
|
+
* `rehype-mermaid`, and `content.config.ts` imports `astro:content`. Kept on the
|
|
37
|
+
* same versions as the two sites in the norsk-ctl monorepo so one theme is only
|
|
38
|
+
* ever exercised against one Astro major; docs-site.test.ts pins the set to the
|
|
39
|
+
* theme's own imports so a theme that grows a dependency cannot ship a site that
|
|
40
|
+
* cannot build.
|
|
41
|
+
*/
|
|
42
|
+
export const SITE_DEPENDENCIES: Record<string, string> = {
|
|
43
|
+
// An OPTIONAL peer of both astro and starlight, so nothing installs it for
|
|
44
|
+
// you: `starlight.ts` builds the markdown pipeline out of it (`unified()`),
|
|
45
|
+
// and a site that leaves it to hoisting is one lockfile away from a build
|
|
46
|
+
// that cannot resolve it.
|
|
47
|
+
"@astrojs/markdown-remark": "^7.3.0",
|
|
48
|
+
"@astrojs/starlight": "^0.41.3",
|
|
49
|
+
astro: "^7.1.0",
|
|
50
|
+
"rehype-mermaid": "^3.0.0",
|
|
51
|
+
// Astro's image pipeline; an optional dep of astro, declared so a build never
|
|
52
|
+
// depends on whether it was hoisted.
|
|
53
|
+
sharp: "^0.33.5",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** rehype-mermaid renders a diagram by driving a real browser, so a page with a
|
|
57
|
+
* mermaid fence needs one. Nothing else in the site does. */
|
|
58
|
+
export const SITE_DEV_DEPENDENCIES: Record<string, string> = { playwright: "^1.59.0" };
|
|
59
|
+
|
|
60
|
+
function packageJson(ctx: ShapeContext): string {
|
|
61
|
+
return `${JSON.stringify(
|
|
62
|
+
{
|
|
63
|
+
name: `${ctx.name}-docs`,
|
|
64
|
+
version: "0.0.1",
|
|
65
|
+
private: true,
|
|
66
|
+
type: "module",
|
|
67
|
+
scripts: {
|
|
68
|
+
dev: "astro dev",
|
|
69
|
+
// --force clears Astro's content cache: the collection is fed by
|
|
70
|
+
// symlinks into ../docs, and a cached entry outlives the file it came
|
|
71
|
+
// from.
|
|
72
|
+
build: "astro build --force",
|
|
73
|
+
preview: "astro preview",
|
|
74
|
+
},
|
|
75
|
+
dependencies: SITE_DEPENDENCIES,
|
|
76
|
+
devDependencies: SITE_DEV_DEPENDENCIES,
|
|
77
|
+
},
|
|
78
|
+
null,
|
|
79
|
+
2,
|
|
80
|
+
)}\n`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const TSCONFIG = `{
|
|
84
|
+
"extends": "astro/tsconfigs/strict"
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
function siteTs(ctx: ShapeContext): string {
|
|
89
|
+
return `/** The canonical public origin of this product's docs.
|
|
90
|
+
*
|
|
91
|
+
* ONE value: Astro's \`site\` (which drives the sitemap and the canonical
|
|
92
|
+
* links) and anything that checks what was published read it from here rather
|
|
93
|
+
* than repeating a literal.
|
|
94
|
+
*
|
|
95
|
+
* The domain is NOT live until somebody creates the Cloudflare Pages project
|
|
96
|
+
* and the DNS record for it — see conventions/docs-site.md, "Publishing". The
|
|
97
|
+
* value belongs here from the start so the sitemap and the canonical links are
|
|
98
|
+
* right the day it is. */
|
|
99
|
+
export const PUBLIC_SITE_ORIGIN = "https://${ctx.dashboardKey}.norsk.video";
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const VERSIONS_TS = `/**
|
|
104
|
+
* The version this docs build was built against, rendered in the sidebar
|
|
105
|
+
* footer, so a reader can tell what the page describes.
|
|
106
|
+
*
|
|
107
|
+
* Read with node fs at build time rather than imported: the site is a
|
|
108
|
+
* standalone package outside the repo's workspaces, so it cannot import from
|
|
109
|
+
* them. \`shared/src/version.ts\` carries the same version as the runtime's own
|
|
110
|
+
* copy; the repo's package.json is the one a docs build can read.
|
|
111
|
+
*/
|
|
112
|
+
import { readFileSync } from "node:fs";
|
|
113
|
+
import { join } from "node:path";
|
|
114
|
+
|
|
115
|
+
export function readProductVersion(repoRoot: string): string {
|
|
116
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")) as { version?: string };
|
|
117
|
+
if (!pkg.version) throw new Error("docs versions: the repo's package.json has no version");
|
|
118
|
+
return pkg.version;
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
function astroConfig(ctx: ShapeContext): string {
|
|
123
|
+
return `import { fileURLToPath } from "node:url";
|
|
124
|
+
import { defineConfig } from "astro/config";
|
|
125
|
+
// The shared Norsk docs theme. \`src/starlight.ts\`, \`src/custom.css\`,
|
|
126
|
+
// \`src/content.config.ts\`, \`src/components/\`, \`src/fonts/\` and
|
|
127
|
+
// \`public/favicon.png\` are COPIES of @norskvideo/ctl-dev-kit's docs-theme,
|
|
128
|
+
// rewritten by \`bun run sync:drift\` and gated by \`bun run check:drift\` —
|
|
129
|
+
// never hand-edited. The dev-kit's docs-theme/README.md says why a copy and not
|
|
130
|
+
// a dependency (this package is standalone: its install is its own, and a file:
|
|
131
|
+
// dep on the dev-kit cannot resolve the dev-kit's workspace deps).
|
|
132
|
+
import { norskDocsSite } from "./src/starlight.ts";
|
|
133
|
+
import { PUBLIC_SITE_ORIGIN } from "./src/lib/site.ts";
|
|
134
|
+
import { readProductVersion } from "./src/lib/versions.ts";
|
|
135
|
+
|
|
136
|
+
// The repo root, one level up — the docs state the version they were built
|
|
137
|
+
// against, read from the product's own files.
|
|
138
|
+
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
139
|
+
|
|
140
|
+
export default defineConfig(
|
|
141
|
+
norskDocsSite({
|
|
142
|
+
origin: PUBLIC_SITE_ORIGIN,
|
|
143
|
+
// The BROWSER TAB suffix ("Install | Norsk") — the product FAMILY, as on
|
|
144
|
+
// every other Norsk manual, so several of them read as one family in a tab
|
|
145
|
+
// strip. The masthead below is deliberately different and names the product
|
|
146
|
+
// in hand.
|
|
147
|
+
tabTitle: "Norsk",
|
|
148
|
+
wordmark: "${ctx.name}",
|
|
149
|
+
buildVersions: [{ label: "${ctx.name}", value: readProductVersion(repoRoot) }],
|
|
150
|
+
// FIVE SECTIONS, the same top level as every other Norsk manual, so a
|
|
151
|
+
// reader who knows one can navigate this one. Keep the shape; replace the
|
|
152
|
+
// placeholder pages. What belongs in each is on the page itself, and the
|
|
153
|
+
// reader each serves is in the dev-kit's conventions/personas.md.
|
|
154
|
+
sidebar: [
|
|
155
|
+
{ label: "Start here", items: [{ label: "${ctx.name}", slug: "" }] },
|
|
156
|
+
{ label: "Guides", items: [{ label: "Overview", slug: "guides" }] },
|
|
157
|
+
{ label: "How-to", items: [{ label: "Overview", slug: "how-to" }] },
|
|
158
|
+
{ label: "Concepts", items: [{ label: "Overview", slug: "concepts" }] },
|
|
159
|
+
{ label: "Reference", items: [{ label: "Overview", slug: "reference" }] },
|
|
160
|
+
],
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** A placeholder page: real front matter, and prose that says what the section is
|
|
167
|
+
* for and who reads it rather than lorem ipsum a product might ship by accident. */
|
|
168
|
+
function page(title: string, description: string, body: string): string {
|
|
169
|
+
return `---
|
|
170
|
+
title: "${title}"
|
|
171
|
+
description: "${description}"
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
${body}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function indexMdx(ctx: ShapeContext): string {
|
|
178
|
+
return page(
|
|
179
|
+
ctx.name,
|
|
180
|
+
`What ${ctx.name} does and where to start.`,
|
|
181
|
+
`**${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.
|
|
182
|
+
|
|
183
|
+
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.
|
|
184
|
+
|
|
185
|
+
## Where to start
|
|
186
|
+
|
|
187
|
+
- **See it working:** a link into [Guides](/docs/guides/), once there is a walkthrough to link to.
|
|
188
|
+
- **Run it somewhere real:** a link into [How-to](/docs/how-to/).
|
|
189
|
+
- **Understand the model:** [Concepts](/docs/concepts/) is the vocabulary the rest of the manual uses.
|
|
190
|
+
|
|
191
|
+
The four readers this manual serves — Evaluator, Builder, Integrator, Administrator — are defined in the dev-kit's
|
|
192
|
+
\`node_modules/@norskvideo/ctl-dev-kit/conventions/personas.md\`, and which of them each section is for is on that section's own page.
|
|
193
|
+
`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const GUIDES_MDX = page(
|
|
198
|
+
"Guides",
|
|
199
|
+
"Walkthroughs: one task, start to finish, in the order a reader does it.",
|
|
200
|
+
`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.
|
|
201
|
+
|
|
202
|
+
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.
|
|
203
|
+
|
|
204
|
+
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.
|
|
205
|
+
|
|
206
|
+
_Replace this page with the first guide, and put it in the sidebar in \`astro.config.mjs\`._
|
|
207
|
+
`,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const HOW_TO_MDX = page(
|
|
211
|
+
"How-to",
|
|
212
|
+
"Operator tasks: install, upgrade, back up, and the contracts a host must meet.",
|
|
213
|
+
`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**.
|
|
214
|
+
|
|
215
|
+
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.
|
|
216
|
+
|
|
217
|
+
_Replace this page with the operator runbook, and put it in the sidebar in \`astro.config.mjs\`._
|
|
218
|
+
`,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const CONCEPTS_MDX = page(
|
|
222
|
+
"Concepts",
|
|
223
|
+
"The vocabulary the rest of this manual uses.",
|
|
224
|
+
`**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.
|
|
225
|
+
|
|
226
|
+
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.
|
|
227
|
+
|
|
228
|
+
_Replace this page with the model, and put its pages in the sidebar in \`astro.config.mjs\`._
|
|
229
|
+
`,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const REFERENCE_MDX = page(
|
|
233
|
+
"Reference",
|
|
234
|
+
"Exhaustive, look-up-able: the API, the configuration, the limits.",
|
|
235
|
+
`**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.
|
|
236
|
+
|
|
237
|
+
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.
|
|
238
|
+
|
|
239
|
+
_Replace this page with the real reference, and put it in the sidebar in \`astro.config.mjs\`._
|
|
240
|
+
`,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
function readmeMd(ctx: ShapeContext): string {
|
|
244
|
+
return `# ${ctx.name} docs site
|
|
245
|
+
|
|
246
|
+
The public manual for ${ctx.name}, an [Astro](https://astro.build) +
|
|
247
|
+
[Starlight](https://starlight.astro.build) site on the **shared Norsk docs
|
|
248
|
+
theme**. \`https://${ctx.dashboardKey}.norsk.video\` once it is published; until
|
|
249
|
+
then it builds and is read locally.
|
|
250
|
+
|
|
251
|
+
## Working on it
|
|
252
|
+
|
|
253
|
+
This is a **standalone package** — its own install, outside the repo's
|
|
254
|
+
workspaces, because Astro's dependency tree has no business in the product's
|
|
255
|
+
install. So the repo-root \`bun install\` does not cover it:
|
|
256
|
+
|
|
257
|
+
\`\`\`
|
|
258
|
+
bun install --cwd website # once
|
|
259
|
+
bun run docs:dev # dev server, hot reload
|
|
260
|
+
bun run docs:site # build (base /docs, into website/dist-embedded)
|
|
261
|
+
bun run docs:site:public # build for the public domain (into website/dist-public)
|
|
262
|
+
\`\`\`
|
|
263
|
+
|
|
264
|
+
## What is this site's, and what is the theme's
|
|
265
|
+
|
|
266
|
+
This site owns four things, all in \`website/astro.config.mjs\` and
|
|
267
|
+
\`website/src/lib/\`: its origin, its two names (the browser-tab suffix and the
|
|
268
|
+
masthead wordmark), its sidebar, and the version rows in the sidebar footer.
|
|
269
|
+
Its content is \`website/src/content/docs/\`.
|
|
270
|
+
|
|
271
|
+
Everything else — the palette, the fonts, the component overrides, the content
|
|
272
|
+
schema, the two-build shape — is the **shared theme**, and reaches this repo as
|
|
273
|
+
COPIES: \`src/starlight.ts\`, \`src/custom.css\`, \`src/content.config.ts\`,
|
|
274
|
+
\`src/components/\`, \`src/fonts/\` and \`public/favicon.png\`. Do not hand-edit
|
|
275
|
+
them: \`bun run check:drift\` fails when they drift from the installed
|
|
276
|
+
\`@norskvideo/ctl-dev-kit\`, and \`bun run sync:drift\` rewrites them. A styling
|
|
277
|
+
change belongs in the dev-kit, where it reaches every product's manual at once.
|
|
278
|
+
|
|
279
|
+
\`node_modules/@norskvideo/ctl-dev-kit/conventions/docs-site.md\` is the full contract — the five
|
|
280
|
+
sections, the two builds, publishing, and how this site relates to the
|
|
281
|
+
illustrated manual the image serves at \`/docs\`.
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function docsSiteFiles(ctx: ShapeContext): GeneratedFile[] {
|
|
286
|
+
return [
|
|
287
|
+
{ path: `${SITE_DIR}/package.json`, content: packageJson(ctx) },
|
|
288
|
+
{ path: `${SITE_DIR}/tsconfig.json`, content: TSCONFIG },
|
|
289
|
+
{ path: `${SITE_DIR}/astro.config.mjs`, content: astroConfig(ctx) },
|
|
290
|
+
{ path: `${SITE_DIR}/README.md`, content: readmeMd(ctx) },
|
|
291
|
+
{ path: `${SITE_DIR}/src/lib/site.ts`, content: siteTs(ctx) },
|
|
292
|
+
{ path: `${SITE_DIR}/src/lib/versions.ts`, content: VERSIONS_TS },
|
|
293
|
+
{ path: `${SITE_DIR}/src/content/docs/index.mdx`, content: indexMdx(ctx) },
|
|
294
|
+
{ path: `${SITE_DIR}/src/content/docs/guides/index.mdx`, content: GUIDES_MDX },
|
|
295
|
+
{ path: `${SITE_DIR}/src/content/docs/how-to/index.mdx`, content: HOW_TO_MDX },
|
|
296
|
+
{ path: `${SITE_DIR}/src/content/docs/concepts/index.mdx`, content: CONCEPTS_MDX },
|
|
297
|
+
{ path: `${SITE_DIR}/src/content/docs/reference/index.mdx`, content: REFERENCE_MDX },
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Astro's build output and cache — gitignored, since the repo-root core block
|
|
302
|
+
* only knows about `dist/`. */
|
|
303
|
+
export const DOCS_SITE_GITIGNORE = `# The docs site (website/) is a standalone Astro package: its own install, and
|
|
304
|
+
# two builds (base /docs for a bundle a binary can embed, / for the public site).
|
|
305
|
+
${SITE_DIR}/dist-embedded/
|
|
306
|
+
${SITE_DIR}/dist-public/
|
|
307
|
+
${SITE_DIR}/.astro/
|
|
308
|
+
`;
|
|
309
|
+
|
|
310
|
+
/** The root package.json scripts that drive it. Named the same in every repo
|
|
311
|
+
* that has a site, including the two in the norsk-ctl monorepo. */
|
|
312
|
+
export const DOCS_SITE_SCRIPTS: Record<string, string> = {
|
|
313
|
+
"docs:dev": `bun run --cwd ${SITE_DIR} dev`,
|
|
314
|
+
"docs:site": `bun run --cwd ${SITE_DIR} build`,
|
|
315
|
+
"docs:site:public": `ASTRO_BUILD=public bun run --cwd ${SITE_DIR} build`,
|
|
316
|
+
};
|