@tokenoftrust/storefront-runner 1.4.1 → 2.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/apps/storefront/astro.config.mjs +17 -1
- package/apps/storefront/env.d.ts +54 -5
- package/apps/storefront/integrations/materialize-guard.mjs +54 -0
- package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
- package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
- package/apps/storefront/migrations-apps/0004_deep_morph.sql +36 -0
- package/apps/storefront/migrations-apps/0005_common_mystique.sql +23 -0
- package/apps/storefront/migrations-apps/0006_broad_microchip.sql +16 -0
- package/apps/storefront/migrations-apps/0007_brave_outlaw_kid.sql +15 -0
- package/apps/storefront/migrations-apps/0008_blue_dazzler.sql +11 -0
- package/apps/storefront/migrations-apps/0009_bored_shinobi_shaw.sql +15 -0
- package/apps/storefront/migrations-apps/0010_ws4_verified_provider_measurement.sql +35 -0
- package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
- package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
- package/apps/storefront/migrations-apps/meta/0004_snapshot.json +1474 -0
- package/apps/storefront/migrations-apps/meta/0005_snapshot.json +1617 -0
- package/apps/storefront/migrations-apps/meta/0006_snapshot.json +1716 -0
- package/apps/storefront/migrations-apps/meta/0007_snapshot.json +1812 -0
- package/apps/storefront/migrations-apps/meta/0008_snapshot.json +1881 -0
- package/apps/storefront/migrations-apps/meta/0009_snapshot.json +1979 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +56 -0
- package/apps/storefront/public/apps/tot.demo.analytics/0.1.0/entry.js +42 -0
- package/apps/storefront/public/js/dashboard-team.js +38 -2
- package/apps/storefront/public/platform/amplitude-http/v1/entry.mjs +19 -0
- package/apps/storefront/public/platform/google-measurement/v1/entry.mjs +19 -0
- package/apps/storefront/public/shared/commerce-chrome.css +20 -2
- package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +68 -0
- package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +111 -0
- package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +2 -0
- package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +247 -0
- package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +73 -0
- package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +79 -0
- package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +10982 -0
- package/apps/storefront/scripts/import-v84-through-ws2.ts +38 -0
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +2 -0
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +4 -0
- package/apps/storefront/scripts/install-v84-native-through-ws1.ts +185 -0
- package/apps/storefront/scripts/prove-clone-isolation.ts +506 -0
- package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +148 -0
- package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +35 -0
- package/apps/storefront/scripts/replay-v84-clean-clone.ts +214 -0
- package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +18 -0
- package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +18 -0
- package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +18 -0
- package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +17 -0
- package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +15 -0
- package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +21 -0
- package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +42 -0
- package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +145 -0
- package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +18 -0
- package/apps/storefront/src/components/Img.astro +1 -1
- package/apps/storefront/src/components/Seo.astro +65 -1
- package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +1435 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
- package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +105 -0
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1980 -2564
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +5 -5
- package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
- package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
- package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
- package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +12 -0
- package/apps/storefront/src/components/chrome/SiteFooter.astro +7 -2
- package/apps/storefront/src/components/chrome/SiteHeader.astro +46 -10
- package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +1 -1
- package/apps/storefront/src/components/compliance/ComplianceNotice.astro +1 -2
- package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +1 -2
- package/apps/storefront/src/components/compliance/PactActNotice.astro +1 -1
- package/apps/storefront/src/components/compliance/Prop65Warning.astro +1 -2
- package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +2 -3
- package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +2 -2
- package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +2 -3
- package/apps/storefront/src/components/content/TrustStrip.astro +4 -4
- package/apps/storefront/src/components/islands/MobileNav.tsx +26 -1
- package/apps/storefront/src/components/islands/VariantSelector.tsx +27 -2
- package/apps/storefront/src/components/islands/useCheckoutHandoffCorrelation.ts +75 -0
- package/apps/storefront/src/components/membership/IllustrativeLabel.astro +1 -2
- package/apps/storefront/src/components/membership/TierCards.astro +2 -2
- package/apps/storefront/src/components/membership/TierComparisonTable.astro +2 -2
- package/apps/storefront/src/components/subscription/SavingsExplainer.astro +2 -2
- package/apps/storefront/src/config/adminTenantLookup.ts +35 -0
- package/apps/storefront/src/config/devTenantSeed.ts +9 -9
- package/apps/storefront/src/config/resolver.ts +4 -4
- package/apps/storefront/src/layouts/Layout.astro +146 -21
- package/apps/storefront/src/lib/activity/alerts.ts +17 -19
- package/apps/storefront/src/lib/activity/candidateSubmitLease.ts +87 -0
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +89 -9
- package/apps/storefront/src/lib/activity/deployVersion.ts +20 -27
- package/apps/storefront/src/lib/activity/ingest.ts +8 -8
- package/apps/storefront/src/lib/activity/ingestAuth.ts +1 -1
- package/apps/storefront/src/lib/activity/killSwitch.ts +6 -6
- package/apps/storefront/src/lib/activity/query.ts +9 -9
- package/apps/storefront/src/lib/activity/recordActivity.ts +11 -15
- package/apps/storefront/src/lib/activity/store.ts +4 -6
- package/apps/storefront/src/lib/activity/uiActor.ts +12 -12
- package/apps/storefront/src/lib/activity/workerCommit.ts +6 -6
- package/apps/storefront/src/lib/admin/adminShell.ts +292 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
- package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
- package/apps/storefront/src/lib/admin/envelope.ts +1 -2
- package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
- package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
- package/apps/storefront/src/lib/apps/adminService.ts +14 -4
- package/apps/storefront/src/lib/apps/adminSession.ts +54 -45
- package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
- package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
- package/apps/storefront/src/lib/apps/appVersionStore.ts +281 -0
- package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
- package/apps/storefront/src/lib/apps/consentConfigStore.ts +228 -0
- package/apps/storefront/src/lib/apps/credentials.ts +2 -2
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
- package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +447 -0
- package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +367 -0
- package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +315 -0
- package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
- package/apps/storefront/src/lib/apps/integrationLifecycle.ts +614 -0
- package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +125 -0
- package/apps/storefront/src/lib/apps/integrationReadModel.ts +563 -0
- package/apps/storefront/src/lib/apps/integrationsTab.ts +547 -0
- package/apps/storefront/src/lib/apps/integrationsView.ts +106 -0
- package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +599 -0
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
- package/apps/storefront/src/lib/apps/orders/orderConversionD1.ts +150 -0
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +9 -1
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +7 -7
- package/apps/storefront/src/lib/apps/orders/verifiedProviderMeasurement.ts +117 -0
- package/apps/storefront/src/lib/apps/registryService.ts +270 -16
- package/apps/storefront/src/lib/apps/scopes.ts +2 -2
- package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +170 -0
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
- package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +16 -0
- package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +105 -0
- package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +177 -0
- package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +251 -0
- package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +241 -0
- package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +211 -0
- package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +409 -0
- package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +368 -0
- package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +188 -0
- package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +291 -0
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
- package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
- package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +95 -0
- package/apps/storefront/src/lib/auth/adminApiGuard.ts +66 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +11 -11
- package/apps/storefront/src/lib/auth/adminLanding.ts +88 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
- package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +2 -4
- package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
- package/apps/storefront/src/lib/auth/session.ts +7 -8
- package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
- package/apps/storefront/src/lib/basePath.ts +9 -2
- package/apps/storefront/src/lib/blog/access.ts +29 -0
- package/apps/storefront/src/lib/blog/collection.ts +400 -0
- package/apps/storefront/src/lib/blog/markdown.ts +174 -0
- package/apps/storefront/src/lib/blog/provider.ts +62 -3
- package/apps/storefront/src/lib/blog/reactions.ts +87 -0
- package/apps/storefront/src/lib/blog/rss.ts +92 -0
- package/apps/storefront/src/lib/blog/teaser.ts +83 -0
- package/apps/storefront/src/lib/blog/types.ts +34 -0
- package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
- package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome-assignments.example.json +6 -0
- package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome.example.json +79 -0
- package/apps/storefront/src/lib/chrome/model.ts +48 -253
- package/apps/storefront/src/lib/chrome/navCurrent.ts +29 -0
- package/apps/storefront/src/lib/chrome/parseConfig.ts +18 -32
- package/apps/storefront/src/lib/chrome/variant.ts +39 -0
- package/apps/storefront/src/lib/commerce/checkoutHandoffCorrelation.ts +105 -0
- package/apps/storefront/src/lib/compliance/tamper-sentinel-boot.ts +152 -0
- package/apps/storefront/src/lib/compliance/tamper-sentinel.ts +327 -0
- package/apps/storefront/src/lib/content/index.ts +5 -5
- package/apps/storefront/src/lib/content-edit/applyDiff.ts +1 -1
- package/apps/storefront/src/lib/content-edit/route.ts +1 -1
- package/apps/storefront/src/lib/d1/catalog.ts +14 -2
- package/apps/storefront/src/lib/d1/schema-apps.ts +372 -17
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
- package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
- package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
- package/apps/storefront/src/lib/dev/cockpit/ask.ts +49 -0
- package/apps/storefront/src/lib/dev/cockpit/clickToSource.ts +71 -0
- package/apps/storefront/src/lib/dev/cockpit/copyCommand.ts +227 -0
- package/apps/storefront/src/lib/dev/cockpit/files.ts +249 -0
- package/apps/storefront/src/lib/dev/cockpit/hostedActivity.ts +500 -0
- package/apps/storefront/src/lib/dev/cockpit/infoPopover.ts +12 -0
- package/apps/storefront/src/lib/dev/cockpit/mcpCopy.ts +34 -0
- package/apps/storefront/src/lib/dev/cockpit/monitor.ts +107 -0
- package/apps/storefront/src/lib/dev/cockpit/stepper.ts +116 -0
- package/apps/storefront/src/lib/dev/cockpit/themeToggle.ts +23 -0
- package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
- package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
- package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
- package/apps/storefront/src/lib/dev/runtimeStore.ts +1 -1
- package/apps/storefront/src/lib/edgeCache.ts +34 -5
- package/apps/storefront/src/lib/email/candidateConflictEmail.ts +80 -0
- package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
- package/apps/storefront/src/lib/i18n.ts +25 -0
- package/apps/storefront/src/lib/integration/browserEnvironment.ts +137 -0
- package/apps/storefront/src/lib/integration/managedInstallDeclarations.ts +72 -0
- package/apps/storefront/src/lib/integration/nonceStamp.ts +49 -0
- package/apps/storefront/src/lib/integration/tenantDeclarations.ts +116 -0
- package/apps/storefront/src/lib/jsonld.ts +118 -0
- package/apps/storefront/src/lib/membership/eligibility.ts +1 -1
- package/apps/storefront/src/lib/membership/model.ts +1 -2
- package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
- package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
- package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
- package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
- package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
- package/apps/storefront/src/lib/newsletter/send.ts +265 -0
- package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
- package/apps/storefront/src/lib/previewReload.ts +57 -1
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +4625 -0
- package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
- package/apps/storefront/src/lib/publish/buildStream.ts +269 -0
- package/apps/storefront/src/lib/publish/collapsePersistence.ts +72 -0
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
- package/apps/storefront/src/lib/publish/domainState.ts +22 -13
- package/apps/storefront/src/lib/publish/lifecycleState.ts +369 -0
- package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +29 -0
- package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +78 -0
- package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +62 -0
- package/apps/storefront/src/lib/publish/mergeRowFindings.ts +124 -0
- package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +75 -0
- package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
- package/apps/storefront/src/lib/publish/publish-guard.ts +88 -0
- package/apps/storefront/src/lib/publish/publishFormat.ts +165 -0
- package/apps/storefront/src/lib/publish/publishHero.ts +177 -0
- package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +66 -0
- package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +141 -0
- package/apps/storefront/src/lib/publish/reviewRelease/index.ts +41 -0
- package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +47 -0
- package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +42 -0
- package/apps/storefront/src/lib/publish/reviewRelease/types.ts +59 -0
- package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +440 -0
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +161 -0
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +325 -0
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +241 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +121 -51
- package/apps/storefront/src/lib/publish/sseRoute.ts +119 -0
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
- package/apps/storefront/src/lib/publish/streamLogConsole.ts +142 -0
- package/apps/storefront/src/lib/publish/versionNumber.ts +52 -0
- package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
- package/apps/storefront/src/lib/rawChrome.ts +125 -47
- package/apps/storefront/src/lib/rawMarketingHtml.ts +74 -3
- package/apps/storefront/src/lib/search/index.ts +2 -2
- package/apps/storefront/src/lib/seo/alternates.ts +42 -0
- package/apps/storefront/src/lib/seo/meta.ts +65 -0
- package/apps/storefront/src/lib/social/golive.ts +212 -0
- package/apps/storefront/src/lib/social/notify.ts +32 -0
- package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
- package/apps/storefront/src/lib/storyblok/content-model.ts +7 -33
- package/apps/storefront/src/lib/storyblok/provider.ts +168 -60
- package/apps/storefront/src/lib/subscription/model.ts +4 -4
- package/apps/storefront/src/lib/the-build/provider.ts +32 -0
- package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
- package/apps/storefront/src/lib/the-build/toc.ts +95 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
- package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
- package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
- package/apps/storefront/src/middleware/index.ts +385 -52
- package/apps/storefront/src/pages/[...slug].astro +9 -4
- package/apps/storefront/src/pages/admin/review-release.astro +265 -0
- package/apps/storefront/src/pages/admin/select.astro +96 -0
- package/apps/storefront/src/pages/admin.astro +25 -1313
- package/apps/storefront/src/pages/api/activity.ts +8 -8
- package/apps/storefront/src/pages/api/admin/activity-alerts.ts +5 -20
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +7 -2
- package/apps/storefront/src/pages/api/admin/orders.ts +8 -2
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +174 -0
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +96 -0
- package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +118 -0
- package/apps/storefront/src/pages/api/apps/admin/integrations.ts +89 -0
- package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +85 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +74 -2
- package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
- package/apps/storefront/src/pages/api/commerce/checkout-handoff.ts +77 -0
- package/apps/storefront/src/pages/api/compliance/tamper.ts +88 -0
- package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +108 -0
- package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
- package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
- package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
- package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +20 -2
- package/apps/storefront/src/pages/blog/[slug].astro +99 -19
- package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
- package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
- package/apps/storefront/src/pages/blog/index.astro +4 -2
- package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
- package/apps/storefront/src/pages/cockpit.astro +77 -1353
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
- package/apps/storefront/src/pages/index.astro +10 -5
- package/apps/storefront/src/pages/llms.txt.ts +1 -1
- package/apps/storefront/src/pages/products/[handle].astro +7 -6
- package/apps/storefront/src/pages/search.astro +1 -1
- package/apps/storefront/src/pages/sitemap.xml.ts +21 -1
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +31 -27
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +1 -1
- package/apps/storefront/src/pages/tenants/[id]/[...path].ts +38 -2
- package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
- package/apps/storefront/src/pages/the-build/index.astro +146 -0
- package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
- package/apps/storefront/src/styles/admin.css +1106 -0
- package/apps/storefront/tsconfig.json +6 -1
- package/package.json +1 -1
- package/packages/public-runtime/package.json +3 -1
- package/packages/public-runtime/schemas/managed-analytics/v1/adapter-definition.schema.json +427 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/create-integration-install-request.schema.json +161 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/create-migration-draft-request.schema.json +157 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/delete-integration-install-request.schema.json +84 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/delete-migration-draft-request.schema.json +84 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/error-envelope.schema.json +180 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-adapter-definition-request.schema.json +52 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-install-request.schema.json +52 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-revision-request.schema.json +79 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-migration-draft-request.schema.json +52 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/integration-install.schema.json +167 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/integration-revision.schema.json +208 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/list-adapter-definitions-request.schema.json +53 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-installs-request.schema.json +53 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-revisions-request.schema.json +82 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/migration-draft.schema.json +218 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/mutation-request.schema.json +52 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/mutation-success-envelope.schema.json +49 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/read-request.schema.json +20 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/read-success-envelope.schema.json +45 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/revise-integration-install-request.schema.json +156 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/set-integration-install-status-request.schema.json +92 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/update-migration-draft-request.schema.json +184 -0
- package/packages/public-runtime/scripts/generate-managed-analytics-api-v1-schemas.mjs +13 -0
- package/packages/public-runtime/src/activity/catalog.ts +89 -33
- package/packages/public-runtime/src/activity/event.ts +6 -6
- package/packages/public-runtime/src/activity/index.ts +2 -2
- package/packages/public-runtime/src/activity/redaction.ts +4 -4
- package/packages/public-runtime/src/candidate-index.ts +42 -8
- package/packages/public-runtime/src/catalog-d1.ts +1 -1
- package/packages/public-runtime/src/checkout-orders.ts +2 -2
- package/packages/public-runtime/src/checkout-style-publish.ts +2 -2
- package/packages/public-runtime/src/checkout-style.ts +7 -7
- package/packages/public-runtime/src/checkout.ts +3 -3
- package/packages/public-runtime/src/chrome.ts +777 -0
- package/packages/public-runtime/src/compliance-obligations.ts +310 -0
- package/packages/public-runtime/src/csp.ts +99 -0
- package/packages/public-runtime/src/customization-preview.ts +25 -2
- package/packages/public-runtime/src/customization-runtime.ts +156 -3
- package/packages/public-runtime/src/customization-versioning.ts +10 -0
- package/packages/public-runtime/src/destination-webhook/contract.ts +353 -0
- package/packages/public-runtime/src/embed-catalog.d.mts +9 -0
- package/packages/public-runtime/src/embed-catalog.mjs +40 -0
- package/packages/public-runtime/src/evidence.ts +288 -0
- package/packages/public-runtime/src/gtm-migration/bounds.ts +138 -0
- package/packages/public-runtime/src/gtm-migration/disposition-rules.ts +361 -0
- package/packages/public-runtime/src/gtm-migration/dispositions.ts +181 -0
- package/packages/public-runtime/src/gtm-migration/draft-generator.ts +400 -0
- package/packages/public-runtime/src/gtm-migration/graph.ts +468 -0
- package/packages/public-runtime/src/gtm-migration/index.ts +95 -0
- package/packages/public-runtime/src/gtm-migration/parse.ts +410 -0
- package/packages/public-runtime/src/gtm-migration/references.ts +266 -0
- package/packages/public-runtime/src/gtm-migration/types.ts +86 -0
- package/packages/public-runtime/src/index.ts +17 -1
- package/packages/public-runtime/src/integration/assurance-decision.ts +259 -0
- package/packages/public-runtime/src/integration/assurance.ts +228 -0
- package/packages/public-runtime/src/integration/candidate-evidence.ts +377 -0
- package/packages/public-runtime/src/integration/commerce-broker.ts +497 -0
- package/packages/public-runtime/src/integration/consent-broker.ts +337 -0
- package/packages/public-runtime/src/integration/consent-cmp-demand.ts +115 -0
- package/packages/public-runtime/src/integration/consent-config.ts +260 -0
- package/packages/public-runtime/src/integration/consent-preview.ts +125 -0
- package/packages/public-runtime/src/integration/correlation-token.ts +444 -0
- package/packages/public-runtime/src/integration/csp-compiler.ts +267 -0
- package/packages/public-runtime/src/integration/csp.ts +105 -0
- package/packages/public-runtime/src/integration/digest.ts +43 -0
- package/packages/public-runtime/src/integration/environment.ts +228 -0
- package/packages/public-runtime/src/integration/events-catalog.ts +186 -0
- package/packages/public-runtime/src/integration/events.ts +701 -0
- package/packages/public-runtime/src/integration/g2-action-boundary.ts +106 -0
- package/packages/public-runtime/src/integration/index.ts +90 -0
- package/packages/public-runtime/src/integration/interim-bridge.ts +138 -0
- package/packages/public-runtime/src/integration/lifecycle-boot.ts +317 -0
- package/packages/public-runtime/src/integration/lifecycle-runtime.ts +393 -0
- package/packages/public-runtime/src/integration/lifecycle.ts +94 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.d.mts +2 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.mjs +10 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +382 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +684 -0
- package/packages/public-runtime/src/integration/managed-analytics-bridge.ts +383 -0
- package/packages/public-runtime/src/integration/managed-analytics-value-safety.ts +14 -0
- package/packages/public-runtime/src/integration/managed-analytics.ts +277 -0
- package/packages/public-runtime/src/integration/manifest.ts +470 -0
- package/packages/public-runtime/src/integration/native-amplitude.ts +362 -0
- package/packages/public-runtime/src/integration/native-ga4.ts +352 -0
- package/packages/public-runtime/src/integration/observability.ts +451 -0
- package/packages/public-runtime/src/integration/order-conversion.ts +0 -0
- package/packages/public-runtime/src/integration/phases.ts +182 -0
- package/packages/public-runtime/src/integration/plan.ts +112 -0
- package/packages/public-runtime/src/integration/platform-adapter-runtime.ts +146 -0
- package/packages/public-runtime/src/integration/platform-adapters.ts +671 -0
- package/packages/public-runtime/src/integration/promotion-gate.ts +451 -0
- package/packages/public-runtime/src/integration/provider-checkout-extension.ts +425 -0
- package/packages/public-runtime/src/integration/receipt-measurement.ts +370 -0
- package/packages/public-runtime/src/integration/regulated-parent-cert.ts +311 -0
- package/packages/public-runtime/src/integration/render-phases.ts +267 -0
- package/packages/public-runtime/src/integration/resolve.ts +465 -0
- package/packages/public-runtime/src/integration/result.ts +239 -0
- package/packages/public-runtime/src/integration/secrets.ts +92 -0
- package/packages/public-runtime/src/integration/tamper-evidence.ts +205 -0
- package/packages/public-runtime/src/integration/tamper-recovery.ts +127 -0
- package/packages/public-runtime/src/integration/version.ts +92 -0
- package/packages/public-runtime/src/integration-capabilities.ts +520 -0
- package/packages/public-runtime/src/membership.ts +2 -2
- package/packages/public-runtime/src/raw-html-policy.ts +2 -4
- package/packages/public-runtime/src/regulated-commerce/broker/consent-dedupe.ts +144 -0
- package/packages/public-runtime/src/regulated-commerce/correlation/contract.ts +177 -0
- package/packages/public-runtime/src/regulated-commerce/diagnostics/local-dispatch.ts +24 -0
- package/packages/public-runtime/src/regulated-commerce/diagnostics/read-model.ts +69 -0
- package/packages/public-runtime/src/regulated-commerce/dispatch/core.ts +134 -0
- package/packages/public-runtime/src/regulated-commerce/events/contract.ts +113 -0
- package/packages/public-runtime/src/regulated-commerce/ledger/core.ts +81 -0
- package/packages/public-runtime/src/regulated-commerce/mappings/ga4-ads.ts +87 -0
- package/packages/public-runtime/src/regulated-commerce/provider-order/bridge.ts +52 -0
- package/packages/public-runtime/src/regulated-commerce/purchase-truth/contract.ts +252 -0
- package/packages/public-runtime/src/regulated-commerce/sources/accepted-browser-operation.ts +67 -0
- package/packages/public-runtime/src/regulated-commerce/testing/fake-destination.ts +93 -0
- package/packages/public-runtime/src/static-bundle.ts +357 -0
- package/packages/public-runtime/src/tenant-assets.ts +143 -4
- package/packages/public-runtime/src/tenant.ts +81 -17
- package/packages/public-runtime/tests/fixtures/integration/browser-environment.example.json +8 -0
- package/packages/public-runtime/tests/fixtures/integration/commerce-event.example.json +44 -0
- package/packages/public-runtime/tests/fixtures/integration/commerce-order-event.example.json +44 -0
- package/packages/public-runtime/tests/fixtures/integration/compliance-assurance-record.example.json +50 -0
- package/packages/public-runtime/tests/fixtures/integration/integration-manifest-v2.example.json +120 -0
- package/packages/public-runtime/tests/fixtures/integration/managed-analytics-v1/fake-adapter-catalog.json +35 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-action-registry.example.json +11 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/README.md +22 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/entry.js +30 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/manifest.json +75 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/public-config.json +3 -0
- package/packages/public-runtime/tests/fixtures/regulated-commerce/fake-destinations.ts +38 -0
- package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.provenance.json +29 -0
- package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.redacted.json +3397 -0
- package/scripts/build/copy-tenant-assets.mjs +14 -4
- package/scripts/dev/ai-edit.mjs +1 -1
- package/scripts/dev/checkout-watch.mjs +2 -2
- package/scripts/dev/file-browser.mjs +4 -6
- package/scripts/dev/file-writer.mjs +3 -3
- package/scripts/dev/git-status.mjs +2 -2
- package/scripts/dev/integration-guardrails.mjs +498 -0
- package/scripts/dev/locate-handler.mjs +3 -3
- package/scripts/dev/port-check.mjs +3 -3
- package/scripts/dev/publish.mjs +5 -9
- package/scripts/dev/safe-path.mjs +2 -2
- package/scripts/dev/shot.mjs +1 -1
- package/scripts/dev/tenant-source-root.mjs +1 -1
- package/scripts/dev/unified-diff.mjs +8 -3
- package/scripts/tot-dev.mjs +8 -11
- package/apps/storefront/src/lib/dev/vcBinding.ts +0 -80
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import type { AuthorBio, BlogPost, BlogPostSummary, PostStatus } from "./types.js";
|
|
2
|
+
import type { ContentProvider } from "../storyblok/provider.js";
|
|
3
|
+
|
|
4
|
+
type ArtifactSource = Pick<ContentProvider, "readArtifact" | "listArtifactPaths">;
|
|
5
|
+
function canReadArtifacts(value: ArtifactSource | undefined): value is ArtifactSource {
|
|
6
|
+
return typeof value?.listArtifactPaths === "function" && typeof value.readArtifact === "function";
|
|
7
|
+
}
|
|
8
|
+
export interface CollectionReadOptions {
|
|
9
|
+
includeUnpublished?: boolean;
|
|
10
|
+
now?: string;
|
|
11
|
+
content?: ArtifactSource;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A blog collection = a route base + a per-tenant content dir. */
|
|
15
|
+
export interface Collection {
|
|
16
|
+
id: string;
|
|
17
|
+
base: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ONE glob covering every tenant + every collection; the tenant id and the
|
|
21
|
+
// collection id are both path segments filtered at call time. The derived index
|
|
22
|
+
// is the set of `posts/*.json` files — there is no hand-maintained index.json.
|
|
23
|
+
const postGlobs = import.meta.glob<BlogPost>(
|
|
24
|
+
"../../../../../tenants/*/content/*/posts/*.json",
|
|
25
|
+
{ import: "default" },
|
|
26
|
+
);
|
|
27
|
+
const redirectGlobs = import.meta.glob<Record<string, string>>(
|
|
28
|
+
"../../../../../tenants/*/content/*/redirects.json",
|
|
29
|
+
{ import: "default" },
|
|
30
|
+
);
|
|
31
|
+
const authorsGlobs = import.meta.glob<Record<string, AuthorBio>>(
|
|
32
|
+
"../../../../../tenants/*/content/*/authors.json",
|
|
33
|
+
{ import: "default" },
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
37
|
+
const SEGMENT_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A "scheduled" post whose `scheduledAt` instant has passed behaves exactly like
|
|
41
|
+
* "published". Before that instant it is treated like a draft (invisible, unlisted).
|
|
42
|
+
* A scheduled post with a missing/unparseable `scheduledAt` is never due (safe: it
|
|
43
|
+
* stays hidden rather than leaking). Because visibility is computed at build/request
|
|
44
|
+
* time, a post committed ahead of time simply needs a build after its instant.
|
|
45
|
+
*/
|
|
46
|
+
function isDueScheduled(p: { status: PostStatus; scheduledAt?: string }, now: string): boolean {
|
|
47
|
+
if (p.status !== "scheduled" || typeof p.scheduledAt !== "string") return false;
|
|
48
|
+
const due = Date.parse(p.scheduledAt);
|
|
49
|
+
return !Number.isNaN(due) && due <= Date.parse(now);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The single visibility chokepoint: is a post reachable at its URL on the live
|
|
54
|
+
* site? Published, unlisted, and due-scheduled resolve; drafts and not-yet-due
|
|
55
|
+
* scheduled posts 404. Listings (index/RSS/sitemap) apply a stricter filter
|
|
56
|
+
* (published or due-scheduled) — unlisted posts resolve at their URL but never
|
|
57
|
+
* appear in any listing. ("scheduled" branch added in B3.1.)
|
|
58
|
+
*/
|
|
59
|
+
export function isVisibleOnLive(
|
|
60
|
+
p: { status: PostStatus; scheduledAt?: string },
|
|
61
|
+
now: string = new Date().toISOString(),
|
|
62
|
+
): boolean {
|
|
63
|
+
return p.status === "published" || p.status === "unlisted" || isDueScheduled(p, now);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** "Category Insurance" -> "category-insurance" */
|
|
67
|
+
export function categorySlug(name: string): string {
|
|
68
|
+
return name
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
71
|
+
.replace(/^-+|-+$/g, "");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Collect the posts whose `field` (categories or tags) contains a term slugifying
|
|
76
|
+
* to `slug`, preserving input order, plus the human label of the first matching
|
|
77
|
+
* term (for the page heading/breadcrumb). Returns null when nothing matches, which
|
|
78
|
+
* a category/tag route turns into a 404. Terms are matched by `categorySlug` so the
|
|
79
|
+
* URL segment is stable regardless of the term's display casing/spacing.
|
|
80
|
+
*/
|
|
81
|
+
export function collectPostsByTerm<T extends BlogPostSummary>(
|
|
82
|
+
posts: T[],
|
|
83
|
+
field: "categories" | "tags",
|
|
84
|
+
slug: string,
|
|
85
|
+
): { label: string; posts: T[] } | null {
|
|
86
|
+
const matched: T[] = [];
|
|
87
|
+
let label: string | null = null;
|
|
88
|
+
for (const post of posts) {
|
|
89
|
+
const hit = post[field].find((term) => categorySlug(term) === slug);
|
|
90
|
+
if (hit !== undefined) {
|
|
91
|
+
if (label === null) label = hit;
|
|
92
|
+
matched.push(post);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return label === null ? null : { label, posts: matched };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* "Ada Lovelace" -> "ada-lovelace". Author bylines are people's names, so two
|
|
100
|
+
* spellings of the same person must resolve to ONE archive slug — otherwise a
|
|
101
|
+
* migrated blog splits an author across two pages. Beyond the shared category
|
|
102
|
+
* slugify we first fold the two variant classes that show up in real bylines:
|
|
103
|
+
* - diacritics: "María García" == "Maria Garcia" (NFKD + strip combining marks)
|
|
104
|
+
* - apostrophes/quotes are DROPPED, not turned into a word break, so
|
|
105
|
+
* "Austin O'Brion" == "Austin Obrion" -> "austin-obrion" (WordPress-style),
|
|
106
|
+
* not "austin-o-brion" != "austin-obrion".
|
|
107
|
+
* categorySlug is intentionally left unfolded so existing category/tag URLs
|
|
108
|
+
* (and their redirects/sitemap entries) stay byte-stable.
|
|
109
|
+
*/
|
|
110
|
+
export function authorSlug(name: string): string {
|
|
111
|
+
const folded = name
|
|
112
|
+
.normalize("NFKD") // decompose "é" -> "e" + combining acute
|
|
113
|
+
.replace(/[\u0300-\u036f]/g, "") // strip the combining marks
|
|
114
|
+
.replace(/['\u2018\u2019\u02bc\u0060]/g, ""); // drop apostrophes/quotes, do not split
|
|
115
|
+
return categorySlug(folded);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeStatus(raw: unknown): PostStatus {
|
|
119
|
+
return raw === "draft" || raw === "unlisted" || raw === "published" || raw === "scheduled"
|
|
120
|
+
? raw
|
|
121
|
+
: "published";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Legacy/seeded post JSON may omit `status`; default it to "published" on load so
|
|
125
|
+
// every downstream reader sees a concrete status.
|
|
126
|
+
function normalize(raw: BlogPost): BlogPost {
|
|
127
|
+
return { ...raw, status: normalizeStatus((raw as { status?: unknown }).status) };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function postsPrefix(tenantId: string, collection: string): string {
|
|
131
|
+
return `/tenants/${tenantId}/content/${collection}/posts/`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function toSummary(p: BlogPost): BlogPostSummary {
|
|
135
|
+
return {
|
|
136
|
+
source: p.source,
|
|
137
|
+
slug: p.slug,
|
|
138
|
+
title: p.title,
|
|
139
|
+
seoTitle: p.seoTitle,
|
|
140
|
+
description: p.description,
|
|
141
|
+
excerpt: p.excerpt,
|
|
142
|
+
publishedAt: p.publishedAt,
|
|
143
|
+
modifiedAt: p.modifiedAt,
|
|
144
|
+
author: p.author,
|
|
145
|
+
categories: p.categories,
|
|
146
|
+
tags: p.tags,
|
|
147
|
+
status: p.status,
|
|
148
|
+
...(p.scheduledAt ? { scheduledAt: p.scheduledAt } : {}),
|
|
149
|
+
...(p.featuredImage ? { featuredImage: p.featuredImage } : {}),
|
|
150
|
+
...(p.canonicalUrl ? { canonicalUrl: p.canonicalUrl } : {}),
|
|
151
|
+
...(p.inLanguage ? { inLanguage: p.inLanguage } : {}),
|
|
152
|
+
...(p.translationGroupId ? { translationGroupId: p.translationGroupId } : {}),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The collection ids that actually have post content for a tenant, derived from
|
|
158
|
+
* the same glob the index is built from (no per-tenant config needed). Sorted for
|
|
159
|
+
* a stable admin ordering. A tenant with no blog content returns `[]`.
|
|
160
|
+
*/
|
|
161
|
+
export async function listCollections(
|
|
162
|
+
tenantId: string,
|
|
163
|
+
content?: ArtifactSource,
|
|
164
|
+
): Promise<string[]> {
|
|
165
|
+
const marker = `/tenants/${tenantId}/content/`;
|
|
166
|
+
const collections = new Set<string>();
|
|
167
|
+
const runtimePaths = canReadArtifacts(content)
|
|
168
|
+
? await content.listArtifactPaths("")
|
|
169
|
+
: null;
|
|
170
|
+
if (runtimePaths !== null) {
|
|
171
|
+
for (const path of runtimePaths) {
|
|
172
|
+
const match = /^([^/]+)\/posts\/[^/]+\.json$/.exec(path);
|
|
173
|
+
if (match?.[1] && SEGMENT_RE.test(match[1])) collections.add(match[1]);
|
|
174
|
+
}
|
|
175
|
+
return [...collections].sort();
|
|
176
|
+
}
|
|
177
|
+
for (const key of Object.keys(postGlobs)) {
|
|
178
|
+
const at = key.indexOf(marker);
|
|
179
|
+
if (at < 0) continue;
|
|
180
|
+
const rest = key.slice(at + marker.length);
|
|
181
|
+
const collection = rest.slice(0, rest.indexOf("/"));
|
|
182
|
+
if (collection && SEGMENT_RE.test(collection)) collections.add(collection);
|
|
183
|
+
}
|
|
184
|
+
return [...collections].sort();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The derived index for `(tenant, collection)`, newest first. Live surfaces get
|
|
189
|
+
* published posts only; admin/preview pass `includeUnpublished` to also see
|
|
190
|
+
* drafts and unlisted posts.
|
|
191
|
+
*/
|
|
192
|
+
export async function listPosts(
|
|
193
|
+
tenantId: string,
|
|
194
|
+
collection: string,
|
|
195
|
+
opts?: CollectionReadOptions,
|
|
196
|
+
): Promise<BlogPostSummary[]> {
|
|
197
|
+
if (!SEGMENT_RE.test(collection)) return [];
|
|
198
|
+
const prefix = postsPrefix(tenantId, collection);
|
|
199
|
+
const runtimeContent = canReadArtifacts(opts?.content) ? opts.content : null;
|
|
200
|
+
const runtimePaths = runtimeContent
|
|
201
|
+
? await runtimeContent.listArtifactPaths(`${collection}/posts/`)
|
|
202
|
+
: null;
|
|
203
|
+
const posts = runtimePaths !== null
|
|
204
|
+
? (await Promise.all(
|
|
205
|
+
runtimePaths
|
|
206
|
+
.filter((path) => path.endsWith(".json"))
|
|
207
|
+
.map(async (path) => {
|
|
208
|
+
const raw = await runtimeContent?.readArtifact(path);
|
|
209
|
+
if (!raw) return null;
|
|
210
|
+
try {
|
|
211
|
+
return normalize(JSON.parse(raw) as BlogPost);
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}),
|
|
216
|
+
)).filter((post): post is BlogPost => post !== null)
|
|
217
|
+
: (await Promise.all(
|
|
218
|
+
Object.keys(postGlobs)
|
|
219
|
+
.filter((key) => key.includes(prefix))
|
|
220
|
+
.map((key) => postGlobs[key]!()),
|
|
221
|
+
)).map(normalize);
|
|
222
|
+
const includeUnpublished = opts?.includeUnpublished ?? false;
|
|
223
|
+
const now = opts?.now ?? new Date().toISOString();
|
|
224
|
+
return posts
|
|
225
|
+
.filter((p) => includeUnpublished || p.status === "published" || isDueScheduled(p, now))
|
|
226
|
+
.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : a.publishedAt > b.publishedAt ? -1 : 0))
|
|
227
|
+
.map(toSummary);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Load a single post. On the live path a draft returns null (404) and an
|
|
232
|
+
* unlisted post still resolves; admin/preview pass `includeUnpublished` to load
|
|
233
|
+
* anything.
|
|
234
|
+
*/
|
|
235
|
+
export async function getPost(
|
|
236
|
+
tenantId: string,
|
|
237
|
+
collection: string,
|
|
238
|
+
slug: string,
|
|
239
|
+
opts?: CollectionReadOptions,
|
|
240
|
+
): Promise<BlogPost | null> {
|
|
241
|
+
if (!SEGMENT_RE.test(collection) || !SLUG_RE.test(slug)) return null;
|
|
242
|
+
if (canReadArtifacts(opts?.content)) {
|
|
243
|
+
const paths = await opts.content.listArtifactPaths(`${collection}/posts/`);
|
|
244
|
+
const raw = await opts.content.readArtifact(`${collection}/posts/${slug}.json`);
|
|
245
|
+
if (raw) {
|
|
246
|
+
try {
|
|
247
|
+
const post = normalize(JSON.parse(raw) as BlogPost);
|
|
248
|
+
if (!(opts.includeUnpublished ?? false) && !isVisibleOnLive(post, opts.now)) return null;
|
|
249
|
+
return post;
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (paths !== null) return null;
|
|
255
|
+
}
|
|
256
|
+
const key = Object.keys(postGlobs).find(
|
|
257
|
+
(candidate) =>
|
|
258
|
+
candidate.includes(postsPrefix(tenantId, collection)) && candidate.endsWith(`/${slug}.json`),
|
|
259
|
+
);
|
|
260
|
+
if (!key) return null;
|
|
261
|
+
const post = normalize(await postGlobs[key]!());
|
|
262
|
+
if (!(opts?.includeUnpublished ?? false) && !isVisibleOnLive(post, opts?.now)) return null;
|
|
263
|
+
return post;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Every language variant sharing a `translationGroupId`, newest first. A filter
|
|
268
|
+
* over `listPosts`, so it inherits the visibility chokepoint verbatim: on the live
|
|
269
|
+
* path a draft/not-yet-due translation never appears, and an unlisted variant —
|
|
270
|
+
* which resolves at its own URL but is absent from every listing — is likewise
|
|
271
|
+
* absent here. Pass `includeUnpublished` for the admin/preview view of the group.
|
|
272
|
+
*/
|
|
273
|
+
export async function listTranslations(
|
|
274
|
+
tenantId: string,
|
|
275
|
+
collection: string,
|
|
276
|
+
translationGroupId: string,
|
|
277
|
+
opts?: CollectionReadOptions,
|
|
278
|
+
): Promise<BlogPostSummary[]> {
|
|
279
|
+
if (!translationGroupId) return [];
|
|
280
|
+
const posts = await listPosts(tenantId, collection, opts);
|
|
281
|
+
return posts.filter((p) => p.translationGroupId === translationGroupId);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Convenience for a route holding a slug: the full variant set for the article at
|
|
286
|
+
* `slug`, including the post itself. A singleton (no `translationGroupId`) returns
|
|
287
|
+
* exactly `[that post]` — the uniform "at least one" contract lets a caller render
|
|
288
|
+
* a switcher purely on `length > 1` without a separate group-id guard. Returns `[]`
|
|
289
|
+
* only when the post itself does not resolve (unknown, or hidden on the live path).
|
|
290
|
+
*/
|
|
291
|
+
export async function getTranslations(
|
|
292
|
+
tenantId: string,
|
|
293
|
+
collection: string,
|
|
294
|
+
slug: string,
|
|
295
|
+
opts?: CollectionReadOptions,
|
|
296
|
+
): Promise<BlogPostSummary[]> {
|
|
297
|
+
const post = await getPost(tenantId, collection, slug, opts);
|
|
298
|
+
if (!post) return [];
|
|
299
|
+
if (!post.translationGroupId) return [toSummary(post)];
|
|
300
|
+
return listTranslations(tenantId, collection, post.translationGroupId, opts);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Live-visible related posts for an article page: same primary category first
|
|
305
|
+
* (newest first, excluding the current post), then fall back to newest overall
|
|
306
|
+
* to fill up to `n`. Only published posts are candidates; the current post may
|
|
307
|
+
* itself be unlisted (its page still resolves) and is always excluded.
|
|
308
|
+
*/
|
|
309
|
+
export async function relatedPosts(
|
|
310
|
+
tenantId: string,
|
|
311
|
+
collection: string,
|
|
312
|
+
slug: string,
|
|
313
|
+
n = 3,
|
|
314
|
+
content?: ArtifactSource,
|
|
315
|
+
): Promise<BlogPostSummary[]> {
|
|
316
|
+
const pool = (await listPosts(tenantId, collection, { content })).filter((p) => p.slug !== slug);
|
|
317
|
+
const current = await getPost(tenantId, collection, slug, { content });
|
|
318
|
+
const category = current?.categories[0];
|
|
319
|
+
const result = category ? pool.filter((p) => p.categories.includes(category)) : [];
|
|
320
|
+
for (const p of pool) {
|
|
321
|
+
if (result.length >= n) break;
|
|
322
|
+
if (!result.some((r) => r.slug === p.slug)) result.push(p);
|
|
323
|
+
}
|
|
324
|
+
return result.slice(0, n);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function resolveRedirect(
|
|
328
|
+
tenantId: string,
|
|
329
|
+
collection: string,
|
|
330
|
+
slug: string,
|
|
331
|
+
content?: ArtifactSource,
|
|
332
|
+
): Promise<string | null> {
|
|
333
|
+
if (!SEGMENT_RE.test(collection)) return null;
|
|
334
|
+
const redirects = await getRedirects(tenantId, collection, content);
|
|
335
|
+
return redirects[slug] ?? null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The full `slug -> path` redirect map for `(tenant, collection)`, or `{}` when
|
|
340
|
+
* there is no `redirects.json`. The admin fast lane (B2.2) reads this to APPEND a
|
|
341
|
+
* redirect when a published post is deleted, without clobbering existing entries.
|
|
342
|
+
*/
|
|
343
|
+
export async function getRedirects(
|
|
344
|
+
tenantId: string,
|
|
345
|
+
collection: string,
|
|
346
|
+
content?: ArtifactSource,
|
|
347
|
+
): Promise<Record<string, string>> {
|
|
348
|
+
if (!SEGMENT_RE.test(collection)) return {};
|
|
349
|
+
if (canReadArtifacts(content)) {
|
|
350
|
+
const paths = await content.listArtifactPaths(`${collection}/`);
|
|
351
|
+
const raw = await content.readArtifact(`${collection}/redirects.json`);
|
|
352
|
+
if (raw) {
|
|
353
|
+
try {
|
|
354
|
+
return JSON.parse(raw) as Record<string, string>;
|
|
355
|
+
} catch {
|
|
356
|
+
return {};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (paths !== null) return {};
|
|
360
|
+
}
|
|
361
|
+
const key = Object.keys(redirectGlobs).find(
|
|
362
|
+
(candidate) =>
|
|
363
|
+
candidate.includes(`/tenants/${tenantId}/content/${collection}/`) &&
|
|
364
|
+
candidate.endsWith("/redirects.json"),
|
|
365
|
+
);
|
|
366
|
+
if (!key) return {};
|
|
367
|
+
return (await redirectGlobs[key]!()) ?? {};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Optional per-tenant × per-collection author bios, keyed by author slug. Missing
|
|
372
|
+
* file returns {} so callers fall back to the byline name. Author pages resolve a
|
|
373
|
+
* bio with `getAuthors(t, c)[authorSlug(post.author)]`.
|
|
374
|
+
*/
|
|
375
|
+
export async function getAuthors(
|
|
376
|
+
tenantId: string,
|
|
377
|
+
collection: string,
|
|
378
|
+
content?: ArtifactSource,
|
|
379
|
+
): Promise<Record<string, AuthorBio>> {
|
|
380
|
+
if (!SEGMENT_RE.test(collection)) return {};
|
|
381
|
+
if (canReadArtifacts(content)) {
|
|
382
|
+
const paths = await content.listArtifactPaths(`${collection}/`);
|
|
383
|
+
const raw = await content.readArtifact(`${collection}/authors.json`);
|
|
384
|
+
if (raw) {
|
|
385
|
+
try {
|
|
386
|
+
return JSON.parse(raw) as Record<string, AuthorBio>;
|
|
387
|
+
} catch {
|
|
388
|
+
return {};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (paths !== null) return {};
|
|
392
|
+
}
|
|
393
|
+
const key = Object.keys(authorsGlobs).find(
|
|
394
|
+
(candidate) =>
|
|
395
|
+
candidate.includes(`/tenants/${tenantId}/content/${collection}/`) &&
|
|
396
|
+
candidate.endsWith("/authors.json"),
|
|
397
|
+
);
|
|
398
|
+
if (!key) return {};
|
|
399
|
+
return (await authorsGlobs[key]!()) ?? {};
|
|
400
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, dependency-free Markdown -> sanitized HTML for the admin blog editor
|
|
3
|
+
* (see docs/plans/2026-08-21-blog-platform-workstream.md). The storefront
|
|
4
|
+
* ships no markdown library (see the "No new deps" convention in §1), and the
|
|
5
|
+
* migration package's `sanitize-html` is a node-only devtool — so authoring from
|
|
6
|
+
* the admin gets this small hand-rolled renderer instead.
|
|
7
|
+
*
|
|
8
|
+
* SAFETY: the output is fed to `set:html` in the reader route
|
|
9
|
+
* (`the-build/[slug].astro`), so it must never emit author-supplied HTML
|
|
10
|
+
* verbatim. All text is HTML-escaped BEFORE any tag is emitted, only the tag set
|
|
11
|
+
* this renderer produces can appear, and link hrefs / image `src`s are
|
|
12
|
+
* scheme-checked (`http(s)`/`mailto`/site-relative/anchor only — `javascript:`
|
|
13
|
+
* etc. fall back to plain text). Admin is already gated, but escaping is
|
|
14
|
+
* defense-in-depth.
|
|
15
|
+
*
|
|
16
|
+
* SCOPE (v1): headings, bold, italic, inline code, fenced code, links, images,
|
|
17
|
+
* blockquotes, unordered/ordered lists, horizontal rules, paragraphs. NOT a
|
|
18
|
+
* CommonMark implementation — no nested lists or tables.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// A control char that never appears in authored prose and is untouched by
|
|
22
|
+
// escapeHtml — a collision-free placeholder while code spans are set aside.
|
|
23
|
+
const CODE_MARK = String.fromCharCode(0);
|
|
24
|
+
const CODE_RE = new RegExp(`${CODE_MARK}(\\d+)${CODE_MARK}`, "g");
|
|
25
|
+
|
|
26
|
+
function escapeHtml(s: string): string {
|
|
27
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Closes the one gap `escapeHtml` leaves open for attribute interpolation.
|
|
31
|
+
// The url substring reaching this point has ALREADY passed through the single
|
|
32
|
+
// escapeHtml(t) pass over the whole inline text (renderInline escapes before
|
|
33
|
+
// running the link regex), so `&`/`<`/`>` are already entity-encoded — only
|
|
34
|
+
// `"`/`'` remain untouched. Re-running full escapeHtml here would double-encode
|
|
35
|
+
// those already-escaped entities, so this only needs to close the quote gap.
|
|
36
|
+
function escapeAttrQuotes(s: string): string {
|
|
37
|
+
return s.replace(/"/g, """).replace(/'/g, "'");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Only permit hrefs that can't execute script. Text is already escaped by the
|
|
41
|
+
// time a URL reaches here, so a `javascript:`/`data:` payload arrives intact and
|
|
42
|
+
// is rejected -> the link renders as its (escaped) label text instead. Quote/
|
|
43
|
+
// angle-bracket characters are rejected outright (second line of defense on
|
|
44
|
+
// top of escapeAttrQuotes at the call site) so an attribute-breakout payload
|
|
45
|
+
// never matches the scheme allowlist in the first place.
|
|
46
|
+
function sanitizeUrl(url: string): string | null {
|
|
47
|
+
const u = url.trim();
|
|
48
|
+
if (/["'<>]/.test(u)) return null;
|
|
49
|
+
// `//host/path` is protocol-relative — a browser resolves it to an absolute
|
|
50
|
+
// cross-origin URL using the current scheme, so it must NOT be accepted
|
|
51
|
+
// under the "site-relative" `/` allowance (only a single, non-doubled slash
|
|
52
|
+
// is genuinely site-relative).
|
|
53
|
+
if (/^\/\//.test(u)) return null;
|
|
54
|
+
return /^(https?:\/\/|mailto:|\/|#)/i.test(u) ? u : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Inline spans, applied to already-block-split text. Code spans are extracted
|
|
58
|
+
// first (and their contents escaped) so emphasis/link syntax inside `code` is
|
|
59
|
+
// left literal; everything else is escaped, then link/bold/italic tags are woven
|
|
60
|
+
// back in, then the code spans are restored.
|
|
61
|
+
function renderInline(text: string): string {
|
|
62
|
+
const codes: string[] = [];
|
|
63
|
+
let t = text.replace(/`([^`]+)`/g, (_m, code: string) => {
|
|
64
|
+
codes.push(`<code>${escapeHtml(code)}</code>`);
|
|
65
|
+
return `${CODE_MARK}${codes.length - 1}${CODE_MARK}`;
|
|
66
|
+
});
|
|
67
|
+
t = escapeHtml(t);
|
|
68
|
+
// Images before links: `` embeds a link-like `[alt](url)` span, so
|
|
69
|
+
// the link regex would otherwise claim it. Each emitted `<img>` is stashed in
|
|
70
|
+
// `codes` (same placeholder machinery as code spans) so the later link and
|
|
71
|
+
// emphasis passes can't reach into its `src`/`alt` attribute values — an
|
|
72
|
+
// `alt` of `a*b*c` must stay literal inside the attribute, not sprout an
|
|
73
|
+
// `<em>`. `src` is scheme-checked (data:/protocol-relative rejected) and both
|
|
74
|
+
// attribute values are quote-encoded on top of the whole-text escapeHtml pass.
|
|
75
|
+
t = t.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (_m, alt: string, url: string) => {
|
|
76
|
+
const safe = sanitizeUrl(url);
|
|
77
|
+
if (!safe) return alt;
|
|
78
|
+
const img = `<img src="${escapeAttrQuotes(safe)}" alt="${escapeAttrQuotes(alt)}" loading="lazy" decoding="async">`;
|
|
79
|
+
codes.push(img);
|
|
80
|
+
return `${CODE_MARK}${codes.length - 1}${CODE_MARK}`;
|
|
81
|
+
});
|
|
82
|
+
t = t.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, label: string, url: string) => {
|
|
83
|
+
const safe = sanitizeUrl(url);
|
|
84
|
+
return safe ? `<a href="${escapeAttrQuotes(safe)}">${label}</a>` : label;
|
|
85
|
+
});
|
|
86
|
+
t = t
|
|
87
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
88
|
+
.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
89
|
+
t = t
|
|
90
|
+
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
|
|
91
|
+
.replace(/(^|[^a-zA-Z0-9])_([^_]+)_(?=[^a-zA-Z0-9]|$)/g, "$1<em>$2</em>");
|
|
92
|
+
return t.replace(CODE_RE, (_m, i: string) => codes[Number(i)]!);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isBlank = (l: string): boolean => l.trim() === "";
|
|
96
|
+
const startsBlock = (l: string): boolean =>
|
|
97
|
+
/^```/.test(l) ||
|
|
98
|
+
/^#{1,6}\s+/.test(l) ||
|
|
99
|
+
/^>\s?/.test(l) ||
|
|
100
|
+
/^\s*[-*+]\s+/.test(l) ||
|
|
101
|
+
/^\s*\d+\.\s+/.test(l) ||
|
|
102
|
+
/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(l);
|
|
103
|
+
|
|
104
|
+
export function renderMarkdown(md: string): string {
|
|
105
|
+
const lines = md.replace(/\r\n?/g, "\n").split("\n");
|
|
106
|
+
const out: string[] = [];
|
|
107
|
+
let i = 0;
|
|
108
|
+
|
|
109
|
+
while (i < lines.length) {
|
|
110
|
+
const line = lines[i]!;
|
|
111
|
+
|
|
112
|
+
if (isBlank(line)) {
|
|
113
|
+
i++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const fence = line.match(/^```\s*([a-zA-Z0-9_-]*)\s*$/);
|
|
118
|
+
if (fence) {
|
|
119
|
+
const buf: string[] = [];
|
|
120
|
+
i++;
|
|
121
|
+
while (i < lines.length && !/^```\s*$/.test(lines[i]!)) buf.push(lines[i++]!);
|
|
122
|
+
i++; // consume closing fence (tolerate an unterminated block at EOF)
|
|
123
|
+
const cls = fence[1] ? ` class="language-${fence[1]}"` : "";
|
|
124
|
+
out.push(`<pre><code${cls}>${escapeHtml(buf.join("\n"))}</code></pre>`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
129
|
+
if (heading) {
|
|
130
|
+
const level = heading[1]!.length;
|
|
131
|
+
out.push(`<h${level}>${renderInline(heading[2]!.trim())}</h${level}>`);
|
|
132
|
+
i++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
|
|
137
|
+
out.push("<hr>");
|
|
138
|
+
i++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (/^>\s?/.test(line)) {
|
|
143
|
+
const buf: string[] = [];
|
|
144
|
+
while (i < lines.length && /^>\s?/.test(lines[i]!)) buf.push(lines[i++]!.replace(/^>\s?/, ""));
|
|
145
|
+
out.push(`<blockquote>${renderMarkdown(buf.join("\n"))}</blockquote>`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (/^\s*[-*+]\s+/.test(line)) {
|
|
150
|
+
const items: string[] = [];
|
|
151
|
+
while (i < lines.length && /^\s*[-*+]\s+/.test(lines[i]!)) {
|
|
152
|
+
items.push(lines[i++]!.replace(/^\s*[-*+]\s+/, "").trim());
|
|
153
|
+
}
|
|
154
|
+
out.push(`<ul>${items.map((it) => `<li>${renderInline(it)}</li>`).join("")}</ul>`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (/^\s*\d+\.\s+/.test(line)) {
|
|
159
|
+
const items: string[] = [];
|
|
160
|
+
while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i]!)) {
|
|
161
|
+
items.push(lines[i++]!.replace(/^\s*\d+\.\s+/, "").trim());
|
|
162
|
+
}
|
|
163
|
+
out.push(`<ol>${items.map((it) => `<li>${renderInline(it)}</li>`).join("")}</ol>`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const buf: string[] = [];
|
|
168
|
+
while (i < lines.length && !isBlank(lines[i]!) && !startsBlock(lines[i]!)) buf.push(lines[i++]!);
|
|
169
|
+
const text = buf.join(" ").trim();
|
|
170
|
+
if (text) out.push(`<p>${renderInline(text)}</p>`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return out.join("\n");
|
|
174
|
+
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
// The live WordPress `/blog` deliberately keeps reading its committed, curated
|
|
2
|
+
// index.json (its order is hand-maintained and SEO-load-bearing). The generic
|
|
3
|
+
// derived engine lives in ./collection.ts and powers `the-build` + any new
|
|
4
|
+
// collection; switching `/blog` onto it is a later migration once order is
|
|
5
|
+
// encoded as data (see B0.1 §"/blog migration caution").
|
|
1
6
|
import type { BlogPost, BlogPostSummary } from "./types.js";
|
|
7
|
+
import type { ContentProvider } from "../storyblok/provider.js";
|
|
8
|
+
|
|
9
|
+
type ArtifactSource = Pick<ContentProvider, "readArtifact" | "listArtifactPaths">;
|
|
10
|
+
|
|
11
|
+
function canReadArtifacts(value: ArtifactSource | undefined): value is ArtifactSource {
|
|
12
|
+
return typeof value?.listArtifactPaths === "function" && typeof value.readArtifact === "function";
|
|
13
|
+
}
|
|
2
14
|
|
|
3
15
|
const indexGlobs = import.meta.glob<BlogPostSummary[]>(
|
|
4
16
|
"../../../../../tenants/*/content/blog/index.json",
|
|
@@ -18,19 +30,66 @@ function tenantKey(keys: string[], tenantId: string, suffix: string): string | u
|
|
|
18
30
|
return keys.find((key) => key.includes(segment) && key.endsWith(suffix));
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
export async function listBlogPosts(
|
|
33
|
+
export async function listBlogPosts(
|
|
34
|
+
tenantId: string,
|
|
35
|
+
source?: ArtifactSource,
|
|
36
|
+
): Promise<BlogPostSummary[]> {
|
|
37
|
+
if (canReadArtifacts(source)) {
|
|
38
|
+
const paths = await source.listArtifactPaths("blog/");
|
|
39
|
+
const raw = await source.readArtifact("blog/index.json");
|
|
40
|
+
if (raw) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(raw) as BlogPostSummary[];
|
|
43
|
+
} catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (paths !== null) return [];
|
|
48
|
+
}
|
|
22
49
|
const key = tenantKey(Object.keys(indexGlobs), tenantId, "/index.json")
|
|
23
50
|
?? Object.keys(indexGlobs).find((candidate) => candidate.includes(`/tenants/${tenantId}/content/blog/index.json`));
|
|
24
51
|
return key ? indexGlobs[key]!() : [];
|
|
25
52
|
}
|
|
26
53
|
|
|
27
|
-
export async function getBlogPost(
|
|
54
|
+
export async function getBlogPost(
|
|
55
|
+
tenantId: string,
|
|
56
|
+
slug: string,
|
|
57
|
+
source?: ArtifactSource,
|
|
58
|
+
): Promise<BlogPost | null> {
|
|
28
59
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) return null;
|
|
60
|
+
if (canReadArtifacts(source)) {
|
|
61
|
+
const paths = await source.listArtifactPaths("blog/posts/");
|
|
62
|
+
const raw = await source.readArtifact(`blog/posts/${slug}.json`);
|
|
63
|
+
if (raw) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(raw) as BlogPost;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (paths !== null) return null;
|
|
71
|
+
}
|
|
29
72
|
const key = tenantKey(Object.keys(postGlobs), tenantId, `/posts/${slug}.json`);
|
|
30
73
|
return key ? postGlobs[key]!() : null;
|
|
31
74
|
}
|
|
32
75
|
|
|
33
|
-
export async function resolveBlogRedirect(
|
|
76
|
+
export async function resolveBlogRedirect(
|
|
77
|
+
tenantId: string,
|
|
78
|
+
slug: string,
|
|
79
|
+
source?: ArtifactSource,
|
|
80
|
+
): Promise<string | null> {
|
|
81
|
+
if (canReadArtifacts(source)) {
|
|
82
|
+
const paths = await source.listArtifactPaths("blog/");
|
|
83
|
+
const raw = await source.readArtifact("blog/redirects.json");
|
|
84
|
+
if (raw) {
|
|
85
|
+
try {
|
|
86
|
+
return (JSON.parse(raw) as Record<string, string>)[slug] ?? null;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (paths !== null) return null;
|
|
92
|
+
}
|
|
34
93
|
const key = tenantKey(Object.keys(redirectGlobs), tenantId, "/redirects.json")
|
|
35
94
|
?? Object.keys(redirectGlobs).find((candidate) => candidate.includes(`/tenants/${tenantId}/content/blog/redirects.json`));
|
|
36
95
|
if (!key) return null;
|