@michaelthielemann/kestrel 4.1.3 → 5.0.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 (340) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +274 -0
  4. package/dist/cast.d.ts +3 -0
  5. package/dist/cast.js +6 -0
  6. package/dist/catalogue.d.ts +11 -0
  7. package/dist/catalogue.js +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +55 -0
  10. package/dist/context.d.ts +62 -0
  11. package/dist/context.js +61 -0
  12. package/dist/dataflow.d.ts +3 -0
  13. package/dist/dataflow.js +65 -0
  14. package/dist/defineConfig.d.ts +157 -0
  15. package/dist/defineConfig.js +41 -0
  16. package/dist/defineContract.d.ts +12 -0
  17. package/dist/defineContract.js +16 -0
  18. package/dist/defineModule.d.ts +75 -0
  19. package/dist/defineModule.js +9 -0
  20. package/dist/definePipeline.d.ts +7 -0
  21. package/dist/definePipeline.js +11 -0
  22. package/dist/errors.d.ts +26 -0
  23. package/dist/errors.js +48 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +14 -0
  26. package/dist/load.d.ts +7 -0
  27. package/dist/load.js +79 -0
  28. package/dist/logger.d.ts +18 -0
  29. package/dist/logger.js +36 -0
  30. package/dist/registry.d.ts +14 -0
  31. package/dist/registry.js +87 -0
  32. package/dist/result.d.ts +19 -0
  33. package/dist/result.js +19 -0
  34. package/dist/runner.d.ts +29 -0
  35. package/dist/runner.js +133 -0
  36. package/dist/schema.d.ts +7 -0
  37. package/dist/schema.js +142 -0
  38. package/dist/sort.d.ts +2 -0
  39. package/dist/sort.js +50 -0
  40. package/dist/testing/runPipeline.d.ts +9 -0
  41. package/dist/testing/runPipeline.js +22 -0
  42. package/dist/triggers/cron.d.ts +10 -0
  43. package/dist/triggers/cron.js +44 -0
  44. package/dist/triggers/cronMatch.d.ts +10 -0
  45. package/dist/triggers/cronMatch.js +68 -0
  46. package/dist/triggers/http.d.ts +58 -0
  47. package/dist/triggers/http.js +292 -0
  48. package/package.json +25 -151
  49. package/NOTICE +0 -12
  50. package/kestrel.config.ts +0 -28
  51. package/layers/access/nuxt.config.ts +0 -5
  52. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  53. package/layers/access/server/middleware/access-guard.ts +0 -32
  54. package/layers/access/server/types.d.ts +0 -7
  55. package/layers/admin/app/components/AdminAccount.vue +0 -212
  56. package/layers/admin/app/components/AdminNav.vue +0 -44
  57. package/layers/admin/app/components/BlockFields.vue +0 -67
  58. package/layers/admin/app/components/BlockPreview.vue +0 -413
  59. package/layers/admin/app/components/BlockTree.vue +0 -433
  60. package/layers/admin/app/components/BlocksBody.vue +0 -163
  61. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  62. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  63. package/layers/admin/app/components/CollectionList.vue +0 -429
  64. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  65. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  66. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  67. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  68. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  69. package/layers/admin/app/components/EditorStatus.vue +0 -211
  70. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  71. package/layers/admin/app/components/FieldsBody.vue +0 -23
  72. package/layers/admin/app/components/LocaleBar.vue +0 -107
  73. package/layers/admin/app/components/PageFields.vue +0 -129
  74. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  75. package/layers/admin/app/components/SeoFields.vue +0 -210
  76. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  77. package/layers/admin/app/composables/useAuth.ts +0 -68
  78. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  79. package/layers/admin/app/composables/useBlocks.ts +0 -7
  80. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  81. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  82. package/layers/admin/app/composables/useCollections.ts +0 -6
  83. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  84. package/layers/admin/app/composables/useEditForm.ts +0 -402
  85. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  86. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  87. package/layers/admin/app/composables/useListColumns.ts +0 -53
  88. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  89. package/layers/admin/app/composables/useListRows.ts +0 -75
  90. package/layers/admin/app/composables/useListSelection.ts +0 -28
  91. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  92. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  93. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  94. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  95. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  96. package/layers/admin/app/composables/useTheme.ts +0 -36
  97. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  98. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  99. package/layers/admin/app/layouts/admin.vue +0 -266
  100. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  101. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  102. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  103. package/layers/admin/app/pages/admin/index.vue +0 -118
  104. package/layers/admin/app/pages/admin/login.vue +0 -68
  105. package/layers/admin/app/pages/admin/media.vue +0 -28
  106. package/layers/admin/app/pages/admin/references.vue +0 -146
  107. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  108. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  109. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  110. package/layers/admin/app/utils/block-tree.ts +0 -214
  111. package/layers/admin/app/utils/collection-ops.ts +0 -69
  112. package/layers/admin/app/utils/dead-refs.ts +0 -30
  113. package/layers/admin/app/utils/edit-form.ts +0 -267
  114. package/layers/admin/app/utils/editor-expose.ts +0 -36
  115. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  116. package/layers/admin/app/utils/editor-registry.ts +0 -20
  117. package/layers/admin/app/utils/field-keys.ts +0 -11
  118. package/layers/admin/app/utils/list-cell.ts +0 -46
  119. package/layers/admin/app/utils/list-columns.ts +0 -142
  120. package/layers/admin/app/utils/list-query.ts +0 -96
  121. package/layers/admin/app/utils/nav-active.ts +0 -9
  122. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  123. package/layers/admin/app/utils/preview-channel.ts +0 -62
  124. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  125. package/layers/admin/app/utils/reauth.ts +0 -67
  126. package/layers/admin/app/utils/record-title.ts +0 -15
  127. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  128. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  129. package/layers/admin/nuxt.config.ts +0 -20
  130. package/layers/auth/nuxt.config.ts +0 -1
  131. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  132. package/layers/collections/nuxt.config.ts +0 -1
  133. package/layers/collections/server/plugins/01.register.ts +0 -29
  134. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  135. package/layers/core/app/composables/layouts.ts +0 -5
  136. package/layers/core/app/utils/field-factories.ts +0 -129
  137. package/layers/core/app/utils/layouts.ts +0 -38
  138. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  139. package/layers/core/modules/auto-discovery/index.ts +0 -112
  140. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  141. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  142. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  143. package/layers/core/modules/component-namespace/index.ts +0 -44
  144. package/layers/core/modules/component-namespace/shared.ts +0 -24
  145. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  146. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  147. package/layers/core/modules/kestrel/index.ts +0 -144
  148. package/layers/core/modules/plugin-order/index.ts +0 -69
  149. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  150. package/layers/core/nuxt.config.ts +0 -10
  151. package/layers/core/server/api/[...path].ts +0 -98
  152. package/layers/core/server/plugins/00.config.ts +0 -12
  153. package/layers/core/server/plugins/00.migrate.ts +0 -12
  154. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  155. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  156. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  157. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  158. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  159. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  160. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  161. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  162. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  163. package/layers/core/server/tasks/db/migrate.ts +0 -50
  164. package/layers/core/server/utils/server-config.ts +0 -81
  165. package/layers/fields/nuxt.config.ts +0 -1
  166. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  167. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  168. package/layers/media/app/components/KestrelImg.vue +0 -94
  169. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  170. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  171. package/layers/media/app/components/MediaGrid.vue +0 -113
  172. package/layers/media/app/components/MediaImage.vue +0 -70
  173. package/layers/media/app/components/MediaLibrary.vue +0 -456
  174. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  175. package/layers/media/app/components/MediaPathBar.vue +0 -163
  176. package/layers/media/app/components/MediaPicker.vue +0 -19
  177. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  178. package/layers/media/app/components/MediaTable.vue +0 -75
  179. package/layers/media/app/components/MediaThumb.vue +0 -25
  180. package/layers/media/app/components/MediaToolbar.vue +0 -90
  181. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  182. package/layers/media/app/components/MediaViewer.vue +0 -137
  183. package/layers/media/app/components/field/Media.vue +0 -263
  184. package/layers/media/app/composables/useBlurUp.ts +0 -55
  185. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  186. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  187. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  188. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  189. package/layers/media/app/composables/useMediaOps.ts +0 -50
  190. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  191. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  192. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  193. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  194. package/layers/media/app/plugins/field-media.client.ts +0 -6
  195. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  196. package/layers/media/app/utils/dnd.ts +0 -69
  197. package/layers/media/app/utils/library.ts +0 -81
  198. package/layers/media/app/utils/ops.ts +0 -60
  199. package/layers/media/app/utils/picture-model.ts +0 -125
  200. package/layers/media/app/utils/thumbhash.ts +0 -19
  201. package/layers/media/app/utils/variant-name.ts +0 -20
  202. package/layers/media/nuxt.config.ts +0 -7
  203. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  204. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  205. package/layers/media/server/plugins/02.register-media.ts +0 -22
  206. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  207. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  208. package/layers/media/server/tasks/media/backfill.ts +0 -17
  209. package/layers/public/app/app.vue +0 -5
  210. package/layers/public/app/components/BlockRenderer.vue +0 -56
  211. package/layers/public/app/components/KestrelLink.vue +0 -26
  212. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  213. package/layers/public/app/composables/public-page.ts +0 -24
  214. package/layers/public/app/error.vue +0 -12
  215. package/layers/public/app/layouts/default.vue +0 -5
  216. package/layers/public/app/pages/[...slug].vue +0 -239
  217. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  218. package/layers/public/app/utils/block-edit-context.ts +0 -16
  219. package/layers/public/app/utils/json-ld.ts +0 -139
  220. package/layers/public/app/utils/link-href.ts +0 -46
  221. package/layers/public/app/utils/page-head.ts +0 -84
  222. package/layers/public/app/utils/page-layout.ts +0 -18
  223. package/layers/public/app/utils/preview-protocol.ts +0 -117
  224. package/layers/public/app/utils/public-route.ts +0 -46
  225. package/layers/public/app/utils/site-head.ts +0 -43
  226. package/layers/public/modules/deploy-output/index.ts +0 -80
  227. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  228. package/layers/public/modules/prerender-routes/index.ts +0 -88
  229. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  230. package/layers/public/modules/prune-media/index.ts +0 -48
  231. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  232. package/layers/public/nuxt.config.ts +0 -14
  233. package/layers/public/server/delivery-live/serve.ts +0 -111
  234. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  235. package/layers/public/server/pipelines/index.ts +0 -23
  236. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  237. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  238. package/layers/public/server/plugins/02.register-links.ts +0 -16
  239. package/layers/public/server/plugins/03.redirects.ts +0 -55
  240. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  241. package/layers/public/server/plugins/zz.publish.ts +0 -127
  242. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  243. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  244. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  245. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  246. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  247. package/layers/public/server/tasks/publish/run.ts +0 -32
  248. package/layers/public/server/utils/publish/render-live.ts +0 -26
  249. package/layers/ui/app/assets/scss/_base.scss +0 -16
  250. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  251. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  252. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  253. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  254. package/layers/ui/app/assets/scss/main.scss +0 -4
  255. package/layers/ui/app/components/field/Boolean.vue +0 -30
  256. package/layers/ui/app/components/field/Choice.vue +0 -73
  257. package/layers/ui/app/components/field/Datetime.vue +0 -98
  258. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  259. package/layers/ui/app/components/field/Json.vue +0 -62
  260. package/layers/ui/app/components/field/Layout.vue +0 -138
  261. package/layers/ui/app/components/field/Link.vue +0 -112
  262. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  263. package/layers/ui/app/components/field/Number.vue +0 -30
  264. package/layers/ui/app/components/field/Relation.vue +0 -49
  265. package/layers/ui/app/components/field/Renderer.vue +0 -30
  266. package/layers/ui/app/components/field/Repeater.vue +0 -460
  267. package/layers/ui/app/components/field/Richtext.vue +0 -26
  268. package/layers/ui/app/components/field/Slug.vue +0 -65
  269. package/layers/ui/app/components/field/Text.vue +0 -35
  270. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  271. package/layers/ui/app/components/ui/Alert.vue +0 -50
  272. package/layers/ui/app/components/ui/Brand.vue +0 -22
  273. package/layers/ui/app/components/ui/Button.vue +0 -116
  274. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  275. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  276. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  277. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  278. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  279. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  280. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  281. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  282. package/layers/ui/app/components/ui/Field.vue +0 -58
  283. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  284. package/layers/ui/app/components/ui/Icon.vue +0 -44
  285. package/layers/ui/app/components/ui/Menu.vue +0 -54
  286. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  287. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  288. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  289. package/layers/ui/app/components/ui/Select.vue +0 -52
  290. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  291. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  292. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  293. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  294. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  295. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  296. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  297. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  298. package/layers/ui/app/composables/useLinkField.ts +0 -70
  299. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  300. package/layers/ui/app/composables/useRepeater.ts +0 -99
  301. package/layers/ui/app/composables/useT.ts +0 -32
  302. package/layers/ui/app/composables/useToast.ts +0 -46
  303. package/layers/ui/app/i18n/de.ts +0 -432
  304. package/layers/ui/app/i18n/en.ts +0 -434
  305. package/layers/ui/app/utils/boolean-field.ts +0 -10
  306. package/layers/ui/app/utils/field-component.ts +0 -22
  307. package/layers/ui/app/utils/field-empty.ts +0 -21
  308. package/layers/ui/app/utils/field-registry.ts +0 -36
  309. package/layers/ui/app/utils/humanize.ts +0 -14
  310. package/layers/ui/app/utils/icons.ts +0 -143
  311. package/layers/ui/app/utils/iso-date.ts +0 -18
  312. package/layers/ui/app/utils/localized.ts +0 -13
  313. package/layers/ui/app/utils/record-context.ts +0 -15
  314. package/layers/ui/app/utils/reorder.ts +0 -8
  315. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  316. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  317. package/layers/ui/nuxt.config.ts +0 -9
  318. package/nuxt.config.ts +0 -108
  319. package/scripts/consumer-template-ci.mjs +0 -160
  320. package/scripts/copy-create-payload.mjs +0 -49
  321. package/scripts/coverage-all.mjs +0 -163
  322. package/scripts/dashboard.mjs +0 -89
  323. package/scripts/hash-password.mjs +0 -29
  324. package/scripts/kestrel.mjs +0 -240
  325. package/scripts/lib/cli.mjs +0 -129
  326. package/scripts/lib/coverage-model.mjs +0 -51
  327. package/scripts/lib/password.mjs +0 -19
  328. package/scripts/lib/scaffold.mjs +0 -205
  329. package/scripts/port-graph.mjs +0 -350
  330. package/scripts/typecheck.mjs +0 -62
  331. package/templates/starter/README.md +0 -65
  332. package/templates/starter/_env.example +0 -17
  333. package/templates/starter/_gitignore +0 -26
  334. package/templates/starter/_package.json +0 -22
  335. package/templates/starter/app/app.vue +0 -7
  336. package/templates/starter/app/blocks/Prose.vue +0 -13
  337. package/templates/starter/app/layouts/default.vue +0 -6
  338. package/templates/starter/nuxt.config.ts +0 -23
  339. package/templates/starter/pnpm-workspace.yaml +0 -8
  340. package/templates/starter/tsconfig.json +0 -3
@@ -1,127 +0,0 @@
1
- import { renderRouteLive } from '../utils/publish/render-live'
2
- import {
3
- createPublishQueue,
4
- setPublishRuntime,
5
- DepsStore,
6
- createSqlitePersistence,
7
- usePublishingDb,
8
- outputDriver,
9
- publishInvalidation,
10
- resumePublishRuns,
11
- startPublishRun,
12
- setRenderRouteLive,
13
- type PublishDelivery,
14
- } from '@michaelthielemann/kestrel-publishing'
15
-
16
- /**
17
- * The in-server publish runtime (the production operation model). Wires the queue + deps index and gates
18
- * whether they exist at all, on `output.auto`, skipped in dev (a dev publish would write Vite-dev HTML
19
- * without hashed `_nuxt`):
20
- *
21
- * 1. THE RUNTIME ITSELF — `setPublishRuntime` publishes the queue + deps index via `usePublishRuntime()`.
22
- * The `planPublish` outbox handler (`handlers/plan-publish.ts`, registered independently by
23
- * `05.plan-publish.ts`) is the actual AUTO-TRIGGER: it classifies every content/media write into an
24
- * invalidation and enqueues it, but only when this plugin has actually set a runtime — a no-op
25
- * otherwise (dev, or `output.auto` off), so the gate lives here even though the trigger does not.
26
- * 2. BOOT PUBLISH — a full publish on startup resyncs this build's `_nuxt` + records every route's deps.
27
- * DETACHED (`runNitroPlugins` is synchronous + unawaited; `localFetch` is already wired before plugins
28
- * run + before the server listens), so it never blocks boot. A FULL run (boot's own, and the
29
- * reconciler's below) goes through `startPublishRun`, so its command -> snapshot -> delivery ->
30
- * done sequence is persisted to `publish_runs` and survives a crash — an incremental (tag) run a
31
- * content/media write triggers stays a direct `publishInvalidation` call, since it is not part of
32
- * the owned sequence.
33
- * 3. RECONCILER — an optional periodic full publish (`output.reconcileMinutes`) self-heals any missed
34
- * invalidation, also tracked as an owned run. The self-heal is real, not just "re-render everything
35
- * and hope": every full/reconcile publish live-renders each published route and compares its
36
- * fingerprint against the currently recorded snapshot (`publisher.ts`'s `PublishMode: 'reconcile'`,
37
- * ADR-0013's amendment) — a mismatch (a missed invalidation, a template/component deploy) records +
38
- * delivers the fresh render; a match costs nothing beyond the render itself.
39
- *
40
- * Crash-resume (ADR-0025): `resumePublishRuns` marks any run a crash left `running` as `failed` — it does
41
- * NOT redeliver — and runs BEFORE the boot enqueue below, which fires unconditionally regardless of what
42
- * resume found. The queue remains the ONLY caller that ever invokes a delivery; resume never bypasses its
43
- * single-flight guard.
44
- */
45
- export default defineNitroPlugin(() => {
46
- if (import.meta.dev) return
47
- const output = (useRuntimeConfig().kestrel as { output?: { auto?: boolean; publishOnSave?: boolean; reconcileMinutes?: number; verbose?: boolean } }).output
48
- if (!output?.auto) return
49
-
50
- // Wired explicitly here (not a module-load side effect) — see tasks/publish/run.ts's own comment for
51
- // why: it removes any dependency on import-order/instance-identity between this plugin's own
52
- // `@michaelthielemann/kestrel-publishing` import and the one `publisher.ts`'s internals resolve. Idempotent.
53
- setRenderRouteLive(renderRouteLive)
54
-
55
- // `output.verbose`: on top of the one-line summary, itemise each incremental republish with a
56
- // timestamped per-route line (rendered / pruned) for traceability. Off by default. (A queued FULL run
57
- // — boot / reconciler — is a bulk resync and stays a summary; publishInvalidation returns no per-route
58
- // list for it.)
59
- const verbose = output.verbose ?? false
60
- const stamp = () => new Date().toISOString()
61
-
62
- const driver = outputDriver()
63
- // Durable deps: rehydrate `route → tags` from SQLite so the boot full-publish below can prune routes
64
- // that were unpublished/deleted while the server was down (staleRoutes over a non-empty tracked set).
65
- const deps = new DepsStore(createSqlitePersistence(usePublishingDb().db))
66
-
67
- // The delivery port for a FULL run: the real renderer, driven the same way a queued full invalidation
68
- // always was. `startPublishRun` persists the command -> snapshot -> delivery -> done sequence around
69
- // this call; this closure is only the "delivery" step itself.
70
- const fullDelivery: PublishDelivery = {
71
- deliver: async () => {
72
- const { rendered, pruned, counts } = await publishInvalidation({ type: 'full' }, driver, deps)
73
- if (counts.rendered === 0 && counts.pruned === 0) return
74
- console.info(`[kestrel] published ${counts.rendered} route(s) (pruned ${counts.pruned})`)
75
- if (verbose) {
76
- for (const route of rendered) console.info(`[kestrel] ${stamp()} rendered ${route}`)
77
- for (const route of pruned) console.info(`[kestrel] ${stamp()} pruned ${route}`)
78
- }
79
- },
80
- }
81
-
82
- const queue = createPublishQueue({
83
- run: async (inv) => {
84
- if (inv.type === 'full') {
85
- const result = await startPublishRun(fullDelivery)
86
- // Rethrow on a recorded failure so the queue's own retry-on-failure fires (its `.catch()` calls
87
- // `onError` below and re-queues the batch) — `startPublishRun` itself never throws for a delivery
88
- // failure (it resolves with the outcome as data), so restoring that retry has to happen here.
89
- // `id: 0` is the synthetic placeholder `startPublishRun`'s missing-table fallback returns (no real
90
- // row backs it) — label it distinctly rather than a misleading "#0".
91
- if (result.status === 'failed') throw new Error(`publish run #${result.id === 0 ? 'untracked' : result.id} failed: ${result.error}`)
92
- return
93
- }
94
- const { rendered, pruned, counts } = await publishInvalidation(inv, driver, deps)
95
- if (counts.rendered === 0 && counts.pruned === 0) return
96
- // Incremental runs name the routes; a queued full run (the reconciler) prints counts only.
97
- const list = rendered.length ? `: ${rendered.join(', ')}` : ''
98
- console.info(`[kestrel] published ${counts.rendered} route(s)${list} (pruned ${counts.pruned})`)
99
- if (verbose) {
100
- for (const route of rendered) console.info(`[kestrel] ${stamp()} rendered ${route}`)
101
- for (const route of pruned) console.info(`[kestrel] ${stamp()} pruned ${route}`)
102
- }
103
- },
104
- onError: (error) => console.error('[kestrel] publish run failed:', error),
105
- })
106
-
107
- // The explicit publish action (`POST /api/publish`) enqueues through this same queue, so a publish and
108
- // a write-driven prune are serialized by one single-flight run rather than racing each other.
109
- setPublishRuntime({ queue, deps })
110
-
111
- // Boot publish goes THROUGH the queue (not a direct publishFull) so it shares the single-flight guard:
112
- // otherwise a boot full-publish could overlap a write-triggered incremental or the reconciler, and both
113
- // runs share the module-level variant accumulator (corrupting the registry) and can interleave
114
- // render/prune (leaving an unpublished page live). The queue serializes them, and it is the ONLY
115
- // AUTOMATIC caller that ever invokes a delivery — `resumePublishRuns` below just updates rows, so there
116
- // is nothing left to race it. (The `publish:run` Nitro task — `tasks/publish/run.ts` — is a documented
117
- // exception: an operator-triggered manual run that calls `publishFull` directly, outside this queue.)
118
- //
119
- // Resume runs first, awaited: it is a plain DB update (never invokes delivery, so it cannot itself
120
- // crash the process the way a redelivered render could), which is what guarantees the boot enqueue below
121
- // always fires — a resume that instead redelivered and crashed again the same way would leave the row
122
- // `running` forever and never reach this line, an infinite boot loop that never makes progress.
123
- void resumePublishRuns().finally(() => queue.enqueue({ type: 'full' }))
124
-
125
- const mins = output.reconcileMinutes ?? 0
126
- if (mins > 0) setInterval(() => queue.enqueue({ type: 'full' }), mins * 60_000).unref?.()
127
- })
@@ -1,99 +0,0 @@
1
- import { getTableColumns } from 'drizzle-orm'
2
- import { localePath, useDb, primaryLocale, prefixPrimaryLocale, allCollections } from '@michaelthielemann/kestrel-core'
3
- import type { BuiltCollection } from '@michaelthielemann/kestrel-core'
4
- import { isPubliclyReadable, publicReadableResources } from '@michaelthielemann/kestrel-access'
5
- import { LLMS_FULL_HEADING_OFFSET, buildLlmsFullTxt, recordMarkdown, collectionHeading, siteBaseUrl, siteName, siteDescription, llmsFullEnabled } from '@michaelthielemann/kestrel-publishing'
6
- import type { LlmsFullSection, LlmsFullPage } from '@michaelthielemann/kestrel-publishing'
7
- // The long form of llms.txt: every published, indexable page's full Markdown body in one document, so an
8
- // answer engine can ground on the site without crawling it. Same registry-driven public set and the same
9
- // status/noindex filters as `llms.txt` and the sitemap (single source: the auth policy).
10
- //
11
- // OPT-IN (`kestrel.seo.llmsFull`, default off), for two reasons that both matter: it aggregates the whole
12
- // site into one scrapeable artifact — a disclosure decision that belongs to the consumer, not to an
13
- // upgrade — and unlike `llms.txt` it must read every row's block content, which the publisher would
14
- // re-render on every incremental publish.
15
- export default defineEventHandler((event) => {
16
- if (!llmsFullEnabled()) throw createError({ statusCode: 404, statusMessage: 'Not Found' })
17
- const db = useDb()
18
- const base = siteBaseUrl()
19
- const primary = primaryLocale()
20
- const prefixPrimary = prefixPrimaryLocale()
21
-
22
- setHeader(event, 'content-type', 'text/plain; charset=utf-8')
23
- // Without an absolute origin every `Source:` line would be a relative path resolving against whatever
24
- // host fetched the file — the same rule `llms.txt` applies to its link list.
25
- if (!base) {
26
- console.warn('[kestrel] llms-full.txt: siteUrl is unset — omitting page bodies (their URLs would be relative)')
27
- return buildLlmsFullTxt({ siteName: siteName(), siteDescription: siteDescription() || undefined, sections: [] })
28
- }
29
-
30
- interface Loaded { c: BuiltCollection; rows: Record<string, unknown>[] }
31
- const pub = publicReadableResources()
32
- const loaded: Loaded[] = []
33
- for (const c of allCollections()) {
34
- if (!c.def.pageLike) continue
35
- if (!isPubliclyReadable(c.def.name, pub)) continue
36
- // Project the listing columns plus ONLY the prose-bearing fields — this route does have to read block
37
- // content, but a media/relation/json column still has no business being loaded to render text.
38
- const cols = getTableColumns(c.table) as Record<string, never>
39
- const proj: Record<string, unknown> = { id: cols.id, path: cols.path }
40
- if (c.def.translatable) proj.locale = cols.locale
41
- if (c.def.status) proj.status = cols.status
42
- if (c.def.seo) proj.seo = cols.seo
43
- if (c.def.blocks?.enabled) proj.content = cols.content
44
- for (const [key, field] of Object.entries(c.def.fields)) {
45
- if (field.type === 'text' || field.type === 'richtext' || field.type === 'repeater') proj[key] = cols[key]
46
- }
47
- try {
48
- loaded.push({ c, rows: db.select(proj as never).from(c.table).all() as Record<string, unknown>[] })
49
- } catch (error) {
50
- // Skipping keeps a bare prerender DB publishable, but a drifted table drops the whole section — a
51
- // silent gap the publisher would write straight over the live artifact.
52
- console.error(`[kestrel] llms-full.txt: skipped collection ${c.def.name}:`, (error as Error)?.message ?? error)
53
- }
54
- }
55
-
56
- const advertisable = (c: BuiltCollection, row: Record<string, unknown>): string | null => {
57
- if (c.def.status && row.status !== 'published') return null
58
- const path = row.path as string | null
59
- if (!path) return null
60
- if ((row.seo as { noindex?: boolean } | null)?.noindex) return null
61
- return base + localePath(path, (row.locale as string | undefined) ?? primary, primary, prefixPrimary)
62
- }
63
-
64
- // Internal richtext links are stored as `kestrel:<collection>:<id>` markers. Resolve them from the rows
65
- // already loaded — the same status/noindex gate, so a marker pointing at a draft or a noindexed page
66
- // declines and the link degrades to its own text instead of leaking an unpublished URL.
67
- const linkTargets = new Map<string, string>()
68
- for (const { c, rows } of loaded) {
69
- for (const row of rows) {
70
- const url = advertisable(c, row)
71
- if (url) linkTargets.set(`${c.def.name}:${row.id as number}`, url)
72
- }
73
- }
74
- const resolveLink = (collection: string, id: number): string | null => linkTargets.get(`${collection}:${id}`) ?? null
75
-
76
- const sections: LlmsFullSection[] = []
77
- for (const { c, rows } of loaded) {
78
- const pages: LlmsFullPage[] = []
79
- for (const row of rows) {
80
- const url = advertisable(c, row)
81
- if (!url) continue
82
- const seo = (row.seo ?? {}) as { title?: string; description?: string }
83
- pages.push({
84
- title: seo.title || (row.title as string | undefined) || (row.path as string),
85
- url,
86
- description: seo.description || undefined,
87
- // `title` already IS this page's heading — emitting it again as body text would repeat every
88
- // page title twice in the document.
89
- body: recordMarkdown(c.def, row, { headingOffset: LLMS_FULL_HEADING_OFFSET, skipFields: ['title'], resolveLink }),
90
- })
91
- }
92
- if (pages.length) {
93
- pages.sort((a, b) => a.url.localeCompare(b.url))
94
- sections.push({ heading: collectionHeading(c.def, primary), pages })
95
- }
96
- }
97
-
98
- return buildLlmsFullTxt({ siteName: siteName(), siteDescription: siteDescription() || undefined, sections })
99
- })
@@ -1,66 +0,0 @@
1
- import { getTableColumns } from 'drizzle-orm'
2
- import { localePath, useDb, primaryLocale, prefixPrimaryLocale, allCollections } from '@michaelthielemann/kestrel-core'
3
- import { isPubliclyReadable, publicReadableResources } from '@michaelthielemann/kestrel-access'
4
- import { collectionHeading, buildLlmsTxt, siteBaseUrl, siteName, siteDescription } from '@michaelthielemann/kestrel-publishing'
5
- import type { LlmsSection, LlmsEntry } from '@michaelthielemann/kestrel-publishing'
6
-
7
- // Generates an `llms.txt` (llmstxt.org): the site name + description, then one section per pageLike
8
- // collection listing its published, indexable records — the SAME registry-driven public set the sitemap
9
- // advertises (single source: the auth policy). Safe to serve publicly and to prerender.
10
- export default defineEventHandler((event) => {
11
- const db = useDb()
12
- const base = siteBaseUrl()
13
- const primary = primaryLocale()
14
- const prefixPrimary = prefixPrimaryLocale()
15
-
16
- const pub = publicReadableResources()
17
- const sections: LlmsSection[] = []
18
- // Without an absolute origin, every resource URL would be a relative path — omit the page sections (keep
19
- // the site name + description) rather than emit a link list that resolves against the wrong host.
20
- if (!base) console.warn('[kestrel] llms.txt: siteUrl is unset — omitting page links (they would be relative)')
21
- for (const c of base ? allCollections() : []) {
22
- if (!c.def.pageLike) continue
23
- if (!isPubliclyReadable(c.def.name, pub)) continue
24
- // Project ONLY the columns this listing reads — never SELECT *, which would pull every row's
25
- // block-content JSON into memory just to emit a link; the runtime publisher re-renders this route on
26
- // every incremental publish. Optional columns exist only when the def declares them (buildTable adds no
27
- // locale/status/seo column otherwise), and a `cols.<missing>` would be undefined and make the select
28
- // throw — gate on the same flags, as the sitemap does. `title` is a plain field, so probe the table.
29
- const cols = getTableColumns(c.table) as Record<string, never>
30
- const proj: Record<string, unknown> = { path: cols.path }
31
- if (c.def.translatable) proj.locale = cols.locale
32
- if (c.def.status) proj.status = cols.status
33
- if (c.def.seo) proj.seo = cols.seo
34
- if (Object.hasOwn(cols, 'title')) proj.title = cols.title
35
- let rows: Record<string, unknown>[]
36
- try {
37
- rows = db.select(proj as never).from(c.table).all() as Record<string, unknown>[]
38
- } catch (error) {
39
- // Skipping keeps a bare prerender DB publishable, but a drifted table drops the whole section — a
40
- // silent gap the publisher would write straight over the live artifact.
41
- console.error(`[kestrel] llms.txt: skipped collection ${c.def.name}:`, (error as Error)?.message ?? error)
42
- continue
43
- }
44
- const entries: LlmsEntry[] = []
45
- for (const row of rows) {
46
- if (c.def.status && row.status !== 'published') continue
47
- const path = row.path as string | null
48
- if (!path) continue
49
- const seo = (row.seo ?? {}) as { title?: string; description?: string; noindex?: boolean }
50
- if (seo.noindex) continue
51
- const locale = (row.locale as string | undefined) ?? primary
52
- entries.push({
53
- title: seo.title || (row.title as string | undefined) || path,
54
- url: base + localePath(path, locale, primary, prefixPrimary),
55
- description: seo.description || undefined,
56
- })
57
- }
58
- if (entries.length) {
59
- entries.sort((a, b) => a.url.localeCompare(b.url))
60
- sections.push({ heading: collectionHeading(c.def, primary), entries })
61
- }
62
- }
63
-
64
- setHeader(event, 'content-type', 'text/plain; charset=utf-8')
65
- return buildLlmsTxt({ siteName: siteName(), siteDescription: siteDescription() || undefined, sections })
66
- })
@@ -1,58 +0,0 @@
1
- import { eq } from 'drizzle-orm'
2
- import { getCollection, useDb } from '@michaelthielemann/kestrel-core'
3
- import { compilePublishableRedirects, serializeRedirects, REDIRECTS_COLLECTION, REDIRECTS_FIELD } from '@michaelthielemann/kestrel-publishing'
4
-
5
- /**
6
- * The redirect artifact the edge polls. A save publishes it directly (see `plugins/03.redirects.ts`);
7
- * this route is what makes it survive everything else — it is prerendered into `.output/public`, so the
8
- * build-time deploy's reconcile keeps it instead of pruning a key it cannot account for, and a full
9
- * publish re-renders it from the live DB. Public and cheap: one row, no user input.
10
- *
11
- * Zero redirects is a supported state, and so is a collection a consumer toggled off — both serve `[]`,
12
- * never a 404, because an edge that cannot fetch the file has to keep its last good state and would
13
- * otherwise burn its cold-start budget retrying.
14
- */
15
-
16
- /** A table that does not exist yet — the release adds one, so a consumer can generate before their
17
- * `db:migrate`. `[]` is the truth there, not a degrade. */
18
- function tableIsAbsent(error: unknown): boolean {
19
- return /no such table/i.test((error as Error)?.message ?? '')
20
- }
21
-
22
- /**
23
- * Render the artifact, or fail loudly. The distinction is deliberate and narrow: only a missing table
24
- * yields `[]`, because only then is "no redirects" a FACT. Any other read failure (a drifted column, an
25
- * I/O error) throws — at build time an errored route costs the deploy its reconcile, which is the
26
- * conservative direction, and at runtime `publishMeta` writes nothing on a non-200, where an empty body
27
- * would instead overwrite a good live artifact with "no redirects". A failed read must never be an
28
- * authoritative empty result.
29
- */
30
- export function renderRedirects(readRows: () => unknown): string {
31
- let rows: unknown
32
- try {
33
- rows = readRows()
34
- } catch (error) {
35
- if (!tableIsAbsent(error)) throw error
36
- console.warn('[kestrel] redirects.json: the redirects table does not exist yet — serving an empty list. Run `db:migrate`.')
37
- return serializeRedirects([])
38
- }
39
- const { rules, skipped } = compilePublishableRedirects(rows)
40
- for (const message of skipped) {
41
- console.error(`[kestrel] redirects.json: skipped an unpublishable rule — ${message}`)
42
- }
43
- return serializeRedirects(rules)
44
- }
45
-
46
- export default defineEventHandler((event) => {
47
- setHeader(event, 'content-type', 'application/json; charset=utf-8')
48
- const collection = getCollection(REDIRECTS_COLLECTION)
49
- if (!collection) return serializeRedirects([])
50
-
51
- const cols = collection.table as unknown as Record<string, never>
52
- return renderRedirects(() => {
53
- const row = useDb().select().from(collection.table).where(eq(cols.singletonKey, REDIRECTS_COLLECTION)).get() as
54
- | Record<string, unknown>
55
- | undefined
56
- return row?.[REDIRECTS_FIELD]
57
- })
58
- })
@@ -1,11 +0,0 @@
1
- import { siteBaseUrl, buildRobots, llmsFullEnabled } from '@michaelthielemann/kestrel-publishing'
2
-
3
- export default defineEventHandler((event) => {
4
- const base = siteBaseUrl()
5
- setHeader(event, 'content-type', 'text/plain; charset=utf-8')
6
- return buildRobots({
7
- sitemapUrl: base ? `${base}/sitemap.xml` : undefined,
8
- llmsUrl: base ? `${base}/llms.txt` : undefined,
9
- llmsFullUrl: base && llmsFullEnabled() ? `${base}/llms-full.txt` : undefined,
10
- })
11
- })
@@ -1,79 +0,0 @@
1
- import { getTableColumns } from 'drizzle-orm'
2
- import { localePath, useDb, primaryLocale, prefixPrimaryLocale, allCollections, isDeliveryLive } from '@michaelthielemann/kestrel-core'
3
- import { isPubliclyReadable, publicReadableResources } from '@michaelthielemann/kestrel-access'
4
- import { currentRoutes, usePublishingDb, buildSitemap, withHreflang, siteBaseUrl } from '@michaelthielemann/kestrel-publishing'
5
- import type { SitemapCandidate } from '@michaelthielemann/kestrel-publishing'
6
-
7
- // Lists every published, indexable page-like record (across all pageLike collections) as a
8
- // sitemap. Filters status/noindex itself, so it is safe to serve publicly and to prerender. Under
9
- // `delivery: 'live'`, a route must ALSO have a current, non-retracted snapshot — the row's own
10
- // status/noindex fields and the snapshot store are two independent "is this reachable" facts (a direct
11
- // snapshot retraction leaves the row untouched, see `@michaelthielemann/kestrel-publishing`'s `server/db/snapshots.ts`), and this route is what a crawler
12
- // actually finds, so both must agree before a route is listed.
13
- export default defineEventHandler((event) => {
14
- const db = useDb()
15
- const base = siteBaseUrl()
16
- // Without an absolute origin (kestrel.siteUrl / KESTREL_SITE_URL) every <loc> would be a RELATIVE path,
17
- // which the sitemaps.org schema rejects outright — a worse result than an empty sitemap. Emit a valid
18
- // empty one + warn, mirroring robots.txt omitting its `Sitemap:` directive when the base is unset.
19
- if (!base) {
20
- console.warn('[kestrel] sitemap.xml: siteUrl is unset — emitting an empty sitemap (relative <loc> URLs would be invalid)')
21
- setHeader(event, 'content-type', 'application/xml; charset=utf-8')
22
- return buildSitemap([])
23
- }
24
- const primary = primaryLocale()
25
- const prefixPrimary = prefixPrimaryLocale()
26
- const candidates: SitemapCandidate[] = []
27
- // One set lookup for the whole candidate list instead of one currentSnapshot query per candidate.
28
- const liveRoutes = isDeliveryLive() ? new Set(currentRoutes(usePublishingDb().db)) : null
29
-
30
- // A sitemap must only advertise publicly-reachable URLs, so it tracks the SAME registry-driven
31
- // public set the public render path uses (single source: the auth policy).
32
- const pub = publicReadableResources()
33
- for (const c of allCollections()) {
34
- if (!c.def.pageLike) continue
35
- if (!isPubliclyReadable(c.def.name, pub)) continue
36
- // Project ONLY the columns the sitemap reads — never SELECT *, which would pull every row's
37
- // block-content JSON (and every other field) into memory just to emit a URL; the runtime publisher
38
- // re-renders this route on every incremental publish. Optional columns exist only when the def
39
- // declares them (buildTable adds no locale/status/seo column otherwise), and a `cols.<missing>` would
40
- // be undefined and make the select throw — gate on the same flags, as allPublishedRoutes does.
41
- const cols = getTableColumns(c.table) as Record<string, never>
42
- const proj: Record<string, unknown> = { path: cols.path, updatedAt: cols.updatedAt }
43
- if (c.def.translatable) proj.locale = cols.locale
44
- if (c.def.translatable && c.def.mode !== 'single') proj.translationGroup = cols.translationGroup
45
- if (c.def.status) proj.status = cols.status
46
- if (c.def.seo) proj.seo = cols.seo
47
- let rows: Record<string, unknown>[]
48
- try {
49
- rows = db.select(proj as never).from(c.table).all() as Record<string, unknown>[]
50
- } catch (error) {
51
- // Skipping keeps a bare prerender DB publishable, but a drifted table de-indexes every page of the
52
- // collection — a silent gap the publisher would write straight over the live sitemap.
53
- console.error(`[kestrel] sitemap.xml: skipped collection ${c.def.name}:`, (error as Error)?.message ?? error)
54
- continue
55
- }
56
- for (const row of rows) {
57
- if (c.def.status && row.status !== 'published') continue
58
- const path = row.path as string | null
59
- if (!path) continue
60
- const seo = (row.seo ?? {}) as { noindex?: boolean }
61
- if (seo.noindex) continue
62
- const locale = (row.locale as string | undefined) ?? primary
63
- const route = localePath(path, locale, primary, prefixPrimary)
64
- if (liveRoutes && !liveRoutes.has(route)) continue
65
- candidates.push({
66
- loc: base + route,
67
- lastmod: row.updatedAt instanceof Date ? row.updatedAt.toISOString() : undefined,
68
- locale,
69
- group: (row.translationGroup as string | undefined) ?? null,
70
- })
71
- }
72
- }
73
-
74
- // Link each page's published, indexable locale variants via hreflang alternates before serializing.
75
- const entries = withHreflang(candidates, primary)
76
- entries.sort((a, b) => a.loc.localeCompare(b.loc))
77
- setHeader(event, 'content-type', 'application/xml; charset=utf-8')
78
- return buildSitemap(entries)
79
- })
@@ -1,32 +0,0 @@
1
- import { renderRouteLive } from '../../utils/publish/render-live'
2
- import { publishFull, outputDriver, DepsStore, createSqlitePersistence, usePublishingDb, setRenderRouteLive } from '@michaelthielemann/kestrel-publishing'
3
-
4
- /**
5
- * The shared publish engine, exposed as a Nitro task. Renders every published page (+ sitemap/robots)
6
- * via the live server, mirrors `_nuxt`/assets, and always prunes the routes that left the published set
7
- * (output ≡ DB). Like every full publish it HOLDS BACK routes with unpublished changes (ADR-0008) — a
8
- * resync must not push work in progress live; those pages go out when they are published.
9
- *
10
- * Triggering (Nuxt 4.4 / Nitro 2.13 — there is NO `nuxi task run`):
11
- * - dev: GET http://localhost:3000/_nitro/tasks/publish:run (the dev-only task route; plumbing smoke test)
12
- * - prod: the boot-publish plugin (`zz.publish.ts`) runs this on startup; for an on-demand prod trigger,
13
- * expose your own authenticated route that calls `runTask('publish:run')` (the built node-server
14
- * has no task endpoint/CLI — only `runTask()` from inside the process + cron `scheduledTasks`).
15
- */
16
- export default defineTask({
17
- meta: { name: 'publish:run', description: 'Render all published pages to the configured static output (HTML + _nuxt + assets)' },
18
- async run() {
19
- // Wired explicitly here (not a module-load side effect): Nitro dev re-evaluates a task's module graph
20
- // per invocation for hot-reload, which can give this call and `publisher.ts`'s a differently-scoped
21
- // `@michaelthielemann/kestrel-publishing` instance if the wiring only ran once, earlier, at a boot-time import — calling
22
- // the setter fresh, synchronously, right before the only call that needs it removes that dependency on
23
- // import-order/instance-identity entirely. Idempotent: safe to call before every run.
24
- setRenderRouteLive(renderRouteLive)
25
- // The same durable store the boot-publish plugin uses, so a manually-triggered run also prunes
26
- // routes that left the published set and records deps for later incremental publishes.
27
- const deps = new DepsStore(createSqlitePersistence(usePublishingDb().db))
28
- const result = await publishFull(outputDriver(), deps)
29
- console.info(`[kestrel] publish:run rendered ${result.rendered} route(s), pruned ${result.pruned}`)
30
- return { result }
31
- },
32
- })
@@ -1,26 +0,0 @@
1
- import { runAsRenderer } from '@michaelthielemann/kestrel-access'
2
-
3
- /** Render a public route via the in-process server, returning its HTML buffer (200) or a null body with the
4
- * HTTP status, so the caller can tell a broken page (5xx — a server-error RESPONSE, not a throw) from an
5
- * expected non-200 (a draft / unpublish race → 404 / redirect). The PRODUCER's own render primitive — the
6
- * delivery-static side never calls this; see `@michaelthielemann/kestrel-delivery-static`'s `render-route.ts`. `@michaelthielemann/kestrel-publishing`
7
- * never imports this directly (a package cannot reach `useNitroApp`) — each real
8
- * entry point (`zz.publish.ts`, `tasks/publish/run.ts`) instead calls `setRenderRouteLive(renderRouteLive)`
9
- * itself, right before the call that needs it (see either file's own comment for why NOT a module-load
10
- * side effect: Nitro dev can re-evaluate a task's module graph per invocation). */
11
- export async function renderRouteLive(route: string): Promise<{ body: Buffer | null; status: number }> {
12
- // Run as the renderer principal so the nested $fetch('/api/route') passes the access guard (it is not a
13
- // public endpoint). The ALS context propagates through localFetch → the page render → the nested fetch.
14
- // useNitroApp is imported LAZILY (not via the Nitro auto-import) on purpose: a static import gives this
15
- // module an edge to nitropack's app.mjs, which both defines useNitroApp AND loads the plugin registry
16
- // that reaches this file (via zz.publish) — a build-time circular dependency. A dynamic import is a chunk
17
- // boundary, so that cycle disappears from the build graph; it's the same nitroApp singleton either way
18
- // (Rollup bundles the literal specifier; useNitroApp is only CALLED here at publish time, long after boot).
19
- const { useNitroApp } = await import('nitropack/runtime')
20
- // Every consumer that needs to know "is this OUR OWN in-process render" (delivery-live's catch-all,
21
- // media's variant-capture scan) reads `isRendererContext()`/`import.meta.prerender` instead of a header —
22
- // both are set by `runAsRenderer` below and an external request can't forge either.
23
- const res = await runAsRenderer(() => useNitroApp().localFetch(route, { method: 'GET' }))
24
- if (res.status !== 200) return { body: null, status: res.status }
25
- return { body: Buffer.from(await res.arrayBuffer()), status: res.status }
26
- }
@@ -1,16 +0,0 @@
1
- body {
2
- font-family: var(--font-sans);
3
- font-size: var(--text-base);
4
- color: var(--color-text);
5
- background: var(--color-bg);
6
- }
7
-
8
- a {
9
- color: var(--color-primary);
10
- }
11
-
12
- /* Opt-in reading measure for form-heavy pages inside the now full-bleed admin shell. */
13
- .u-measure {
14
- max-width: var(--content-measure);
15
- margin-inline: auto;
16
- }
@@ -1,82 +0,0 @@
1
- @use 'mixins' as *;
2
-
3
- // Shared chrome for the date pickers. The calendar popover is teleported to <body> by Reka, so these
4
- // must stay GLOBAL (non-scoped) — a `scoped` block would silently drop them. Both DatePicker and
5
- // DateRangePicker @include this so neither silently borrows the other's styles (`__sep` is only used
6
- // by the range variant, the rest by both).
7
- @mixin ui-datepicker {
8
- .ui-datepicker {
9
- &__field {
10
- display: inline-flex;
11
- align-items: center;
12
- gap: 1px;
13
- padding: var(--space-2) var(--space-3);
14
- border: 1px solid var(--color-control-border, var(--color-border));
15
- border-radius: var(--radius-md);
16
- background: var(--color-surface);
17
-
18
- &[aria-invalid='true'] { border-color: var(--color-danger); }
19
- }
20
- &__segment {
21
- @include focus-ring;
22
- padding: 0 1px;
23
- border-radius: var(--radius-sm);
24
-
25
- &[data-placeholder] { color: var(--color-text-muted); }
26
- // The fill alone can't tell a keyboard user which segment the arrow keys will change (reka hides
27
- // the caret), so the ring has to survive it — only pointer focus drops the outline.
28
- &:focus { background: var(--color-active); color: var(--color-text); }
29
- &:focus:not(:focus-visible) { outline: none; }
30
- }
31
- &__sep { margin: 0 var(--space-2); color: var(--color-text-muted); }
32
- &__trigger {
33
- @include focus-ring;
34
- margin-inline-start: var(--space-2);
35
- border: 0;
36
- background: transparent;
37
- cursor: pointer;
38
- }
39
- &__content {
40
- background: var(--color-surface);
41
- border: 1px solid var(--color-border);
42
- border-radius: var(--radius-md);
43
- box-shadow: var(--shadow-md);
44
- padding: var(--space-3);
45
- z-index: var(--z-dropdown);
46
- }
47
- &__header {
48
- display: flex;
49
- align-items: center;
50
- justify-content: space-between;
51
- margin-bottom: var(--space-2);
52
- }
53
- &__nav {
54
- @include focus-ring;
55
- border: 0;
56
- background: transparent;
57
- cursor: pointer;
58
- font-size: var(--text-lg);
59
- padding: 0 var(--space-2);
60
- }
61
- &__heading { font-weight: var(--weight-medium); font-size: var(--text-sm); }
62
- &__weekday { font-size: var(--text-sm); color: var(--color-text-muted); padding: var(--space-1); font-weight: var(--weight-normal); }
63
- &__cell {
64
- @include focus-ring;
65
- display: inline-flex;
66
- align-items: center;
67
- justify-content: center;
68
- width: 2rem;
69
- height: 2rem;
70
- border: 0;
71
- border-radius: var(--radius-sm);
72
- background: transparent;
73
- color: var(--color-text);
74
- cursor: pointer;
75
-
76
- &[data-today] { font-weight: var(--weight-bold); }
77
- &[data-selected] { background: var(--color-primary); color: var(--color-on-primary); }
78
- &[data-outside-view] { color: var(--color-text-muted); }
79
- &[data-disabled] { opacity: 0.4; cursor: not-allowed; }
80
- }
81
- }
82
- }
@@ -1,50 +0,0 @@
1
- // Focus ring is INSET (negative offset, drawn within the border-box) so a scrolling/overflow ancestor
2
- // can never clip it at an edge — and so the ring looks identical across every component, whether it
3
- // @includes this mixin or mirrors it inline.
4
- @mixin focus-ring {
5
- &:focus-visible {
6
- outline: 2px solid var(--color-focus);
7
- outline-offset: -2px;
8
- }
9
- }
10
-
11
- @mixin sr-only {
12
- position: absolute;
13
- width: 1px;
14
- height: 1px;
15
- padding: 0;
16
- margin: -1px;
17
- overflow: hidden;
18
- clip: rect(0, 0, 0, 0);
19
- white-space: nowrap;
20
- border: 0;
21
- }
22
-
23
- // Shared chrome for text-like form controls (input/textarea/select/time). Per-component extras
24
- // (width, ::placeholder, resize, …) stay in the component.
25
- @mixin input-base {
26
- @include focus-ring;
27
- padding: var(--space-2) var(--space-3);
28
- // Interactive control boundary needs 3:1 (WCAG 1.4.11). Admin themes define --color-control-border;
29
- // the public SSG site has no such token, so it falls back to --color-border (unchanged there).
30
- border: 1px solid var(--color-control-border, var(--color-border));
31
- border-radius: var(--radius-md);
32
- background: var(--color-surface);
33
- color: var(--color-text);
34
- font-size: var(--text-base);
35
-
36
- &[aria-invalid='true'] {
37
- border-color: var(--color-danger);
38
- }
39
- &:disabled {
40
- opacity: 0.6;
41
- cursor: not-allowed;
42
- }
43
- }
44
-
45
- // Slim variant of `input-base`'s box, for dense chrome (e.g. the preview toolbar's W×H fields). ONLY the
46
- // padding shrinks: at the unchanged --text-base the control still measures ~29px tall, clearing the 24×24
47
- // minimum target size (WCAG 2.2 SC 2.5.8, AA). Do not also shrink the font — that would drop under it.
48
- @mixin input-slim {
49
- padding: var(--space-1) var(--space-2);
50
- }