@cogenta/starters 0.1.5

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.
Files changed (200) hide show
  1. package/LICENSE +373 -0
  2. package/dist/blueprints/assets/photos/association/food-bank.jpg +0 -0
  3. package/dist/blueprints/assets/photos/association/fundraising-dinner.jpg +0 -0
  4. package/dist/blueprints/assets/photos/association/garden-planting.jpg +0 -0
  5. package/dist/blueprints/assets/photos/association/hero.jpg +0 -0
  6. package/dist/blueprints/assets/photos/association/homework-club.jpg +0 -0
  7. package/dist/blueprints/assets/photos/association/partner-ashworth-college.png +0 -0
  8. package/dist/blueprints/assets/photos/association/partner-ashworth-council.png +0 -0
  9. package/dist/blueprints/assets/photos/association/partner-brindle-and-sons.png +0 -0
  10. package/dist/blueprints/assets/photos/association/partner-kestrel-homes.png +0 -0
  11. package/dist/blueprints/assets/photos/association/partner-linden-trust.png +0 -0
  12. package/dist/blueprints/assets/photos/association/partner-marlow-street-pharmacy.png +0 -0
  13. package/dist/blueprints/assets/photos/association/volunteer-orientation.jpg +0 -0
  14. package/dist/blueprints/assets/photos/association/volunteer-portrait.jpg +0 -0
  15. package/dist/blueprints/assets/photos/association/winter-coat-collection.jpg +0 -0
  16. package/dist/blueprints/assets/photos/blog/desk-setup.jpg +0 -0
  17. package/dist/blueprints/assets/photos/blog/notebook-and-coffee.jpg +0 -0
  18. package/dist/blueprints/assets/photos/blog/notebooks.jpg +0 -0
  19. package/dist/blueprints/assets/photos/blog/platform.jpg +0 -0
  20. package/dist/blueprints/assets/photos/blog/pour-over.jpg +0 -0
  21. package/dist/blueprints/assets/photos/documentation/delivery-flow.png +0 -0
  22. package/dist/blueprints/assets/photos/magazine/archive-room.jpg +0 -0
  23. package/dist/blueprints/assets/photos/magazine/banquet-hall.jpg +0 -0
  24. package/dist/blueprints/assets/photos/magazine/city-hall.jpg +0 -0
  25. package/dist/blueprints/assets/photos/magazine/dining-room.jpg +0 -0
  26. package/dist/blueprints/assets/photos/magazine/downtown-towers.jpg +0 -0
  27. package/dist/blueprints/assets/photos/magazine/gallery-opening.jpg +0 -0
  28. package/dist/blueprints/assets/photos/magazine/garden-volunteers.jpg +0 -0
  29. package/dist/blueprints/assets/photos/magazine/station-platform.jpg +0 -0
  30. package/dist/blueprints/assets/photos/magazine/theater-stage.jpg +0 -0
  31. package/dist/blueprints/assets/photos/portfolio/fenmore-branch.jpg +0 -0
  32. package/dist/blueprints/assets/photos/portfolio/fenmore-cover.jpg +0 -0
  33. package/dist/blueprints/assets/photos/portfolio/fenmore-stationery.jpg +0 -0
  34. package/dist/blueprints/assets/photos/portfolio/gannet-cover.jpg +0 -0
  35. package/dist/blueprints/assets/photos/portfolio/gannet-sardines.jpg +0 -0
  36. package/dist/blueprints/assets/photos/portfolio/gannet-shelf.jpg +0 -0
  37. package/dist/blueprints/assets/photos/portfolio/hollis-crane-cover.jpg +0 -0
  38. package/dist/blueprints/assets/photos/portfolio/hollis-crane-grid.jpg +0 -0
  39. package/dist/blueprints/assets/photos/portfolio/hollis-crane-salt-year.jpg +0 -0
  40. package/dist/blueprints/assets/photos/portfolio/hollis-crane-spines.jpg +0 -0
  41. package/dist/blueprints/assets/photos/portfolio/rookery-hall-brochure.jpg +0 -0
  42. package/dist/blueprints/assets/photos/portfolio/rookery-hall-cover.jpg +0 -0
  43. package/dist/blueprints/assets/photos/portfolio/rookery-hall-poster-carnival.jpg +0 -0
  44. package/dist/blueprints/assets/photos/portfolio/rookery-hall-poster-sibelius.jpg +0 -0
  45. package/dist/blueprints/assets/photos/portfolio/rookery-hall-series.jpg +0 -0
  46. package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-cover.jpg +0 -0
  47. package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-gate.jpg +0 -0
  48. package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-pictograms.jpg +0 -0
  49. package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-totem.jpg +0 -0
  50. package/dist/blueprints/assets/photos/portfolio/tidewater-cover.jpg +0 -0
  51. package/dist/blueprints/assets/photos/portfolio/tidewater-data.jpg +0 -0
  52. package/dist/blueprints/assets/photos/portfolio/tidewater-report-cover.jpg +0 -0
  53. package/dist/blueprints/assets/photos/portfolio/warp-and-weft-cover.jpg +0 -0
  54. package/dist/blueprints/assets/photos/portfolio/warp-and-weft-room.jpg +0 -0
  55. package/dist/blueprints/assets/photos/portfolio/westmoor-rail-components.jpg +0 -0
  56. package/dist/blueprints/assets/photos/portfolio/westmoor-rail-cover.jpg +0 -0
  57. package/dist/blueprints/assets/photos/portfolio/westmoor-rail-departures.jpg +0 -0
  58. package/dist/blueprints/assets/photos/restaurant/beetroot-goats-curd.jpg +0 -0
  59. package/dist/blueprints/assets/photos/restaurant/ceps-risotto.jpg +0 -0
  60. package/dist/blueprints/assets/photos/restaurant/chocolate-tart.jpg +0 -0
  61. package/dist/blueprints/assets/photos/restaurant/duck-leg-red-cabbage.jpg +0 -0
  62. package/dist/blueprints/assets/photos/restaurant/grilled-octopus.jpg +0 -0
  63. package/dist/blueprints/assets/photos/restaurant/hake-green-peppercorns.jpg +0 -0
  64. package/dist/blueprints/assets/photos/restaurant/hero.jpg +0 -0
  65. package/dist/blueprints/assets/photos/restaurant/poached-pear.jpg +0 -0
  66. package/dist/blueprints/assets/photos/restaurant/vegetable-broth.jpg +0 -0
  67. package/dist/blueprints/assets/photos/saas/adrian-tan.jpg +0 -0
  68. package/dist/blueprints/assets/photos/saas/api-webhooks.png +0 -0
  69. package/dist/blueprints/assets/photos/saas/approvals-queue.png +0 -0
  70. package/dist/blueprints/assets/photos/saas/audit-log.png +0 -0
  71. package/dist/blueprints/assets/photos/saas/erp-sync.png +0 -0
  72. package/dist/blueprints/assets/photos/saas/logo-brightwell-clinics.png +0 -0
  73. package/dist/blueprints/assets/photos/saas/logo-castlemere-foods.png +0 -0
  74. package/dist/blueprints/assets/photos/saas/logo-halvorsen-freight.png +0 -0
  75. package/dist/blueprints/assets/photos/saas/logo-northgate-homes.png +0 -0
  76. package/dist/blueprints/assets/photos/saas/logo-orrin-and-vale.png +0 -0
  77. package/dist/blueprints/assets/photos/saas/logo-tessaly-energy.png +0 -0
  78. package/dist/blueprints/assets/photos/saas/policy-editor.png +0 -0
  79. package/dist/blueprints/assets/photos/saas/spend-reporting.png +0 -0
  80. package/dist/blueprints/assets/photos/saas/sso-scim.png +0 -0
  81. package/dist/blueprints/assets/photos/store/brushed-cotton-overshirt.jpg +0 -0
  82. package/dist/blueprints/assets/photos/store/cable-knit-beanie.jpg +0 -0
  83. package/dist/blueprints/assets/photos/store/canvas-shoulder-bag.jpg +0 -0
  84. package/dist/blueprints/assets/photos/store/canvas-tote.jpg +0 -0
  85. package/dist/blueprints/assets/photos/store/cast-iron-skillet.jpg +0 -0
  86. package/dist/blueprints/assets/photos/store/enamel-mug.jpg +0 -0
  87. package/dist/blueprints/assets/photos/store/field-jacket.jpg +0 -0
  88. package/dist/blueprints/assets/photos/store/heavyweight-t-shirt.jpg +0 -0
  89. package/dist/blueprints/assets/photos/store/hero.jpg +0 -0
  90. package/dist/blueprints/assets/photos/store/leather-card-holder.jpg +0 -0
  91. package/dist/blueprints/assets/photos/store/linen-table-runner.jpg +0 -0
  92. package/dist/blueprints/assets/photos/store/porcelain-pour-over-set.jpg +0 -0
  93. package/dist/blueprints/assets/photos/store/striped-wool-blanket.jpg +0 -0
  94. package/dist/blueprints/assets/photos/vitrine/avatar-1.jpg +0 -0
  95. package/dist/blueprints/assets/photos/vitrine/avatar-2.jpg +0 -0
  96. package/dist/blueprints/assets/photos/vitrine/avatar-3.jpg +0 -0
  97. package/dist/blueprints/assets/photos/vitrine/case-council.jpg +0 -0
  98. package/dist/blueprints/assets/photos/vitrine/case-hotels.jpg +0 -0
  99. package/dist/blueprints/assets/photos/vitrine/case-rail.jpg +0 -0
  100. package/dist/blueprints/assets/photos/vitrine/exhibit-working-capital.png +0 -0
  101. package/dist/blueprints/assets/photos/vitrine/hero-review.jpg +0 -0
  102. package/dist/blueprints/assets/photos/vitrine/logo-aldermoor.png +0 -0
  103. package/dist/blueprints/assets/photos/vitrine/logo-brightwater.png +0 -0
  104. package/dist/blueprints/assets/photos/vitrine/logo-castell-vane.png +0 -0
  105. package/dist/blueprints/assets/photos/vitrine/logo-meridian-rail.png +0 -0
  106. package/dist/blueprints/assets/photos/vitrine/logo-nordvik.png +0 -0
  107. package/dist/blueprints/assets/photos/vitrine/logo-oakfield-health.png +0 -0
  108. package/dist/blueprints/association.d.ts +322 -0
  109. package/dist/blueprints/association.d.ts.map +1 -0
  110. package/dist/blueprints/association.js +1440 -0
  111. package/dist/blueprints/association.js.map +1 -0
  112. package/dist/blueprints/blog.d.ts +221 -0
  113. package/dist/blueprints/blog.d.ts.map +1 -0
  114. package/dist/blueprints/blog.js +1132 -0
  115. package/dist/blueprints/blog.js.map +1 -0
  116. package/dist/blueprints/content-pack.d.ts +148 -0
  117. package/dist/blueprints/content-pack.d.ts.map +1 -0
  118. package/dist/blueprints/content-pack.js +111 -0
  119. package/dist/blueprints/content-pack.js.map +1 -0
  120. package/dist/blueprints/content-packs.d.ts +12 -0
  121. package/dist/blueprints/content-packs.d.ts.map +1 -0
  122. package/dist/blueprints/content-packs.js +30 -0
  123. package/dist/blueprints/content-packs.js.map +1 -0
  124. package/dist/blueprints/demo-media.d.ts +39 -0
  125. package/dist/blueprints/demo-media.d.ts.map +1 -0
  126. package/dist/blueprints/demo-media.js +31 -0
  127. package/dist/blueprints/demo-media.js.map +1 -0
  128. package/dist/blueprints/documentation.d.ts +165 -0
  129. package/dist/blueprints/documentation.d.ts.map +1 -0
  130. package/dist/blueprints/documentation.js +1236 -0
  131. package/dist/blueprints/documentation.js.map +1 -0
  132. package/dist/blueprints/magazine.d.ts +207 -0
  133. package/dist/blueprints/magazine.d.ts.map +1 -0
  134. package/dist/blueprints/magazine.js +1076 -0
  135. package/dist/blueprints/magazine.js.map +1 -0
  136. package/dist/blueprints/menus.d.ts +25 -0
  137. package/dist/blueprints/menus.d.ts.map +1 -0
  138. package/dist/blueprints/menus.js +33 -0
  139. package/dist/blueprints/menus.js.map +1 -0
  140. package/dist/blueprints/photo-assets.d.ts +8 -0
  141. package/dist/blueprints/photo-assets.d.ts.map +1 -0
  142. package/dist/blueprints/photo-assets.js +41 -0
  143. package/dist/blueprints/photo-assets.js.map +1 -0
  144. package/dist/blueprints/portfolio.d.ts +207 -0
  145. package/dist/blueprints/portfolio.d.ts.map +1 -0
  146. package/dist/blueprints/portfolio.js +1032 -0
  147. package/dist/blueprints/portfolio.js.map +1 -0
  148. package/dist/blueprints/restaurant.d.ts +191 -0
  149. package/dist/blueprints/restaurant.d.ts.map +1 -0
  150. package/dist/blueprints/restaurant.js +1076 -0
  151. package/dist/blueprints/restaurant.js.map +1 -0
  152. package/dist/blueprints/saas.d.ts +268 -0
  153. package/dist/blueprints/saas.d.ts.map +1 -0
  154. package/dist/blueprints/saas.js +1472 -0
  155. package/dist/blueprints/saas.js.map +1 -0
  156. package/dist/blueprints/site-settings-seed.d.ts +17 -0
  157. package/dist/blueprints/site-settings-seed.d.ts.map +1 -0
  158. package/dist/blueprints/site-settings-seed.js +45 -0
  159. package/dist/blueprints/site-settings-seed.js.map +1 -0
  160. package/dist/blueprints/starting-skins.d.ts +19 -0
  161. package/dist/blueprints/starting-skins.d.ts.map +1 -0
  162. package/dist/blueprints/starting-skins.js +310 -0
  163. package/dist/blueprints/starting-skins.js.map +1 -0
  164. package/dist/blueprints/store.d.ts +290 -0
  165. package/dist/blueprints/store.d.ts.map +1 -0
  166. package/dist/blueprints/store.js +1031 -0
  167. package/dist/blueprints/store.js.map +1 -0
  168. package/dist/blueprints/vitrine.d.ts +266 -0
  169. package/dist/blueprints/vitrine.d.ts.map +1 -0
  170. package/dist/blueprints/vitrine.js +1056 -0
  171. package/dist/blueprints/vitrine.js.map +1 -0
  172. package/dist/blueprints/widgets.d.ts +26 -0
  173. package/dist/blueprints/widgets.d.ts.map +1 -0
  174. package/dist/blueprints/widgets.js +27 -0
  175. package/dist/blueprints/widgets.js.map +1 -0
  176. package/dist/demo-art/compositions.d.ts +48 -0
  177. package/dist/demo-art/compositions.d.ts.map +1 -0
  178. package/dist/demo-art/compositions.js +690 -0
  179. package/dist/demo-art/compositions.js.map +1 -0
  180. package/dist/demo-art/index.d.ts +8 -0
  181. package/dist/demo-art/index.d.ts.map +1 -0
  182. package/dist/demo-art/index.js +5 -0
  183. package/dist/demo-art/index.js.map +1 -0
  184. package/dist/demo-art/oklch.d.ts +39 -0
  185. package/dist/demo-art/oklch.d.ts.map +1 -0
  186. package/dist/demo-art/oklch.js +79 -0
  187. package/dist/demo-art/oklch.js.map +1 -0
  188. package/dist/demo-art/png.d.ts +24 -0
  189. package/dist/demo-art/png.d.ts.map +1 -0
  190. package/dist/demo-art/png.js +110 -0
  191. package/dist/demo-art/png.js.map +1 -0
  192. package/dist/demo-art/render.d.ts +228 -0
  193. package/dist/demo-art/render.d.ts.map +1 -0
  194. package/dist/demo-art/render.js +478 -0
  195. package/dist/demo-art/render.js.map +1 -0
  196. package/dist/index.d.ts +11 -0
  197. package/dist/index.d.ts.map +1 -0
  198. package/dist/index.js +7 -0
  199. package/dist/index.js.map +1 -0
  200. package/package.json +52 -0
@@ -0,0 +1,1236 @@
1
+ import { CogentaError } from '@cogenta/core';
2
+ import { createContentStore, createMenuStore, defineCollection, ensureMenuTables, f, validateCollectionSet, } from '@cogenta/schema';
3
+ import { coverArt } from '../demo-art/compositions.js';
4
+ import { definePageCollection, SEO_FIELDS, toBlockZoneEntry, } from './content-pack.js';
5
+ import { STARTING_SKINS } from './starting-skins.js';
6
+ /**
7
+ * The `documentation` blueprint: the documentation site of a developer tool,
8
+ * rewritten to studio level by L27.
9
+ *
10
+ * The product is a self-hosted webhook delivery server: an application sends
11
+ * it events over HTTP, and it signs, sends, retries and records a request for
12
+ * every endpoint subscribed to each event. It was chosen because it gives a
13
+ * documentation site everything it is made of (a CLI, a configuration file,
14
+ * an HTTP API, code in several languages, failure modes worth a
15
+ * troubleshooting page) without pretending to be a hosted service that would
16
+ * need a sign-up flow the site cannot offer. It runs on `localhost`, so every
17
+ * example is one a reader could actually type.
18
+ *
19
+ * The product is named after the site (`SeedContext.siteName`, with a
20
+ * trailing "Docs" or "Documentation" dropped), falling back to "Relay". Its
21
+ * command, environment variables and header names are derived from that
22
+ * name, so the quickstart, the CLI reference and the API reference always
23
+ * agree with each other.
24
+ *
25
+ * Content model:
26
+ *
27
+ * - `doc_page`: one page of documentation at `/docs/:slug`, with its
28
+ * `section`, its `order` in the whole documentation, a one-sentence
29
+ * `summary` shown under its title, and a block zone whose first block is
30
+ * the `collectionList` on `doc_page` that `@cogenta/theme-docs` renders as
31
+ * the navigation (see the theme's `doc-page.ts`).
32
+ * - `page`: the home page.
33
+ *
34
+ * Rich text carries four shapes the theme renders as documentation furniture
35
+ * (code blocks with a file name, notes, reference tables, keys); see
36
+ * `@cogenta/theme-docs`'s `rich-text.ts`. Every other theme renders the same
37
+ * data as plain paragraphs and lists.
38
+ */
39
+ export const DEFAULT_PRODUCT_NAME = 'Relay';
40
+ const DOCS_SUFFIX = /\s+(docs|documentation|developer docs|dev docs)$/i;
41
+ /** "Relay Docs" → "Relay"; an empty or absent name → "Relay". */
42
+ export function productName(siteName) {
43
+ const trimmed = siteName?.trim().replace(DOCS_SUFFIX, '').trim();
44
+ return trimmed === undefined || trimmed === '' ? DEFAULT_PRODUCT_NAME : trimmed;
45
+ }
46
+ export function productNames(siteName) {
47
+ const name = productName(siteName);
48
+ const ascii = name.normalize('NFKD').replace(/[^\x20-\x7e]/g, '');
49
+ const words = ascii.split(/[^A-Za-z0-9]+/).filter((word) => word !== '');
50
+ const cli = words.join('-').toLowerCase() || 'relay';
51
+ return {
52
+ name,
53
+ cli,
54
+ env: words.join('_').toUpperCase() || 'RELAY',
55
+ header: words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join('-') || 'Relay',
56
+ };
57
+ }
58
+ export const docPage = defineCollection({
59
+ name: 'doc_page',
60
+ labels: { singular: 'Doc page', plural: 'Doc pages' },
61
+ routing: { pattern: '/docs/:slug' },
62
+ fields: {
63
+ title: f.text({ required: true, max: 200 }),
64
+ slug: f.slug({ from: 'title', unique: true }),
65
+ section: f.text({ required: true, max: 80 }),
66
+ order: f.number({ required: true, integer: true, min: 0 }),
67
+ summary: f.text({
68
+ max: 300,
69
+ multiline: true,
70
+ admin: { label: 'Summary', help: 'One sentence shown under the title.' },
71
+ }),
72
+ body: f.blocks({ required: true }),
73
+ ...SEO_FIELDS,
74
+ },
75
+ indexes: [['slug'], ['section']],
76
+ permissions: {
77
+ read: ['public'],
78
+ create: ['editor', 'admin'],
79
+ update: ['editor', 'admin'],
80
+ delete: ['admin'],
81
+ },
82
+ });
83
+ export const page = definePageCollection('/:slug');
84
+ export const DOCUMENTATION_COLLECTIONS = [docPage, page];
85
+ validateCollectionSet(DOCUMENTATION_COLLECTIONS);
86
+ const BLOCK_VERSION = '1.0.0';
87
+ /** `code`, **strong**, *em* and [a link](/docs/page), nothing else. */
88
+ const INLINE = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)\s]+\))/g;
89
+ /**
90
+ * One page's rich text, written in a handful of short calls. Every string
91
+ * passes through `fill`, which puts the product's own names in place of
92
+ * `%N%` (name), `%cli%` (command), `%ENV%` (environment prefix) and `%HDR%`
93
+ * (header prefix).
94
+ */
95
+ function writer(prefix, names) {
96
+ let counter = 0;
97
+ const key = () => `${prefix}-${++counter}`;
98
+ const fill = (value) => value
99
+ .replaceAll('%N%', names.name)
100
+ .replaceAll('%cli%', names.cli)
101
+ .replaceAll('%ENV%', names.env)
102
+ .replaceAll('%HDR%', names.header);
103
+ function inline(source) {
104
+ const children = [];
105
+ const markDefs = [];
106
+ for (const part of fill(source).split(INLINE)) {
107
+ if (part === '')
108
+ continue;
109
+ if (part.startsWith('`') && part.endsWith('`') && part.length > 2) {
110
+ children.push({ _key: key(), _type: 'span', text: part.slice(1, -1), marks: ['code'] });
111
+ }
112
+ else if (part.startsWith('**') && part.endsWith('**') && part.length > 4) {
113
+ children.push({ _key: key(), _type: 'span', text: part.slice(2, -2), marks: ['strong'] });
114
+ }
115
+ else if (part.startsWith('[') && part.endsWith(')') && part.includes('](')) {
116
+ const split = part.indexOf('](');
117
+ const mark = key();
118
+ markDefs.push({ _key: mark, _type: 'link', href: part.slice(split + 2, -1) });
119
+ children.push({ _key: key(), _type: 'span', text: part.slice(1, split), marks: [mark] });
120
+ }
121
+ else if (part.startsWith('*') && part.endsWith('*') && part.length > 2) {
122
+ children.push({ _key: key(), _type: 'span', text: part.slice(1, -1), marks: ['em'] });
123
+ }
124
+ else {
125
+ children.push({ _key: key(), _type: 'span', text: part, marks: [] });
126
+ }
127
+ }
128
+ return { children, markDefs };
129
+ }
130
+ const block = (style, source) => ({
131
+ _key: key(),
132
+ _type: 'block',
133
+ style,
134
+ ...inline(source),
135
+ });
136
+ return {
137
+ p: (source) => block('normal', source),
138
+ h2: (source) => block('h2', source),
139
+ h3: (source) => block('h3', source),
140
+ ul: (items) => items.map((item) => ({ ...block('normal', item), listItem: 'bullet', level: 1 })),
141
+ ol: (items) => items.map((item) => ({ ...block('normal', item), listItem: 'number', level: 1 })),
142
+ /** A code block: a paragraph whose spans all carry `code`, the first one (bold) naming the file or the language. */
143
+ code: (label, source) => ({
144
+ _key: key(),
145
+ _type: 'block',
146
+ style: 'normal',
147
+ children: [
148
+ ...(label === ''
149
+ ? []
150
+ : [
151
+ { _key: key(), _type: 'span', text: fill(label), marks: ['code', 'strong'] },
152
+ ]),
153
+ { _key: key(), _type: 'span', text: fill(source), marks: ['code'] },
154
+ ],
155
+ markDefs: [],
156
+ }),
157
+ /** A note: a blockquote that opens on a bold label. */
158
+ note: (label, source) => {
159
+ const body = inline(` ${source}`);
160
+ return {
161
+ _key: key(),
162
+ _type: 'block',
163
+ style: 'blockquote',
164
+ children: [
165
+ { _key: key(), _type: 'span', text: label, marks: ['strong'] },
166
+ ...body.children,
167
+ ],
168
+ markDefs: body.markDefs,
169
+ };
170
+ },
171
+ /** A reference table: bullet items that open on a code term, then an optional italic type, then the description. */
172
+ ref: (rows) => rows.map(([term, meta, description]) => {
173
+ const rest = inline(` ${description}`);
174
+ return {
175
+ _key: key(),
176
+ _type: 'block',
177
+ style: 'normal',
178
+ listItem: 'bullet',
179
+ level: 1,
180
+ children: [
181
+ { _key: key(), _type: 'span', text: fill(term), marks: ['code'] },
182
+ ...(meta === null
183
+ ? []
184
+ : [{ _key: key(), _type: 'span', text: ` ${fill(meta)}.`, marks: ['em'] }]),
185
+ ...rest.children,
186
+ ],
187
+ markDefs: rest.markDefs,
188
+ };
189
+ }),
190
+ };
191
+ }
192
+ function flat(parts) {
193
+ return parts.flatMap((part) => (Array.isArray(part) ? part : [part]));
194
+ }
195
+ const PAGES = [
196
+ {
197
+ title: 'Introduction',
198
+ slug: 'introduction',
199
+ section: 'Getting started',
200
+ summary: 'What %N% does, where it runs next to your application, and where to read next.',
201
+ body: (w) => [
202
+ w.p('%N% delivers webhooks for your application. You send it an event over HTTP, and it signs a copy for every endpoint subscribed to that event type, sends each one, records every attempt, and retries failures on a fixed schedule until they succeed or run out of attempts.'),
203
+ w.p('It runs as a single Node.js process next to a database: SQLite on a laptop, PostgreSQL in production. There is no hosted account to create and no message queue to operate.'),
204
+ w.h2('What it handles for you'),
205
+ w.ul([
206
+ '**Signing.** Every request carries a `%HDR%-Signature` header that a receiver checks with a few lines of code.',
207
+ '**Retries.** A failed delivery is tried again after 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours and 24 hours.',
208
+ '**A delivery log.** The status code, the duration and the first kilobyte of the response of every attempt, kept for 30 days.',
209
+ '**Replay.** Any event from the last 30 days can be sent again, to one endpoint or to all of them.',
210
+ ]),
211
+ w.h2('What stays in your application'),
212
+ w.p('Your application decides which events exist and what their payloads contain. %N% stores the JSON it receives as it is and delivers exactly those bytes, so a receiver sees the same payload your code sent.'),
213
+ w.h2('How this documentation is organised'),
214
+ w.ul([
215
+ '[Getting started](/docs/installation) installs the command-line tool, runs a local server and sends a first event.',
216
+ '[Guides](/docs/verifying-signatures) cover the work that follows: verifying signatures, handling failures, configuring and deploying a server.',
217
+ '[Reference](/docs/cli-reference) lists every command, configuration key and HTTP endpoint.',
218
+ ]),
219
+ w.note('Tip', 'With ten minutes to spare, go straight to the [Quickstart](/docs/quickstart). It ends with a signed event arriving at a receiver on your own machine.'),
220
+ ],
221
+ },
222
+ {
223
+ title: 'Installation',
224
+ slug: 'installation',
225
+ section: 'Getting started',
226
+ summary: 'Install the command-line tool or run the container image, then check that a server can start.',
227
+ body: (w) => [
228
+ w.p('%N% is published as one npm package that contains the server and the `%cli%` command. The same release is available as a container image.'),
229
+ w.h2('Requirements'),
230
+ w.ref([
231
+ ['Node.js', '20.11 or later', 'Check with `node --version`.'],
232
+ ['SQLite', 'included', 'Used by `%cli% dev`. Nothing to install.'],
233
+ ['PostgreSQL', '14 or later', 'Required by `%cli% start` in production.'],
234
+ ]),
235
+ w.h2('Install the command'),
236
+ w.code('Terminal', '$ npm install --global %cli%\n$ %cli% --version\n%cli% 2.4.1'),
237
+ w.p('To pin the version for one project, add it as a development dependency and run it through npx:'),
238
+ w.code('Terminal', '$ npm install --save-dev %cli%@2.4.1\n$ npx %cli% --version\n%cli% 2.4.1'),
239
+ w.note('Note', 'On Linux, install Node.js with a version manager such as nvm or fnm rather than running npm with sudo. A global package installed with sudo leaves files the next upgrade cannot replace.'),
240
+ w.h2('Run the container image'),
241
+ w.p('The image reads its settings from environment variables, so a first run needs no configuration file. It listens on port 8787.'),
242
+ w.code('Terminal', '$ docker run --rm -p 8787:8787 \\\n -e %ENV%_API_KEY=rk_dev_local \\\n ghcr.io/example/%cli%:2.4.1\nListening on http://0.0.0.0:8787'),
243
+ w.h2('Check the installation'),
244
+ w.p('`%cli% doctor` checks the Node.js version, the configuration file, the database and the API key, and says what to fix.'),
245
+ w.code('Terminal', '$ %cli% doctor\nNode.js 22.11.0 ok\nConfiguration %cli%.yaml ok\nDatabase sqlite:.%cli%/dev.db ok\n%ENV%_API_KEY missing, run %cli% init to create one'),
246
+ w.h2('Upgrade'),
247
+ w.p('Install the new version over the old one, then run `%cli% migrate` before the server starts again. Each release lists its migrations in [What’s new](/docs/whats-new).'),
248
+ w.note('Warning', 'A migration marked as locking in the release notes needs every server stopped first. Servers of the previous version keep running during all the other migrations.'),
249
+ ],
250
+ },
251
+ {
252
+ title: 'Quickstart',
253
+ slug: 'quickstart',
254
+ section: 'Getting started',
255
+ summary: 'Run a local server, create an endpoint and send it a signed event, in about five minutes.',
256
+ body: (w) => [
257
+ w.p('You need %N% installed (see [Installation](/docs/installation)) and two terminal windows. Everything below runs on your own machine.'),
258
+ w.h2('Create a project'),
259
+ w.p('In an empty directory, `%cli% init` writes a configuration file and a `.env` file holding a new API key.'),
260
+ w.code('Terminal', '$ mkdir billing-webhooks && cd billing-webhooks\n$ %cli% init\nCreated %cli%.yaml\nCreated .env with %ENV%_API_KEY'),
261
+ w.h2('Start the development server'),
262
+ w.p('`%cli% dev` starts a server on port 8787 with a SQLite database in `.%cli%/dev.db`, and prints each delivery as it happens. Leave it running; `Ctrl+C` stops it.'),
263
+ w.code('Terminal', '$ %cli% dev\nDatabase sqlite:.%cli%/dev.db, 14 migrations applied\nListening on http://localhost:8787'),
264
+ w.h2('Start a receiver'),
265
+ w.p('In the second terminal, `%cli% listen` starts a small receiver that prints every request it gets, headers included. It stands in for the endpoint one of your customers would run.'),
266
+ w.code('Terminal', '$ %cli% listen --port 4000\nReceiving on http://localhost:4000/webhooks'),
267
+ w.h2('Create an endpoint'),
268
+ w.p('An endpoint is a URL and the event types it subscribes to. The wildcard `invoice.*` matches every type that starts with `invoice.`.'),
269
+ w.code('Terminal', '$ %cli% endpoints create \\\n --url http://localhost:4000/webhooks --events "invoice.*"\nEndpoint ep_01J8Z3HQ7N5V created\nSecret whsec_4f9c2a7e1b0d6c3a'),
270
+ w.h2('Send an event'),
271
+ w.code('Terminal', '$ %cli% events send invoice.paid \\\n --data \'{"invoice_id":"in_1042","amount":4900,"currency":"eur"}\'\nEvent evt_01J8Z3K6QF2M created, 1 delivery scheduled'),
272
+ w.p('Within a second, the receiver prints the request:'),
273
+ w.code('Receiver output', 'POST /webhooks\nContent-Type: application/json\n%HDR%-Event-Id: evt_01J8Z3K6QF2M\n%HDR%-Event-Type: invoice.paid\n%HDR%-Delivery-Attempt: 1\n%HDR%-Signature: t=1757854800,v1=9b1c4e0f7a2d63b8c5e1f09a4d7b2c6e\n\n{"invoice_id":"in_1042","amount":4900,"currency":"eur"}'),
274
+ w.h2('Next steps'),
275
+ w.ul([
276
+ '[Verify the signature](/docs/verifying-signatures) in your receiving code before you trust a payload.',
277
+ 'Read [Core concepts](/docs/core-concepts) to see what happens when an endpoint fails.',
278
+ 'Send events from your application with the [HTTP API](/docs/http-api) instead of the command line.',
279
+ ]),
280
+ ],
281
+ },
282
+ {
283
+ title: 'Core concepts',
284
+ slug: 'core-concepts',
285
+ section: 'Getting started',
286
+ summary: 'Events, endpoints, deliveries and attempts, and how one event becomes several signed requests.',
287
+ body: (w, media) => [
288
+ w.p('Four objects describe everything %N% does. Once they are clear, the command line and the HTTP API read as two ways of creating and inspecting the same things.'),
289
+ media.deliveryFlow === undefined
290
+ ? []
291
+ : [
292
+ {
293
+ _key: 'core-concepts-figure',
294
+ _type: 'media',
295
+ id: media.deliveryFlow,
296
+ caption: 'One event, three endpoints. Each delivery is signed and sent on its own, and a failed one is retried without holding up the others.',
297
+ },
298
+ ],
299
+ w.h2('Events'),
300
+ w.p('An event records something that happened in your application, such as an invoice being paid. It has a type (`invoice.paid`), a JSON payload of up to 256 KB, and an id that starts with `evt_`. Events never change after they are created; to correct one, send a new event.'),
301
+ w.h2('Endpoints'),
302
+ w.p('An endpoint is a URL that receives events, the event types it subscribes to, and a secret that starts with `whsec_`. A type can end in a wildcard: `invoice.*` matches `invoice.paid` and `invoice.voided`.'),
303
+ w.h2('Deliveries and attempts'),
304
+ w.p('When an event arrives, %N% creates one delivery for each endpoint subscribed to its type. Every delivery is sent on its own, so a slow endpoint never delays the others.'),
305
+ w.p('An attempt is one HTTP request for a delivery. A 2xx response within 10 seconds completes the delivery. Any other result, a redirect included, fails the attempt and schedules the next one.'),
306
+ w.h3('Delivery states'),
307
+ w.ref([
308
+ ['pending', null, 'Waiting for its first attempt, usually for less than a second.'],
309
+ ['retrying', null, 'At least one attempt failed and another one is scheduled.'],
310
+ ['succeeded', null, 'An attempt received a 2xx response. No further attempts are made.'],
311
+ ['failed', null, 'All seven attempts failed. The delivery can still be retried by hand.'],
312
+ ]),
313
+ w.note('Note', 'An endpoint whose deliveries have failed for five days in a row is disabled. Run `%cli% endpoints enable` once the receiver works again; see [Retries and replay](/docs/retries-and-replay).'),
314
+ ],
315
+ },
316
+ {
317
+ title: 'Verifying signatures',
318
+ slug: 'verifying-signatures',
319
+ section: 'Guides',
320
+ summary: 'Check that a request came from your %N% server and was not modified in transit, before your code acts on it.',
321
+ body: (w) => [
322
+ w.p('Every delivery carries a `%HDR%-Signature` header. Checking it proves two things: the request was signed with the endpoint’s secret, and its body has not changed since.'),
323
+ w.h2('How the signature is built'),
324
+ w.p('The header holds a Unix timestamp and one or more signatures:'),
325
+ w.code('', '%HDR%-Signature: t=1757854800,v1=5f2b8c0e61d3a9b7e4f0c2d1a8b6e3f9c7d5a1b2e4f6a8c0d2e4f6a8b0c2d4e6'),
326
+ w.p('Each `v1` value is the hex-encoded HMAC-SHA256 of the timestamp, a full stop and the raw request body, keyed with the endpoint secret. While a secret is being rotated, the header carries one `v1` value per secret.'),
327
+ w.h2('Verify in Node.js'),
328
+ w.code('verify-signature.ts', `import { createHmac, timingSafeEqual } from 'node:crypto'
329
+
330
+ const TOLERANCE_SECONDS = 300
331
+
332
+ export function verifySignature(
333
+ rawBody: string,
334
+ header: string,
335
+ secret: string,
336
+ ): boolean {
337
+ const parts = header.split(',').map((part) => part.split('='))
338
+ const timestamp = Number(parts.find(([key]) => key === 't')?.[1])
339
+ // Refuse old signatures: a captured request cannot be replayed later
340
+ const age = Math.abs(Date.now() / 1000 - timestamp)
341
+ if (Number.isNaN(timestamp) || age > TOLERANCE_SECONDS) return false
342
+
343
+ const expected = createHmac('sha256', secret)
344
+ .update(timestamp + '.' + rawBody)
345
+ .digest()
346
+ return parts
347
+ .filter(([key]) => key === 'v1')
348
+ .some(([, value]) => {
349
+ const received = Buffer.from(value ?? '', 'hex')
350
+ return (
351
+ received.length === expected.length &&
352
+ timingSafeEqual(received, expected)
353
+ )
354
+ })
355
+ }`),
356
+ w.h3('In an Express route'),
357
+ w.p('Read the body as raw bytes on the webhook route only, so the signature is computed over exactly what was sent.'),
358
+ w.code('server.ts', `import express from 'express'
359
+ import { handleEvent } from './events.js'
360
+ import { verifySignature } from './verify-signature.js'
361
+
362
+ const app = express()
363
+ const rawJson = express.raw({ type: 'application/json' })
364
+
365
+ app.post('/webhooks', rawJson, (req, res) => {
366
+ const body = req.body.toString('utf8')
367
+ const header = req.get('%HDR%-Signature') ?? ''
368
+ const secret = process.env.WEBHOOK_SECRET ?? ''
369
+ if (verifySignature(body, header, secret) === false) {
370
+ res.status(400).send('invalid signature')
371
+ return
372
+ }
373
+ // Answer first, then do the work: an attempt times out after 10 s
374
+ res.status(204).end()
375
+ handleEvent(JSON.parse(body))
376
+ })`),
377
+ w.note('Warning', 'Most frameworks parse JSON before your handler runs. Serialising the parsed object again changes whitespace and key order, and the signature stops matching. Always verify the raw body.'),
378
+ w.h2('Verify in Python'),
379
+ w.code('verify_signature.py', `import hashlib
380
+ import hmac
381
+ import time
382
+
383
+ TOLERANCE = 300
384
+
385
+ def verify_signature(raw_body: bytes, header: str, secret: str) -> bool:
386
+ parts = [part.split("=", 1) for part in header.split(",")]
387
+ stamps = [value for name, value in parts if name == "t"]
388
+ if not stamps or abs(time.time() - int(stamps[0])) > TOLERANCE:
389
+ return False
390
+ signed = stamps[0].encode() + b"." + raw_body
391
+ key = secret.encode()
392
+ digest = hmac.new(key, signed, hashlib.sha256).hexdigest()
393
+ received = [value for name, value in parts if name == "v1"]
394
+ return any(hmac.compare_digest(digest, value) for value in received)`),
395
+ w.h2('Rotate a secret'),
396
+ w.p('A rotation issues a new secret and keeps the previous one valid for a grace period, so receivers can switch without rejecting a single request.'),
397
+ w.code('Terminal', '$ %cli% endpoints rotate-secret ep_01J8Z3HQ7N5V --grace 24h\nNew secret whsec_7d31b0c9e25a4f18\nPrevious secret valid until 2026-09-15 09:12 UTC'),
398
+ ],
399
+ },
400
+ {
401
+ title: 'Retries and replay',
402
+ slug: 'retries-and-replay',
403
+ section: 'Guides',
404
+ summary: 'What happens when an endpoint fails, how to retry a delivery by hand, and how to send past events again.',
405
+ body: (w) => [
406
+ w.p('Endpoints go down, deploys return errors and certificates expire. A failed attempt is the start of a schedule, and the delivery log shows every step of it.'),
407
+ w.h2('The retry schedule'),
408
+ w.p('A delivery gets up to seven attempts. After a failure the next attempt waits for the interval below, plus up to 10 percent of random jitter, so an endpoint coming back from an outage does not receive every retry at the same moment.'),
409
+ w.ref([
410
+ ['30s', 'attempt 2', 'Covers a restart or a deploy in progress.'],
411
+ ['2m', 'attempt 3', 'Covers a short network interruption.'],
412
+ ['10m', 'attempt 4', 'Covers a failed deploy that gets rolled back.'],
413
+ ['1h', 'attempt 5', 'Covers most incidents on the receiving side.'],
414
+ ['6h', 'attempt 6', 'Covers an outage that lasts into the evening.'],
415
+ ['24h', 'attempt 7', 'The last attempt, a little over 31 hours after the first.'],
416
+ ]),
417
+ w.p('Change the intervals with `delivery.retries.schedule` in the [configuration file](/docs/configuration-reference). There is always one more attempt than there are intervals.'),
418
+ w.h2('Retry a delivery by hand'),
419
+ w.p('Once a receiver is fixed there is no need to wait for the next scheduled attempt. List the failed deliveries of the endpoint and retry the ones you need:'),
420
+ w.code('Terminal', '$ %cli% deliveries list --endpoint ep_01J8Z3HQ7N5V --status failed\nID EVENT ATTEMPTS LAST RESULT\ndlv_01J8Z9AQ2C7R invoice.paid 7 503\ndlv_01J8Z9B4TMW1 invoice.voided 7 timeout\n$ %cli% deliveries retry dlv_01J8Z9AQ2C7R\nAttempt 8 scheduled for dlv_01J8Z9AQ2C7R'),
421
+ w.p('A manual retry adds an attempt to the same delivery. The receiver sees the same `%HDR%-Event-Id` as before and can recognise a request it has already processed.'),
422
+ w.h2('Replay past events'),
423
+ w.p('A replay creates new deliveries for events that already exist. Use it after adding an endpoint that should have received earlier events, or after a receiver lost data.'),
424
+ w.code('Terminal', '$ %cli% events replay --since 2026-09-12T08:00:00Z \\\n --types "invoice.*" --endpoint ep_01J8Z3HQ7N5V\n38 events matched, 38 deliveries scheduled'),
425
+ w.note('Note', 'A replay reaches only the events still inside the retention window, 30 days by default. A replayed event keeps its original id, so receivers should use `%HDR%-Event-Id` to skip events they have already handled.'),
426
+ ],
427
+ },
428
+ {
429
+ title: 'Configuration',
430
+ slug: 'configuration',
431
+ section: 'Guides',
432
+ summary: 'Where the server reads its settings, and which ones to review before a server goes to production.',
433
+ body: (w) => [
434
+ w.p('The server reads `%cli%.yaml` from its working directory, then applies environment variables on top. A value set in the environment always wins, so one file serves development and production.'),
435
+ w.code('%cli%.yaml', `server:
436
+ port: 8787
437
+ public_url: http://localhost:8787
438
+
439
+ database:
440
+ url: sqlite:.%cli%/dev.db
441
+
442
+ delivery:
443
+ timeout: 10s
444
+ concurrency: 32
445
+ retries:
446
+ schedule: [30s, 2m, 10m, 1h, 6h, 24h]
447
+
448
+ retention:
449
+ events: 30d`),
450
+ w.h2('Environment variables'),
451
+ w.p('Every key has an environment variable: write its path in capitals, replace the dots with underscores and add the `%ENV%_` prefix. `database.url` becomes `%ENV%_DATABASE_URL`.'),
452
+ w.ref([
453
+ [
454
+ '%ENV%_API_KEY',
455
+ 'required',
456
+ 'Authenticates calls to the HTTP API, including the ones the command line makes.',
457
+ ],
458
+ [
459
+ '%ENV%_DATABASE_URL',
460
+ 'optional',
461
+ 'Overrides `database.url`. Use it for production credentials.',
462
+ ],
463
+ ['%ENV%_CONFIG', 'optional', 'Path of a configuration file other than `%cli%.yaml`.'],
464
+ ]),
465
+ w.note('Warning', 'Keep secrets out of `%cli%.yaml`. The file is meant to be committed with your code; the API key and the database password belong in the environment or in your platform’s secret store.'),
466
+ w.h2('Before a server goes to production'),
467
+ w.ul([
468
+ '`server.public_url`, so links in the delivery log point at the right host.',
469
+ '`delivery.concurrency`, the number of requests one server sends at the same time. Raise it when deliveries start to wait.',
470
+ '`retention.events`, when your customers expect replays reaching further back than 30 days.',
471
+ ]),
472
+ w.p('The [configuration reference](/docs/configuration-reference) lists every key with its type and default value.'),
473
+ ],
474
+ },
475
+ {
476
+ title: 'Deploying to production',
477
+ slug: 'deploying-to-production',
478
+ section: 'Guides',
479
+ summary: 'Run %N% on PostgreSQL behind a reverse proxy, with health checks, and with more than one server.',
480
+ body: (w) => [
481
+ w.p('A production server needs three things: a PostgreSQL database, an API key in the environment, and a supervisor that restarts the process if it exits.'),
482
+ w.h2('Prepare the database'),
483
+ w.code('Terminal', '# The password comes from PGPASSWORD or your platform’s secret store\n$ export %ENV%_DATABASE_URL=postgres://%cli%@db.internal:5432/%cli%\n$ %cli% migrate\nApplied 14 migrations in 1.8 s'),
484
+ w.p('Run `%cli% migrate` once for each release, before the new version starts. A server refuses to start against a database with pending migrations.'),
485
+ w.h2('Run the server'),
486
+ w.p('The image runs `%cli% start` by default. With Docker Compose:'),
487
+ w.code('compose.yaml', `services:
488
+ %cli%:
489
+ image: ghcr.io/example/%cli%:2.4.1
490
+ ports:
491
+ - "8787:8787"
492
+ environment:
493
+ %ENV%_API_KEY: \${%ENV%_API_KEY}
494
+ %ENV%_DATABASE_URL: \${%ENV%_DATABASE_URL}
495
+ healthcheck:
496
+ test: ["CMD", "wget", "-qO-", "http://localhost:8787/healthz"]
497
+ interval: 10s
498
+ restart: unless-stopped`),
499
+ w.h2('Put a proxy in front'),
500
+ w.p('Terminate TLS at a reverse proxy or a load balancer and forward traffic to port 8787. Only the HTTP API needs inbound traffic; deliveries are outbound requests.'),
501
+ w.h2('Health checks'),
502
+ w.p('`GET /healthz` answers 200 when the database is reachable and its migrations are current, and 503 otherwise. Point both liveness and readiness probes at it.'),
503
+ w.h2('Run more than one server'),
504
+ w.p('Servers can share a database. Each one claims deliveries with a row lock (`FOR UPDATE SKIP LOCKED`), so two servers never send the same attempt. Add a server when `%cli% status` shows deliveries waiting for more than a few seconds.'),
505
+ w.note('Tip', 'Stop a server with SIGTERM. It finishes the attempts already in flight, for up to the delivery timeout, before it exits.'),
506
+ ],
507
+ },
508
+ {
509
+ title: 'CLI reference',
510
+ slug: 'cli-reference',
511
+ section: 'Reference',
512
+ summary: 'Every command and global flag of the %N% command line, and what each exit code means.',
513
+ body: (w) => [
514
+ w.p('Every command reads `%cli%.yaml` and the `%ENV%_` environment variables described in [Configuration](/docs/configuration). `%cli% help COMMAND` prints the flags of one command.'),
515
+ w.h2('Project and server'),
516
+ w.ref([
517
+ [
518
+ '%cli% init',
519
+ null,
520
+ 'Writes `%cli%.yaml` and a `.env` file with a new API key in the current directory.',
521
+ ],
522
+ ['%cli% dev', null, 'Starts a development server on SQLite and prints each delivery.'],
523
+ [
524
+ '%cli% start',
525
+ null,
526
+ 'Starts a production server. `database.url` must point at PostgreSQL.',
527
+ ],
528
+ ['%cli% migrate', null, 'Applies pending database migrations, then exits.'],
529
+ [
530
+ '%cli% status',
531
+ null,
532
+ 'Shows running servers, queued deliveries and the oldest waiting delivery.',
533
+ ],
534
+ ['%cli% doctor', null, 'Checks Node.js, the configuration, the database and the API key.'],
535
+ ]),
536
+ w.h2('Endpoints'),
537
+ w.ref([
538
+ [
539
+ '%cli% endpoints create',
540
+ null,
541
+ 'Creates an endpoint from `--url` and `--events`, and prints its secret once.',
542
+ ],
543
+ [
544
+ '%cli% endpoints list',
545
+ null,
546
+ 'Lists endpoints with their state and their failure rate over 24 hours.',
547
+ ],
548
+ [
549
+ '%cli% endpoints disable ID',
550
+ null,
551
+ 'Stops deliveries to an endpoint. New deliveries wait until it is enabled.',
552
+ ],
553
+ ['%cli% endpoints enable ID', null, 'Resumes deliveries to an endpoint.'],
554
+ [
555
+ '%cli% endpoints rotate-secret ID',
556
+ null,
557
+ 'Issues a new secret. `--grace` keeps the previous one valid, 24 hours by default.',
558
+ ],
559
+ ]),
560
+ w.h2('Events and deliveries'),
561
+ w.ref([
562
+ [
563
+ '%cli% events send TYPE',
564
+ null,
565
+ 'Sends an event. The payload comes from `--data` or from standard input.',
566
+ ],
567
+ [
568
+ '%cli% events replay',
569
+ null,
570
+ 'Creates new deliveries for past events, filtered by `--since`, `--types` and `--endpoint`.',
571
+ ],
572
+ [
573
+ '%cli% deliveries list',
574
+ null,
575
+ 'Lists deliveries, filtered by `--endpoint` and `--status`.',
576
+ ],
577
+ [
578
+ '%cli% deliveries retry ID',
579
+ null,
580
+ 'Schedules an extra attempt for a delivery, right away.',
581
+ ],
582
+ [
583
+ '%cli% listen',
584
+ null,
585
+ 'Starts a local receiver that prints every request. For development only.',
586
+ ],
587
+ ]),
588
+ w.h2('Global flags'),
589
+ w.ref([
590
+ ['--config PATH', 'string', 'Reads another configuration file.'],
591
+ [
592
+ '--api-url URL',
593
+ 'string, default http://localhost:8787',
594
+ 'The server a command talks to.',
595
+ ],
596
+ ['--json', 'boolean', 'Prints one JSON object per line instead of a table.'],
597
+ ['--quiet', 'boolean', 'Prints errors only.'],
598
+ ]),
599
+ w.h2('Exit codes'),
600
+ w.ref([
601
+ ['0', null, 'The command succeeded.'],
602
+ ['1', null, 'The command failed. The reason is printed on standard error.'],
603
+ ['2', null, 'An argument or the configuration is invalid.'],
604
+ ['3', null, 'The server could not be reached.'],
605
+ ]),
606
+ ],
607
+ },
608
+ {
609
+ title: 'Configuration reference',
610
+ slug: 'configuration-reference',
611
+ section: 'Reference',
612
+ summary: 'Every key the configuration file accepts, with its type and default value.',
613
+ body: (w) => [
614
+ w.p('Durations accept the suffixes `ms`, `s`, `m`, `h` and `d`. Every key can also be set as an environment variable, as described in [Configuration](/docs/configuration).'),
615
+ w.h2('server'),
616
+ w.ref([
617
+ ['server.port', 'integer, default 8787', 'Port the HTTP API listens on.'],
618
+ ['server.host', 'string, default 0.0.0.0', 'Network interface to bind.'],
619
+ ['server.public_url', 'string', 'Address used in links in the delivery log.'],
620
+ ]),
621
+ w.h2('database'),
622
+ w.ref([
623
+ [
624
+ 'database.url',
625
+ 'string, required',
626
+ 'A `sqlite:` path or a `postgres://` connection string.',
627
+ ],
628
+ ['database.pool_size', 'integer, default 10', 'Connections one server keeps open.'],
629
+ ]),
630
+ w.h2('delivery'),
631
+ w.ref([
632
+ [
633
+ 'delivery.timeout',
634
+ 'duration, default 10s',
635
+ 'How long an endpoint has to answer an attempt.',
636
+ ],
637
+ [
638
+ 'delivery.concurrency',
639
+ 'integer, default 32',
640
+ 'Requests one server sends at the same time.',
641
+ ],
642
+ [
643
+ 'delivery.retries.schedule',
644
+ 'list of durations',
645
+ 'Waits between attempts. The default is `[30s, 2m, 10m, 1h, 6h, 24h]`.',
646
+ ],
647
+ ['delivery.retries.jitter', 'number, default 0.1', 'Share of each wait added at random.'],
648
+ [
649
+ 'delivery.disable_after',
650
+ 'duration, default 5d',
651
+ 'Disables an endpoint that has failed for this long.',
652
+ ],
653
+ ]),
654
+ w.h2('signing'),
655
+ w.ref([
656
+ [
657
+ 'signing.tolerance',
658
+ 'duration, default 5m',
659
+ 'Age after which the official libraries reject a signature.',
660
+ ],
661
+ ['signing.header', 'string, default %HDR%-Signature', 'Name of the signature header.'],
662
+ ]),
663
+ w.h2('retention'),
664
+ w.ref([
665
+ [
666
+ 'retention.events',
667
+ 'duration, default 30d',
668
+ 'How long events, deliveries and attempts are kept.',
669
+ ],
670
+ [
671
+ 'retention.response_body',
672
+ 'size, default 1KB',
673
+ 'How much of each response body is stored.',
674
+ ],
675
+ ]),
676
+ ],
677
+ },
678
+ {
679
+ title: 'HTTP API',
680
+ slug: 'http-api',
681
+ section: 'Reference',
682
+ summary: 'Send events and inspect deliveries over HTTP, with example requests, responses and errors.',
683
+ body: (w) => [
684
+ w.p('Every %N% server serves the API under `/v1`. Requests and responses are JSON, and every request except the health check authenticates with the API key as a bearer token.'),
685
+ w.code('Terminal', '$ curl http://localhost:8787/v1/endpoints \\\n -H "Authorization: Bearer $%ENV%_API_KEY"'),
686
+ w.h2('Resources'),
687
+ w.ref([
688
+ ['POST /v1/events', null, 'Sends an event and schedules its deliveries.'],
689
+ ['GET /v1/events/{id}', null, 'Returns an event and the state of each of its deliveries.'],
690
+ ['POST /v1/endpoints', null, 'Creates an endpoint.'],
691
+ ['GET /v1/endpoints', null, 'Lists endpoints.'],
692
+ [
693
+ 'PATCH /v1/endpoints/{id}',
694
+ null,
695
+ 'Changes the URL, the event types or the state of an endpoint.',
696
+ ],
697
+ [
698
+ 'GET /v1/deliveries',
699
+ null,
700
+ 'Lists deliveries, filtered by `status`, `endpoint` or `event`.',
701
+ ],
702
+ [
703
+ 'GET /v1/deliveries/{id}/attempts',
704
+ null,
705
+ 'Lists the attempts of a delivery, newest first.',
706
+ ],
707
+ ['POST /v1/deliveries/{id}/retry', null, 'Schedules an extra attempt.'],
708
+ ['GET /healthz', null, 'Reports whether the server is ready. Needs no API key.'],
709
+ ]),
710
+ w.h2('Send an event'),
711
+ w.p('`idempotency_key` is optional. A second request with the same key within 24 hours returns the first event instead of creating a new one.'),
712
+ w.code('Request', `POST /v1/events HTTP/1.1
713
+ Host: localhost:8787
714
+ Authorization: Bearer rk_live_2Jx8Qd4mT7
715
+ Content-Type: application/json
716
+
717
+ {
718
+ "type": "invoice.paid",
719
+ "idempotency_key": "invoice-in_1042-paid",
720
+ "data": {
721
+ "invoice_id": "in_1042",
722
+ "amount": 4900,
723
+ "currency": "eur"
724
+ }
725
+ }`),
726
+ w.code('Response: 202 Accepted', `{
727
+ "id": "evt_01J8Z3K6QF2M",
728
+ "type": "invoice.paid",
729
+ "created_at": "2026-09-14T09:12:04Z",
730
+ "deliveries": 3
731
+ }`),
732
+ w.h2('List failed deliveries'),
733
+ w.code('Request', 'GET /v1/deliveries?status=failed&limit=2 HTTP/1.1\nHost: localhost:8787\nAuthorization: Bearer rk_live_2Jx8Qd4mT7'),
734
+ w.code('Response: 200 OK', `{
735
+ "data": [
736
+ {
737
+ "id": "dlv_01J8Z9AQ2C7R",
738
+ "event": "evt_01J8Z3K6QF2M",
739
+ "endpoint": "ep_01J8Z3HQ7N5V",
740
+ "status": "failed",
741
+ "attempts": 7,
742
+ "last_response": { "status": 503, "duration_ms": 10000 }
743
+ }
744
+ ],
745
+ "next_cursor": null
746
+ }`),
747
+ w.h2('Errors'),
748
+ w.p('An error has a standard status code and a body with a stable `code` and a readable `message`.'),
749
+ w.code('Response: 409 Conflict', `{
750
+ "error": {
751
+ "code": "idempotency_conflict",
752
+ "message": "This idempotency key was used with a different body."
753
+ }
754
+ }`),
755
+ w.ref([
756
+ [
757
+ '400 invalid_request',
758
+ null,
759
+ 'The body is not valid JSON, or a required field is missing.',
760
+ ],
761
+ ['401 unauthorized', null, 'The API key is missing or has been revoked.'],
762
+ ['404 not_found', null, 'No object with this id exists.'],
763
+ [
764
+ '409 idempotency_conflict',
765
+ null,
766
+ 'The idempotency key was used with a different body in the last 24 hours.',
767
+ ],
768
+ ['413 payload_too_large', null, 'The event payload is larger than 256 KB.'],
769
+ [
770
+ '429 rate_limited',
771
+ null,
772
+ 'Too many requests. Wait for the number of seconds in the `Retry-After` header.',
773
+ ],
774
+ ]),
775
+ ],
776
+ },
777
+ {
778
+ title: 'Troubleshooting',
779
+ slug: 'troubleshooting',
780
+ section: 'Help',
781
+ summary: 'The problems that come up most often, what causes each one and how to fix it.',
782
+ body: (w) => [
783
+ w.p('Start with `%cli% doctor`, which catches most configuration problems. The sections below cover what it cannot see from the server.'),
784
+ w.h2('Signatures never match'),
785
+ w.p('The receiver is almost always verifying a body that was parsed and serialised again. Verify the raw bytes, as shown in [Verifying signatures](/docs/verifying-signatures). If the body is raw and signatures still fail, check that the secret belongs to this endpoint: every endpoint has its own.'),
786
+ w.h2('Timestamp outside the tolerance'),
787
+ w.p('The receiver’s clock is more than five minutes away from the server’s. Turn on NTP on that host, or raise `signing.tolerance` while it gets fixed.'),
788
+ w.h2('Deliveries stay pending'),
789
+ w.p('No server is claiming deliveries. `%cli% migrate` exits once it has run, so check that a process runs `%cli% start`, then look at `%cli% status`:'),
790
+ w.code('Terminal', '$ %cli% status\nServers 2 running\nQueued 1,284 deliveries\nOldest waiting 3 min 12 s\nAttempts 2,410 per minute'),
791
+ w.p('A queue that keeps growing while servers run means `delivery.concurrency` is too low for your traffic, or one endpoint is slow enough to hold every request slot. Raise the concurrency or add a server.'),
792
+ w.h2('Every attempt fails with a redirect'),
793
+ w.p('%N% does not follow redirects: a signed request sent on to another host would hand its payload to a server nobody chose. Update the endpoint to its final URL, which is usually the `https://` form of the address.'),
794
+ w.h2('Connection refused from a container'),
795
+ w.p('Inside a container, `localhost` is the container itself. Use `host.docker.internal` with Docker Desktop, or the service name on a Compose network.'),
796
+ w.h2('An endpoint was disabled'),
797
+ w.p('An endpoint is disabled after five days of failed deliveries. Fix the receiver, run `%cli% endpoints enable ID`, then send what it missed with `%cli% events replay --endpoint ID --since` and the time the failures started.'),
798
+ ],
799
+ },
800
+ {
801
+ title: 'What’s new',
802
+ slug: 'whats-new',
803
+ section: 'Help',
804
+ summary: 'The changes in each release of %N%, newest first, with what to do when upgrading.',
805
+ body: (w) => [
806
+ w.p('%N% follows semantic versioning. A minor release never changes the configuration format or the HTTP API in a way that breaks an existing integration.'),
807
+ w.h2('2.4, September 2, 2026'),
808
+ w.ul([
809
+ 'Endpoint event types accept a trailing wildcard, such as `invoice.*`.',
810
+ '`%cli% events replay` accepts `--since` and `--types`, so a replay no longer needs a list of event ids.',
811
+ 'The `%HDR%-Delivery-Attempt` header tells a receiver which attempt it is handling.',
812
+ '`delivery.retries.jitter` spreads retries after an outage. The default is 10 percent.',
813
+ ]),
814
+ w.h3('Upgrading from 2.3'),
815
+ w.p('Run `%cli% migrate` before starting 2.4. The migration adds an index to the deliveries table and takes under a minute on ten million rows.'),
816
+ w.h2('2.3, July 15, 2026'),
817
+ w.ul([
818
+ '`%cli% dev` runs on SQLite, so a local server no longer needs PostgreSQL.',
819
+ 'New `%cli% doctor` command.',
820
+ '`retention.response_body` sets how much of each response is stored.',
821
+ ]),
822
+ w.h3('Upgrading from 2.2'),
823
+ w.p('Nothing to change in the configuration file. Development projects that pointed `database.url` at a local PostgreSQL can switch to SQLite by removing the key, since `%cli% dev` now creates its own database.'),
824
+ w.h2('2.2, May 20, 2026'),
825
+ w.ul([
826
+ 'Idempotency keys on `POST /v1/events`.',
827
+ 'Endpoints are disabled after five days of failures instead of seven. Set `delivery.disable_after` to keep the previous behaviour.',
828
+ ]),
829
+ ],
830
+ },
831
+ ];
832
+ /** The documentation, in reading order, named after the site. `media.deliveryFlow` illustrates "Core concepts" when it was seeded. */
833
+ export function buildDocumentationDocPages(siteName, media = {}) {
834
+ const names = productNames(siteName);
835
+ return PAGES.map((draft, index) => {
836
+ const w = writer(draft.slug, names);
837
+ const fillText = (value) => value
838
+ .replaceAll('%N%', names.name)
839
+ .replaceAll('%cli%', names.cli)
840
+ .replaceAll('%ENV%', names.env)
841
+ .replaceAll('%HDR%', names.header);
842
+ return {
843
+ title: draft.title,
844
+ slug: draft.slug,
845
+ section: draft.section,
846
+ order: (index + 1) * 10,
847
+ summary: fillText(draft.summary),
848
+ body: flat(draft.body(w, media)),
849
+ };
850
+ });
851
+ }
852
+ export const DOCUMENTATION_DEMO_DOC_PAGES = buildDocumentationDocPages();
853
+ /**
854
+ * `home`: the statement and the search (the theme's hero draws the search
855
+ * field), three places to start, the whole documentation by section, the
856
+ * first lines of code a reader will write, common questions answered in the
857
+ * open, and where to go when something does not work.
858
+ */
859
+ export function buildDocumentationDemoPages(_media, siteName) {
860
+ const names = productNames(siteName);
861
+ const w = writer('home', names);
862
+ const { name } = names;
863
+ return [
864
+ {
865
+ title: `${name} documentation`,
866
+ slug: 'home',
867
+ blocks: [
868
+ {
869
+ _key: 'home-hero',
870
+ _type: 'hero',
871
+ _version: BLOCK_VERSION,
872
+ eyebrow: `${name} 2.4`,
873
+ title: `${name} documentation`,
874
+ subtitle: 'Send signed webhooks from your application, retry failed deliveries on a schedule, and replay events from the last 30 days. Start with the quickstart, or search the guides and the reference.',
875
+ actions: [
876
+ { label: 'Quickstart', target: { href: '/docs/quickstart' }, emphasis: 'primary' },
877
+ { label: 'HTTP API reference', target: { href: '/docs/http-api' } },
878
+ ],
879
+ },
880
+ {
881
+ _key: 'home-start',
882
+ _type: 'featureGrid',
883
+ _version: BLOCK_VERSION,
884
+ title: 'Start here',
885
+ items: [
886
+ {
887
+ _key: 'home-start-install',
888
+ title: 'Install the command',
889
+ text: 'Node.js 20.11 or later, or the container image.',
890
+ link: { href: '/docs/installation' },
891
+ },
892
+ {
893
+ _key: 'home-start-quickstart',
894
+ title: 'Send your first event',
895
+ text: 'A local server, an endpoint and a signed request, in about five minutes.',
896
+ link: { href: '/docs/quickstart' },
897
+ },
898
+ {
899
+ _key: 'home-start-concepts',
900
+ title: 'Learn the concepts',
901
+ text: 'Events, endpoints, deliveries and attempts, and what happens on a failure.',
902
+ link: { href: '/docs/core-concepts' },
903
+ },
904
+ ],
905
+ },
906
+ {
907
+ _key: 'home-browse',
908
+ _type: 'collectionList',
909
+ _version: BLOCK_VERSION,
910
+ title: 'Browse the documentation',
911
+ collection: 'doc_page',
912
+ sort: { field: 'createdAt', direction: 'asc' },
913
+ limit: 100,
914
+ layout: 'list',
915
+ },
916
+ {
917
+ _key: 'home-send',
918
+ _type: 'prose',
919
+ _version: BLOCK_VERSION,
920
+ body: [
921
+ w.h2('Send an event from your application'),
922
+ w.p('Once a server runs, your application sends events over HTTP. Every endpoint subscribed to the event type receives its own signed copy.'),
923
+ w.code('send-event.ts', `const response = await fetch('http://localhost:8787/v1/events', {
924
+ method: 'POST',
925
+ headers: {
926
+ Authorization: 'Bearer ' + process.env.%ENV%_API_KEY,
927
+ 'Content-Type': 'application/json',
928
+ },
929
+ body: JSON.stringify({
930
+ type: 'invoice.paid',
931
+ data: { invoice_id: 'in_1042', amount: 4900, currency: 'eur' },
932
+ }),
933
+ })
934
+
935
+ // 202 Accepted: the event is stored and its deliveries are scheduled
936
+ const { id, deliveries } = await response.json()`),
937
+ w.p('The [HTTP API reference](/docs/http-api) lists every resource, with example responses and errors.'),
938
+ ],
939
+ },
940
+ {
941
+ _key: 'home-faq',
942
+ _type: 'faq',
943
+ _version: BLOCK_VERSION,
944
+ title: 'Common questions',
945
+ items: [
946
+ {
947
+ _key: 'home-faq-databases',
948
+ question: `Which databases does ${name} support?`,
949
+ answer: [
950
+ w.p('SQLite for local development with `%cli% dev`, and PostgreSQL 14 or later for `%cli% start` in production.'),
951
+ ],
952
+ },
953
+ {
954
+ _key: 'home-faq-licence',
955
+ question: `Is ${name} open source?`,
956
+ answer: [
957
+ w.p('Yes. The server, the command line and the signature libraries are released under the Apache License 2.0.'),
958
+ ],
959
+ },
960
+ {
961
+ _key: 'home-faq-retention',
962
+ question: 'How long are events kept?',
963
+ answer: [
964
+ w.p('30 days by default, with every attempt and the first kilobyte of each response. Change it with `retention.events`.'),
965
+ ],
966
+ },
967
+ {
968
+ _key: 'home-faq-outage',
969
+ question: 'What happens when an endpoint is down for a day?',
970
+ answer: [
971
+ w.p('Its deliveries are retried for a little over 31 hours. After that, [retry them by hand or replay them](/docs/retries-and-replay) once the endpoint is back.'),
972
+ ],
973
+ },
974
+ ],
975
+ },
976
+ {
977
+ _key: 'home-help',
978
+ _type: 'cta',
979
+ _version: BLOCK_VERSION,
980
+ title: 'Stuck on something',
981
+ text: 'Most delivery problems come down to a signature, a clock, or an endpoint that answers with a redirect.',
982
+ actions: [
983
+ { label: 'Troubleshooting', target: { href: '/docs/troubleshooting' } },
984
+ { label: 'What’s new in 2.4', target: { href: '/docs/whats-new' } },
985
+ ],
986
+ },
987
+ ],
988
+ },
989
+ ];
990
+ }
991
+ // ---------------------------------------------------------------------------
992
+ // Pictures, navigation, settings
993
+ // ---------------------------------------------------------------------------
994
+ function documentationPalette() {
995
+ const skin = STARTING_SKINS.documentation;
996
+ if (skin === undefined) {
997
+ throw new CogentaError({
998
+ code: 'BLUEPRINT_REGISTRY_CORRUPT',
999
+ message: 'STARTING_SKINS.documentation is missing.',
1000
+ hint: 'The "documentation" entry must stay declared in starting-skins.ts for this blueprint to seed its media.',
1001
+ });
1002
+ }
1003
+ return skin.color;
1004
+ }
1005
+ /**
1006
+ * One picture: the delivery-flow diagram on "Core concepts", drawn once with
1007
+ * IBM Plex (OFL) and committed as a PNG. Documentation needs no photographs,
1008
+ * and a decorative picture on the home page would only push the search field
1009
+ * down. The diagram carries no product name, so it stays true whatever the
1010
+ * site is called. `spec` is only the fallback `seedDemoMedia` requires should
1011
+ * the file ever be missing.
1012
+ */
1013
+ export const DOCUMENTATION_MEDIA_SPECS = [
1014
+ {
1015
+ name: 'deliveryFlow',
1016
+ spec: coverArt(documentationPalette(), 5),
1017
+ photo: 'documentation/delivery-flow.png',
1018
+ alt: 'Diagram: your application sends an event to the delivery server, which stores it, signs a copy for each endpoint and sends three requests; two endpoints answer 200 and one answers 503 and is retried in two minutes.',
1019
+ },
1020
+ ];
1021
+ export const DOCUMENTATION_MENUS = {
1022
+ header: [
1023
+ { label: 'Get started', url: '/docs/quickstart' },
1024
+ { label: 'Guides', url: '/docs/verifying-signatures' },
1025
+ { label: 'Reference', url: '/docs/cli-reference' },
1026
+ { label: 'Changelog', url: '/docs/whats-new' },
1027
+ ],
1028
+ // Seeded with the content instead (`seedFooterMenu`): a footer in columns
1029
+ // needs an unlinked heading per column, which `BlueprintMenus` cannot say.
1030
+ footer: [],
1031
+ };
1032
+ export const DOCUMENTATION_FOOTER = [
1033
+ {
1034
+ heading: 'Get started',
1035
+ links: [
1036
+ { label: 'Introduction', url: '/docs/introduction' },
1037
+ { label: 'Installation', url: '/docs/installation' },
1038
+ { label: 'Quickstart', url: '/docs/quickstart' },
1039
+ ],
1040
+ },
1041
+ {
1042
+ heading: 'Guides',
1043
+ links: [
1044
+ { label: 'Verifying signatures', url: '/docs/verifying-signatures' },
1045
+ { label: 'Retries and replay', url: '/docs/retries-and-replay' },
1046
+ { label: 'Deploying to production', url: '/docs/deploying-to-production' },
1047
+ ],
1048
+ },
1049
+ {
1050
+ heading: 'Reference',
1051
+ links: [
1052
+ { label: 'CLI', url: '/docs/cli-reference' },
1053
+ { label: 'Configuration', url: '/docs/configuration-reference' },
1054
+ { label: 'HTTP API', url: '/docs/http-api' },
1055
+ ],
1056
+ },
1057
+ {
1058
+ heading: 'Help',
1059
+ links: [
1060
+ { label: 'Troubleshooting', url: '/docs/troubleshooting' },
1061
+ { label: 'What’s new', url: '/docs/whats-new' },
1062
+ ],
1063
+ },
1064
+ ];
1065
+ export const DOCUMENTATION_SITE_SETTINGS = {
1066
+ 'general.tagline': 'Signed webhook delivery for your application, with retries and replay.',
1067
+ 'general.socialLinks': [
1068
+ { label: 'GitHub', url: 'https://github.com/example' },
1069
+ { label: 'Bluesky', url: 'https://bsky.app/profile/example.com' },
1070
+ { label: 'X', url: 'https://x.com/example' },
1071
+ ],
1072
+ 'general.footerNote': 'Released under the Apache License 2.0.\nDocumentation text under CC BY 4.0.',
1073
+ // Documentation pages are not discussions.
1074
+ 'discussion.enabled': false,
1075
+ };
1076
+ /**
1077
+ * The widgets a documentation site really carries, placed by `@cogenta/theme-docs`
1078
+ * in the page rail: under "On this page", level with the end of the page.
1079
+ *
1080
+ * Nothing that repeats what the theme already draws on a doc page: no table
1081
+ * of contents (the theme builds "On this page" from the same headings), no
1082
+ * related or recent pages (the sidebar and previous/next are the reading
1083
+ * order, and a list sorted by creation date would contradict it), no search
1084
+ * field (the header holds one on every page). What is left is what a reader
1085
+ * who reached the end of a page needs: where to get help, and the upgrade
1086
+ * note of the current release, except on the release notes themselves.
1087
+ * Beside search results, a short list of the pages most readers come for,
1088
+ * useful above all when a search found nothing.
1089
+ */
1090
+ const DOC_PAGES_ONLY = {
1091
+ pages: { mode: 'only', targets: [{ kind: 'collection', collection: 'doc_page' }] },
1092
+ };
1093
+ export const DOCUMENTATION_WIDGETS = [
1094
+ {
1095
+ area: 'sidebar',
1096
+ type: 'links',
1097
+ title: 'Need help?',
1098
+ settings: {
1099
+ items: [
1100
+ { label: 'Troubleshooting', href: '/docs/troubleshooting' },
1101
+ { label: 'Configuration reference', href: '/docs/configuration-reference' },
1102
+ { label: 'Report a problem on GitHub', href: 'https://github.com/example', newTab: true },
1103
+ ],
1104
+ },
1105
+ visibility: DOC_PAGES_ONLY,
1106
+ },
1107
+ {
1108
+ area: 'sidebar',
1109
+ type: 'cta',
1110
+ settings: {
1111
+ heading: 'Upgrading from 2.3?',
1112
+ body: 'Run the migration before starting 2.4. It adds one index and takes under a minute on ten million deliveries.',
1113
+ label: 'Read the release notes',
1114
+ href: '/docs/whats-new',
1115
+ },
1116
+ visibility: {
1117
+ pages: {
1118
+ mode: 'only',
1119
+ targets: PAGES.filter((draft) => draft.slug !== 'whats-new').map((draft) => ({
1120
+ kind: 'path',
1121
+ path: `/docs/${draft.slug}`,
1122
+ })),
1123
+ },
1124
+ },
1125
+ },
1126
+ {
1127
+ area: 'sidebar',
1128
+ type: 'links',
1129
+ title: 'Popular pages',
1130
+ settings: {
1131
+ items: [
1132
+ { label: 'Quickstart', href: '/docs/quickstart' },
1133
+ { label: 'Verifying signatures', href: '/docs/verifying-signatures' },
1134
+ { label: 'Retries and replay', href: '/docs/retries-and-replay' },
1135
+ { label: 'HTTP API', href: '/docs/http-api' },
1136
+ { label: 'CLI reference', href: '/docs/cli-reference' },
1137
+ ],
1138
+ },
1139
+ visibility: { pages: { mode: 'only', targets: [{ kind: 'search' }] } },
1140
+ },
1141
+ ];
1142
+ export const DOCUMENTATION_RECOMMENDED_AGENTS = [
1143
+ {
1144
+ name: 'contentAgent',
1145
+ package: '@cogenta/agents-builtin',
1146
+ reason: 'Flags terminology that drifts from one doc page to another, where consistent wording matters most.',
1147
+ },
1148
+ {
1149
+ name: 'seoAgent',
1150
+ package: '@cogenta/agents-builtin',
1151
+ reason: 'Audits internal links between doc pages, so readers can move on without the sidebar.',
1152
+ },
1153
+ ];
1154
+ /** The documentation's own first block on every page: the index the theme draws as the navigation. */
1155
+ function sidebarBlock(key) {
1156
+ return {
1157
+ _key: key,
1158
+ _type: 'collectionList',
1159
+ _version: BLOCK_VERSION,
1160
+ collection: 'doc_page',
1161
+ sort: { field: 'createdAt', direction: 'asc' },
1162
+ limit: 100,
1163
+ layout: 'list',
1164
+ };
1165
+ }
1166
+ async function seedFooterMenu(db, locale) {
1167
+ await ensureMenuTables(db);
1168
+ const store = createMenuStore({ db });
1169
+ if ((await store.byLocation('footer', locale)) !== null)
1170
+ return;
1171
+ const menu = await store.create({ name: 'footer', locale, label: 'footer', location: 'footer' });
1172
+ for (const column of DOCUMENTATION_FOOTER) {
1173
+ const heading = await store.createItem(menu.id, {
1174
+ label: column.heading,
1175
+ kind: 'submenu-placeholder',
1176
+ });
1177
+ for (const link of column.links) {
1178
+ await store.createItem(menu.id, {
1179
+ label: link.label,
1180
+ kind: 'url',
1181
+ url: link.url ?? '/',
1182
+ parent: heading.id,
1183
+ });
1184
+ }
1185
+ }
1186
+ }
1187
+ /**
1188
+ * Inserts the documentation through the real `ContentStore` and `MenuStore`,
1189
+ * never mocked (house rule). Pages are created in reading order and carry
1190
+ * their `order`, so the navigation reads the same whatever order the database
1191
+ * returns them in. Everything is published: a theme lists only published
1192
+ * entries.
1193
+ */
1194
+ async function seedDocumentationDemoContent(ctx) {
1195
+ const { db, defaultLocale, adminId, media, siteName } = ctx;
1196
+ const docPageStore = createContentStore({ db, collection: docPage, defaultLocale });
1197
+ const pageStore = createContentStore({ db, collection: page, defaultLocale });
1198
+ for (const demo of buildDocumentationDocPages(siteName, media)) {
1199
+ const body = [
1200
+ sidebarBlock(`sidebar-${demo.slug}`),
1201
+ { _key: `content-${demo.slug}`, _type: 'prose', _version: BLOCK_VERSION, body: demo.body },
1202
+ ];
1203
+ await docPageStore.create({
1204
+ status: 'published',
1205
+ createdBy: adminId,
1206
+ values: {
1207
+ title: demo.title,
1208
+ slug: demo.slug,
1209
+ section: demo.section,
1210
+ order: demo.order,
1211
+ summary: demo.summary,
1212
+ },
1213
+ blocks: { body: body.map(toBlockZoneEntry) },
1214
+ });
1215
+ }
1216
+ for (const demo of buildDocumentationDemoPages(media, siteName)) {
1217
+ await pageStore.create({
1218
+ status: 'published',
1219
+ createdBy: adminId,
1220
+ values: { title: demo.title, slug: demo.slug },
1221
+ blocks: { blocks: demo.blocks.map(toBlockZoneEntry) },
1222
+ });
1223
+ }
1224
+ await seedFooterMenu(db, defaultLocale);
1225
+ }
1226
+ export const documentationContentPack = {
1227
+ collections: DOCUMENTATION_COLLECTIONS,
1228
+ recommendedAgents: DOCUMENTATION_RECOMMENDED_AGENTS,
1229
+ seedDemoContent: seedDocumentationDemoContent,
1230
+ defaultTheme: '@cogenta/theme-docs',
1231
+ menus: DOCUMENTATION_MENUS,
1232
+ widgets: DOCUMENTATION_WIDGETS,
1233
+ siteSettings: DOCUMENTATION_SITE_SETTINGS,
1234
+ mediaSpecs: DOCUMENTATION_MEDIA_SPECS,
1235
+ };
1236
+ //# sourceMappingURL=documentation.js.map