@norskvideo/ctl-dev-kit 0.2.3 → 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 +34 -10
- package/docs-theme/public/favicon.png +0 -0
- package/docs-theme/sync.ts +31 -13
- package/package.json +1 -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
|
+
};
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// validate(), INVARIANTS + parity + byte-snapshot + studio-load wired into
|
|
15
15
|
// test:unit, and an image smoke tier.
|
|
16
16
|
import { type GeneratedFile, type ShapeContext, type ShapeModule, studioPackage } from "./create-product.ts";
|
|
17
|
+
import { DOCS_SITE_GITIGNORE, DOCS_SITE_SCRIPTS, docsSiteFiles } from "./docs-site.ts";
|
|
17
18
|
import {
|
|
18
19
|
COMPONENTS_GITIGNORE,
|
|
19
20
|
componentsFiles,
|
|
@@ -188,6 +189,32 @@ fresh image and checks the pins before relaunching.
|
|
|
188
189
|
6. \`bun run docs:handover\` — once a daemon has this product added; fills the
|
|
189
190
|
handover's generated block from the launch truth.
|
|
190
191
|
|
|
192
|
+
## The docs site (\`website/\`)
|
|
193
|
+
|
|
194
|
+
A **standalone package**, deliberately outside the root \`workspaces\` array:
|
|
195
|
+
Astro's dependency tree has no business in the product's install. Two
|
|
196
|
+
consequences that bite in that order —
|
|
197
|
+
|
|
198
|
+
- \`bun install\` at the root does NOT install it. \`bun install --cwd website\`
|
|
199
|
+
does, once; then \`bun run docs:dev\` / \`bun run docs:site\`.
|
|
200
|
+
- \`website/src/starlight.ts\`, \`website/src/custom.css\`,
|
|
201
|
+
\`website/src/content.config.ts\`, \`website/src/components/\`,
|
|
202
|
+
\`website/src/fonts/\` and \`website/public/favicon.png\` are **copies of the
|
|
203
|
+
dev-kit's shared theme**, not this repo's files. \`bun run check:drift\`
|
|
204
|
+
fails on a hand-edited one; \`bun run sync:drift\` rewrites them. A styling or
|
|
205
|
+
layout change belongs in the dev-kit, where it reaches every product's manual.
|
|
206
|
+
|
|
207
|
+
This repo owns four things: the origin (\`website/src/lib/site.ts\`), the two
|
|
208
|
+
names and the sidebar (\`website/astro.config.mjs\`), and the content
|
|
209
|
+
(\`website/src/content/docs/\`). Keep the five top-level sections — Start here,
|
|
210
|
+
Guides, How-to, Concepts, Reference — they are what make several Norsk manuals
|
|
211
|
+
navigable by one reader. The placeholder pages say what belongs in each.
|
|
212
|
+
|
|
213
|
+
It is NOT the bundle the image serves at /docs (\`bun run docs:manual\`); both
|
|
214
|
+
channels exist on purpose, and the dev-kit's
|
|
215
|
+
\`node_modules/@norskvideo/ctl-dev-kit/conventions/docs-site.md\` says which to
|
|
216
|
+
reach for.
|
|
217
|
+
|
|
191
218
|
## Version pins
|
|
192
219
|
|
|
193
220
|
Media + Studio container tags derive from **\`manifest.seed.json\`** (the
|
|
@@ -273,6 +300,9 @@ function rootPackageJson(ctx: ShapeContext): string {
|
|
|
273
300
|
"docs:manual": `bun run node_modules/@norskvideo/ctl-dev-kit/doc-guide/prose-bundle.js --brand "${ctx.name}" --title "${ctx.name} — Documentation" --link "norsk.video=https://norsk.video/" --out docs/generated/bundle docs/handover.md docs/known-limitations.md`,
|
|
274
301
|
"docs:handover": "bash scripts/docs-handover.sh",
|
|
275
302
|
"docs:handover:check": "bash scripts/docs-handover.sh --check",
|
|
303
|
+
// The docs SITE (website/) — a standalone package, so its install is its
|
|
304
|
+
// own: `bun install --cwd website` before the first build.
|
|
305
|
+
...DOCS_SITE_SCRIPTS,
|
|
276
306
|
"sync:drift": "bun run node_modules/@norskvideo/ctl-dev-kit/conventions/sync-drift.ts",
|
|
277
307
|
// codegen first: components/_gen/types.ts is gitignored build output,
|
|
278
308
|
// so on a clean checkout tsc has nothing to resolve against without it.
|
|
@@ -1880,6 +1910,7 @@ ${optional}- \`backend/\` — the control-plane service (manifest, product-templ
|
|
|
1880
1910
|
- \`examples/\` — starter configs; \`examples/default/input.json\` is the default template.
|
|
1881
1911
|
- \`tests/\` — unit, image and demo tiers; \`tests/demo.spec.ts\` is the customer journey.
|
|
1882
1912
|
- \`docs/\` — reader-facing documents; \`docs/README.md\` is the index.
|
|
1913
|
+
- \`website/\` — the public docs site (Astro + Starlight on the shared Norsk theme).
|
|
1883
1914
|
- \`deployment/\` — the image build wrapper and the iterate loop.
|
|
1884
1915
|
|
|
1885
1916
|
## First run
|
|
@@ -1903,11 +1934,20 @@ norsk-ctl and the pinned Norsk SDK:
|
|
|
1903
1934
|
|
|
1904
1935
|
## Documentation
|
|
1905
1936
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
docs
|
|
1910
|
-
|
|
1937
|
+
Two channels, deliberately:
|
|
1938
|
+
|
|
1939
|
+
- **The bundle the image serves.** \`bun run docs:manual\` builds it from
|
|
1940
|
+
\`docs/handover.md\` and \`docs/known-limitations.md\`; the image build runs
|
|
1941
|
+
it, so every image serves them at /docs at the version that is running
|
|
1942
|
+
(linked from the product's page in norsk-ctl). \`bun run docs:handover\`
|
|
1943
|
+
regenerates the handover's generated block from a daemon that has this
|
|
1944
|
+
product added; CI runs its \`--check\`.
|
|
1945
|
+
- **The docs site**, \`website/\` — the public manual on the shared Norsk theme,
|
|
1946
|
+
five sections deep, faster-moving and readable without the product in hand.
|
|
1947
|
+
\`bun install --cwd website\` once, then \`bun run docs:dev\` /
|
|
1948
|
+
\`bun run docs:site\`. \`website/README.md\` says what is the site's and what
|
|
1949
|
+
is the theme's; the dev-kit's
|
|
1950
|
+
\`node_modules/@norskvideo/ctl-dev-kit/conventions/docs-site.md\` is the contract.
|
|
1911
1951
|
|
|
1912
1952
|
\`CLAUDE.md\` has the conventions and the traps.
|
|
1913
1953
|
`;
|
|
@@ -1924,6 +1964,10 @@ served at /docs at the version that is running.
|
|
|
1924
1964
|
| \`docs/handover.md\` | the platform team deploying this product | Deploying from the exported bundle without norsk-ctl: every mount, environment and injected setting |
|
|
1925
1965
|
| \`docs/known-limitations.md\` | operators | What the product does not do yet, with the consequence and the workaround |
|
|
1926
1966
|
|
|
1967
|
+
The **docs site** is a separate channel and lives in \`website/\`: the public
|
|
1968
|
+
manual, on the shared Norsk theme, not built from these files. See
|
|
1969
|
+
\`website/README.md\`.
|
|
1970
|
+
|
|
1927
1971
|
\`bun run docs:manual\` builds the bundle from these files. The tables in the
|
|
1928
1972
|
handover are generated by norsk-ctl (\`bun run docs:handover\`) — regenerate
|
|
1929
1973
|
them, never edit them.
|
|
@@ -2012,6 +2056,7 @@ function gitignoreTail(ctx: ShapeContext): string {
|
|
|
2012
2056
|
const blocks = ["# Repo-specific entries go below (the block above is drift-gated verbatim).\n"];
|
|
2013
2057
|
if (ctx.features.has("components")) blocks.push(COMPONENTS_GITIGNORE);
|
|
2014
2058
|
if (ctx.features.has("dashboard")) blocks.push(DASHBOARD_GITIGNORE);
|
|
2059
|
+
blocks.push(DOCS_SITE_GITIGNORE);
|
|
2015
2060
|
return blocks.join("\n");
|
|
2016
2061
|
}
|
|
2017
2062
|
|
|
@@ -2033,6 +2078,7 @@ export const turnkey: ShapeModule = {
|
|
|
2033
2078
|
{ path: "deployment/iterate.sh", content: iterateSh(ctx), executable: true },
|
|
2034
2079
|
{ path: "scripts/demo", content: ctx.canon.demoShim, executable: true },
|
|
2035
2080
|
{ path: "README.md", content: readmeMd(ctx) },
|
|
2081
|
+
...docsSiteFiles(ctx),
|
|
2036
2082
|
{ path: "docs/README.md", content: docsReadmeMd(ctx) },
|
|
2037
2083
|
{ path: "docs/handover.md", content: handoverMd(ctx) },
|
|
2038
2084
|
{ path: "docs/known-limitations.md", content: KNOWN_LIMITATIONS_MD },
|
package/docs-theme/README.md
CHANGED
|
@@ -7,6 +7,7 @@ genuinely differs — its content, its sidebar, its origin and its two names.
|
|
|
7
7
|
|
|
8
8
|
custom.css the palette + self-hosted Geist faces
|
|
9
9
|
fonts/ the woff2 those @font-face rules point at
|
|
10
|
+
public/favicon.png the favicon `starlight.ts` names (copied to the site ROOT, not src/)
|
|
10
11
|
components/Footer.astro the "captured against" line on a generated guide
|
|
11
12
|
components/Sidebar.astro the "Built against" versions footer
|
|
12
13
|
components/SiteTitle.astro the masthead wordmark, decoupled from the tab title
|
|
@@ -63,20 +64,43 @@ bun node_modules/@norskvideo/ctl-dev-kit/docs-theme/sync.ts check <site>
|
|
|
63
64
|
|
|
64
65
|
`<site>` is the directory holding `astro.config.mjs`; the copies land in its
|
|
65
66
|
`src/`, so this directory mirrors a site's `src/` and the path map is an
|
|
66
|
-
identity.
|
|
67
|
+
identity. The one exception is `public/`, which mirrors the site's `public/` —
|
|
68
|
+
Astro serves it verbatim from the site root, and `starlight.ts` already dictates
|
|
69
|
+
the favicon's path (`favicon: "/favicon.png"`), so its bytes belong here too.
|
|
70
|
+
|
|
71
|
+
A site runs `check` in its own test suite (norsk-ctl:
|
|
67
72
|
`website/tests/theme-drift.test.ts`, which `bun run check` runs), so a
|
|
68
|
-
hand-edited copy fails before it is published.
|
|
73
|
+
hand-edited copy fails before it is published. A PRODUCT repo has no such suite;
|
|
74
|
+
there, `conventions/check-drift.ts` runs the same check as part of
|
|
75
|
+
`bun run check:drift`, and `sync-drift.ts` rewrites the copies as part of
|
|
76
|
+
`bun run sync:drift` — see below.
|
|
69
77
|
|
|
70
78
|
**Keeping the copies at those exact paths is load-bearing.** Astro derives a
|
|
71
79
|
component's scoped-style class from its path, so moving `Footer.astro` rewrites
|
|
72
80
|
a class attribute on every page of the site.
|
|
73
81
|
|
|
74
|
-
##
|
|
82
|
+
## The two gates are one gate
|
|
83
|
+
|
|
84
|
+
`conventions/check-drift.ts` gains a clause when — and only when — the repo has a
|
|
85
|
+
site: `hasDocsSite()` tests for `website/astro.config.mjs`, and
|
|
86
|
+
`docsThemeProblems()` routes this directory's `checkTheme` into the same report as
|
|
87
|
+
`biome.json` and the shared workflows. `conventions/sync-drift.ts` is the writer
|
|
88
|
+
half, so `sync-dev-kit.yml` rolls a theme change out to every product hands-free
|
|
89
|
+
like any other convention. The site directory is `website/` across the fleet
|
|
90
|
+
(`DOCS_SITE_DIR`).
|
|
91
|
+
|
|
92
|
+
A site inside THIS monorepo still runs `sync.ts check` from its own test suite,
|
|
93
|
+
because it has one and because `bun run check` is what a monorepo contributor
|
|
94
|
+
runs; a product repo has no second suite, which is why the clause exists.
|
|
95
|
+
|
|
96
|
+
## `create-product` renders a site
|
|
97
|
+
|
|
98
|
+
Every generated repo comes out with a `website/` on this theme, five sections
|
|
99
|
+
deep, that builds — `create-product/docs-site.ts`. Two notes on how:
|
|
75
100
|
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
- **`create-product` does not render a site.** No generated repo has one yet.
|
|
101
|
+
- the theme files are written by **this directory's `syncTheme`**, called from
|
|
102
|
+
`createProduct` after the plan is written, not re-rendered by the generator: one
|
|
103
|
+
copier, one gate, and the woff2 faces are not strings;
|
|
104
|
+
- what a product repo needs in order to have a site at all — the five sections,
|
|
105
|
+
the two builds, publishing, and how the site relates to the illustrated manual
|
|
106
|
+
the image serves — is `conventions/docs-site.md`.
|
|
Binary file
|
package/docs-theme/sync.ts
CHANGED
|
@@ -30,10 +30,17 @@
|
|
|
30
30
|
*
|
|
31
31
|
* The copies land at the paths Starlight and Astro already expect
|
|
32
32
|
* (`src/custom.css`, `src/components/*.astro`, `src/content.config.ts`), which
|
|
33
|
-
* is also why the canonical directory mirrors a site's
|
|
34
|
-
* an identity. Keeping the component paths identical is load-bearing for
|
|
35
|
-
* than tidiness — Astro derives a component's scoped-style class from its
|
|
36
|
-
* so moving one rewrites a class attribute on every page.
|
|
33
|
+
* is also why the canonical directory mirrors a site's own tree: the map below
|
|
34
|
+
* is an identity. Keeping the component paths identical is load-bearing for
|
|
35
|
+
* more than tidiness — Astro derives a component's scoped-style class from its
|
|
36
|
+
* path, so moving one rewrites a class attribute on every page.
|
|
37
|
+
*
|
|
38
|
+
* One canonical subdirectory is NOT under `src/`: `public/` mirrors the site's
|
|
39
|
+
* `public/`, because `starlight.ts` already names the favicon
|
|
40
|
+
* (`favicon: "/favicon.png"`) and a path the theme dictates whose bytes each
|
|
41
|
+
* site supplies for itself is half a convention. Both sites' copies were
|
|
42
|
+
* byte-identical when it moved here, so the gate gained a file rather than the
|
|
43
|
+
* sites gaining a difference.
|
|
37
44
|
*
|
|
38
45
|
* Canonical bytes are resolved relative to THIS file, so the script works both
|
|
39
46
|
* workspace-symlinked and installed from the published tarball — the same
|
|
@@ -48,11 +55,21 @@ const CANONICAL = dirname(fileURLToPath(import.meta.url));
|
|
|
48
55
|
/** The site subdirectory the canonical tree is copied into. */
|
|
49
56
|
export const SITE_DIR = "src";
|
|
50
57
|
|
|
58
|
+
/** The one canonical subdirectory that mirrors the site's root, not its `src/`
|
|
59
|
+
* — Astro serves `public/` verbatim, and `starlight.ts` names `/favicon.png`. */
|
|
60
|
+
export const PUBLIC_DIR = "public";
|
|
61
|
+
|
|
62
|
+
/** Where a canonical file lands in a site, relative to the site root. */
|
|
63
|
+
export function sitePath(rel: string): string {
|
|
64
|
+
return rel === PUBLIC_DIR || rel.startsWith(`${PUBLIC_DIR}/`) ? rel : join(SITE_DIR, rel);
|
|
65
|
+
}
|
|
66
|
+
|
|
51
67
|
/**
|
|
52
68
|
* Every canonical file, as a path relative to this directory — which is also
|
|
53
|
-
* its path relative to the site's `src
|
|
54
|
-
* listed, so a file added here reaches every
|
|
55
|
-
* script's own source and its test are the only
|
|
69
|
+
* its path relative to the site's `src/` ({@link sitePath}, bar `public/`).
|
|
70
|
+
* Derived from the tree rather than listed, so a file added here reaches every
|
|
71
|
+
* site without a second edit; the script's own source and its test are the only
|
|
72
|
+
* exclusions.
|
|
56
73
|
*/
|
|
57
74
|
export function themeFiles(canonical: string = CANONICAL): string[] {
|
|
58
75
|
const out: string[] = [];
|
|
@@ -84,17 +101,17 @@ const RESYNC = "Edit the dev-kit source and re-run `sync`, never hand-edit the c
|
|
|
84
101
|
export function checkTheme(siteRoot: string, canonical: string = CANONICAL): ThemeDrift {
|
|
85
102
|
const problems: string[] = [];
|
|
86
103
|
for (const rel of themeFiles(canonical)) {
|
|
87
|
-
const
|
|
104
|
+
const at = sitePath(rel);
|
|
88
105
|
const want = readFileSync(join(canonical, rel));
|
|
89
106
|
let got: Buffer;
|
|
90
107
|
try {
|
|
91
|
-
got = readFileSync(
|
|
108
|
+
got = readFileSync(join(siteRoot, at));
|
|
92
109
|
} catch {
|
|
93
|
-
problems.push(`${
|
|
110
|
+
problems.push(`${at}: missing — the docs theme is not synced. ${RESYNC}`);
|
|
94
111
|
continue;
|
|
95
112
|
}
|
|
96
113
|
if (got.equals(want)) continue;
|
|
97
|
-
problems.push(`${
|
|
114
|
+
problems.push(`${at}: ${firstDiff(got, want)}. ${RESYNC}`);
|
|
98
115
|
}
|
|
99
116
|
return { ok: problems.length === 0, problems };
|
|
100
117
|
}
|
|
@@ -117,10 +134,11 @@ function firstDiff(got: Buffer, want: Buffer): string {
|
|
|
117
134
|
export function syncTheme(siteRoot: string, canonical: string = CANONICAL): string[] {
|
|
118
135
|
const written: string[] = [];
|
|
119
136
|
for (const rel of themeFiles(canonical)) {
|
|
120
|
-
const
|
|
137
|
+
const at = sitePath(rel);
|
|
138
|
+
const to = join(siteRoot, at);
|
|
121
139
|
mkdirSync(dirname(to), { recursive: true });
|
|
122
140
|
cpSync(join(canonical, rel), to);
|
|
123
|
-
written.push(
|
|
141
|
+
written.push(at);
|
|
124
142
|
}
|
|
125
143
|
return written;
|
|
126
144
|
}
|