@michaelthielemann/kestrel 3.0.0 → 4.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.
- package/README.md +35 -110
- package/kestrel.config.ts +1 -1
- package/layers/access/server/middleware/00.ip-allowlist.ts +3 -18
- package/layers/access/server/middleware/access-guard.ts +25 -23
- package/layers/access/server/types.d.ts +1 -2
- package/layers/admin/app/components/AdminAccount.vue +1 -1
- package/layers/admin/app/components/BlockFields.vue +1 -2
- package/layers/admin/app/components/BlockPreview.vue +17 -5
- package/layers/admin/app/components/BlocksBody.vue +1 -1
- package/layers/admin/app/components/CollectionEditor.vue +35 -16
- package/layers/admin/app/components/CollectionList.vue +34 -3
- package/layers/admin/app/components/CollectionListBulkBar.vue +17 -3
- package/layers/admin/app/components/CollectionListFilterPanel.vue +1 -1
- package/layers/admin/app/components/CollectionListPager.vue +1 -1
- package/layers/admin/app/components/CollectionListTable.vue +41 -2
- package/layers/admin/app/components/EditorStatus.vue +8 -7
- package/layers/admin/app/components/LocaleBar.vue +3 -1
- package/layers/admin/app/components/PageFields.vue +2 -4
- package/layers/admin/app/components/SeoFields.vue +2 -4
- package/layers/admin/app/components/SingletonEditor.vue +3 -2
- package/layers/admin/app/composables/useAuth.ts +3 -3
- package/layers/admin/app/composables/useBlockTree.ts +1 -1
- package/layers/admin/app/composables/useBlocks.ts +1 -1
- package/layers/admin/app/composables/useCollectionOps.ts +45 -8
- package/layers/admin/app/composables/useCollections.ts +1 -2
- package/layers/admin/app/composables/useEditForm.ts +35 -17
- package/layers/admin/app/composables/useLinkResolver.ts +1 -1
- package/layers/admin/app/composables/useListBatchActions.ts +20 -3
- package/layers/admin/app/composables/useListColumns.ts +1 -1
- package/layers/admin/app/composables/useListFilterDraft.ts +3 -2
- package/layers/admin/app/composables/useListRows.ts +7 -2
- package/layers/admin/app/composables/useListUrlState.ts +1 -1
- package/layers/admin/app/composables/usePublishStatus.ts +2 -2
- package/layers/admin/app/layouts/admin.vue +0 -1
- package/layers/admin/app/pages/admin/[collection]/[id].vue +5 -4
- package/layers/admin/app/pages/admin/index.vue +1 -1
- package/layers/admin/app/pages/admin/references.vue +2 -2
- package/layers/admin/app/utils/block-tree.ts +1 -1
- package/layers/admin/app/utils/collection-ops.ts +28 -3
- package/layers/admin/app/utils/edit-form.ts +34 -2
- package/layers/admin/app/utils/editor-expose.ts +2 -0
- package/layers/admin/app/utils/editor-form-context.ts +1 -2
- package/layers/admin/app/utils/field-keys.ts +1 -1
- package/layers/admin/app/utils/list-columns.ts +3 -2
- package/layers/admin/app/utils/list-query.ts +2 -2
- package/layers/admin/app/utils/populate-blocks.ts +2 -2
- package/layers/admin/app/utils/reauth.ts +5 -6
- package/layers/auth/server/plugins/01.register-auth-pipelines.ts +7 -0
- package/layers/collections/server/plugins/01.register.ts +9 -12
- package/layers/collections/server/plugins/02.register-relation-populate.ts +5 -7
- package/layers/{fields → core}/app/utils/field-factories.ts +1 -1
- package/layers/core/modules/auto-discovery/extract-block.ts +3 -3
- package/layers/core/modules/auto-discovery/index.ts +68 -11
- package/layers/core/modules/auto-discovery/package-registry.ts +19 -0
- package/layers/core/modules/auto-discovery/scan.ts +77 -0
- package/layers/core/modules/auto-discovery/virtual.d.ts +5 -0
- package/layers/core/modules/component-namespace/index.ts +24 -0
- package/layers/core/modules/kestrel/index.ts +6 -1
- package/layers/core/modules/plugin-order/index.ts +69 -0
- package/layers/core/modules/plugin-order/plugin-order.ts +273 -0
- package/layers/core/nuxt.config.ts +1 -0
- package/layers/core/server/api/[...path].ts +98 -0
- package/layers/core/server/plugins/00.config.ts +12 -0
- package/layers/core/server/plugins/00.migrate.ts +1 -0
- package/layers/core/server/plugins/01.register-introspection-pipeline.ts +5 -0
- package/layers/core/server/plugins/01.register-openapi-pipeline.ts +5 -0
- package/layers/core/server/plugins/01.register-tooling-pipelines.ts +5 -0
- package/layers/core/server/plugins/02.schema-sync.ts +2 -7
- package/layers/core/server/plugins/04.outbox-worker.ts +31 -0
- package/layers/core/server/plugins/05.reindex-refs.ts +5 -0
- package/layers/core/server/routes/__kestrel/dashboard.get.ts +29 -0
- package/layers/core/server/tasks/db/migrate-module.ts +83 -0
- package/layers/core/server/tasks/db/migrate-revisions.ts +20 -0
- package/layers/core/server/tasks/db/migrate.ts +2 -6
- package/layers/core/server/utils/server-config.ts +66 -3
- package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +7 -0
- package/layers/fields/server/plugins/01.register-field-populate.ts +2 -2
- package/layers/media/app/components/KestrelImg.vue +3 -3
- package/layers/media/app/components/MediaLibrary.vue +2 -2
- package/layers/media/app/components/field/Media.vue +1 -1
- package/layers/media/app/composables/useMediaResolver.ts +1 -1
- package/layers/media/app/composables/useMediaUpload.ts +1 -1
- package/layers/media/app/composables/useMediaVariant.ts +2 -2
- package/layers/media/app/utils/ai-disclosure.ts +1 -1
- package/layers/media/app/utils/library.ts +1 -1
- package/layers/media/app/utils/ops.ts +1 -3
- package/layers/media/app/utils/picture-model.ts +2 -2
- package/layers/media/app/utils/variant-name.ts +1 -1
- package/layers/media/server/middleware/ondemand-variants.ts +6 -5
- package/layers/media/server/plugins/01.register-media-pipelines.ts +7 -0
- package/layers/media/server/plugins/02.register-media.ts +10 -7
- package/layers/media/server/plugins/04.variant-capture.ts +12 -8
- package/layers/media/server/plugins/05.media-cleanup.ts +5 -0
- package/layers/media/server/tasks/media/backfill.ts +3 -4
- package/layers/public/app/components/KestrelLink.vue +1 -1
- package/layers/public/app/pages/[...slug].vue +1 -1
- package/layers/public/app/pages/__kestrel/preview.vue +2 -2
- package/layers/public/app/utils/json-ld.ts +1 -1
- package/layers/public/app/utils/link-href.ts +1 -1
- package/layers/public/app/utils/page-head.ts +1 -1
- package/layers/public/modules/deploy-output/index.ts +4 -6
- package/layers/public/modules/prerender-routes/index.ts +4 -4
- package/layers/public/modules/prerender-routes/routes.ts +1 -1
- package/layers/public/modules/prune-media/index.ts +2 -2
- package/layers/public/server/delivery-live/serve.ts +111 -0
- package/layers/public/server/middleware/delivery-live-catchall.ts +7 -0
- package/layers/public/server/pipelines/index.ts +23 -0
- package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +14 -0
- package/layers/public/server/plugins/01.register-public-pipelines.ts +7 -0
- package/layers/public/server/plugins/02.register-links.ts +2 -3
- package/layers/public/server/plugins/03.redirects.ts +41 -23
- package/layers/public/server/plugins/05.plan-publish.ts +5 -0
- package/layers/public/server/plugins/zz.publish.ts +79 -24
- package/layers/public/server/routes/llms-full.txt.get.ts +5 -5
- package/layers/public/server/routes/llms.txt.get.ts +4 -2
- package/layers/public/server/routes/redirects.json.get.ts +2 -2
- package/layers/public/server/routes/robots.txt.get.ts +2 -0
- package/layers/public/server/routes/sitemap.xml.get.ts +16 -6
- package/layers/public/server/tasks/publish/run.ts +9 -4
- package/layers/public/server/utils/publish/render-live.ts +26 -0
- package/layers/ui/app/components/field/Choice.vue +1 -1
- package/layers/ui/app/components/field/Datetime.vue +1 -1
- package/layers/ui/app/components/field/Json.vue +1 -2
- package/layers/ui/app/components/field/Layout.vue +2 -4
- package/layers/ui/app/components/field/Link.vue +1 -1
- package/layers/ui/app/components/field/LinkInternalPicker.vue +3 -3
- package/layers/ui/app/components/field/Number.vue +2 -2
- package/layers/ui/app/components/field/Relation.vue +1 -1
- package/layers/ui/app/components/field/Repeater.vue +6 -8
- package/layers/ui/app/components/field/Slug.vue +2 -3
- package/layers/ui/app/components/field/Text.vue +1 -1
- package/layers/ui/app/components/ui/Button.vue +2 -2
- package/layers/ui/app/components/ui/ButtonGroup.vue +2 -2
- package/layers/ui/app/components/ui/Checkbox.vue +2 -2
- package/layers/ui/app/components/ui/Combobox.vue +5 -5
- package/layers/ui/app/components/ui/DatePicker.vue +2 -2
- package/layers/ui/app/components/ui/DateRangePicker.vue +2 -2
- package/layers/ui/app/components/ui/NumberInput.vue +4 -4
- package/layers/ui/app/components/ui/Richtext.vue +1 -1
- package/layers/ui/app/components/ui/RichtextToolbar.vue +1 -3
- package/layers/ui/app/components/ui/Select.vue +2 -2
- package/layers/ui/app/components/ui/TextInput.vue +4 -4
- package/layers/ui/app/components/ui/Textarea.vue +2 -2
- package/layers/ui/app/components/ui/TimeInput.vue +2 -2
- package/layers/ui/app/components/ui/Toasts.vue +3 -3
- package/layers/ui/app/composables/useLinkField.ts +1 -1
- package/layers/ui/app/composables/useRepeater.ts +1 -1
- package/layers/ui/app/i18n/de.ts +7 -7
- package/layers/ui/app/i18n/en.ts +7 -39
- package/layers/ui/app/utils/field-component.ts +1 -1
- package/layers/ui/app/utils/field-registry.ts +1 -1
- package/layers/ui/app/utils/icons.ts +2 -6
- package/layers/ui/app/utils/localized.ts +1 -1
- package/nuxt.config.ts +1 -1
- package/package.json +53 -19
- package/scripts/consumer-template-ci.mjs +157 -0
- package/scripts/coverage-all.mjs +163 -0
- package/scripts/dashboard.mjs +89 -0
- package/scripts/hash-password.mjs +1 -1
- package/scripts/lib/coverage-model.mjs +51 -0
- package/scripts/port-graph.mjs +350 -0
- package/scripts/typecheck.mjs +11 -0
- package/templates/starter/app/blocks/Prose.vue +1 -1
- package/layers/access/server/utils/csrf.ts +0 -39
- package/layers/access/server/utils/grant-registry.ts +0 -36
- package/layers/access/server/utils/guard.ts +0 -44
- package/layers/access/server/utils/ip-allowlist.ts +0 -79
- package/layers/access/server/utils/policy.ts +0 -98
- package/layers/access/server/utils/public-resources.ts +0 -11
- package/layers/access/server/utils/render-context.ts +0 -41
- package/layers/access/server/utils/require-admin.ts +0 -15
- package/layers/admin/app/pages/admin/[collection]/[id].nuxt.test.ts +0 -200
- package/layers/admin/app/pages/admin/[collection]/publish-preview.nuxt.test.ts +0 -142
- package/layers/auth/server/api/auth/login.post.ts +0 -38
- package/layers/auth/server/api/auth/logout.post.ts +0 -9
- package/layers/auth/server/api/auth/session.get.ts +0 -1
- package/layers/auth/server/utils/admin-credential.ts +0 -42
- package/layers/auth/server/utils/client-ip.ts +0 -88
- package/layers/auth/server/utils/login-throttle.ts +0 -126
- package/layers/auth/server/utils/password.ts +0 -52
- package/layers/auth/server/utils/session-cookie.ts +0 -44
- package/layers/auth/server/utils/session-epoch.ts +0 -96
- package/layers/auth/server/utils/session.ts +0 -88
- package/layers/collections/server/collections/pages.ts +0 -19
- package/layers/collections/server/utils/populate-relations.ts +0 -79
- package/layers/core/app/utils/bulk-actions.ts +0 -9
- package/layers/core/app/utils/filter-ops.ts +0 -98
- package/layers/core/app/utils/list-limits.ts +0 -27
- package/layers/core/app/utils/locale-path.ts +0 -12
- package/layers/core/app/utils/slugify.ts +0 -16
- package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +0 -8
- package/layers/core/server/api/[collection]/[id]/translations.get.ts +0 -4
- package/layers/core/server/api/[collection]/[id].delete.ts +0 -5
- package/layers/core/server/api/[collection]/[id].get.ts +0 -11
- package/layers/core/server/api/[collection]/[id].patch.ts +0 -8
- package/layers/core/server/api/[collection]/bulk.post.ts +0 -37
- package/layers/core/server/api/[collection]/index.get.ts +0 -24
- package/layers/core/server/api/[collection]/index.post.ts +0 -8
- package/layers/core/server/api/[collection]/index.put.test.ts +0 -76
- package/layers/core/server/api/[collection]/index.put.ts +0 -24
- package/layers/core/server/api/[collection]/options.get.test.ts +0 -60
- package/layers/core/server/api/[collection]/options.get.ts +0 -18
- package/layers/core/server/api/[collection]/populate-scope.test.ts +0 -119
- package/layers/core/server/api/[collection]/translations.get.test.ts +0 -90
- package/layers/core/server/api/[collection]/translations.get.ts +0 -25
- package/layers/core/server/api/collections/[name].get.ts +0 -4
- package/layers/core/server/api/collections/index.get.ts +0 -3
- package/layers/core/server/api/references/broken.get.ts +0 -9
- package/layers/core/server/api/references/referrers.get.ts +0 -24
- package/layers/core/server/database/record-refs.ts +0 -20
- package/layers/core/server/plugins/03.record-refs.ts +0 -12
- package/layers/core/server/schema/bootstrap.ts +0 -41
- package/layers/core/server/schema/desired.ts +0 -99
- package/layers/core/server/schema/dev-sync.ts +0 -23
- package/layers/core/server/schema/dialect.ts +0 -61
- package/layers/core/server/schema/diff.ts +0 -110
- package/layers/core/server/schema/introspect.ts +0 -68
- package/layers/core/server/schema/model.ts +0 -61
- package/layers/core/server/schema/render-sqlite.ts +0 -89
- package/layers/core/server/schema/sync.ts +0 -201
- package/layers/core/server/utils/blocks.ts +0 -6
- package/layers/core/server/utils/collection-types.ts +0 -28
- package/layers/core/server/utils/crud.ts +0 -578
- package/layers/core/server/utils/db.ts +0 -26
- package/layers/core/server/utils/defineCollection.ts +0 -213
- package/layers/core/server/utils/duplicate.ts +0 -162
- package/layers/core/server/utils/field-layout.ts +0 -125
- package/layers/core/server/utils/filter-predicate.ts +0 -101
- package/layers/core/server/utils/http.ts +0 -50
- package/layers/core/server/utils/if-unmodified.ts +0 -13
- package/layers/core/server/utils/kestrel-config.ts +0 -451
- package/layers/core/server/utils/key-lock.ts +0 -33
- package/layers/core/server/utils/locale.ts +0 -53
- package/layers/core/server/utils/page-route.ts +0 -58
- package/layers/core/server/utils/page-slug.ts +0 -127
- package/layers/core/server/utils/picker.ts +0 -82
- package/layers/core/server/utils/populate.ts +0 -50
- package/layers/core/server/utils/prerender-memo.ts +0 -23
- package/layers/core/server/utils/read-capture.ts +0 -37
- package/layers/core/server/utils/record-ref-index.ts +0 -200
- package/layers/core/server/utils/registry.ts +0 -24
- package/layers/core/server/utils/resolve-scope.ts +0 -79
- package/layers/core/server/utils/seo.ts +0 -42
- package/layers/core/server/utils/serialize-collection.ts +0 -147
- package/layers/core/server/utils/sql.ts +0 -2
- package/layers/core/server/utils/storage.local.ts +0 -117
- package/layers/core/server/utils/storage.s3.ts +0 -197
- package/layers/core/server/utils/storage.ts +0 -90
- package/layers/core/server/utils/write-effects.ts +0 -40
- package/layers/core/server/utils/write-events.ts +0 -31
- package/layers/fields/app/utils/condition.ts +0 -71
- package/layers/fields/app/utils/define-block.ts +0 -13
- package/layers/fields/app/utils/field-constraints.ts +0 -17
- package/layers/fields/app/utils/field-meta.ts +0 -33
- package/layers/fields/app/utils/field-validate.ts +0 -75
- package/layers/fields/app/utils/field-value.ts +0 -7
- package/layers/fields/app/utils/richtext-links.ts +0 -48
- package/layers/fields/server/api/blocks/index.get.ts +0 -9
- package/layers/fields/server/field-registry/index.ts +0 -249
- package/layers/fields/server/field-registry/naming.ts +0 -10
- package/layers/fields/server/field-registry/sanitize.ts +0 -43
- package/layers/fields/server/field-registry/types.ts +0 -19
- package/layers/fields/server/utils/block-ids.ts +0 -43
- package/layers/fields/server/utils/block-populate.ts +0 -41
- package/layers/fields/server/utils/buildCollection.ts +0 -90
- package/layers/fields/server/utils/buildFieldSchema.ts +0 -22
- package/layers/fields/server/utils/buildTable.ts +0 -113
- package/layers/fields/server/utils/conditional-required.ts +0 -25
- package/layers/fields/server/utils/defineBlock.ts +0 -86
- package/layers/fields/server/utils/defineFieldType.ts +0 -33
- package/layers/fields/server/utils/extract-refs.ts +0 -152
- package/layers/fields/server/utils/field-populate.ts +0 -68
- package/layers/media/server/api/media/[id]/usages.get.ts +0 -8
- package/layers/media/server/api/media/[id].delete.ts +0 -18
- package/layers/media/server/api/media/[id].get.ts +0 -10
- package/layers/media/server/api/media/[id].patch.test.ts +0 -66
- package/layers/media/server/api/media/[id].patch.ts +0 -65
- package/layers/media/server/api/media/backfill.post.ts +0 -18
- package/layers/media/server/api/media/copy.post.ts +0 -7
- package/layers/media/server/api/media/delete.post.ts +0 -13
- package/layers/media/server/api/media/folders.post.ts +0 -18
- package/layers/media/server/api/media/index.get.ts +0 -15
- package/layers/media/server/api/media/index.post.ts +0 -173
- package/layers/media/server/api/media/library.get.ts +0 -17
- package/layers/media/server/api/media/move.post.ts +0 -7
- package/layers/media/server/api/media/rename.post.ts +0 -7
- package/layers/media/server/api/media/resolve.get.ts +0 -13
- package/layers/media/server/collections/media-settings.ts +0 -25
- package/layers/media/server/collections/media.ts +0 -43
- package/layers/media/server/database/folders.ts +0 -10
- package/layers/media/server/plugins/03.media-cleanup.ts +0 -20
- package/layers/media/server/schema-tables/folders.ts +0 -4
- package/layers/media/server/utils/ai-disclosure-enabled.ts +0 -16
- package/layers/media/server/utils/ai-signal-detect.ts +0 -155
- package/layers/media/server/utils/backfill.ts +0 -144
- package/layers/media/server/utils/derive.ts +0 -59
- package/layers/media/server/utils/folder-paths.ts +0 -42
- package/layers/media/server/utils/folders.ts +0 -12
- package/layers/media/server/utils/library.ts +0 -91
- package/layers/media/server/utils/media-cleanup.ts +0 -27
- package/layers/media/server/utils/media-enabled.ts +0 -23
- package/layers/media/server/utils/media-ops.ts +0 -118
- package/layers/media/server/utils/media-write.ts +0 -13
- package/layers/media/server/utils/naming.ts +0 -52
- package/layers/media/server/utils/ondemand.ts +0 -69
- package/layers/media/server/utils/persist-upload.ts +0 -62
- package/layers/media/server/utils/populate.ts +0 -47
- package/layers/media/server/utils/record.ts +0 -71
- package/layers/media/server/utils/relocate-ops.ts +0 -341
- package/layers/media/server/utils/relocate-plan.ts +0 -63
- package/layers/media/server/utils/resolve.ts +0 -98
- package/layers/media/server/utils/sanitize-svg.ts +0 -12
- package/layers/media/server/utils/sniff.ts +0 -68
- package/layers/media/server/utils/storage-relocate.ts +0 -139
- package/layers/media/server/utils/translations.ts +0 -19
- package/layers/media/server/utils/usages.ts +0 -66
- package/layers/media/server/utils/variant-capture.ts +0 -70
- package/layers/media/server/utils/variants.ts +0 -123
- package/layers/public/modules/deploy-output/deploy-output.ts +0 -384
- package/layers/public/server/api/links/resolve.get.ts +0 -19
- package/layers/public/server/api/preview.get.ts +0 -28
- package/layers/public/server/api/preview.post.ts +0 -93
- package/layers/public/server/api/publish-status.get.ts +0 -58
- package/layers/public/server/api/publish.post.ts +0 -84
- package/layers/public/server/api/route.get.ts +0 -54
- package/layers/public/server/collections/redirects.ts +0 -75
- package/layers/public/server/collections/site.ts +0 -44
- package/layers/public/server/database/publish-deps.ts +0 -12
- package/layers/public/server/database/publish-status.ts +0 -19
- package/layers/public/server/schema-tables/publish-deps.ts +0 -4
- package/layers/public/server/schema-tables/publish-status.ts +0 -4
- package/layers/public/server/utils/link-resolve.ts +0 -38
- package/layers/public/server/utils/llms-full.ts +0 -125
- package/layers/public/server/utils/llms.ts +0 -52
- package/layers/public/server/utils/page-resolve.ts +0 -208
- package/layers/public/server/utils/populate-links.ts +0 -66
- package/layers/public/server/utils/preview-token.ts +0 -109
- package/layers/public/server/utils/publish/coalesce.ts +0 -22
- package/layers/public/server/utils/publish/deps-persistence.ts +0 -43
- package/layers/public/server/utils/publish/deps.ts +0 -87
- package/layers/public/server/utils/publish/invalidation.ts +0 -225
- package/layers/public/server/utils/publish/pending.ts +0 -74
- package/layers/public/server/utils/publish/publish-runtime.ts +0 -28
- package/layers/public/server/utils/publish/publish-status.ts +0 -72
- package/layers/public/server/utils/publish/publisher.ts +0 -362
- package/layers/public/server/utils/publish/queue.ts +0 -67
- package/layers/public/server/utils/publish/redirect-rules.ts +0 -221
- package/layers/public/server/utils/publish/redirects-artifact.ts +0 -20
- package/layers/public/server/utils/publish/route-for-record.ts +0 -16
- package/layers/public/server/utils/publish/route-keys.ts +0 -13
- package/layers/public/server/utils/richtext-markdown.ts +0 -260
- package/layers/public/server/utils/site-url.ts +0 -40
- package/layers/public/server/utils/sitemap.ts +0 -99
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { StorageDriver } from '../../../../core/server/utils/storage'
|
|
2
|
-
import { contentTypeFor, cacheControlFor } from '../../../modules/deploy-output/deploy-output'
|
|
3
|
-
import { compileRedirects, serializeRedirects } from './redirect-rules'
|
|
4
|
-
|
|
5
|
-
/** The collection and the repeater field the artifact is compiled from, named once. */
|
|
6
|
-
export const REDIRECTS_COLLECTION = 'redirects'
|
|
7
|
-
export const REDIRECTS_FIELD = 'rules'
|
|
8
|
-
|
|
9
|
-
/** Literal key at the output root — a sibling of `index.html`, not a child of it. The driver's root IS
|
|
10
|
-
* the output root (local `output.dir`, or the S3 prefix), so a key can never sit *beside* that tree. */
|
|
11
|
-
export const REDIRECTS_KEY = 'redirects.json'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Compile the editor's rows and publish them. Compilation runs first so an unpublishable rule fails
|
|
15
|
-
* before the driver is touched, and the writer never swallows: a rejection is the caller's to surface.
|
|
16
|
-
*/
|
|
17
|
-
export async function writeRedirectsArtifact(rows: unknown, driver: StorageDriver): Promise<void> {
|
|
18
|
-
const body = Buffer.from(serializeRedirects(compileRedirects(rows)))
|
|
19
|
-
await driver.put(REDIRECTS_KEY, body, contentTypeFor(REDIRECTS_KEY), { cacheControl: cacheControlFor(REDIRECTS_KEY) })
|
|
20
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { pageRowHref } from '../../../../core/server/utils/page-route'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The static route a page-like record publishes to, or null when it has none — a non-pageLike collection,
|
|
5
|
-
* or a row with no `path` (a blank slug, or no row at all). Adds the `pageLike` gate on top of the shared
|
|
6
|
-
* `pageRowHref` rule (the publish-status endpoint's null cases); the locale falls back to the primary for
|
|
7
|
-
* a row without one (non-translatable).
|
|
8
|
-
*/
|
|
9
|
-
export function routeForRecord(
|
|
10
|
-
row: { path?: unknown; locale?: unknown } | null | undefined,
|
|
11
|
-
pageLike: boolean,
|
|
12
|
-
primaryLocale: string,
|
|
13
|
-
prefixPrimary: boolean,
|
|
14
|
-
): string | null {
|
|
15
|
-
return pageLike ? pageRowHref(row, primaryLocale, prefixPrimary) : null
|
|
16
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Map a public route to its static HTML file key, mirroring Nitro's prerender layout:
|
|
3
|
-
* `/` → `index.html`, `/about` → `about/index.html`, `/de/x` → `de/x/index.html`.
|
|
4
|
-
* Throws on a route that can't be a safe relative key (traversal `..`, query `?`, fragment `#`,
|
|
5
|
-
* or a backslash) so a bad path can never escape the output dir / S3 prefix.
|
|
6
|
-
*/
|
|
7
|
-
export function htmlKeyForRoute(route: string): string {
|
|
8
|
-
if (/[?#\\]/.test(route) || route.split('/').some((seg) => seg === '..')) {
|
|
9
|
-
throw new Error(`unsafe route for a static key: ${JSON.stringify(route)}`)
|
|
10
|
-
}
|
|
11
|
-
const trimmed = route.replace(/^\/+/, '').replace(/\/+$/, '')
|
|
12
|
-
return trimmed === '' ? 'index.html' : `${trimmed}/index.html`
|
|
13
|
-
}
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Richtext HTML → Markdown, for the agent-facing `llms-full.txt`. Answer engines consume Markdown, not
|
|
3
|
-
* markup, and a stripped-tags dump would lose exactly the structure they retrieve on (headings, lists,
|
|
4
|
-
* links).
|
|
5
|
-
*
|
|
6
|
-
* A hand-rolled parser rather than a DOM library because the input dialect is CLOSED and already
|
|
7
|
-
* well-formed: every stored richtext value passed `sanitizeRichtext` on write, so it is sanitize-html's
|
|
8
|
-
* own re-serialization of a parsed tree, restricted to `RICHTEXT_ALLOWLIST`. That keeps this pure — no
|
|
9
|
-
* DOM, no Nuxt, no new dependency in the public layer's server bundle — so it unit-tests hard.
|
|
10
|
-
*
|
|
11
|
-
* Deliberately minimal escaping: only link text (whose brackets would break the link syntax) and a
|
|
12
|
-
* leading block marker (which would silently turn a paragraph into a list item or heading). Escaping
|
|
13
|
-
* every markdown metacharacter would make the output markedly harder to read for the model that reads it.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export interface RichtextMarkdownOptions {
|
|
17
|
-
/** Push every heading down by this many levels (clamped at h6), so a body nests under the document
|
|
18
|
-
* headings the caller already emitted. */
|
|
19
|
-
headingOffset?: number
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type MdNode =
|
|
23
|
-
| { kind: 'text'; value: string }
|
|
24
|
-
| { kind: 'el'; tag: string; attrs: Record<string, string>; children: MdNode[] }
|
|
25
|
-
|
|
26
|
-
const VOID_TAGS = new Set(['br', 'hr', 'img', 'input', 'meta', 'link'])
|
|
27
|
-
const HEADINGS: Record<string, number> = { h1: 1, h2: 2, h3: 3, h4: 4, h5: 5, h6: 6 }
|
|
28
|
-
const BLOCK_TAGS = new Set(['p', 'blockquote', 'pre', 'ul', 'ol', 'hr', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
|
29
|
-
|
|
30
|
-
const TAG = /<(\/)?([a-zA-Z][a-zA-Z0-9]*)((?:\s+[^\s"'>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*(\/)?>/g
|
|
31
|
-
const ATTR = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g
|
|
32
|
-
|
|
33
|
-
const NAMED_ENTITIES: Record<string, string> = {
|
|
34
|
-
amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ',
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Decode the entity set sanitize-html emits, plus numeric references. ` ` becomes an ordinary
|
|
38
|
-
* space on purpose: a U+00A0 survives whitespace collapsing and shows up as a stray gap. */
|
|
39
|
-
function decodeEntities(raw: string): string {
|
|
40
|
-
return raw.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (match, body: string) => {
|
|
41
|
-
if (body[0] === '#') {
|
|
42
|
-
const code = body[1] === 'x' || body[1] === 'X' ? Number.parseInt(body.slice(2), 16) : Number(body.slice(1))
|
|
43
|
-
return Number.isFinite(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : match
|
|
44
|
-
}
|
|
45
|
-
return NAMED_ENTITIES[body.toLowerCase()] ?? match
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function parseAttrs(raw: string): Record<string, string> {
|
|
50
|
-
const attrs: Record<string, string> = {}
|
|
51
|
-
if (!raw.trim()) return attrs
|
|
52
|
-
ATTR.lastIndex = 0
|
|
53
|
-
let m: RegExpExecArray | null
|
|
54
|
-
while ((m = ATTR.exec(raw))) {
|
|
55
|
-
attrs[m[1]!.toLowerCase()] = decodeEntities(m[2] ?? m[3] ?? m[4] ?? '')
|
|
56
|
-
}
|
|
57
|
-
return attrs
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Tag soup → a node tree. A stray close tag with no matching open is dropped; anything still open at
|
|
61
|
-
* the end is closed implicitly, so a truncated value degrades to the content it did carry. */
|
|
62
|
-
function parse(html: string): MdNode[] {
|
|
63
|
-
const root: MdNode[] = []
|
|
64
|
-
const stack: Array<{ tag: string; children: MdNode[] }> = []
|
|
65
|
-
const top = (): MdNode[] => (stack.length ? stack[stack.length - 1]!.children : root)
|
|
66
|
-
const pushText = (raw: string): void => {
|
|
67
|
-
if (raw) top().push({ kind: 'text', value: decodeEntities(raw) })
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
TAG.lastIndex = 0
|
|
71
|
-
let cursor = 0
|
|
72
|
-
let m: RegExpExecArray | null
|
|
73
|
-
while ((m = TAG.exec(html))) {
|
|
74
|
-
pushText(html.slice(cursor, m.index))
|
|
75
|
-
cursor = TAG.lastIndex
|
|
76
|
-
const tag = m[2]!.toLowerCase()
|
|
77
|
-
if (m[1]) {
|
|
78
|
-
// Close the innermost matching element; an unmatched close tag is ignored rather than unwinding
|
|
79
|
-
// the whole stack, which would drop every sibling that followed it.
|
|
80
|
-
const at = stack.map((s) => s.tag).lastIndexOf(tag)
|
|
81
|
-
if (at >= 0) stack.length = at
|
|
82
|
-
continue
|
|
83
|
-
}
|
|
84
|
-
const node: MdNode = { kind: 'el', tag, attrs: parseAttrs(m[3] ?? ''), children: [] }
|
|
85
|
-
top().push(node)
|
|
86
|
-
if (!m[4] && !VOID_TAGS.has(tag)) stack.push({ tag, children: node.children })
|
|
87
|
-
}
|
|
88
|
-
pushText(html.slice(cursor))
|
|
89
|
-
return root
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Raw text of a subtree, entities already decoded and whitespace untouched (code blocks). */
|
|
93
|
-
function rawText(nodes: MdNode[]): string {
|
|
94
|
-
return nodes.map((n) => (n.kind === 'text' ? n.value : rawText(n.children))).join('')
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const escapeLinkText = (s: string): string => s.replace(/[[\]]/g, '\\$&')
|
|
98
|
-
|
|
99
|
-
const INLINE_WRAP: Record<string, string> = { strong: '**', b: '**', em: '*', i: '*', s: '~~' }
|
|
100
|
-
|
|
101
|
-
/** The longest run of `char` anywhere in `text` — the width a fence/delimiter has to beat to contain it. */
|
|
102
|
-
function longestRun(text: string, char: string): number {
|
|
103
|
-
let longest = 0
|
|
104
|
-
let run = 0
|
|
105
|
-
for (const c of text) {
|
|
106
|
-
run = c === char ? run + 1 : 0
|
|
107
|
-
if (run > longest) longest = run
|
|
108
|
-
}
|
|
109
|
-
return longest
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** `` `code` `` with a delimiter wider than any backtick run inside it (CommonMark's own escape hatch),
|
|
113
|
-
* padded with a space when the content would otherwise fuse with the delimiter. */
|
|
114
|
-
function inlineCode(text: string): string {
|
|
115
|
-
const delimiter = '`'.repeat(longestRun(text, '`') + 1)
|
|
116
|
-
const pad = text.startsWith('`') || text.endsWith('`') ? ' ' : ''
|
|
117
|
-
return `${delimiter}${pad}${text}${pad}${delimiter}`
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function renderInline(nodes: MdNode[]): string {
|
|
121
|
-
let out = ''
|
|
122
|
-
for (const node of nodes) {
|
|
123
|
-
if (node.kind === 'text') { out += node.value.replace(/\s+/g, ' '); continue }
|
|
124
|
-
if (node.tag === 'br') { out += '\n'; continue }
|
|
125
|
-
if (node.tag === 'a') {
|
|
126
|
-
const text = renderInline(node.children).trim()
|
|
127
|
-
const href = node.attrs.href
|
|
128
|
-
// `#` is what the internal-link resolver writes for a target it declined (missing or unpublished) —
|
|
129
|
-
// a dead anchor is worth less than the words it wraps, so keep the words and drop the link.
|
|
130
|
-
out += !href || href === '#' ? text : `[${escapeLinkText(text)}](${href})`
|
|
131
|
-
continue
|
|
132
|
-
}
|
|
133
|
-
const inner = renderInline(node.children)
|
|
134
|
-
if (node.tag === 'code') { out += inner.trim() ? inlineCode(inner.trim()) : inner; continue }
|
|
135
|
-
const wrap = INLINE_WRAP[node.tag]
|
|
136
|
-
if (!wrap || !inner.trim()) { out += inner; continue }
|
|
137
|
-
// CommonMark will not open emphasis on `** bold **` — whitespace immediately inside the delimiter
|
|
138
|
-
// makes it literal asterisks. Editors leave it there routinely (selecting a word plus its space), so
|
|
139
|
-
// move it outside rather than emitting markup that does not parse.
|
|
140
|
-
const [, lead = '', body = '', trail = ''] = /^(\s*)([\s\S]*?)(\s*)$/.exec(inner) ?? []
|
|
141
|
-
out += `${lead}${wrap}${body}${wrap}${trail}`
|
|
142
|
-
}
|
|
143
|
-
return out
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const prefixLines = (text: string, first: string, rest: string): string =>
|
|
147
|
-
text.split('\n').map((line, i) => (i === 0 ? first + line : (line ? rest + line : rest.trimEnd()))).join('\n')
|
|
148
|
-
|
|
149
|
-
/** Escape a line that would otherwise open a Markdown block: a heading, a list item, a quote, a code
|
|
150
|
-
* fence or a thematic break. One backslash on the first marker character defuses all of them (every one
|
|
151
|
-
* is ASCII punctuation, so the escape is valid CommonMark and renders as the literal character). */
|
|
152
|
-
function escapeMarkerLine(line: string): string {
|
|
153
|
-
// CommonMark allows a block marker up to three spaces in; deeper is an indented code block, which no
|
|
154
|
-
// backslash can defuse — and which whitespace collapsing has already removed from converted text.
|
|
155
|
-
const [, indent = '', rest = ''] = /^(\s{0,3})([\s\S]*)$/.exec(line) ?? []
|
|
156
|
-
const ordered = /^(\d{1,9})[.)](\s|$)/.exec(rest)
|
|
157
|
-
if (ordered) return `${indent}${ordered[1]}\\${rest.slice(ordered[1]!.length)}`
|
|
158
|
-
const opensBlock = /^#{1,6}(\s|$)/.test(rest) // heading, any level
|
|
159
|
-
|| /^[-+*](\s|$)/.test(rest) // bullet list
|
|
160
|
-
|| rest.startsWith('>') // block quote
|
|
161
|
-
|| /^(`{3,}|~{3,})/.test(rest) // fenced code
|
|
162
|
-
|| /^([-*_])[\s]*(\1[\s]*){2,}$/.test(rest) // thematic break (and the setext h2 underline)
|
|
163
|
-
|| /^=+\s*$/.test(rest) // setext h1 underline
|
|
164
|
-
return opensBlock ? `${indent}\\${rest}` : line
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Escape every line of a text block that would open a Markdown construct. Applied per LINE, not once per
|
|
169
|
-
* block: a `<br>` becomes a real newline, so an anchored first-line-only check guards nothing after it.
|
|
170
|
-
*
|
|
171
|
-
* This is the whole defence between editor-authored prose and the structure of `llms-full.txt`, whose own
|
|
172
|
-
* document uses `##` for collection sections and `###` for pages — an unescaped `## …` in a body would
|
|
173
|
-
* not merely add a heading, it would forge a sibling of the generator's own and re-parent every page
|
|
174
|
-
* after it.
|
|
175
|
-
*/
|
|
176
|
-
export function escapeMarkdownBlock(text: string): string {
|
|
177
|
-
return text.split('\n').map(escapeMarkerLine).join('\n')
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function renderList(node: MdNode & { kind: 'el' }, opts: RichtextMarkdownOptions): string {
|
|
181
|
-
const ordered = node.tag === 'ol'
|
|
182
|
-
const items = node.children.filter((c): c is MdNode & { kind: 'el' } => c.kind === 'el' && c.tag === 'li')
|
|
183
|
-
const lines: string[] = []
|
|
184
|
-
let n = 0
|
|
185
|
-
for (const item of items) {
|
|
186
|
-
const blocks = renderBlocks(item.children, opts)
|
|
187
|
-
n += 1
|
|
188
|
-
const marker = ordered ? `${n}. ` : '- '
|
|
189
|
-
const indent = ' '.repeat(marker.length)
|
|
190
|
-
// The first block is the item's own text; anything after it (a nested list, a second paragraph)
|
|
191
|
-
// continues the item and must be indented to the marker's width to stay inside it.
|
|
192
|
-
const [head = '', ...rest] = blocks
|
|
193
|
-
lines.push(prefixLines(head, marker, indent))
|
|
194
|
-
for (const block of rest) lines.push(prefixLines(block, indent, indent))
|
|
195
|
-
}
|
|
196
|
-
return lines.join('\n')
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Render a node list as block-level markdown: an array of blocks, joined by a blank line by the caller. */
|
|
200
|
-
function renderBlocks(nodes: MdNode[], opts: RichtextMarkdownOptions): string[] {
|
|
201
|
-
const blocks: string[] = []
|
|
202
|
-
let run: MdNode[] = []
|
|
203
|
-
const flushRun = (): void => {
|
|
204
|
-
if (!run.length) return
|
|
205
|
-
const text = renderInline(run).trim()
|
|
206
|
-
run = []
|
|
207
|
-
if (text) blocks.push(escapeMarkdownBlock(text))
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
for (const node of nodes) {
|
|
211
|
-
// An unrecognised wrapper is transparent: splice its children into this level rather than dropping
|
|
212
|
-
// the content or forcing it inline, so a consumer's stray <div>/<section> keeps its structure.
|
|
213
|
-
if (node.kind === 'el' && !BLOCK_TAGS.has(node.tag) && !VOID_TAGS.has(node.tag) && !INLINE_WRAP[node.tag]
|
|
214
|
-
&& node.tag !== 'a' && node.tag !== 'li' && node.children.some(isBlockNode)) {
|
|
215
|
-
flushRun()
|
|
216
|
-
blocks.push(...renderBlocks(node.children, opts))
|
|
217
|
-
continue
|
|
218
|
-
}
|
|
219
|
-
if (node.kind !== 'el' || !BLOCK_TAGS.has(node.tag)) { run.push(node); continue }
|
|
220
|
-
flushRun()
|
|
221
|
-
|
|
222
|
-
const level = HEADINGS[node.tag]
|
|
223
|
-
if (level !== undefined) {
|
|
224
|
-
// A heading is one line by definition: a `<br>` inside it would end the heading and leave the rest
|
|
225
|
-
// as a bare paragraph, silently splitting the text an editor wrote as one title.
|
|
226
|
-
const text = renderInline(node.children).replace(/\s+/g, ' ').trim()
|
|
227
|
-
if (text) blocks.push(`${'#'.repeat(Math.min(6, level + (opts.headingOffset ?? 0)))} ${text}`)
|
|
228
|
-
} else if (node.tag === 'hr') {
|
|
229
|
-
blocks.push('---')
|
|
230
|
-
} else if (node.tag === 'pre') {
|
|
231
|
-
const code = rawText(node.children).replace(/^\n+|\n+$/g, '')
|
|
232
|
-
// Fence wider than any backtick run in the code, or the code closes the fence it sits in and
|
|
233
|
-
// everything after it — the rest of the page, and the pages after that — reparses as prose.
|
|
234
|
-
const fence = '`'.repeat(Math.max(3, longestRun(code, '`') + 1))
|
|
235
|
-
if (code) blocks.push(`${fence}\n${code}\n${fence}`)
|
|
236
|
-
} else if (node.tag === 'blockquote') {
|
|
237
|
-
const inner = renderBlocks(node.children, opts).join('\n\n')
|
|
238
|
-
if (inner) blocks.push(prefixLines(inner, '> ', '> '))
|
|
239
|
-
} else if (node.tag === 'ul' || node.tag === 'ol') {
|
|
240
|
-
const list = renderList(node, opts)
|
|
241
|
-
if (list) blocks.push(list)
|
|
242
|
-
} else {
|
|
243
|
-
const text = renderInline(node.children).trim()
|
|
244
|
-
if (text) blocks.push(escapeMarkdownBlock(text))
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
flushRun()
|
|
248
|
-
return blocks
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function isBlockNode(node: MdNode): boolean {
|
|
252
|
-
return node.kind === 'el' && (BLOCK_TAGS.has(node.tag) || node.tag === 'li')
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Sanitized richtext HTML → Markdown. Non-string / empty input, and markup that carries no words at
|
|
256
|
-
* all, both yield `''` so a caller can drop the field without a second emptiness test. */
|
|
257
|
-
export function richtextToMarkdown(html: string | null | undefined, opts: RichtextMarkdownOptions = {}): string {
|
|
258
|
-
if (typeof html !== 'string' || !html.trim()) return ''
|
|
259
|
-
return renderBlocks(parse(html), opts).join('\n\n')
|
|
260
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { resolveServerKestrel, serverRuntimeConfig } from '../../../core/server/utils/server-config'
|
|
2
|
-
import { normalizeBase } from './sitemap'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Absolute site origin for emitted artifacts (sitemap `<loc>`, robots `Sitemap:`).
|
|
6
|
-
* Configured via `kestrel.siteUrl` (or `KESTREL_SITE_URL`); empty when unset, in which case callers
|
|
7
|
-
* fall back to relative paths and omit the robots `Sitemap:` directive.
|
|
8
|
-
*/
|
|
9
|
-
export function siteBaseUrl(): string {
|
|
10
|
-
// Prefer the siteUrl the kestrel module resolved from the consumer's `kestrel: {}` (via runtimeConfig);
|
|
11
|
-
// fall back to Kestrel's own config file + env for non-Nitro callers. Mirrors `useDb`'s db-path handling.
|
|
12
|
-
const fromRc = serverRuntimeConfig()?.kestrel as { siteUrl?: string } | undefined
|
|
13
|
-
return normalizeBase(fromRc?.siteUrl ?? resolveServerKestrel().siteUrl)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Human site name for `llms.txt` — config/env, else the siteUrl host, else 'Website'. */
|
|
17
|
-
export function siteName(): string {
|
|
18
|
-
const fromRc = serverRuntimeConfig()?.kestrel as { siteName?: string } | undefined
|
|
19
|
-
const name = (fromRc?.siteName ?? resolveServerKestrel().siteName)?.trim()
|
|
20
|
-
if (name) return name
|
|
21
|
-
const base = siteBaseUrl()
|
|
22
|
-
if (base) {
|
|
23
|
-
try { return new URL(base).host } catch { /* not a parseable URL — fall through */ }
|
|
24
|
-
}
|
|
25
|
-
return 'Website'
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** One-line site description for `llms.txt` (empty when unset). */
|
|
29
|
-
export function siteDescription(): string {
|
|
30
|
-
const fromRc = serverRuntimeConfig()?.kestrel as { siteDescription?: string } | undefined
|
|
31
|
-
return (fromRc?.siteDescription ?? resolveServerKestrel().siteDescription ?? '').trim()
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Whether `/llms-full.txt` is served, prerendered and published (`kestrel.seo.llmsFull`, default off).
|
|
35
|
-
* Read the same two-tier way as every other server setting, so a consumed layer sees the CONSUMER's
|
|
36
|
-
* config rather than Kestrel's own. */
|
|
37
|
-
export function llmsFullEnabled(): boolean {
|
|
38
|
-
const fromRc = serverRuntimeConfig()?.kestrel as { seo?: { llmsFull?: boolean } } | undefined
|
|
39
|
-
return (fromRc?.seo?.llmsFull ?? resolveServerKestrel().seo.llmsFull) === true
|
|
40
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
export interface SitemapAlternate {
|
|
2
|
-
hreflang: string
|
|
3
|
-
href: string
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface SitemapEntry {
|
|
7
|
-
loc: string
|
|
8
|
-
lastmod?: string
|
|
9
|
-
alternates?: SitemapAlternate[]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** A page-like row reduced to what the sitemap needs, before grouping translations into hreflang sets. */
|
|
13
|
-
export interface SitemapCandidate {
|
|
14
|
-
loc: string
|
|
15
|
-
lastmod?: string
|
|
16
|
-
locale: string
|
|
17
|
-
/** The translation-group key, or null/undefined for rows that have no localized siblings. */
|
|
18
|
-
group?: string | null
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Drop any trailing slashes from a site base URL so `base + path` never doubles up. */
|
|
22
|
-
export function normalizeBase(base: string): string {
|
|
23
|
-
return base.replace(/\/+$/, '')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function xmlEscape(value: string): string {
|
|
27
|
-
return value
|
|
28
|
-
.replace(/&/g, '&')
|
|
29
|
-
.replace(/</g, '<')
|
|
30
|
-
.replace(/>/g, '>')
|
|
31
|
-
.replace(/"/g, '"')
|
|
32
|
-
.replace(/'/g, ''')
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Group page-like candidates into `<url>` entries, attaching the hreflang alternate set to every
|
|
37
|
-
* member of a multi-locale translation group. Pure: the route feeds it published+indexable rows only,
|
|
38
|
-
* so alternates never advertise a draft/noindex variant. Order is preserved (the route sorts after).
|
|
39
|
-
* A group of one (or a row with no group) gets no alternates — hreflang is meaningless for a lone page.
|
|
40
|
-
* `x-default` points at the primary-locale variant when that variant is part of the group.
|
|
41
|
-
*/
|
|
42
|
-
export function withHreflang(candidates: SitemapCandidate[], primaryLocale: string): SitemapEntry[] {
|
|
43
|
-
const groups = new Map<string, SitemapCandidate[]>()
|
|
44
|
-
for (const c of candidates) {
|
|
45
|
-
if (c.group == null) continue
|
|
46
|
-
const arr = groups.get(c.group)
|
|
47
|
-
if (arr) arr.push(c)
|
|
48
|
-
else groups.set(c.group, [c])
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return candidates.map((c) => {
|
|
52
|
-
const entry: SitemapEntry = { loc: c.loc }
|
|
53
|
-
if (c.lastmod) entry.lastmod = c.lastmod
|
|
54
|
-
const variants = c.group != null ? groups.get(c.group)! : []
|
|
55
|
-
if (variants.length >= 2) entry.alternates = buildAlternates(variants, primaryLocale)
|
|
56
|
-
return entry
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Sorted-by-locale alternate links for a group, plus an `x-default` to the primary variant if present. */
|
|
61
|
-
function buildAlternates(variants: SitemapCandidate[], primaryLocale: string): SitemapAlternate[] {
|
|
62
|
-
const sorted = [...variants].sort((a, b) => a.locale.localeCompare(b.locale))
|
|
63
|
-
const seen = new Set<string>()
|
|
64
|
-
const alternates: SitemapAlternate[] = []
|
|
65
|
-
for (const v of sorted) {
|
|
66
|
-
if (seen.has(v.locale)) continue
|
|
67
|
-
seen.add(v.locale)
|
|
68
|
-
alternates.push({ hreflang: v.locale, href: v.loc })
|
|
69
|
-
}
|
|
70
|
-
const primary = sorted.find((v) => v.locale === primaryLocale)
|
|
71
|
-
if (primary) alternates.push({ hreflang: 'x-default', href: primary.loc })
|
|
72
|
-
return alternates
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function buildSitemap(entries: SitemapEntry[]): string {
|
|
76
|
-
const hasAlternates = entries.some((e) => e.alternates && e.alternates.length > 0)
|
|
77
|
-
const xhtmlNs = hasAlternates ? ' xmlns:xhtml="http://www.w3.org/1999/xhtml"' : ''
|
|
78
|
-
const urls = entries
|
|
79
|
-
.map((e) => {
|
|
80
|
-
const lastmod = e.lastmod ? `<lastmod>${xmlEscape(e.lastmod)}</lastmod>` : ''
|
|
81
|
-
const links = (e.alternates ?? [])
|
|
82
|
-
.map((a) => `<xhtml:link rel="alternate" hreflang="${xmlEscape(a.hreflang)}" href="${xmlEscape(a.href)}"/>`)
|
|
83
|
-
.join('')
|
|
84
|
-
return `<url><loc>${xmlEscape(e.loc)}</loc>${lastmod}${links}</url>`
|
|
85
|
-
})
|
|
86
|
-
.join('')
|
|
87
|
-
return `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"${xhtmlNs}>${urls}</urlset>`
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function buildRobots(opts: { sitemapUrl?: string; llmsUrl?: string; llmsFullUrl?: string } = {}): string {
|
|
91
|
-
const lines = ['User-agent: *', 'Allow: /']
|
|
92
|
-
// Comments (robots.txt has no standard llms directive) so crawlers that read robots also find the
|
|
93
|
-
// agent-oriented site map at /llms.txt — and its full-text long form when that is switched on.
|
|
94
|
-
// Kept above the blank-line-separated Sitemap directive.
|
|
95
|
-
if (opts.llmsUrl) lines.push(`# llms.txt: ${opts.llmsUrl}`)
|
|
96
|
-
if (opts.llmsFullUrl) lines.push(`# llms-full.txt: ${opts.llmsFullUrl}`)
|
|
97
|
-
if (opts.sitemapUrl) lines.push('', `Sitemap: ${opts.sitemapUrl}`)
|
|
98
|
-
return `${lines.join('\n')}\n`
|
|
99
|
-
}
|