@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,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E1.1 — per-post reader reactions: an anonymous, additive emoji tally, the
|
|
3
|
+
* first reader-sentiment signal for the blog platform (complements view counts,
|
|
4
|
+
* which measure reads). A near-clone of `viewCounts.ts`: KV counters keyed by
|
|
5
|
+
* (tenant, collection, slug, reaction), best-effort read-modify-write, no PII,
|
|
6
|
+
* no per-visitor identity, no moderation (an emoji count carries no user text —
|
|
7
|
+
* worst case is an inflated tally, self-limiting and low-stakes).
|
|
8
|
+
*
|
|
9
|
+
* Storage is the TENANT_CACHE KV namespace (same binding view-counts uses).
|
|
10
|
+
* Under concurrency some increments are lost to KV's last-write-wins semantics:
|
|
11
|
+
* acceptable for a demand signal, not a billing counter.
|
|
12
|
+
*
|
|
13
|
+
* Tenant/collection-generic by construction — nothing here is bound to a
|
|
14
|
+
* specific tenant or collection.
|
|
15
|
+
*/
|
|
16
|
+
import { getPost } from "./collection.js";
|
|
17
|
+
|
|
18
|
+
/** The fixed reaction vocabulary. Not free-form — a closed set like the pillar vocabulary. */
|
|
19
|
+
export const REACTIONS = ["like", "insightful", "celebrate"] as const;
|
|
20
|
+
export type Reaction = (typeof REACTIONS)[number];
|
|
21
|
+
|
|
22
|
+
/** Narrow an untrusted string to a known reaction, or null. */
|
|
23
|
+
export function isReaction(value: unknown): value is Reaction {
|
|
24
|
+
return typeof value === "string" && (REACTIONS as readonly string[]).includes(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** KV key for one (post, reaction) rolling tally. */
|
|
28
|
+
export function reactionsKey(
|
|
29
|
+
tenantId: string,
|
|
30
|
+
collection: string,
|
|
31
|
+
slug: string,
|
|
32
|
+
reaction: Reaction,
|
|
33
|
+
): string {
|
|
34
|
+
return `blogreact:${tenantId}:${collection}:${slug}:${reaction}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readCount(kv: KVNamespace, key: string): Promise<number> {
|
|
38
|
+
const raw = await kv.get(key);
|
|
39
|
+
const n = raw == null ? 0 : Number.parseInt(raw, 10);
|
|
40
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Increment the tally for `reaction` on a post IF the reaction is in the vocab
|
|
45
|
+
* AND the post is live (published/unlisted/due-scheduled) for this tenant —
|
|
46
|
+
* `getPost` returns null on the live path for a draft/404/non-post. Best-effort:
|
|
47
|
+
* never throws (a reaction must never fail the reader's request).
|
|
48
|
+
*/
|
|
49
|
+
export async function recordReaction(
|
|
50
|
+
kv: KVNamespace,
|
|
51
|
+
tenantId: string,
|
|
52
|
+
collection: string,
|
|
53
|
+
slug: string,
|
|
54
|
+
reaction: string,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
if (!isReaction(reaction)) return;
|
|
58
|
+
const post = await getPost(tenantId, collection, slug);
|
|
59
|
+
if (!post) return; // not a live post (draft / 404 / non-blog) — do not count
|
|
60
|
+
const key = reactionsKey(tenantId, collection, slug, reaction);
|
|
61
|
+
const next = (await readCount(kv, key)) + 1;
|
|
62
|
+
await kv.put(key, String(next));
|
|
63
|
+
} catch {
|
|
64
|
+
/* swallow — reactions are best-effort */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The full tally map for a post: every vocab reaction to its count (0 if unset).
|
|
70
|
+
* A null KV (local dev, no binding) yields all-zero counts. Read by the reader
|
|
71
|
+
* reaction bar and the admin engagement panel (E1.2).
|
|
72
|
+
*/
|
|
73
|
+
export async function getReactionCounts(
|
|
74
|
+
kv: KVNamespace | undefined,
|
|
75
|
+
tenantId: string,
|
|
76
|
+
collection: string,
|
|
77
|
+
slug: string,
|
|
78
|
+
): Promise<Record<Reaction, number>> {
|
|
79
|
+
const out = Object.fromEntries(REACTIONS.map((r) => [r, 0])) as Record<Reaction, number>;
|
|
80
|
+
if (!kv) return out;
|
|
81
|
+
await Promise.all(
|
|
82
|
+
REACTIONS.map(async (reaction) => {
|
|
83
|
+
out[reaction] = await readCount(kv, reactionsKey(tenantId, collection, slug, reaction));
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { BlogPostSummary } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hand-rolled RSS 2.0 renderer (no `@astrojs/rss` dependency), the syndication
|
|
5
|
+
* mirror of `sitemap.xml.ts`. Tenant- and collection-agnostic: the caller passes
|
|
6
|
+
* the resolved canonical base + the collection's route base, so the SAME renderer
|
|
7
|
+
* serves `/the-build/rss.xml`, `/blog/rss.xml`, and any tenant. Feeding it the
|
|
8
|
+
* engine's live-visible list (`listPosts` default) inherits the visibility
|
|
9
|
+
* chokepoint — drafts/unlisted never reach a feed.
|
|
10
|
+
*/
|
|
11
|
+
export interface RssFeedInput {
|
|
12
|
+
channelTitle: string;
|
|
13
|
+
channelDescription: string;
|
|
14
|
+
/** Resolved canonical base, e.g. `https://x.com` or a path-prefixed preview host. */
|
|
15
|
+
canonicalBase: string;
|
|
16
|
+
/** The collection's route base, e.g. `/the-build`. */
|
|
17
|
+
collectionBase: string;
|
|
18
|
+
/**
|
|
19
|
+
* Feed items. `BlogPost` is assignable here (it extends `BlogPostSummary`),
|
|
20
|
+
* so an item may carry a full `contentHtml` body — which is emitted as
|
|
21
|
+
* `<content:encoded>` for public posts only. Gated posts stay in the feed but
|
|
22
|
+
* never syndicate their body (see the per-item guard below).
|
|
23
|
+
*/
|
|
24
|
+
posts: Array<BlogPostSummary & { contentHtml?: string }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** XML-escape text inside a tag (mirrors sitemap.xml.ts). */
|
|
28
|
+
export function escapeXml(value: string): string {
|
|
29
|
+
return value
|
|
30
|
+
.replace(/&/g, "&")
|
|
31
|
+
.replace(/</g, "<")
|
|
32
|
+
.replace(/>/g, ">")
|
|
33
|
+
.replace(/"/g, """)
|
|
34
|
+
.replace(/'/g, "'");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** RFC-822 date (`Sun, 01 Mar 2026 00:00:00 GMT`), the RSS `pubDate` format. */
|
|
38
|
+
function rfc822(iso: string): string {
|
|
39
|
+
const d = new Date(iso);
|
|
40
|
+
return Number.isNaN(d.getTime()) ? "" : d.toUTCString();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function renderRssFeed(input: RssFeedInput): string {
|
|
44
|
+
const { channelTitle, channelDescription, posts } = input;
|
|
45
|
+
const base = input.canonicalBase.replace(/\/$/, "");
|
|
46
|
+
const collectionBase = `/${input.collectionBase.replace(/^\/+|\/+$/g, "")}`;
|
|
47
|
+
const channelLink = `${base}${collectionBase}/`;
|
|
48
|
+
const feedLink = `${base}${collectionBase}/rss.xml`;
|
|
49
|
+
|
|
50
|
+
const items = posts
|
|
51
|
+
.map((post) => {
|
|
52
|
+
const link = `${base}${collectionBase}/${post.slug}/`;
|
|
53
|
+
const pubDate = rfc822(post.publishedAt);
|
|
54
|
+
const categories = post.categories
|
|
55
|
+
.map((c) => ` <category>${escapeXml(c)}</category>`)
|
|
56
|
+
.join("\n");
|
|
57
|
+
// Entitlement guard: a gated post (access !== "public") stays discoverable
|
|
58
|
+
// in the feed but NEVER syndicates its body — otherwise a feed reader would
|
|
59
|
+
// unlock paywalled content. Only public posts emit <content:encoded>.
|
|
60
|
+
const gated = (post.access ?? "public") !== "public";
|
|
61
|
+
const contentEncoded =
|
|
62
|
+
!gated && post.contentHtml
|
|
63
|
+
? ` <content:encoded><![CDATA[${post.contentHtml}]]></content:encoded>`
|
|
64
|
+
: "";
|
|
65
|
+
return [
|
|
66
|
+
" <item>",
|
|
67
|
+
` <title>${escapeXml(post.title)}</title>`,
|
|
68
|
+
` <link>${escapeXml(link)}</link>`,
|
|
69
|
+
` <guid isPermaLink="true">${escapeXml(link)}</guid>`,
|
|
70
|
+
pubDate ? ` <pubDate>${pubDate}</pubDate>` : "",
|
|
71
|
+
` <dc:creator>${escapeXml(post.author)}</dc:creator>`,
|
|
72
|
+
` <description>${escapeXml(post.excerpt)}</description>`,
|
|
73
|
+
contentEncoded,
|
|
74
|
+
categories,
|
|
75
|
+
]
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.join("\n");
|
|
78
|
+
})
|
|
79
|
+
.join("\n");
|
|
80
|
+
|
|
81
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
82
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
|
83
|
+
<channel>
|
|
84
|
+
<title>${escapeXml(channelTitle)}</title>
|
|
85
|
+
<link>${escapeXml(channelLink)}</link>
|
|
86
|
+
<atom:link href="${escapeXml(feedLink)}" rel="self" type="application/rss+xml" />
|
|
87
|
+
<description>${escapeXml(channelDescription)}</description>
|
|
88
|
+
${items}
|
|
89
|
+
</channel>
|
|
90
|
+
</rss>
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teaser truncation for gated posts. A members-only post is still a 200 with
|
|
3
|
+
* partial content (indexable — see the B4.0 spike's SEO note), so an anonymous /
|
|
4
|
+
* unentitled reader gets the first few blocks of the real body plus a paywall
|
|
5
|
+
* card, not a hard wall.
|
|
6
|
+
*
|
|
7
|
+
* Truncation is by TOP-LEVEL BLOCK COUNT, cutting only on an element boundary so
|
|
8
|
+
* the returned string is never a partial tag. The blog renderer emits each block
|
|
9
|
+
* (`<p>`, `<h2>`, `<ul>`, `<blockquote>`, `<pre>`, …) as a top-level sibling, so
|
|
10
|
+
* "first N blocks" is a clean prefix of the rendered HTML; the boundary scan also
|
|
11
|
+
* tolerates legacy/migrated `contentHtml` from other sources.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const VOID_TAGS = new Set([
|
|
15
|
+
"area", "base", "br", "col", "embed", "hr", "img", "input",
|
|
16
|
+
"link", "meta", "param", "source", "track", "wbr",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/** Default teaser length: enough to be a genuine content sample, not the whole story. */
|
|
20
|
+
const DEFAULT_BLOCKS = 3;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The first `blocks` top-level block elements of `contentHtml`. Returns the whole
|
|
24
|
+
* string when it has fewer than `blocks` top-level elements. Cuts only after a
|
|
25
|
+
* top-level element closes (or a void/self-closing element), so HTML is never
|
|
26
|
+
* severed mid-tag.
|
|
27
|
+
*/
|
|
28
|
+
export function teaserHtml(contentHtml: string, blocks: number = DEFAULT_BLOCKS): string {
|
|
29
|
+
if (blocks <= 0) return "";
|
|
30
|
+
const n = contentHtml.length;
|
|
31
|
+
let i = 0;
|
|
32
|
+
let depth = 0;
|
|
33
|
+
let topLevel = 0;
|
|
34
|
+
|
|
35
|
+
while (i < n) {
|
|
36
|
+
if (contentHtml[i] !== "<") {
|
|
37
|
+
i++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (contentHtml.startsWith("<!--", i)) {
|
|
41
|
+
const end = contentHtml.indexOf("-->", i + 4);
|
|
42
|
+
i = end === -1 ? n : end + 3;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const gt = contentHtml.indexOf(">", i);
|
|
46
|
+
if (gt === -1) break; // malformed tail — stop rather than emit a partial tag
|
|
47
|
+
const tag = contentHtml.slice(i, gt + 1);
|
|
48
|
+
i = gt + 1;
|
|
49
|
+
|
|
50
|
+
const closing = tag.startsWith("</");
|
|
51
|
+
const selfClosing = tag.endsWith("/>");
|
|
52
|
+
const name = tag.match(/^<\/?\s*([a-zA-Z][a-zA-Z0-9-]*)/)?.[1]?.toLowerCase() ?? "";
|
|
53
|
+
|
|
54
|
+
if (closing) {
|
|
55
|
+
if (depth > 0) depth--;
|
|
56
|
+
if (depth === 0) {
|
|
57
|
+
topLevel++;
|
|
58
|
+
if (topLevel >= blocks) return contentHtml.slice(0, i);
|
|
59
|
+
}
|
|
60
|
+
} else if (selfClosing || VOID_TAGS.has(name)) {
|
|
61
|
+
if (depth === 0) {
|
|
62
|
+
topLevel++;
|
|
63
|
+
if (topLevel >= blocks) return contentHtml.slice(0, i);
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
depth++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return contentHtml;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The body HTML to render for an article: full content when the viewer is
|
|
74
|
+
* entitled (or the post is ungated), a teaser otherwise. The single place the
|
|
75
|
+
* gate/teaser branch lives, so the public/full-content path stays provably
|
|
76
|
+
* unchanged — `articleBodyHtml(html, { gated: false, ... }) === html`.
|
|
77
|
+
*/
|
|
78
|
+
export function articleBodyHtml(
|
|
79
|
+
contentHtml: string,
|
|
80
|
+
opts: { gated: boolean; unlock: boolean },
|
|
81
|
+
): string {
|
|
82
|
+
return opts.gated && !opts.unlock ? teaserHtml(contentHtml) : contentHtml;
|
|
83
|
+
}
|
|
@@ -5,6 +5,16 @@ export interface BlogImage {
|
|
|
5
5
|
height?: number;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export type PostStatus = "draft" | "published" | "unlisted" | "scheduled";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Per-post entitlement gate — who may read the body. Orthogonal to `PostStatus`
|
|
12
|
+
* (which answers "is this published to the live site"): a post has both. Checked
|
|
13
|
+
* at render time via `viewerHasBlogAccess`, never folded into `isVisibleOnLive`.
|
|
14
|
+
* Room for future `"tier:<id>"` / `"assurance:<acr>"`; only these three today.
|
|
15
|
+
*/
|
|
16
|
+
export type BlogAccess = "public" | "authenticated" | "members";
|
|
17
|
+
|
|
8
18
|
export interface BlogPostSummary {
|
|
9
19
|
source: { platform: "wordpress"; id: string };
|
|
10
20
|
slug: string;
|
|
@@ -18,9 +28,33 @@ export interface BlogPostSummary {
|
|
|
18
28
|
categories: string[];
|
|
19
29
|
tags: string[];
|
|
20
30
|
featuredImage?: BlogImage;
|
|
31
|
+
status: PostStatus; // seeded/legacy posts without this field default to "published"
|
|
32
|
+
scheduledAt?: string; // ISO date; when status === "scheduled", the post goes live once this instant has passed
|
|
33
|
+
canonicalUrl?: string; // syndication override
|
|
34
|
+
inLanguage?: string; // default "en"
|
|
35
|
+
access?: BlogAccess; // entitlement gate; default "public". Orthogonal to `status`
|
|
36
|
+
translationGroupId?: string; // stable id shared by every language variant of one article; absent = singleton
|
|
21
37
|
}
|
|
22
38
|
|
|
23
39
|
export interface BlogPost extends BlogPostSummary {
|
|
24
40
|
oldSlugs: string[];
|
|
25
41
|
contentHtml: string;
|
|
42
|
+
// Markdown source for the admin editor. When present, `contentHtml` is
|
|
43
|
+
// its rendered output; migrated/legacy posts may carry only `contentHtml`.
|
|
44
|
+
bodyMarkdown?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Optional per-tenant × per-collection author bios, keyed by the author's slug
|
|
49
|
+
* (see `authorSlug`). Sourced from `tenants/<id>/content/<collection>/authors.json`;
|
|
50
|
+
* absent file or missing key falls back to just the byline name.
|
|
51
|
+
*/
|
|
52
|
+
export interface AuthorBio {
|
|
53
|
+
name: string;
|
|
54
|
+
bio?: string;
|
|
55
|
+
title?: string; // role/title, e.g. "Founder"
|
|
56
|
+
avatar?: string; // image path (site-relative or absolute)
|
|
57
|
+
url?: string; // personal/site URL
|
|
58
|
+
sameAs?: string[]; // social profile URLs — E-E-A-T signal (feeds Person.sameAs)
|
|
59
|
+
twitter?: string; // handle, e.g. "@ada" (feeds twitter:creator)
|
|
26
60
|
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-post view counts: the "first demand signal" for the blog platform.
|
|
3
|
+
*
|
|
4
|
+
* Rides the EXISTING RUM beacon (`api/rum/vitals.ts`) rather than adding a new
|
|
5
|
+
* ingestion path. Each page load emits exactly one CLS field-vitals sample (LCP
|
|
6
|
+
* and INP are conditional), so the beacon handler treats that CLS sample as the
|
|
7
|
+
* one-per-view signal and increments counters when it lands on a blog-post URL.
|
|
8
|
+
* All counts are keyed by (tenant, collection, slug) — no PII, no per-visitor
|
|
9
|
+
* identity.
|
|
10
|
+
*
|
|
11
|
+
* Two counters per view, written together:
|
|
12
|
+
* - a rolling LIFETIME total (`blogViewsKey`), unbounded; and
|
|
13
|
+
* - a per-UTC-day BUCKET (`blogViewsDayKey`, TTL-expiring) that powers the
|
|
14
|
+
* time-series sparkline and the "views over last N days" top-posts ranking.
|
|
15
|
+
* The day buckets are additive to the lifetime total, not a replacement.
|
|
16
|
+
*
|
|
17
|
+
* Storage is the TENANT_CACHE KV namespace — the same binding the newsletter stub
|
|
18
|
+
* writes to and the only durable store bound on the live `preview` worker. The
|
|
19
|
+
* increment is a read-modify-write, so under concurrency some increments can be
|
|
20
|
+
* lost to KV's last-write-wins semantics: acceptable for a rough demand signal,
|
|
21
|
+
* not a billing counter.
|
|
22
|
+
*
|
|
23
|
+
* Tenant/collection-generic by construction — nothing here is bound to
|
|
24
|
+
* `the-build` or `tokenoftrust.com`.
|
|
25
|
+
*/
|
|
26
|
+
import { getPost, listPosts } from "./collection.js";
|
|
27
|
+
|
|
28
|
+
// Mirror the engine's guards (collection.ts): a collection is one path segment;
|
|
29
|
+
// a slug is lower-kebab. Keeping them local avoids widening the engine's surface.
|
|
30
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
31
|
+
const SEGMENT_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
32
|
+
|
|
33
|
+
// Daily buckets auto-expire this many days after their last write. Comfortably
|
|
34
|
+
// past the widest window the dashboard reads (30d), so a "last N days" query
|
|
35
|
+
// never asks for a key that should still exist but was reaped.
|
|
36
|
+
const DAY_BUCKET_TTL_DAYS = 45;
|
|
37
|
+
const DAY_MS = 86_400_000;
|
|
38
|
+
|
|
39
|
+
/** KV key for one post's rolling lifetime view count. */
|
|
40
|
+
export function blogViewsKey(tenantId: string, collection: string, slug: string): string {
|
|
41
|
+
return `blogviews:${tenantId}:${collection}:${slug}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `yyyy-mm-dd` (UTC) stamp for a Date — the day-bucket suffix. */
|
|
45
|
+
export function dayStamp(date: Date): string {
|
|
46
|
+
return date.toISOString().slice(0, 10);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* KV key for one post's views on ONE UTC day. Distinct from `blogViewsKey` by
|
|
51
|
+
* the trailing `:<yyyy-mm-dd>` segment, so the lifetime counter and the daily
|
|
52
|
+
* buckets never collide.
|
|
53
|
+
*/
|
|
54
|
+
export function blogViewsDayKey(
|
|
55
|
+
tenantId: string,
|
|
56
|
+
collection: string,
|
|
57
|
+
slug: string,
|
|
58
|
+
day: string,
|
|
59
|
+
): string {
|
|
60
|
+
return `${blogViewsKey(tenantId, collection, slug)}:${day}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse a request pathname into a candidate (collection, slug) post ref, or null
|
|
65
|
+
* if it is not shaped like a post URL. A post lives at `<base>/<slug>/`, so the
|
|
66
|
+
* LAST two non-empty path segments are the candidate collection + slug. Taking
|
|
67
|
+
* the last two (not the first) naturally ignores any `/<domain>` basePath prefix
|
|
68
|
+
* (DNS-free preview routing) and rejects listing/section pages (`/the-build/`,
|
|
69
|
+
* `/the-build/category/x/`, `/the-build/author/x/`) and file routes
|
|
70
|
+
* (`/the-build/rss.xml`, whose slug fails `SLUG_RE`) — those either fail the
|
|
71
|
+
* guards or resolve to no post. This is only a SHAPE guard; the caller confirms
|
|
72
|
+
* the post actually exists and is live via `getPost`.
|
|
73
|
+
*/
|
|
74
|
+
export function parseBlogPostPath(path: string): { collection: string; slug: string } | null {
|
|
75
|
+
const segs = path.split("/").filter(Boolean);
|
|
76
|
+
if (segs.length < 2) return null;
|
|
77
|
+
const slug = segs[segs.length - 1]!;
|
|
78
|
+
const collection = segs[segs.length - 2]!;
|
|
79
|
+
if (!SEGMENT_RE.test(collection) || !SLUG_RE.test(slug)) return null;
|
|
80
|
+
return { collection, slug };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function readCount(kv: KVNamespace, key: string): Promise<number> {
|
|
84
|
+
const raw = await kv.get(key);
|
|
85
|
+
const n = raw == null ? 0 : Number.parseInt(raw, 10);
|
|
86
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Increment the view counter for a post IF the pathname resolves to a live
|
|
91
|
+
* (published or unlisted) post for this tenant. Drafts, 404s, and non-blog paths
|
|
92
|
+
* are ignored — `getPost` returns null on the live path for anything not visible.
|
|
93
|
+
* Best-effort: never throws (a telemetry beacon must never fail the request).
|
|
94
|
+
*/
|
|
95
|
+
export async function recordBlogView(
|
|
96
|
+
kv: KVNamespace,
|
|
97
|
+
tenantId: string,
|
|
98
|
+
path: string,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
try {
|
|
101
|
+
const ref = parseBlogPostPath(path);
|
|
102
|
+
if (!ref) return;
|
|
103
|
+
const post = await getPost(tenantId, ref.collection, ref.slug);
|
|
104
|
+
if (!post) return; // not a live post (draft / 404 / non-blog) — do not count
|
|
105
|
+
const key = blogViewsKey(tenantId, ref.collection, ref.slug);
|
|
106
|
+
const dayKey = blogViewsDayKey(tenantId, ref.collection, ref.slug, dayStamp(new Date()));
|
|
107
|
+
const [lifetime, today] = await Promise.all([readCount(kv, key), readCount(kv, dayKey)]);
|
|
108
|
+
await Promise.all([
|
|
109
|
+
// Rolling lifetime counter — unbounded.
|
|
110
|
+
kv.put(key, String(lifetime + 1)),
|
|
111
|
+
// Additive daily bucket for the time-series/top-posts dashboard. TTL keeps
|
|
112
|
+
// the day-bucket keyspace bounded (a day is only ever written on that day,
|
|
113
|
+
// so it expires ~45d after it stops receiving writes).
|
|
114
|
+
kv.put(dayKey, String(today + 1), { expirationTtl: (DAY_BUCKET_TTL_DAYS * DAY_MS) / 1000 }),
|
|
115
|
+
]);
|
|
116
|
+
} catch {
|
|
117
|
+
/* swallow — view counting is best-effort telemetry */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* READER for the future admin "Blog" tab (B2.1, not yet in this base branch).
|
|
123
|
+
* The tab is server-rendered with `Astro.locals.tenant` and can obtain the KV via
|
|
124
|
+
* `readKv("TENANT_CACHE")`, so it calls this directly with the slugs from
|
|
125
|
+
* `listPosts(tenantId, collection, { includeUnpublished: true })` to render a
|
|
126
|
+
* "Views" column. Returns a slug -> count map (missing keys read as 0); a null KV
|
|
127
|
+
* (local dev, no binding) yields all-zero counts.
|
|
128
|
+
*/
|
|
129
|
+
export async function getBlogViewCounts(
|
|
130
|
+
kv: KVNamespace | undefined,
|
|
131
|
+
tenantId: string,
|
|
132
|
+
collection: string,
|
|
133
|
+
slugs: readonly string[],
|
|
134
|
+
): Promise<Record<string, number>> {
|
|
135
|
+
const out: Record<string, number> = {};
|
|
136
|
+
for (const slug of slugs) out[slug] = 0;
|
|
137
|
+
if (!kv) return out;
|
|
138
|
+
await Promise.all(
|
|
139
|
+
slugs.map(async (slug) => {
|
|
140
|
+
out[slug] = await readCount(kv, blogViewsKey(tenantId, collection, slug));
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The UTC day stamps for the last `days` days, oldest first (today is last). */
|
|
147
|
+
export function recentDayStamps(days: number, now: Date = new Date()): string[] {
|
|
148
|
+
const n = Math.max(1, Math.floor(days));
|
|
149
|
+
const end = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
150
|
+
const out: string[] = [];
|
|
151
|
+
for (let i = n - 1; i >= 0; i--) out.push(dayStamp(new Date(end - i * DAY_MS)));
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** One day of a post's view series. */
|
|
156
|
+
export interface DailyViews {
|
|
157
|
+
day: string;
|
|
158
|
+
views: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Per-day view series for one post over the last `days` days (oldest first),
|
|
163
|
+
* for a sparkline/trend. Missing days read as 0; a null KV yields an all-zero
|
|
164
|
+
* series of the right length.
|
|
165
|
+
*/
|
|
166
|
+
export async function getPostViewSeries(
|
|
167
|
+
kv: KVNamespace | undefined,
|
|
168
|
+
tenantId: string,
|
|
169
|
+
collection: string,
|
|
170
|
+
slug: string,
|
|
171
|
+
days: number,
|
|
172
|
+
now: Date = new Date(),
|
|
173
|
+
): Promise<DailyViews[]> {
|
|
174
|
+
const stamps = recentDayStamps(days, now);
|
|
175
|
+
if (!kv) return stamps.map((day) => ({ day, views: 0 }));
|
|
176
|
+
return Promise.all(
|
|
177
|
+
stamps.map(async (day) => ({
|
|
178
|
+
day,
|
|
179
|
+
views: await readCount(kv, blogViewsDayKey(tenantId, collection, slug, day)),
|
|
180
|
+
})),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** One ranked row in the top-posts dashboard. */
|
|
185
|
+
export interface TopPost {
|
|
186
|
+
collection: string;
|
|
187
|
+
slug: string;
|
|
188
|
+
title: string;
|
|
189
|
+
status: string;
|
|
190
|
+
/** Views within the window (`sinceDays`), or lifetime when no window given. */
|
|
191
|
+
views: number;
|
|
192
|
+
/** Lifetime total, always populated. */
|
|
193
|
+
lifetime: number;
|
|
194
|
+
/** Per-day series over the window, oldest first — omitted when unwindowed. */
|
|
195
|
+
series?: DailyViews[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Rank a collection's posts by view count, most-viewed first.
|
|
200
|
+
*
|
|
201
|
+
* KV has no cross-key aggregation, so we take the simplest correct approach:
|
|
202
|
+
* enumerate the known posts via `listPosts` and read each one's counter(s). At
|
|
203
|
+
* blog scale (tens of posts) this is a bounded fan-out of parallel KV gets, not
|
|
204
|
+
* a scan — fine for an admin-tab render, and it needs no write-time index to
|
|
205
|
+
* keep in sync. `opts.sinceDays` ranks by a windowed sum of the daily buckets
|
|
206
|
+
* (and returns the series for a sparkline); without it, ranking is by lifetime.
|
|
207
|
+
*/
|
|
208
|
+
export async function getTopPosts(
|
|
209
|
+
kv: KVNamespace | undefined,
|
|
210
|
+
tenantId: string,
|
|
211
|
+
collection: string,
|
|
212
|
+
opts?: { limit?: number; sinceDays?: number; now?: Date },
|
|
213
|
+
): Promise<TopPost[]> {
|
|
214
|
+
const posts = await listPosts(tenantId, collection, { includeUnpublished: true });
|
|
215
|
+
const now = opts?.now ?? new Date();
|
|
216
|
+
const rows = await Promise.all(
|
|
217
|
+
posts.map(async (post) => {
|
|
218
|
+
const lifetime = kv ? await readCount(kv, blogViewsKey(tenantId, collection, post.slug)) : 0;
|
|
219
|
+
if (opts?.sinceDays == null) {
|
|
220
|
+
return {
|
|
221
|
+
collection,
|
|
222
|
+
slug: post.slug,
|
|
223
|
+
title: post.title,
|
|
224
|
+
status: post.status,
|
|
225
|
+
views: lifetime,
|
|
226
|
+
lifetime,
|
|
227
|
+
} satisfies TopPost;
|
|
228
|
+
}
|
|
229
|
+
const series = await getPostViewSeries(
|
|
230
|
+
kv,
|
|
231
|
+
tenantId,
|
|
232
|
+
collection,
|
|
233
|
+
post.slug,
|
|
234
|
+
opts.sinceDays,
|
|
235
|
+
now,
|
|
236
|
+
);
|
|
237
|
+
const views = series.reduce((sum, d) => sum + d.views, 0);
|
|
238
|
+
return { collection, slug: post.slug, title: post.title, status: post.status, views, lifetime, series } satisfies TopPost;
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
rows.sort((a, b) => b.views - a.views || b.lifetime - a.lifetime);
|
|
242
|
+
return opts?.limit != null ? rows.slice(0, Math.max(0, opts.limit)) : rows;
|
|
243
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lang": "en",
|
|
3
|
+
"head": {
|
|
4
|
+
"favicon": "/tenants/tokenoftrust.com/favicon.png",
|
|
5
|
+
"themeStylesheets": ["/tenants/tokenoftrust.com/themes/tot-navy.css"]
|
|
6
|
+
},
|
|
7
|
+
"sharedMarketingCss": true,
|
|
8
|
+
"header": {
|
|
9
|
+
"variant": "primary",
|
|
10
|
+
"brand": {
|
|
11
|
+
"href": "/",
|
|
12
|
+
"label": "Token of Trust",
|
|
13
|
+
"logo": {
|
|
14
|
+
"src": "/tenants/tokenoftrust.com/logo-wordmark.png",
|
|
15
|
+
"alt": "Token of Trust",
|
|
16
|
+
"width": 180,
|
|
17
|
+
"height": 32
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"nav": [
|
|
21
|
+
{ "id": "nav-platform", "label": "Platform", "href": "/#platform" },
|
|
22
|
+
{ "id": "nav-industries", "label": "Industries", "href": "/#industries" },
|
|
23
|
+
{ "id": "nav-resources", "label": "Resources", "href": "https://help.tokenoftrust.com", "external": true },
|
|
24
|
+
{ "id": "nav-pricing", "label": "Pricing", "href": "https://tokenoftrust.com/pricing/", "external": true },
|
|
25
|
+
{ "id": "nav-login", "label": "Login", "href": "https://app.tokenoftrust.com", "external": true }
|
|
26
|
+
],
|
|
27
|
+
"ctas": [
|
|
28
|
+
{ "id": "cta-book-demo", "label": "Book Demo", "href": "https://tokenoftrust.com/contact/", "variant": "secondary" },
|
|
29
|
+
{ "id": "cta-start-free", "label": "Start For Free", "href": "https://app.tokenoftrust.com", "variant": "primary" }
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"footer": {
|
|
33
|
+
"variant": "default",
|
|
34
|
+
"brand": {
|
|
35
|
+
"text": "Customer verification and compliance for regulated ecommerce. 200+ countries. We will never sell your data."
|
|
36
|
+
},
|
|
37
|
+
"columns": [
|
|
38
|
+
{
|
|
39
|
+
"title": "Platform",
|
|
40
|
+
"links": [
|
|
41
|
+
{ "id": "ftr-platform-idv", "label": "Identity Verification", "href": "/#platform" },
|
|
42
|
+
{ "id": "ftr-platform-age", "label": "Age Assurance", "href": "/#platform" },
|
|
43
|
+
{ "id": "ftr-platform-fraud", "label": "Fraud Prevention", "href": "/#platform" },
|
|
44
|
+
{ "id": "ftr-platform-evidence", "label": "Compliance Evidence", "href": "/#platform" },
|
|
45
|
+
{ "id": "ftr-platform-aml", "label": "AML · PACT Act · Tax", "href": "/#platform" }
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"title": "Industries",
|
|
50
|
+
"links": [
|
|
51
|
+
{ "id": "ftr-industries-alcohol", "label": "Alcohol", "href": "/#industries" },
|
|
52
|
+
{ "id": "ftr-industries-vape", "label": "Tobacco & Vape", "href": "/#industries" },
|
|
53
|
+
{ "id": "ftr-industries-age-restricted", "label": "Age-Restricted Goods", "href": "/#industries" },
|
|
54
|
+
{ "id": "ftr-industries-marketplaces", "label": "Marketplaces", "href": "/#industries" }
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"title": "Company",
|
|
59
|
+
"links": [
|
|
60
|
+
{ "id": "ftr-company-pricing", "label": "Pricing", "href": "https://tokenoftrust.com/pricing/", "external": true },
|
|
61
|
+
{ "id": "ftr-company-contact", "label": "Contact Sales", "href": "https://tokenoftrust.com/contact/", "external": true },
|
|
62
|
+
{ "id": "ftr-company-resources", "label": "Resources", "href": "https://help.tokenoftrust.com", "external": true },
|
|
63
|
+
{ "id": "ftr-company-login", "label": "Login", "href": "https://app.tokenoftrust.com", "external": true }
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"title": "Legal",
|
|
68
|
+
"links": [
|
|
69
|
+
{ "id": "ftr-legal-privacy", "label": "Privacy Policy", "href": "https://tokenoftrust.com/privacy/", "external": true },
|
|
70
|
+
{ "id": "ftr-legal-terms", "label": "Terms", "href": "https://tokenoftrust.com/terms/", "external": true }
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"social": [],
|
|
75
|
+
"bottom": {
|
|
76
|
+
"note": "GDPR · CCPA · PIPEDA compliant. 256-bit SSL. We will never sell your data."
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|