@michaelthielemann/kestrel 4.1.3 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -201
- package/dist/boot.d.ts +45 -0
- package/dist/boot.js +284 -0
- package/dist/cast.d.ts +3 -0
- package/dist/cast.js +6 -0
- package/dist/catalogue.d.ts +11 -0
- package/dist/catalogue.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +55 -0
- package/dist/context.d.ts +62 -0
- package/dist/context.js +61 -0
- package/dist/dataflow.d.ts +3 -0
- package/dist/dataflow.js +65 -0
- package/dist/defineConfig.d.ts +162 -0
- package/dist/defineConfig.js +42 -0
- package/dist/defineContract.d.ts +12 -0
- package/dist/defineContract.js +16 -0
- package/dist/defineModule.d.ts +75 -0
- package/dist/defineModule.js +9 -0
- package/dist/definePipeline.d.ts +7 -0
- package/dist/definePipeline.js +11 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/load.d.ts +7 -0
- package/dist/load.js +79 -0
- package/dist/logger.d.ts +18 -0
- package/dist/logger.js +36 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +87 -0
- package/dist/result.d.ts +19 -0
- package/dist/result.js +19 -0
- package/dist/runner.d.ts +29 -0
- package/dist/runner.js +133 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +142 -0
- package/dist/sort.d.ts +2 -0
- package/dist/sort.js +50 -0
- package/dist/testing/runPipeline.d.ts +9 -0
- package/dist/testing/runPipeline.js +22 -0
- package/dist/triggers/allowlist.d.ts +9 -0
- package/dist/triggers/allowlist.js +49 -0
- package/dist/triggers/cron.d.ts +10 -0
- package/dist/triggers/cron.js +44 -0
- package/dist/triggers/cronMatch.d.ts +10 -0
- package/dist/triggers/cronMatch.js +68 -0
- package/dist/triggers/http.d.ts +59 -0
- package/dist/triggers/http.js +300 -0
- package/package.json +25 -151
- package/NOTICE +0 -12
- package/kestrel.config.ts +0 -28
- package/layers/access/nuxt.config.ts +0 -5
- package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
- package/layers/access/server/middleware/access-guard.ts +0 -32
- package/layers/access/server/types.d.ts +0 -7
- package/layers/admin/app/components/AdminAccount.vue +0 -212
- package/layers/admin/app/components/AdminNav.vue +0 -44
- package/layers/admin/app/components/BlockFields.vue +0 -67
- package/layers/admin/app/components/BlockPreview.vue +0 -413
- package/layers/admin/app/components/BlockTree.vue +0 -433
- package/layers/admin/app/components/BlocksBody.vue +0 -163
- package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
- package/layers/admin/app/components/CollectionEditor.vue +0 -324
- package/layers/admin/app/components/CollectionList.vue +0 -429
- package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
- package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
- package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
- package/layers/admin/app/components/CollectionListPager.vue +0 -70
- package/layers/admin/app/components/CollectionListTable.vue +0 -347
- package/layers/admin/app/components/EditorStatus.vue +0 -211
- package/layers/admin/app/components/EditorUnsupported.vue +0 -28
- package/layers/admin/app/components/FieldsBody.vue +0 -23
- package/layers/admin/app/components/LocaleBar.vue +0 -107
- package/layers/admin/app/components/PageFields.vue +0 -129
- package/layers/admin/app/components/PageFieldsPane.vue +0 -39
- package/layers/admin/app/components/SeoFields.vue +0 -210
- package/layers/admin/app/components/SingletonEditor.vue +0 -93
- package/layers/admin/app/composables/useAuth.ts +0 -68
- package/layers/admin/app/composables/useBlockTree.ts +0 -95
- package/layers/admin/app/composables/useBlocks.ts +0 -7
- package/layers/admin/app/composables/useCachedResource.ts +0 -35
- package/layers/admin/app/composables/useCollectionOps.ts +0 -102
- package/layers/admin/app/composables/useCollections.ts +0 -6
- package/layers/admin/app/composables/useContentLocales.ts +0 -23
- package/layers/admin/app/composables/useEditForm.ts +0 -402
- package/layers/admin/app/composables/useLinkResolver.ts +0 -40
- package/layers/admin/app/composables/useListBatchActions.ts +0 -78
- package/layers/admin/app/composables/useListColumns.ts +0 -53
- package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
- package/layers/admin/app/composables/useListRows.ts +0 -75
- package/layers/admin/app/composables/useListSelection.ts +0 -28
- package/layers/admin/app/composables/useListUrlState.ts +0 -53
- package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
- package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
- package/layers/admin/app/composables/usePublishStatus.ts +0 -107
- package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
- package/layers/admin/app/composables/useTheme.ts +0 -36
- package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
- package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
- package/layers/admin/app/layouts/admin.vue +0 -266
- package/layers/admin/app/middleware/admin-auth.ts +0 -4
- package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
- package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
- package/layers/admin/app/pages/admin/index.vue +0 -118
- package/layers/admin/app/pages/admin/login.vue +0 -68
- package/layers/admin/app/pages/admin/media.vue +0 -28
- package/layers/admin/app/pages/admin/references.vue +0 -146
- package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
- package/layers/admin/app/plugins/reauth.client.ts +0 -20
- package/layers/admin/app/utils/admin-style-guard.ts +0 -20
- package/layers/admin/app/utils/block-tree.ts +0 -214
- package/layers/admin/app/utils/collection-ops.ts +0 -69
- package/layers/admin/app/utils/dead-refs.ts +0 -30
- package/layers/admin/app/utils/edit-form.ts +0 -267
- package/layers/admin/app/utils/editor-expose.ts +0 -36
- package/layers/admin/app/utils/editor-form-context.ts +0 -82
- package/layers/admin/app/utils/editor-registry.ts +0 -20
- package/layers/admin/app/utils/field-keys.ts +0 -11
- package/layers/admin/app/utils/list-cell.ts +0 -46
- package/layers/admin/app/utils/list-columns.ts +0 -142
- package/layers/admin/app/utils/list-query.ts +0 -96
- package/layers/admin/app/utils/nav-active.ts +0 -9
- package/layers/admin/app/utils/populate-blocks.ts +0 -288
- package/layers/admin/app/utils/preview-channel.ts +0 -62
- package/layers/admin/app/utils/preview-viewport.ts +0 -80
- package/layers/admin/app/utils/reauth.ts +0 -67
- package/layers/admin/app/utils/record-title.ts +0 -15
- package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
- package/layers/admin/app/utils/safe-redirect.ts +0 -12
- package/layers/admin/nuxt.config.ts +0 -20
- package/layers/auth/nuxt.config.ts +0 -1
- package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
- package/layers/collections/nuxt.config.ts +0 -1
- package/layers/collections/server/plugins/01.register.ts +0 -29
- package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
- package/layers/core/app/composables/layouts.ts +0 -5
- package/layers/core/app/utils/field-factories.ts +0 -129
- package/layers/core/app/utils/layouts.ts +0 -38
- package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
- package/layers/core/modules/auto-discovery/index.ts +0 -112
- package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
- package/layers/core/modules/auto-discovery/scan.ts +0 -170
- package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
- package/layers/core/modules/component-namespace/index.ts +0 -44
- package/layers/core/modules/component-namespace/shared.ts +0 -24
- package/layers/core/modules/kestrel/app-shell.ts +0 -55
- package/layers/core/modules/kestrel/auto-imports.ts +0 -44
- package/layers/core/modules/kestrel/index.ts +0 -144
- package/layers/core/modules/plugin-order/index.ts +0 -69
- package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
- package/layers/core/nuxt.config.ts +0 -10
- package/layers/core/server/api/[...path].ts +0 -98
- package/layers/core/server/plugins/00.config.ts +0 -12
- package/layers/core/server/plugins/00.migrate.ts +0 -12
- package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
- package/layers/core/server/plugins/02.schema-sync.ts +0 -58
- package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
- package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
- package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
- package/layers/core/server/tasks/db/migrate-module.ts +0 -83
- package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
- package/layers/core/server/tasks/db/migrate.ts +0 -50
- package/layers/core/server/utils/server-config.ts +0 -81
- package/layers/fields/nuxt.config.ts +0 -1
- package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
- package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
- package/layers/media/app/components/KestrelImg.vue +0 -94
- package/layers/media/app/components/MediaConflictDialog.vue +0 -35
- package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
- package/layers/media/app/components/MediaGrid.vue +0 -113
- package/layers/media/app/components/MediaImage.vue +0 -70
- package/layers/media/app/components/MediaLibrary.vue +0 -456
- package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
- package/layers/media/app/components/MediaPathBar.vue +0 -163
- package/layers/media/app/components/MediaPicker.vue +0 -19
- package/layers/media/app/components/MediaRenameDialog.vue +0 -32
- package/layers/media/app/components/MediaTable.vue +0 -75
- package/layers/media/app/components/MediaThumb.vue +0 -25
- package/layers/media/app/components/MediaToolbar.vue +0 -90
- package/layers/media/app/components/MediaUploadDialog.vue +0 -87
- package/layers/media/app/components/MediaViewer.vue +0 -137
- package/layers/media/app/components/field/Media.vue +0 -263
- package/layers/media/app/composables/useBlurUp.ts +0 -55
- package/layers/media/app/composables/useMediaClipboard.ts +0 -14
- package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
- package/layers/media/app/composables/useMediaDnd.ts +0 -56
- package/layers/media/app/composables/useMediaLibrary.ts +0 -152
- package/layers/media/app/composables/useMediaOps.ts +0 -50
- package/layers/media/app/composables/useMediaRelocate.ts +0 -39
- package/layers/media/app/composables/useMediaResolver.ts +0 -50
- package/layers/media/app/composables/useMediaUpload.ts +0 -93
- package/layers/media/app/composables/useMediaVariant.ts +0 -34
- package/layers/media/app/plugins/field-media.client.ts +0 -6
- package/layers/media/app/utils/ai-disclosure.ts +0 -19
- package/layers/media/app/utils/dnd.ts +0 -69
- package/layers/media/app/utils/library.ts +0 -81
- package/layers/media/app/utils/ops.ts +0 -60
- package/layers/media/app/utils/picture-model.ts +0 -125
- package/layers/media/app/utils/thumbhash.ts +0 -19
- package/layers/media/app/utils/variant-name.ts +0 -20
- package/layers/media/nuxt.config.ts +0 -7
- package/layers/media/server/middleware/ondemand-variants.ts +0 -38
- package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
- package/layers/media/server/plugins/02.register-media.ts +0 -22
- package/layers/media/server/plugins/04.variant-capture.ts +0 -27
- package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
- package/layers/media/server/tasks/media/backfill.ts +0 -17
- package/layers/public/app/app.vue +0 -5
- package/layers/public/app/components/BlockRenderer.vue +0 -56
- package/layers/public/app/components/KestrelLink.vue +0 -26
- package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
- package/layers/public/app/composables/public-page.ts +0 -24
- package/layers/public/app/error.vue +0 -12
- package/layers/public/app/layouts/default.vue +0 -5
- package/layers/public/app/pages/[...slug].vue +0 -239
- package/layers/public/app/pages/__kestrel/preview.vue +0 -46
- package/layers/public/app/utils/block-edit-context.ts +0 -16
- package/layers/public/app/utils/json-ld.ts +0 -139
- package/layers/public/app/utils/link-href.ts +0 -46
- package/layers/public/app/utils/page-head.ts +0 -84
- package/layers/public/app/utils/page-layout.ts +0 -18
- package/layers/public/app/utils/preview-protocol.ts +0 -117
- package/layers/public/app/utils/public-route.ts +0 -46
- package/layers/public/app/utils/site-head.ts +0 -43
- package/layers/public/modules/deploy-output/index.ts +0 -80
- package/layers/public/modules/prerender-routes/discover.ts +0 -37
- package/layers/public/modules/prerender-routes/index.ts +0 -88
- package/layers/public/modules/prerender-routes/routes.ts +0 -16
- package/layers/public/modules/prune-media/index.ts +0 -48
- package/layers/public/modules/prune-media/prune-media.ts +0 -81
- package/layers/public/nuxt.config.ts +0 -14
- package/layers/public/server/delivery-live/serve.ts +0 -111
- package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
- package/layers/public/server/pipelines/index.ts +0 -23
- package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
- package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
- package/layers/public/server/plugins/02.register-links.ts +0 -16
- package/layers/public/server/plugins/03.redirects.ts +0 -55
- package/layers/public/server/plugins/05.plan-publish.ts +0 -5
- package/layers/public/server/plugins/zz.publish.ts +0 -127
- package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
- package/layers/public/server/routes/llms.txt.get.ts +0 -66
- package/layers/public/server/routes/redirects.json.get.ts +0 -58
- package/layers/public/server/routes/robots.txt.get.ts +0 -11
- package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
- package/layers/public/server/tasks/publish/run.ts +0 -32
- package/layers/public/server/utils/publish/render-live.ts +0 -26
- package/layers/ui/app/assets/scss/_base.scss +0 -16
- package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
- package/layers/ui/app/assets/scss/_mixins.scss +0 -50
- package/layers/ui/app/assets/scss/_reset.scss +0 -65
- package/layers/ui/app/assets/scss/_tokens.scss +0 -183
- package/layers/ui/app/assets/scss/main.scss +0 -4
- package/layers/ui/app/components/field/Boolean.vue +0 -30
- package/layers/ui/app/components/field/Choice.vue +0 -73
- package/layers/ui/app/components/field/Datetime.vue +0 -98
- package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
- package/layers/ui/app/components/field/Json.vue +0 -62
- package/layers/ui/app/components/field/Layout.vue +0 -138
- package/layers/ui/app/components/field/Link.vue +0 -112
- package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
- package/layers/ui/app/components/field/Number.vue +0 -30
- package/layers/ui/app/components/field/Relation.vue +0 -49
- package/layers/ui/app/components/field/Renderer.vue +0 -30
- package/layers/ui/app/components/field/Repeater.vue +0 -460
- package/layers/ui/app/components/field/Richtext.vue +0 -26
- package/layers/ui/app/components/field/Slug.vue +0 -65
- package/layers/ui/app/components/field/Text.vue +0 -35
- package/layers/ui/app/components/field/Unsupported.vue +0 -34
- package/layers/ui/app/components/ui/Alert.vue +0 -50
- package/layers/ui/app/components/ui/Brand.vue +0 -22
- package/layers/ui/app/components/ui/Button.vue +0 -116
- package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
- package/layers/ui/app/components/ui/Checkbox.vue +0 -53
- package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
- package/layers/ui/app/components/ui/Combobox.vue +0 -345
- package/layers/ui/app/components/ui/DatePicker.vue +0 -92
- package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
- package/layers/ui/app/components/ui/Dialog.vue +0 -107
- package/layers/ui/app/components/ui/EmptyState.vue +0 -46
- package/layers/ui/app/components/ui/Field.vue +0 -58
- package/layers/ui/app/components/ui/Fieldset.vue +0 -65
- package/layers/ui/app/components/ui/Icon.vue +0 -44
- package/layers/ui/app/components/ui/Menu.vue +0 -54
- package/layers/ui/app/components/ui/NumberInput.vue +0 -175
- package/layers/ui/app/components/ui/Richtext.vue +0 -141
- package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
- package/layers/ui/app/components/ui/Select.vue +0 -52
- package/layers/ui/app/components/ui/TextInput.vue +0 -111
- package/layers/ui/app/components/ui/Textarea.vue +0 -39
- package/layers/ui/app/components/ui/TimeInput.vue +0 -18
- package/layers/ui/app/components/ui/Toasts.vue +0 -103
- package/layers/ui/app/components/ui/Tooltip.vue +0 -45
- package/layers/ui/app/composables/useAdminLang.ts +0 -8
- package/layers/ui/app/composables/useDragReorder.ts +0 -41
- package/layers/ui/app/composables/useEchoGuard.ts +0 -28
- package/layers/ui/app/composables/useLinkField.ts +0 -70
- package/layers/ui/app/composables/useRecordOptions.ts +0 -84
- package/layers/ui/app/composables/useRepeater.ts +0 -99
- package/layers/ui/app/composables/useT.ts +0 -32
- package/layers/ui/app/composables/useToast.ts +0 -46
- package/layers/ui/app/i18n/de.ts +0 -432
- package/layers/ui/app/i18n/en.ts +0 -434
- package/layers/ui/app/utils/boolean-field.ts +0 -10
- package/layers/ui/app/utils/field-component.ts +0 -22
- package/layers/ui/app/utils/field-empty.ts +0 -21
- package/layers/ui/app/utils/field-registry.ts +0 -36
- package/layers/ui/app/utils/humanize.ts +0 -14
- package/layers/ui/app/utils/icons.ts +0 -143
- package/layers/ui/app/utils/iso-date.ts +0 -18
- package/layers/ui/app/utils/localized.ts +0 -13
- package/layers/ui/app/utils/record-context.ts +0 -15
- package/layers/ui/app/utils/reorder.ts +0 -8
- package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
- package/layers/ui/app/utils/useFieldA11y.ts +0 -40
- package/layers/ui/nuxt.config.ts +0 -9
- package/nuxt.config.ts +0 -108
- package/scripts/consumer-template-ci.mjs +0 -160
- package/scripts/copy-create-payload.mjs +0 -49
- package/scripts/coverage-all.mjs +0 -163
- package/scripts/dashboard.mjs +0 -89
- package/scripts/hash-password.mjs +0 -29
- package/scripts/kestrel.mjs +0 -240
- package/scripts/lib/cli.mjs +0 -129
- package/scripts/lib/coverage-model.mjs +0 -51
- package/scripts/lib/password.mjs +0 -19
- package/scripts/lib/scaffold.mjs +0 -205
- package/scripts/port-graph.mjs +0 -350
- package/scripts/typecheck.mjs +0 -62
- package/templates/starter/README.md +0 -65
- package/templates/starter/_env.example +0 -17
- package/templates/starter/_gitignore +0 -26
- package/templates/starter/_package.json +0 -22
- package/templates/starter/app/app.vue +0 -7
- package/templates/starter/app/blocks/Prose.vue +0 -13
- package/templates/starter/app/layouts/default.vue +0 -6
- package/templates/starter/nuxt.config.ts +0 -23
- package/templates/starter/pnpm-workspace.yaml +0 -8
- 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
|
-
}
|