@cogenta/starters 0.2.2 → 0.4.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.
Files changed (371) hide show
  1. package/dist/blueprints/assets/photos/association/coat-bank.jpg +0 -0
  2. package/dist/blueprints/assets/photos/association/food-bank.jpg +0 -0
  3. package/dist/blueprints/assets/photos/association/garden-corner.jpg +0 -0
  4. package/dist/blueprints/assets/photos/association/garden-planting.jpg +0 -0
  5. package/dist/blueprints/assets/photos/association/hall.jpg +0 -0
  6. package/dist/blueprints/assets/photos/association/hero.jpg +0 -0
  7. package/dist/blueprints/assets/photos/association/homework-club.jpg +0 -0
  8. package/dist/blueprints/assets/photos/association/orientation.jpg +0 -0
  9. package/dist/blueprints/assets/photos/association/partner-fr-college.png +0 -0
  10. package/dist/blueprints/assets/photos/association/partner-fr-delcourt.png +0 -0
  11. package/dist/blueprints/assets/photos/association/partner-fr-fondation.png +0 -0
  12. package/dist/blueprints/assets/photos/association/partner-fr-habitat.png +0 -0
  13. package/dist/blueprints/assets/photos/association/partner-fr-mairie.png +0 -0
  14. package/dist/blueprints/assets/photos/association/partner-fr-pharmacie.png +0 -0
  15. package/dist/blueprints/assets/photos/association/supper.jpg +0 -0
  16. package/dist/blueprints/assets/photos/blog/desk-setup.jpg +0 -0
  17. package/dist/blueprints/assets/photos/blog/library.jpg +0 -0
  18. package/dist/blueprints/assets/photos/blog/notebook-and-coffee.jpg +0 -0
  19. package/dist/blueprints/assets/photos/blog/notebooks.jpg +0 -0
  20. package/dist/blueprints/assets/photos/blog/platform.jpg +0 -0
  21. package/dist/blueprints/assets/photos/blog/pour-over.jpg +0 -0
  22. package/dist/blueprints/assets/photos/blog/typing.jpg +0 -0
  23. package/dist/blueprints/assets/photos/documentation/delivery-flow-en.png +0 -0
  24. package/dist/blueprints/assets/photos/documentation/delivery-flow-fr.png +0 -0
  25. package/dist/blueprints/assets/photos/magazine/allotments.jpg +0 -0
  26. package/dist/blueprints/assets/photos/magazine/archive-stacks.jpg +0 -0
  27. package/dist/blueprints/assets/photos/magazine/baguettes.jpg +0 -0
  28. package/dist/blueprints/assets/photos/magazine/dining-room.jpg +0 -0
  29. package/dist/blueprints/assets/photos/magazine/empty-tram-stop.jpg +0 -0
  30. package/dist/blueprints/assets/photos/magazine/gallery.jpg +0 -0
  31. package/dist/blueprints/assets/photos/magazine/office-block-dusk.jpg +0 -0
  32. package/dist/blueprints/assets/photos/magazine/reading-room.jpg +0 -0
  33. package/dist/blueprints/assets/photos/magazine/reception-hall.jpg +0 -0
  34. package/dist/blueprints/assets/photos/magazine/theatre.jpg +0 -0
  35. package/dist/blueprints/assets/photos/magazine/town-hall.jpg +0 -0
  36. package/dist/blueprints/assets/photos/magazine/tram-stop.jpg +0 -0
  37. package/dist/blueprints/assets/photos/portfolio/coast-boats.jpg +0 -0
  38. package/dist/blueprints/assets/photos/portfolio/coast-cover.jpg +0 -0
  39. package/dist/blueprints/assets/photos/portfolio/coast-ridge.jpg +0 -0
  40. package/dist/blueprints/assets/photos/portfolio/coast-water.jpg +0 -0
  41. package/dist/blueprints/assets/photos/portfolio/forest-beech.jpg +0 -0
  42. package/dist/blueprints/assets/photos/portfolio/forest-birch.jpg +0 -0
  43. package/dist/blueprints/assets/photos/portfolio/forest-branches.jpg +0 -0
  44. package/dist/blueprints/assets/photos/portfolio/forest-cover.jpg +0 -0
  45. package/dist/blueprints/assets/photos/portfolio/glacier-cave.jpg +0 -0
  46. package/dist/blueprints/assets/photos/portfolio/glacier-cover.jpg +0 -0
  47. package/dist/blueprints/assets/photos/portfolio/glacier-melt.jpg +0 -0
  48. package/dist/blueprints/assets/photos/portfolio/glacier-tongue.jpg +0 -0
  49. package/dist/blueprints/assets/photos/portfolio/lagoon-cover.jpg +0 -0
  50. package/dist/blueprints/assets/photos/portfolio/lagoon-egret.jpg +0 -0
  51. package/dist/blueprints/assets/photos/portfolio/lagoon-shore.jpg +0 -0
  52. package/dist/blueprints/assets/photos/portfolio/lagoon-storm.jpg +0 -0
  53. package/dist/blueprints/assets/photos/portfolio/port-cover.jpg +0 -0
  54. package/dist/blueprints/assets/photos/portfolio/port-dusk.jpg +0 -0
  55. package/dist/blueprints/assets/photos/portfolio/port-marina.jpg +0 -0
  56. package/dist/blueprints/assets/photos/portfolio/port-quay.jpg +0 -0
  57. package/dist/blueprints/assets/photos/portfolio/towers-cover.jpg +0 -0
  58. package/dist/blueprints/assets/photos/portfolio/towers-green.jpg +0 -0
  59. package/dist/blueprints/assets/photos/portfolio/towers-single.jpg +0 -0
  60. package/dist/blueprints/assets/photos/portfolio/towers-valley.jpg +0 -0
  61. package/dist/blueprints/assets/photos/portfolio/vines-cover.jpg +0 -0
  62. package/dist/blueprints/assets/photos/portfolio/vines-grapes.jpg +0 -0
  63. package/dist/blueprints/assets/photos/portfolio/vines-shed.jpg +0 -0
  64. package/dist/blueprints/assets/photos/portfolio/vines-tree.jpg +0 -0
  65. package/dist/blueprints/assets/photos/restaurant/beetroot-carpaccio.jpg +0 -0
  66. package/dist/blueprints/assets/photos/restaurant/chocolate-tart.jpg +0 -0
  67. package/dist/blueprints/assets/photos/restaurant/cod-vin-jaune.jpg +0 -0
  68. package/dist/blueprints/assets/photos/restaurant/duck-confit.jpg +0 -0
  69. package/dist/blueprints/assets/photos/restaurant/grilled-octopus.jpg +0 -0
  70. package/dist/blueprints/assets/photos/restaurant/hero.jpg +0 -0
  71. package/dist/blueprints/assets/photos/restaurant/market-stall.jpg +0 -0
  72. package/dist/blueprints/assets/photos/restaurant/mushroom-risotto.jpg +0 -0
  73. package/dist/blueprints/assets/photos/restaurant/pike-quenelle.jpg +0 -0
  74. package/dist/blueprints/assets/photos/restaurant/poached-pear.jpg +0 -0
  75. package/dist/blueprints/assets/photos/restaurant/praline-cake.jpg +0 -0
  76. package/dist/blueprints/assets/photos/restaurant/pumpkin-soup.jpg +0 -0
  77. package/dist/blueprints/assets/photos/restaurant/street-burdeau.jpg +0 -0
  78. package/dist/blueprints/assets/photos/saas/api-webhooks-en.png +0 -0
  79. package/dist/blueprints/assets/photos/saas/api-webhooks-fr.png +0 -0
  80. package/dist/blueprints/assets/photos/saas/approvals-queue-en.png +0 -0
  81. package/dist/blueprints/assets/photos/saas/approvals-queue-fr.png +0 -0
  82. package/dist/blueprints/assets/photos/saas/audit-log-en.png +0 -0
  83. package/dist/blueprints/assets/photos/saas/audit-log-fr.png +0 -0
  84. package/dist/blueprints/assets/photos/saas/erp-sync-en.png +0 -0
  85. package/dist/blueprints/assets/photos/saas/erp-sync-fr.png +0 -0
  86. package/dist/blueprints/assets/photos/saas/logo-castellan-alimentaire.png +0 -0
  87. package/dist/blueprints/assets/photos/saas/logo-cliniques-du-vivier.png +0 -0
  88. package/dist/blueprints/assets/photos/saas/logo-marenne-logistique.png +0 -0
  89. package/dist/blueprints/assets/photos/saas/logo-nordgate-habitat.png +0 -0
  90. package/dist/blueprints/assets/photos/saas/logo-tessaly-energies.png +0 -0
  91. package/dist/blueprints/assets/photos/saas/office-london.jpg +0 -0
  92. package/dist/blueprints/assets/photos/saas/office-montreal.jpg +0 -0
  93. package/dist/blueprints/assets/photos/saas/office-paris.jpg +0 -0
  94. package/dist/blueprints/assets/photos/saas/office-toronto.jpg +0 -0
  95. package/dist/blueprints/assets/photos/saas/policy-editor-en.png +0 -0
  96. package/dist/blueprints/assets/photos/saas/policy-editor-fr.png +0 -0
  97. package/dist/blueprints/assets/photos/saas/security-racks.jpg +0 -0
  98. package/dist/blueprints/assets/photos/saas/spend-reporting-en.png +0 -0
  99. package/dist/blueprints/assets/photos/saas/spend-reporting-fr.png +0 -0
  100. package/dist/blueprints/assets/photos/saas/sso-scim-en.png +0 -0
  101. package/dist/blueprints/assets/photos/saas/sso-scim-fr.png +0 -0
  102. package/dist/blueprints/assets/photos/store/aleppo-soap.jpg +0 -0
  103. package/dist/blueprints/assets/photos/store/copper-pan.jpg +0 -0
  104. package/dist/blueprints/assets/photos/store/crochet-throw.jpg +0 -0
  105. package/dist/blueprints/assets/photos/store/enamel-mug.jpg +0 -0
  106. package/dist/blueprints/assets/photos/store/hero.jpg +0 -0
  107. package/dist/blueprints/assets/photos/store/knit-sweater.jpg +0 -0
  108. package/dist/blueprints/assets/photos/store/leather-boots.jpg +0 -0
  109. package/dist/blueprints/assets/photos/store/leather-duffel.jpg +0 -0
  110. package/dist/blueprints/assets/photos/store/leather-wallet.jpg +0 -0
  111. package/dist/blueprints/assets/photos/store/moka-pot.jpg +0 -0
  112. package/dist/blueprints/assets/photos/store/skillet.jpg +0 -0
  113. package/dist/blueprints/assets/photos/store/stoneware-bottles.jpg +0 -0
  114. package/dist/blueprints/assets/photos/store/story.jpg +0 -0
  115. package/dist/blueprints/assets/photos/store/turned-bowl.jpg +0 -0
  116. package/dist/blueprints/association-copy-en.d.ts +3 -0
  117. package/dist/blueprints/association-copy-en.d.ts.map +1 -0
  118. package/dist/blueprints/association-copy-en.js +834 -0
  119. package/dist/blueprints/association-copy-en.js.map +1 -0
  120. package/dist/blueprints/association-copy-fr.d.ts +3 -0
  121. package/dist/blueprints/association-copy-fr.d.ts.map +1 -0
  122. package/dist/blueprints/association-copy-fr.js +841 -0
  123. package/dist/blueprints/association-copy-fr.js.map +1 -0
  124. package/dist/blueprints/association-copy.d.ts +301 -0
  125. package/dist/blueprints/association-copy.d.ts.map +1 -0
  126. package/dist/blueprints/association-copy.js +17 -0
  127. package/dist/blueprints/association-copy.js.map +1 -0
  128. package/dist/blueprints/association-credits.d.ts +16 -0
  129. package/dist/blueprints/association-credits.d.ts.map +1 -0
  130. package/dist/blueprints/association-credits.js +80 -0
  131. package/dist/blueprints/association-credits.js.map +1 -0
  132. package/dist/blueprints/association-typography.d.ts +23 -0
  133. package/dist/blueprints/association-typography.d.ts.map +1 -0
  134. package/dist/blueprints/association-typography.js +55 -0
  135. package/dist/blueprints/association-typography.js.map +1 -0
  136. package/dist/blueprints/association.d.ts +86 -287
  137. package/dist/blueprints/association.d.ts.map +1 -1
  138. package/dist/blueprints/association.js +510 -1082
  139. package/dist/blueprints/association.js.map +1 -1
  140. package/dist/blueprints/blog-copy-en.d.ts +4 -0
  141. package/dist/blueprints/blog-copy-en.d.ts.map +1 -0
  142. package/dist/blueprints/blog-copy-en.js +701 -0
  143. package/dist/blueprints/blog-copy-en.js.map +1 -0
  144. package/dist/blueprints/blog-copy-fr.d.ts +12 -0
  145. package/dist/blueprints/blog-copy-fr.d.ts.map +1 -0
  146. package/dist/blueprints/blog-copy-fr.js +722 -0
  147. package/dist/blueprints/blog-copy-fr.js.map +1 -0
  148. package/dist/blueprints/blog-copy.d.ts +157 -0
  149. package/dist/blueprints/blog-copy.d.ts.map +1 -0
  150. package/dist/blueprints/blog-copy.js +2 -0
  151. package/dist/blueprints/blog-copy.js.map +1 -0
  152. package/dist/blueprints/blog-credits.d.ts +28 -0
  153. package/dist/blueprints/blog-credits.d.ts.map +1 -0
  154. package/dist/blueprints/blog-credits.js +73 -0
  155. package/dist/blueprints/blog-credits.js.map +1 -0
  156. package/dist/blueprints/blog-rich.d.ts +19 -0
  157. package/dist/blueprints/blog-rich.d.ts.map +1 -0
  158. package/dist/blueprints/blog-rich.js +51 -0
  159. package/dist/blueprints/blog-rich.js.map +1 -0
  160. package/dist/blueprints/blog.d.ts +51 -176
  161. package/dist/blueprints/blog.d.ts.map +1 -1
  162. package/dist/blueprints/blog.js +358 -947
  163. package/dist/blueprints/blog.js.map +1 -1
  164. package/dist/blueprints/content-pack.d.ts.map +1 -1
  165. package/dist/blueprints/content-pack.js +11 -16
  166. package/dist/blueprints/content-pack.js.map +1 -1
  167. package/dist/blueprints/content-packs.d.ts +2 -4
  168. package/dist/blueprints/content-packs.d.ts.map +1 -1
  169. package/dist/blueprints/content-packs.js +27 -15
  170. package/dist/blueprints/content-packs.js.map +1 -1
  171. package/dist/blueprints/documentation-copy-en.d.ts +3 -0
  172. package/dist/blueprints/documentation-copy-en.d.ts.map +1 -0
  173. package/dist/blueprints/documentation-copy-en.js +883 -0
  174. package/dist/blueprints/documentation-copy-en.js.map +1 -0
  175. package/dist/blueprints/documentation-copy-fr.d.ts +3 -0
  176. package/dist/blueprints/documentation-copy-fr.d.ts.map +1 -0
  177. package/dist/blueprints/documentation-copy-fr.js +939 -0
  178. package/dist/blueprints/documentation-copy-fr.js.map +1 -0
  179. package/dist/blueprints/documentation-copy.d.ts +52 -0
  180. package/dist/blueprints/documentation-copy.d.ts.map +1 -0
  181. package/dist/blueprints/documentation-copy.js +2 -0
  182. package/dist/blueprints/documentation-copy.js.map +1 -0
  183. package/dist/blueprints/documentation-writer.d.ts +67 -0
  184. package/dist/blueprints/documentation-writer.d.ts.map +1 -0
  185. package/dist/blueprints/documentation-writer.js +190 -0
  186. package/dist/blueprints/documentation-writer.js.map +1 -0
  187. package/dist/blueprints/documentation.d.ts +26 -29
  188. package/dist/blueprints/documentation.d.ts.map +1 -1
  189. package/dist/blueprints/documentation.js +106 -1064
  190. package/dist/blueprints/documentation.js.map +1 -1
  191. package/dist/blueprints/magazine-copy-en.d.ts +13 -0
  192. package/dist/blueprints/magazine-copy-en.d.ts.map +1 -0
  193. package/dist/blueprints/magazine-copy-en.js +553 -0
  194. package/dist/blueprints/magazine-copy-en.js.map +1 -0
  195. package/dist/blueprints/magazine-copy-fr.d.ts +14 -0
  196. package/dist/blueprints/magazine-copy-fr.d.ts.map +1 -0
  197. package/dist/blueprints/magazine-copy-fr.js +568 -0
  198. package/dist/blueprints/magazine-copy-fr.js.map +1 -0
  199. package/dist/blueprints/magazine-copy.d.ts +166 -0
  200. package/dist/blueprints/magazine-copy.d.ts.map +1 -0
  201. package/dist/blueprints/magazine-copy.js +58 -0
  202. package/dist/blueprints/magazine-copy.js.map +1 -0
  203. package/dist/blueprints/magazine-credits.d.ts +19 -0
  204. package/dist/blueprints/magazine-credits.d.ts.map +1 -0
  205. package/dist/blueprints/magazine-credits.js +109 -0
  206. package/dist/blueprints/magazine-credits.js.map +1 -0
  207. package/dist/blueprints/magazine.d.ts +30 -78
  208. package/dist/blueprints/magazine.d.ts.map +1 -1
  209. package/dist/blueprints/magazine.js +269 -690
  210. package/dist/blueprints/magazine.js.map +1 -1
  211. package/dist/blueprints/portfolio-copy-en.d.ts +4 -0
  212. package/dist/blueprints/portfolio-copy-en.d.ts.map +1 -0
  213. package/dist/blueprints/portfolio-copy-en.js +745 -0
  214. package/dist/blueprints/portfolio-copy-en.js.map +1 -0
  215. package/dist/blueprints/portfolio-copy-fr.d.ts +9 -0
  216. package/dist/blueprints/portfolio-copy-fr.d.ts.map +1 -0
  217. package/dist/blueprints/portfolio-copy-fr.js +737 -0
  218. package/dist/blueprints/portfolio-copy-fr.js.map +1 -0
  219. package/dist/blueprints/portfolio-copy.d.ts +204 -0
  220. package/dist/blueprints/portfolio-copy.d.ts.map +1 -0
  221. package/dist/blueprints/portfolio-copy.js +15 -0
  222. package/dist/blueprints/portfolio-copy.js.map +1 -0
  223. package/dist/blueprints/portfolio-credits.d.ts +18 -0
  224. package/dist/blueprints/portfolio-credits.d.ts.map +1 -0
  225. package/dist/blueprints/portfolio-credits.js +239 -0
  226. package/dist/blueprints/portfolio-credits.js.map +1 -0
  227. package/dist/blueprints/portfolio-typography.d.ts +19 -0
  228. package/dist/blueprints/portfolio-typography.d.ts.map +1 -0
  229. package/dist/blueprints/portfolio-typography.js +41 -0
  230. package/dist/blueprints/portfolio-typography.js.map +1 -0
  231. package/dist/blueprints/portfolio.d.ts +68 -157
  232. package/dist/blueprints/portfolio.d.ts.map +1 -1
  233. package/dist/blueprints/portfolio.js +475 -727
  234. package/dist/blueprints/portfolio.js.map +1 -1
  235. package/dist/blueprints/restaurant-copy-en.d.ts +3 -0
  236. package/dist/blueprints/restaurant-copy-en.d.ts.map +1 -0
  237. package/dist/blueprints/restaurant-copy-en.js +641 -0
  238. package/dist/blueprints/restaurant-copy-en.js.map +1 -0
  239. package/dist/blueprints/restaurant-copy-fr.d.ts +3 -0
  240. package/dist/blueprints/restaurant-copy-fr.d.ts.map +1 -0
  241. package/dist/blueprints/restaurant-copy-fr.js +665 -0
  242. package/dist/blueprints/restaurant-copy-fr.js.map +1 -0
  243. package/dist/blueprints/restaurant-copy.d.ts +234 -0
  244. package/dist/blueprints/restaurant-copy.d.ts.map +1 -0
  245. package/dist/blueprints/restaurant-copy.js +15 -0
  246. package/dist/blueprints/restaurant-copy.js.map +1 -0
  247. package/dist/blueprints/restaurant-credits.d.ts +18 -0
  248. package/dist/blueprints/restaurant-credits.d.ts.map +1 -0
  249. package/dist/blueprints/restaurant-credits.js +121 -0
  250. package/dist/blueprints/restaurant-credits.js.map +1 -0
  251. package/dist/blueprints/restaurant.d.ts +55 -128
  252. package/dist/blueprints/restaurant.d.ts.map +1 -1
  253. package/dist/blueprints/restaurant.js +425 -770
  254. package/dist/blueprints/restaurant.js.map +1 -1
  255. package/dist/blueprints/saas-copy-en.d.ts +3 -0
  256. package/dist/blueprints/saas-copy-en.d.ts.map +1 -0
  257. package/dist/blueprints/saas-copy-en.js +901 -0
  258. package/dist/blueprints/saas-copy-en.js.map +1 -0
  259. package/dist/blueprints/saas-copy-fr.d.ts +3 -0
  260. package/dist/blueprints/saas-copy-fr.d.ts.map +1 -0
  261. package/dist/blueprints/saas-copy-fr.js +912 -0
  262. package/dist/blueprints/saas-copy-fr.js.map +1 -0
  263. package/dist/blueprints/saas-copy.d.ts +266 -0
  264. package/dist/blueprints/saas-copy.d.ts.map +1 -0
  265. package/dist/blueprints/saas-copy.js +18 -0
  266. package/dist/blueprints/saas-copy.js.map +1 -0
  267. package/dist/blueprints/saas-credits.d.ts +22 -0
  268. package/dist/blueprints/saas-credits.d.ts.map +1 -0
  269. package/dist/blueprints/saas-credits.js +44 -0
  270. package/dist/blueprints/saas-credits.js.map +1 -0
  271. package/dist/blueprints/saas.d.ts +59 -203
  272. package/dist/blueprints/saas.d.ts.map +1 -1
  273. package/dist/blueprints/saas.js +505 -1108
  274. package/dist/blueprints/saas.js.map +1 -1
  275. package/dist/blueprints/store-copy-en.d.ts +4 -0
  276. package/dist/blueprints/store-copy-en.d.ts.map +1 -0
  277. package/dist/blueprints/store-copy-en.js +521 -0
  278. package/dist/blueprints/store-copy-en.js.map +1 -0
  279. package/dist/blueprints/store-copy-fr.d.ts +4 -0
  280. package/dist/blueprints/store-copy-fr.d.ts.map +1 -0
  281. package/dist/blueprints/store-copy-fr.js +521 -0
  282. package/dist/blueprints/store-copy-fr.js.map +1 -0
  283. package/dist/blueprints/store-copy.d.ts +219 -0
  284. package/dist/blueprints/store-copy.d.ts.map +1 -0
  285. package/dist/blueprints/store-copy.js +16 -0
  286. package/dist/blueprints/store-copy.js.map +1 -0
  287. package/dist/blueprints/store-credits.d.ts +3 -0
  288. package/dist/blueprints/store-credits.d.ts.map +1 -0
  289. package/dist/blueprints/store-credits.js +117 -0
  290. package/dist/blueprints/store-credits.js.map +1 -0
  291. package/dist/blueprints/store.d.ts +43 -231
  292. package/dist/blueprints/store.d.ts.map +1 -1
  293. package/dist/blueprints/store.js +466 -789
  294. package/dist/blueprints/store.js.map +1 -1
  295. package/dist/blueprints/vitrine-copy-en.d.ts.map +1 -1
  296. package/dist/blueprints/vitrine-copy-en.js +1 -0
  297. package/dist/blueprints/vitrine-copy-en.js.map +1 -1
  298. package/dist/blueprints/vitrine-copy-fr.d.ts.map +1 -1
  299. package/dist/blueprints/vitrine-copy-fr.js +1 -0
  300. package/dist/blueprints/vitrine-copy-fr.js.map +1 -1
  301. package/dist/blueprints/vitrine-copy.d.ts +1 -0
  302. package/dist/blueprints/vitrine-copy.d.ts.map +1 -1
  303. package/dist/blueprints/vitrine.d.ts +1 -0
  304. package/dist/blueprints/vitrine.d.ts.map +1 -1
  305. package/dist/blueprints/vitrine.js +24 -3
  306. package/dist/blueprints/vitrine.js.map +1 -1
  307. package/package.json +10 -10
  308. package/dist/blueprints/assets/photos/association/fundraising-dinner.jpg +0 -0
  309. package/dist/blueprints/assets/photos/association/volunteer-orientation.jpg +0 -0
  310. package/dist/blueprints/assets/photos/association/volunteer-portrait.jpg +0 -0
  311. package/dist/blueprints/assets/photos/association/winter-coat-collection.jpg +0 -0
  312. package/dist/blueprints/assets/photos/documentation/delivery-flow.png +0 -0
  313. package/dist/blueprints/assets/photos/magazine/archive-room.jpg +0 -0
  314. package/dist/blueprints/assets/photos/magazine/banquet-hall.jpg +0 -0
  315. package/dist/blueprints/assets/photos/magazine/city-hall.jpg +0 -0
  316. package/dist/blueprints/assets/photos/magazine/downtown-towers.jpg +0 -0
  317. package/dist/blueprints/assets/photos/magazine/gallery-opening.jpg +0 -0
  318. package/dist/blueprints/assets/photos/magazine/garden-volunteers.jpg +0 -0
  319. package/dist/blueprints/assets/photos/magazine/station-platform.jpg +0 -0
  320. package/dist/blueprints/assets/photos/magazine/theater-stage.jpg +0 -0
  321. package/dist/blueprints/assets/photos/portfolio/fenmore-branch.jpg +0 -0
  322. package/dist/blueprints/assets/photos/portfolio/fenmore-cover.jpg +0 -0
  323. package/dist/blueprints/assets/photos/portfolio/fenmore-stationery.jpg +0 -0
  324. package/dist/blueprints/assets/photos/portfolio/gannet-cover.jpg +0 -0
  325. package/dist/blueprints/assets/photos/portfolio/gannet-sardines.jpg +0 -0
  326. package/dist/blueprints/assets/photos/portfolio/gannet-shelf.jpg +0 -0
  327. package/dist/blueprints/assets/photos/portfolio/hollis-crane-cover.jpg +0 -0
  328. package/dist/blueprints/assets/photos/portfolio/hollis-crane-grid.jpg +0 -0
  329. package/dist/blueprints/assets/photos/portfolio/hollis-crane-salt-year.jpg +0 -0
  330. package/dist/blueprints/assets/photos/portfolio/hollis-crane-spines.jpg +0 -0
  331. package/dist/blueprints/assets/photos/portfolio/rookery-hall-brochure.jpg +0 -0
  332. package/dist/blueprints/assets/photos/portfolio/rookery-hall-cover.jpg +0 -0
  333. package/dist/blueprints/assets/photos/portfolio/rookery-hall-poster-carnival.jpg +0 -0
  334. package/dist/blueprints/assets/photos/portfolio/rookery-hall-poster-sibelius.jpg +0 -0
  335. package/dist/blueprints/assets/photos/portfolio/rookery-hall-series.jpg +0 -0
  336. package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-cover.jpg +0 -0
  337. package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-gate.jpg +0 -0
  338. package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-pictograms.jpg +0 -0
  339. package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-totem.jpg +0 -0
  340. package/dist/blueprints/assets/photos/portfolio/tidewater-cover.jpg +0 -0
  341. package/dist/blueprints/assets/photos/portfolio/tidewater-data.jpg +0 -0
  342. package/dist/blueprints/assets/photos/portfolio/tidewater-report-cover.jpg +0 -0
  343. package/dist/blueprints/assets/photos/portfolio/warp-and-weft-cover.jpg +0 -0
  344. package/dist/blueprints/assets/photos/portfolio/warp-and-weft-room.jpg +0 -0
  345. package/dist/blueprints/assets/photos/portfolio/westmoor-rail-components.jpg +0 -0
  346. package/dist/blueprints/assets/photos/portfolio/westmoor-rail-cover.jpg +0 -0
  347. package/dist/blueprints/assets/photos/portfolio/westmoor-rail-departures.jpg +0 -0
  348. package/dist/blueprints/assets/photos/restaurant/beetroot-goats-curd.jpg +0 -0
  349. package/dist/blueprints/assets/photos/restaurant/ceps-risotto.jpg +0 -0
  350. package/dist/blueprints/assets/photos/restaurant/duck-leg-red-cabbage.jpg +0 -0
  351. package/dist/blueprints/assets/photos/restaurant/hake-green-peppercorns.jpg +0 -0
  352. package/dist/blueprints/assets/photos/restaurant/vegetable-broth.jpg +0 -0
  353. package/dist/blueprints/assets/photos/saas/adrian-tan.jpg +0 -0
  354. package/dist/blueprints/assets/photos/saas/api-webhooks.png +0 -0
  355. package/dist/blueprints/assets/photos/saas/approvals-queue.png +0 -0
  356. package/dist/blueprints/assets/photos/saas/audit-log.png +0 -0
  357. package/dist/blueprints/assets/photos/saas/erp-sync.png +0 -0
  358. package/dist/blueprints/assets/photos/saas/policy-editor.png +0 -0
  359. package/dist/blueprints/assets/photos/saas/spend-reporting.png +0 -0
  360. package/dist/blueprints/assets/photos/saas/sso-scim.png +0 -0
  361. package/dist/blueprints/assets/photos/store/brushed-cotton-overshirt.jpg +0 -0
  362. package/dist/blueprints/assets/photos/store/cable-knit-beanie.jpg +0 -0
  363. package/dist/blueprints/assets/photos/store/canvas-shoulder-bag.jpg +0 -0
  364. package/dist/blueprints/assets/photos/store/canvas-tote.jpg +0 -0
  365. package/dist/blueprints/assets/photos/store/cast-iron-skillet.jpg +0 -0
  366. package/dist/blueprints/assets/photos/store/field-jacket.jpg +0 -0
  367. package/dist/blueprints/assets/photos/store/heavyweight-t-shirt.jpg +0 -0
  368. package/dist/blueprints/assets/photos/store/leather-card-holder.jpg +0 -0
  369. package/dist/blueprints/assets/photos/store/linen-table-runner.jpg +0 -0
  370. package/dist/blueprints/assets/photos/store/porcelain-pour-over-set.jpg +0 -0
  371. package/dist/blueprints/assets/photos/store/striped-wool-blanket.jpg +0 -0
@@ -2,10 +2,15 @@ import { CogentaError } from '@cogenta/core';
2
2
  import { createContentStore, createMenuStore, defineCollection, ensureMenuTables, f, validateCollectionSet, } from '@cogenta/schema';
3
3
  import { coverArt } from '../demo-art/compositions.js';
4
4
  import { definePageCollection, SEO_FIELDS, toBlockZoneEntry, } from './content-pack.js';
5
+ import { DOCUMENTATION_COPY_EN } from './documentation-copy-en.js';
6
+ import { DOCUMENTATION_COPY_FR } from './documentation-copy-fr.js';
7
+ import { fillNames, productNames, writer } from './documentation-writer.js';
5
8
  import { STARTING_SKINS } from './starting-skins.js';
9
+ export { DEFAULT_PRODUCT_NAME, productName, productNames } from './documentation-writer.js';
6
10
  /**
7
11
  * The `documentation` blueprint: the documentation site of a developer tool,
8
- * rewritten to studio level by L27.
12
+ * rewritten to studio level by L27 and written in French and in English by
13
+ * L36 (`documentation-copy-fr.ts`, `documentation-copy-en.ts`).
9
14
  *
10
15
  * The product is a self-hosted webhook delivery server: an application sends
11
16
  * it events over HTTP, and it signs, sends, retries and records a request for
@@ -36,27 +41,13 @@ import { STARTING_SKINS } from './starting-skins.js';
36
41
  * `@cogenta/theme-docs`'s `rich-text.ts`. Every other theme renders the same
37
42
  * data as plain paragraphs and lists.
38
43
  */
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
- };
44
+ /** The copy for a site's default locale: French for any `fr*` locale, English otherwise. */
45
+ export function documentationCopyFor(locale) {
46
+ return locale.toLowerCase().startsWith('fr') ? DOCUMENTATION_COPY_FR : DOCUMENTATION_COPY_EN;
57
47
  }
58
48
  export const docPage = defineCollection({
59
49
  name: 'doc_page',
50
+ versioning: { drafts: true, history: true },
60
51
  labels: { singular: 'Doc page', plural: 'Doc pages' },
61
52
  routing: { pattern: '/docs/:slug' },
62
53
  fields: {
@@ -64,11 +55,7 @@ export const docPage = defineCollection({
64
55
  slug: f.slug({ from: 'title', unique: true }),
65
56
  section: f.text({ required: true, max: 80 }),
66
57
  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
- }),
58
+ summary: f.text({ max: 300, multiline: true }),
72
59
  body: f.blocks({ required: true }),
73
60
  ...SEO_FIELDS,
74
61
  },
@@ -78,773 +65,28 @@ export const docPage = defineCollection({
78
65
  create: ['editor', 'admin'],
79
66
  update: ['editor', 'admin'],
80
67
  delete: ['admin'],
68
+ publish: ['admin'],
81
69
  },
82
70
  });
83
71
  export const page = definePageCollection('/:slug');
84
72
  export const DOCUMENTATION_COLLECTIONS = [docPage, page];
85
73
  validateCollectionSet(DOCUMENTATION_COLLECTIONS);
86
74
  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
75
  function flat(parts) {
193
76
  return parts.flatMap((part) => (Array.isArray(part) ? part : [part]));
194
77
  }
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
78
  /** The documentation, in reading order, named after the site. `media.deliveryFlow` illustrates "Core concepts" when it was seeded. */
833
- export function buildDocumentationDocPages(siteName, media = {}) {
79
+ export function buildDocumentationDocPages(siteName, media = {}, locale = 'en') {
80
+ const copy = documentationCopyFor(locale);
834
81
  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);
82
+ return copy.pages.map((draft, index) => {
83
+ const w = writer(draft.slug, names, { locale: copy.locale });
842
84
  return {
843
- title: draft.title,
85
+ title: copy.typography(draft.title),
844
86
  slug: draft.slug,
845
- section: draft.section,
87
+ section: copy.sections[draft.section],
846
88
  order: (index + 1) * 10,
847
- summary: fillText(draft.summary),
89
+ summary: copy.typography(fillNames(draft.summary, names)),
848
90
  body: flat(draft.body(w, media)),
849
91
  };
850
92
  });
@@ -856,135 +98,15 @@ export const DOCUMENTATION_DEMO_DOC_PAGES = buildDocumentationDocPages();
856
98
  * first lines of code a reader will write, common questions answered in the
857
99
  * open, and where to go when something does not work.
858
100
  */
859
- export function buildDocumentationDemoPages(_media, siteName) {
101
+ export function buildDocumentationDemoPages(_media, siteName, locale = 'en') {
102
+ const copy = documentationCopyFor(locale);
860
103
  const names = productNames(siteName);
861
- const w = writer('home', names);
862
- const { name } = names;
104
+ const w = writer('home', names, { locale: copy.locale });
863
105
  return [
864
106
  {
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
- ],
107
+ title: copy.typography(copy.home.title(names.name)),
108
+ slug: copy.home.slug,
109
+ blocks: copy.home.blocks(names, w),
988
110
  },
989
111
  ];
990
112
  }
@@ -1003,76 +125,32 @@ function documentationPalette() {
1003
125
  return skin.color;
1004
126
  }
1005
127
  /**
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
128
+ * One picture: the delivery-flow diagram on "Core concepts", drawn once per
129
+ * language with IBM Plex (OFL) and committed as a PNG
130
+ * (`scripts/documentation-assets/`). Documentation needs no photographs, and
131
+ * a decorative picture on the home page would only push the search field
1009
132
  * down. The diagram carries no product name, so it stays true whatever the
1010
133
  * site is called. `spec` is only the fallback `seedDemoMedia` requires should
1011
134
  * the file ever be missing.
1012
135
  */
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
- };
136
+ export function documentationMediaSpecs(copy) {
137
+ return [
138
+ {
139
+ name: 'deliveryFlow',
140
+ spec: coverArt(documentationPalette(), 5),
141
+ photo: copy.diagram.file,
142
+ alt: copy.typography(copy.diagram.alt),
143
+ },
144
+ ];
145
+ }
146
+ function documentationMenus(copy) {
147
+ return {
148
+ header: copy.menus.header,
149
+ // Seeded with the content instead (`seedFooterMenu`): a footer in columns
150
+ // needs an unlinked heading per column, which `BlueprintMenus` cannot say.
151
+ footer: [],
152
+ };
153
+ }
1076
154
  /**
1077
155
  * The widgets a documentation site really carries, placed by `@cogenta/theme-docs`
1078
156
  * in the page rail: under "On this page", level with the end of the page.
@@ -1087,58 +165,14 @@ export const DOCUMENTATION_SITE_SETTINGS = {
1087
165
  * Beside search results, a short list of the pages most readers come for,
1088
166
  * useful above all when a search found nothing.
1089
167
  */
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
- ];
168
+ function documentationWidgets(copy) {
169
+ return copy.widgets(copy.pages.map((draft) => draft.slug));
170
+ }
171
+ export const DOCUMENTATION_MENUS = documentationMenus(DOCUMENTATION_COPY_EN);
172
+ export const DOCUMENTATION_FOOTER = DOCUMENTATION_COPY_EN.footer;
173
+ export const DOCUMENTATION_SITE_SETTINGS = DOCUMENTATION_COPY_EN.settings;
174
+ export const DOCUMENTATION_WIDGETS = documentationWidgets(DOCUMENTATION_COPY_EN);
175
+ export const DOCUMENTATION_MEDIA_SPECS = documentationMediaSpecs(DOCUMENTATION_COPY_EN);
1142
176
  export const DOCUMENTATION_RECOMMENDED_AGENTS = [
1143
177
  {
1144
178
  name: 'contentAgent',
@@ -1163,13 +197,13 @@ function sidebarBlock(key) {
1163
197
  layout: 'list',
1164
198
  };
1165
199
  }
1166
- async function seedFooterMenu(db, locale) {
200
+ async function seedFooterMenu(db, locale, columns) {
1167
201
  await ensureMenuTables(db);
1168
202
  const store = createMenuStore({ db });
1169
203
  if ((await store.byLocation('footer', locale)) !== null)
1170
204
  return;
1171
205
  const menu = await store.create({ name: 'footer', locale, label: 'footer', location: 'footer' });
1172
- for (const column of DOCUMENTATION_FOOTER) {
206
+ for (const column of columns) {
1173
207
  const heading = await store.createItem(menu.id, {
1174
208
  label: column.heading,
1175
209
  kind: 'submenu-placeholder',
@@ -1191,46 +225,54 @@ async function seedFooterMenu(db, locale) {
1191
225
  * returns them in. Everything is published: a theme lists only published
1192
226
  * entries.
1193
227
  */
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);
228
+ function seedDocumentationDemoContent(copy) {
229
+ return async (ctx) => {
230
+ const { db, defaultLocale, adminId, media, siteName } = ctx;
231
+ const docPageStore = createContentStore({ db, collection: docPage, defaultLocale });
232
+ const pageStore = createContentStore({ db, collection: page, defaultLocale });
233
+ for (const demo of buildDocumentationDocPages(siteName, media, copy.locale)) {
234
+ const body = [
235
+ sidebarBlock(`sidebar-${demo.slug}`),
236
+ { _key: `content-${demo.slug}`, _type: 'prose', _version: BLOCK_VERSION, body: demo.body },
237
+ ];
238
+ await docPageStore.create({
239
+ status: 'published',
240
+ createdBy: adminId,
241
+ values: {
242
+ title: demo.title,
243
+ slug: demo.slug,
244
+ section: demo.section,
245
+ order: demo.order,
246
+ summary: demo.summary,
247
+ },
248
+ blocks: { body: body.map(toBlockZoneEntry) },
249
+ });
250
+ }
251
+ for (const demo of buildDocumentationDemoPages(media, siteName, copy.locale)) {
252
+ await pageStore.create({
253
+ status: 'published',
254
+ createdBy: adminId,
255
+ values: { title: demo.title, slug: demo.slug },
256
+ blocks: { blocks: demo.blocks.map(toBlockZoneEntry) },
257
+ });
258
+ }
259
+ await seedFooterMenu(db, defaultLocale, copy.footer);
260
+ };
261
+ }
262
+ /** The whole blueprint, in one language. */
263
+ export function createDocumentationContentPack(locale) {
264
+ const copy = documentationCopyFor(locale);
265
+ return {
266
+ collections: DOCUMENTATION_COLLECTIONS,
267
+ recommendedAgents: DOCUMENTATION_RECOMMENDED_AGENTS,
268
+ seedDemoContent: seedDocumentationDemoContent(copy),
269
+ defaultTheme: '@cogenta/theme-docs',
270
+ menus: documentationMenus(copy),
271
+ widgets: documentationWidgets(copy),
272
+ siteSettings: copy.settings,
273
+ mediaSpecs: documentationMediaSpecs(copy),
274
+ };
1225
275
  }
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
- };
276
+ /** The English pack, which `BLUEPRINT_CONTENT_PACKS` lists; `contentPackFor` picks by locale. */
277
+ export const documentationContentPack = createDocumentationContentPack('en');
1236
278
  //# sourceMappingURL=documentation.js.map