@zebradil/docs-kit 0.0.0-stage → 0.11.0

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.
@@ -0,0 +1,563 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/Zebradil/docs-kit/schema/site.schema.json",
4
+ "title": "docs-kit site.yaml",
5
+ "description": "Content contract for a consuming project's docs/site.yaml: everything the docs site needs that is not a docs page.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["name", "tagline", "repo", "install", "features"],
9
+ "properties": {
10
+ "name": {
11
+ "description": "Project name as shown in the site title and landing hero. Usually the binary or repository name.",
12
+ "type": "string",
13
+ "minLength": 1
14
+ },
15
+ "tagline": {
16
+ "description": "One sentence under the name on the landing hero and in the site meta description: what the project is and for whom.",
17
+ "type": "string",
18
+ "minLength": 1,
19
+ "maxLength": 160
20
+ },
21
+ "headline": {
22
+ "description": "Landing hero title. Defaults to 'name'.",
23
+ "type": "string",
24
+ "minLength": 1,
25
+ "maxLength": 80
26
+ },
27
+ "eyebrow": {
28
+ "description": "Short label above the landing hero title, e.g. the project category.",
29
+ "type": "string",
30
+ "minLength": 1,
31
+ "maxLength": 60
32
+ },
33
+ "description": {
34
+ "description": "Paragraph under the landing hero title, one to three sentences; renders `inline code`. Defaults to 'tagline'.",
35
+ "type": "string",
36
+ "minLength": 1,
37
+ "maxLength": 400
38
+ },
39
+ "theme": {
40
+ "description": "Design theme, one of the CSS token files shipped in packages/docs-kit/themes/<name>.css. Changes look only, never layout.",
41
+ "enum": ["default", "mono", "porridge", "sieve", "vortex"],
42
+ "default": "default"
43
+ },
44
+ "logo": {
45
+ "description": "Project logo shown next to the site title. Paths are relative to the docs directory and start with './', e.g. './src/assets/logo.svg'. For a favicon, put 'favicon.svg' in the docs directory's public/ folder; 'favicon.ico' and 'apple-touch-icon.png' there are linked too.",
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "required": ["light"],
49
+ "properties": {
50
+ "light": {
51
+ "description": "Logo for the light color scheme, and for both schemes when 'dark' is omitted.",
52
+ "type": "string",
53
+ "pattern": "^\\./"
54
+ },
55
+ "dark": {
56
+ "description": "Logo for the dark color scheme.",
57
+ "type": "string",
58
+ "pattern": "^\\./"
59
+ }
60
+ }
61
+ },
62
+ "art": {
63
+ "description": "Illustration in the hero, beside the title. When set, the flow panel moves to a section below the hero. Path relative to the docs directory, starting with './'. The SVG is inlined, so its own <style> can animate it and follow the color scheme with :root[data-theme='light'] selectors; it is hidden from screen readers.",
64
+ "type": "string",
65
+ "pattern": "^\\./.+\\.svg$"
66
+ },
67
+ "repo": {
68
+ "description": "Repository URL, used for the GitHub link and 'edit this page' links.",
69
+ "type": "string",
70
+ "pattern": "^https://"
71
+ },
72
+ "domain": {
73
+ "description": "Custom domain the GitHub Pages site is served at, such as 'example.dev'. The site is then built for the domain root instead of https://<owner>.github.io/<repo>/.",
74
+ "type": "string",
75
+ "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$"
76
+ },
77
+ "analytics": {
78
+ "description": "Web analytics script loaded, deferred, on every page. Fits any tracker configured through data attributes, such as Umami, GoatCounter, Plausible or the Cloudflare Web Analytics beacon.",
79
+ "type": "object",
80
+ "additionalProperties": false,
81
+ "required": ["script"],
82
+ "properties": {
83
+ "script": {
84
+ "description": "URL of the tracker script.",
85
+ "type": "string",
86
+ "pattern": "^https://"
87
+ },
88
+ "attributes": {
89
+ "description": "Attributes of the script tag, such as data-website-id for Umami. Only data-* attributes, so the tag cannot carry inline event handlers.",
90
+ "type": "object",
91
+ "propertyNames": { "pattern": "^data-[a-z0-9-]+$" },
92
+ "additionalProperties": { "type": "string" }
93
+ }
94
+ }
95
+ },
96
+ "install": {
97
+ "description": "Install methods in order of preference. The first one is shown on the landing hero with a copy button; all are listed below it.",
98
+ "type": "array",
99
+ "minItems": 1,
100
+ "items": {
101
+ "type": "object",
102
+ "additionalProperties": false,
103
+ "required": ["label", "code"],
104
+ "properties": {
105
+ "label": {
106
+ "description": "Short method name, e.g. 'Nix', 'Cargo', 'Go', 'Homebrew'.",
107
+ "type": "string",
108
+ "minLength": 1
109
+ },
110
+ "code": {
111
+ "description": "Shell command that installs or runs the project, copied verbatim.",
112
+ "type": "string",
113
+ "minLength": 1
114
+ }
115
+ }
116
+ }
117
+ },
118
+ "features": {
119
+ "description": "Landing page feature grid: 3 to 6 short selling points, each grounded in what the code actually does.",
120
+ "type": "array",
121
+ "minItems": 3,
122
+ "maxItems": 6,
123
+ "items": {
124
+ "type": "object",
125
+ "additionalProperties": false,
126
+ "required": ["title", "body"],
127
+ "properties": {
128
+ "title": {
129
+ "description": "A few words.",
130
+ "type": "string",
131
+ "minLength": 1
132
+ },
133
+ "body": {
134
+ "description": "One or two sentences.",
135
+ "type": "string",
136
+ "minLength": 1
137
+ }
138
+ }
139
+ }
140
+ },
141
+ "flow": {
142
+ "description": "Panel beside the landing hero showing how the project fits with other systems: one row per connection.",
143
+ "type": "object",
144
+ "additionalProperties": false,
145
+ "required": ["steps"],
146
+ "properties": {
147
+ "title": {
148
+ "description": "Panel heading. Defaults to 'How it fits'.",
149
+ "type": "string",
150
+ "minLength": 1
151
+ },
152
+ "highlight": {
153
+ "description": "Node name drawn filled, usually the project itself.",
154
+ "type": "string",
155
+ "minLength": 1
156
+ },
157
+ "steps": {
158
+ "type": "array",
159
+ "minItems": 1,
160
+ "maxItems": 5,
161
+ "items": {
162
+ "type": "object",
163
+ "additionalProperties": false,
164
+ "required": ["from", "to", "label"],
165
+ "properties": {
166
+ "from": {
167
+ "description": "Left node.",
168
+ "type": "string",
169
+ "minLength": 1,
170
+ "maxLength": 24
171
+ },
172
+ "to": {
173
+ "description": "Right node.",
174
+ "type": "string",
175
+ "minLength": 1,
176
+ "maxLength": 24
177
+ },
178
+ "label": {
179
+ "description": "What travels between them, a few words.",
180
+ "type": "string",
181
+ "minLength": 1,
182
+ "maxLength": 40
183
+ },
184
+ "arrow": {
185
+ "description": "Arrow direction between from and to.",
186
+ "enum": ["forward", "back", "both"],
187
+ "default": "forward"
188
+ }
189
+ }
190
+ }
191
+ },
192
+ "note": {
193
+ "description": "One sentence under the rows; renders `inline code`.",
194
+ "type": "string",
195
+ "minLength": 1
196
+ },
197
+ "art": {
198
+ "description": "Illustration drawn in place of the rows, which stay for screen readers. Path relative to the docs directory, starting with './'. The SVG is inlined, so its own <style> can animate it and follow the color scheme with :root[data-theme='light'] selectors; it is hidden from screen readers.",
199
+ "type": "string",
200
+ "pattern": "^\\./.+\\.svg$"
201
+ }
202
+ }
203
+ },
204
+ "diff": {
205
+ "description": "Landing section showing a change and what it changes in each target, as diffs: one tab per target. For projects whose output is reviewed as a diff; omit otherwise.",
206
+ "type": "object",
207
+ "additionalProperties": false,
208
+ "required": ["tabs"],
209
+ "properties": {
210
+ "title": {
211
+ "description": "Section heading. Defaults to 'What a change looks like'.",
212
+ "type": "string",
213
+ "minLength": 1
214
+ },
215
+ "intro": {
216
+ "description": "One or two sentences under the heading; renders `inline code`.",
217
+ "type": "string",
218
+ "minLength": 1
219
+ },
220
+ "change": {
221
+ "description": "The edit a user makes, shown beside the tabs.",
222
+ "type": "object",
223
+ "additionalProperties": false,
224
+ "required": ["files"],
225
+ "properties": {
226
+ "label": {
227
+ "description": "Label above the files. Defaults to 'The change'.",
228
+ "type": "string",
229
+ "minLength": 1,
230
+ "maxLength": 40
231
+ },
232
+ "files": {
233
+ "type": "array",
234
+ "maxItems": 4,
235
+ "items": {
236
+ "type": "object",
237
+ "additionalProperties": false,
238
+ "required": ["path", "diff"],
239
+ "properties": {
240
+ "path": {
241
+ "description": "File path, shown as the title; its extension picks the syntax highlighting.",
242
+ "type": "string",
243
+ "minLength": 1
244
+ },
245
+ "diff": {
246
+ "description": "Unified diff lines: '+' added, '-' removed, ' ' context, '@@' hunk headers. No file headers.",
247
+ "type": "string",
248
+ "minLength": 1
249
+ }
250
+ }
251
+ },
252
+ "minItems": 1
253
+ }
254
+ }
255
+ },
256
+ "label": {
257
+ "description": "Label above the tabs. Defaults to 'What it changes'.",
258
+ "type": "string",
259
+ "minLength": 1,
260
+ "maxLength": 40
261
+ },
262
+ "tabs": {
263
+ "type": "array",
264
+ "minItems": 1,
265
+ "maxItems": 6,
266
+ "items": {
267
+ "type": "object",
268
+ "additionalProperties": false,
269
+ "required": ["label", "files"],
270
+ "properties": {
271
+ "label": {
272
+ "description": "Target name, e.g. an environment.",
273
+ "type": "string",
274
+ "minLength": 1,
275
+ "maxLength": 24
276
+ },
277
+ "files": {
278
+ "type": "array",
279
+ "maxItems": 4,
280
+ "items": {
281
+ "type": "object",
282
+ "additionalProperties": false,
283
+ "required": ["path", "diff"],
284
+ "properties": {
285
+ "path": {
286
+ "description": "File path, shown as the title; its extension picks the syntax highlighting.",
287
+ "type": "string",
288
+ "minLength": 1
289
+ },
290
+ "diff": {
291
+ "description": "Unified diff lines: '+' added, '-' removed, ' ' context, '@@' hunk headers. No file headers.",
292
+ "type": "string",
293
+ "minLength": 1
294
+ }
295
+ }
296
+ },
297
+ "description": "Changed files; empty shows \"No changes\"."
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+ },
304
+ "tree": {
305
+ "description": "Landing section showing an inheritance tree: one tab per node with an `id`, numbering the nodes it inherits from. For projects with layered configuration; omit otherwise.",
306
+ "type": "object",
307
+ "additionalProperties": false,
308
+ "required": ["root"],
309
+ "properties": {
310
+ "title": {
311
+ "description": "Section heading. Defaults to 'How settings inherit'.",
312
+ "type": "string",
313
+ "minLength": 1
314
+ },
315
+ "intro": {
316
+ "description": "One or two sentences beside the tree; renders `inline code`.",
317
+ "type": "string",
318
+ "minLength": 1
319
+ },
320
+ "root": {
321
+ "$ref": "#/$defs/treeNode"
322
+ }
323
+ }
324
+ },
325
+ "cascade": {
326
+ "description": "Landing panel showing one request falling through ordered tiers of backends, with one tab per scenario. For projects that route or fall back between backends; omit otherwise.",
327
+ "type": "object",
328
+ "additionalProperties": false,
329
+ "required": ["tiers", "scenarios"],
330
+ "properties": {
331
+ "title": {
332
+ "description": "Panel heading. Defaults to 'How a request falls through'.",
333
+ "type": "string",
334
+ "minLength": 1
335
+ },
336
+ "request": {
337
+ "description": "The request entering the panel, e.g. 'GET /<hash>.narinfo'; shown as code.",
338
+ "type": "string",
339
+ "minLength": 1,
340
+ "maxLength": 80
341
+ },
342
+ "hub": {
343
+ "description": "The project's own row between the request and the tiers.",
344
+ "type": "object",
345
+ "additionalProperties": false,
346
+ "required": ["name"],
347
+ "properties": {
348
+ "name": { "description": "Usually the project name.", "type": "string", "minLength": 1, "maxLength": 24 },
349
+ "body": { "description": "What it does with the request, one short sentence.", "type": "string", "minLength": 1, "maxLength": 120 }
350
+ }
351
+ },
352
+ "tiers": {
353
+ "description": "Tiers in the order a request tries them.",
354
+ "type": "array",
355
+ "minItems": 1,
356
+ "maxItems": 4,
357
+ "items": {
358
+ "type": "object",
359
+ "additionalProperties": false,
360
+ "required": ["name", "backends"],
361
+ "properties": {
362
+ "name": { "description": "Tier name.", "type": "string", "minLength": 1, "maxLength": 24 },
363
+ "label": { "description": "Small label above the name, e.g. 'tier 1 · sequential'.", "type": "string", "minLength": 1, "maxLength": 40 },
364
+ "backends": {
365
+ "type": "array",
366
+ "minItems": 1,
367
+ "maxItems": 4,
368
+ "items": {
369
+ "type": "object",
370
+ "additionalProperties": false,
371
+ "required": ["id", "name"],
372
+ "properties": {
373
+ "id": { "description": "Key that scenarios use to set this backend's state.", "type": "string", "pattern": "^[a-z0-9-]+$" },
374
+ "name": { "description": "Shown as code, usually a URL.", "type": "string", "minLength": 1, "maxLength": 48 }
375
+ }
376
+ }
377
+ }
378
+ }
379
+ }
380
+ },
381
+ "scenarios": {
382
+ "description": "One tab each. A backend a scenario does not mention is shown as not asked.",
383
+ "type": "array",
384
+ "minItems": 1,
385
+ "maxItems": 4,
386
+ "items": {
387
+ "type": "object",
388
+ "additionalProperties": false,
389
+ "required": ["label", "states"],
390
+ "properties": {
391
+ "label": { "description": "Tab label, a few words.", "type": "string", "minLength": 1, "maxLength": 24 },
392
+ "note": { "description": "One or two sentences under the tiers; renders `inline code`.", "type": "string", "minLength": 1 },
393
+ "states": {
394
+ "description": "Backend id to its state in this scenario.",
395
+ "type": "object",
396
+ "additionalProperties": {
397
+ "type": "object",
398
+ "additionalProperties": false,
399
+ "required": ["state"],
400
+ "properties": {
401
+ "state": { "description": "hit: answered; miss: answered without the item; down: skipped as unreachable; idle: not asked.", "enum": ["hit", "miss", "down", "idle"] },
402
+ "text": { "description": "Why, a few words.", "type": "string", "minLength": 1, "maxLength": 48 }
403
+ }
404
+ }
405
+ }
406
+ }
407
+ }
408
+ }
409
+ }
410
+ },
411
+ "components": {
412
+ "description": "Landing section listing the parts the project ships: commands, modules, packages.",
413
+ "type": "object",
414
+ "additionalProperties": false,
415
+ "required": ["items"],
416
+ "properties": {
417
+ "title": {
418
+ "description": "Section heading. Defaults to 'Components'.",
419
+ "type": "string",
420
+ "minLength": 1
421
+ },
422
+ "intro": {
423
+ "description": "One or two sentences beside the list; renders `inline code`.",
424
+ "type": "string",
425
+ "minLength": 1
426
+ },
427
+ "items": {
428
+ "type": "array",
429
+ "minItems": 1,
430
+ "maxItems": 12,
431
+ "items": {
432
+ "type": "object",
433
+ "additionalProperties": false,
434
+ "required": ["name", "body"],
435
+ "properties": {
436
+ "name": {
437
+ "description": "Name as users type it, shown as code.",
438
+ "type": "string",
439
+ "minLength": 1
440
+ },
441
+ "body": {
442
+ "description": "One short sentence; renders `inline code`.",
443
+ "type": "string",
444
+ "minLength": 1
445
+ }
446
+ }
447
+ }
448
+ }
449
+ }
450
+ },
451
+ "links": {
452
+ "description": "Extra links shown on the landing page, e.g. docs.rs, pkg.go.dev, changelog. Library API docs are linked, never copied.",
453
+ "type": "array",
454
+ "items": {
455
+ "type": "object",
456
+ "additionalProperties": false,
457
+ "required": ["label", "href"],
458
+ "properties": {
459
+ "label": { "description": "Link text.", "type": "string", "minLength": 1 },
460
+ "href": { "description": "Absolute URL.", "type": "string", "pattern": "^https?://" }
461
+ }
462
+ }
463
+ },
464
+ "versions": {
465
+ "description": "Publishes the docs of past releases next to the default branch's, which stay at the site root. Each selected minor version is served at <site>/vX.Y/ from its newest patch tag (vX.Y.Z), with a version switcher in the header that marks the newest release. A release's pages, src/assets and public/ come from its tag; site.yaml and the kit come from the default branch, so every version gets the current theme. Pre-releases and tags without the docs directory's src/content/docs are skipped.",
466
+ "type": "object",
467
+ "additionalProperties": false,
468
+ "properties": {
469
+ "minors": {
470
+ "description": "Publishes the newest this many minor versions.",
471
+ "type": "integer",
472
+ "minimum": 0,
473
+ "maximum": 20,
474
+ "default": 3
475
+ },
476
+ "majors": {
477
+ "description": "Publishes the newest minor version of each of the newest this many major versions. Below 1.0 every minor version counts as a major one, since semver lets it break.",
478
+ "type": "integer",
479
+ "minimum": 0,
480
+ "maximum": 20,
481
+ "default": 3
482
+ },
483
+ "pin": {
484
+ "description": "Minor versions published whatever 'minors' and 'majors' select, e.g. a long-term support line.",
485
+ "type": "array",
486
+ "items": { "type": "string", "pattern": "^v\\d+\\.\\d+$" }
487
+ }
488
+ }
489
+ },
490
+ "reference": {
491
+ "description": "How refgen produces the generated pages under reference/. Omit when the project has no CLI.",
492
+ "type": "object",
493
+ "additionalProperties": false,
494
+ "properties": {
495
+ "cli": {
496
+ "description": "CLI reference generated by walking '<bin> --help' and every subcommand.",
497
+ "type": "object",
498
+ "additionalProperties": false,
499
+ "required": ["bin"],
500
+ "dependentRequired": { "build": ["path"] },
501
+ "properties": {
502
+ "bin": {
503
+ "description": "Command name as users type it; used for page titles and file names.",
504
+ "type": "string",
505
+ "pattern": "^[a-z0-9][a-z0-9._-]*$"
506
+ },
507
+ "build": {
508
+ "description": "Shell command, run from the repository root, that builds the binary, e.g. 'cargo build --release', 'go build -o bin/x .' or 'nix build'. Requires 'path'.",
509
+ "type": "string",
510
+ "minLength": 1
511
+ },
512
+ "path": {
513
+ "description": "Path to the built binary relative to the repository root, e.g. 'target/release/kasha' or 'result/bin/kasha'. When omitted, 'bin' is looked up on PATH.",
514
+ "type": "string",
515
+ "minLength": 1
516
+ }
517
+ }
518
+ }
519
+ }
520
+ }
521
+ },
522
+ "$defs": {
523
+ "treeNode": {
524
+ "type": "object",
525
+ "additionalProperties": false,
526
+ "required": ["name"],
527
+ "properties": {
528
+ "name": {
529
+ "description": "Node name, e.g. a directory; shown as code.",
530
+ "type": "string",
531
+ "minLength": 1,
532
+ "maxLength": 40
533
+ },
534
+ "file": {
535
+ "description": "File the node contributes, shown beside the name.",
536
+ "type": "string",
537
+ "minLength": 1,
538
+ "maxLength": 40
539
+ },
540
+ "note": {
541
+ "description": "A few words on what this level sets; renders `inline code`.",
542
+ "type": "string",
543
+ "minLength": 1,
544
+ "maxLength": 80
545
+ },
546
+ "id": {
547
+ "description": "Makes the node a tab: the label of a leaf that inherits from every node above it.",
548
+ "type": "string",
549
+ "minLength": 1,
550
+ "maxLength": 24
551
+ },
552
+ "children": {
553
+ "type": "array",
554
+ "minItems": 1,
555
+ "maxItems": 6,
556
+ "items": {
557
+ "$ref": "#/$defs/treeNode"
558
+ }
559
+ }
560
+ }
561
+ }
562
+ }
563
+ }
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ // Validates site.yaml files against the content contract.
3
+ // Usage: node schema/validate.mjs <site.yaml>...
4
+ import { readFileSync } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import Ajv2020 from 'ajv/dist/2020.js';
7
+ import { parse } from 'yaml';
8
+
9
+ const schema = JSON.parse(readFileSync(new URL('./site.schema.json', import.meta.url), 'utf8'));
10
+ const check = new Ajv2020({ allErrors: true }).compile(schema);
11
+
12
+ /** Returns a list of human-readable errors; empty when the site config is valid. */
13
+ export function validateSite(site) {
14
+ if (check(site)) return [...cascadeErrors(site.cascade), ...treeErrors(site.tree)];
15
+ return check.errors.map((e) => {
16
+ const where = e.instancePath || '(root)';
17
+ if (e.keyword === 'enum') return `${where} must be one of: ${e.params.allowedValues.join(', ')}`;
18
+ if (e.keyword === 'additionalProperties') return `${where} has unknown field '${e.params.additionalProperty}'`;
19
+ return `${where} ${e.message}`;
20
+ });
21
+ }
22
+
23
+ /** Scenario states must name backends the cascade's tiers declare, each declared once. */
24
+ function cascadeErrors(cascade) {
25
+ if (!cascade) return [];
26
+ const ids = cascade.tiers.flatMap((t) => t.backends.map((b) => b.id));
27
+ const errors = [...new Set(ids.filter((id, i) => ids.indexOf(id) !== i))].map((id) => `/cascade/tiers declares backend '${id}' more than once`);
28
+ cascade.scenarios.forEach((s, i) => {
29
+ for (const id of Object.keys(s.states)) {
30
+ if (!ids.includes(id)) errors.push(`/cascade/scenarios/${i}/states names unknown backend '${id}'`);
31
+ }
32
+ });
33
+ return errors;
34
+ }
35
+
36
+ /** A tree needs at least one node with an `id`, since each one is a tab, and the ids must be unique. */
37
+ function treeErrors(tree) {
38
+ if (!tree) return [];
39
+ const ids = [];
40
+ const walk = (node) => {
41
+ if (node.id) ids.push(node.id);
42
+ node.children?.forEach(walk);
43
+ };
44
+ walk(tree.root);
45
+ if (!ids.length) return ['/tree has no node with an id'];
46
+ return [...new Set(ids.filter((id, i) => ids.indexOf(id) !== i))].map((id) => `/tree declares id '${id}' more than once`);
47
+ }
48
+
49
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
50
+ const files = process.argv.slice(2);
51
+ if (!files.length) {
52
+ console.error('usage: validate.mjs <site.yaml>...');
53
+ process.exit(2);
54
+ }
55
+ let failed = false;
56
+ for (const file of files) {
57
+ const errors = validateSite(parse(readFileSync(file, 'utf8')));
58
+ for (const err of errors) console.error(`${file}: ${err}`);
59
+ if (errors.length) failed = true;
60
+ else console.log(`${file}: ok`);
61
+ }
62
+ process.exit(failed ? 1 : 0);
63
+ }