@aglyn/aglyn 1.0.0-beta.218 → 1.0.0-beta.219
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/package.json +11 -11
- package/src/lib/app-utils/actions.d.ts +25 -374
- package/src/lib/app-utils/actions.js +101 -561
- package/src/lib/app-utils/actions.js.map +1 -1
- package/src/lib/app-utils/activity-presenter.d.ts +3 -3
- package/src/lib/app-utils/activity-presenter.js +36 -52
- package/src/lib/app-utils/activity-presenter.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +1 -1
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/analytics-provider.d.ts +182 -0
- package/src/lib/app-utils/analytics-provider.js +133 -0
- package/src/lib/app-utils/analytics-provider.js.map +1 -0
- package/src/lib/app-utils/api-plugins.d.ts +46 -0
- package/src/lib/app-utils/api-plugins.js +19 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/{marketplace-overrides.d.ts → artifact-overrides.d.ts} +9 -6
- package/src/lib/app-utils/{marketplace-overrides.js → artifact-overrides.js} +10 -7
- package/src/lib/app-utils/artifact-overrides.js.map +1 -0
- package/src/lib/app-utils/{marketplace-provenance.d.ts → artifact-provenance.d.ts} +12 -10
- package/src/lib/app-utils/{marketplace-provenance.js → artifact-provenance.js} +9 -7
- package/src/lib/app-utils/artifact-provenance.js.map +1 -0
- package/src/lib/app-utils/assist-credits.js +2 -1
- package/src/lib/app-utils/assist-credits.js.map +1 -1
- package/src/lib/app-utils/binding-token-catalog.d.ts +5 -13
- package/src/lib/app-utils/binding-token-catalog.js +17 -41
- package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
- package/src/lib/app-utils/brand-support-line.d.ts +36 -0
- package/src/lib/app-utils/brand-support-line.js +38 -0
- package/src/lib/app-utils/brand-support-line.js.map +1 -0
- package/src/lib/app-utils/campaign-touch.d.ts +5 -5
- package/src/lib/app-utils/campaign-touch.js +8 -8
- package/src/lib/app-utils/campaign-touch.js.map +1 -1
- package/src/lib/app-utils/collection-entries.d.ts +2 -2
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/consent-group-change.d.ts +4 -0
- package/src/lib/app-utils/consent-group-change.js.map +1 -1
- package/src/lib/app-utils/console-record-links.d.ts +10 -89
- package/src/lib/app-utils/console-record-links.js +3 -107
- package/src/lib/app-utils/console-record-links.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +16 -7
- package/src/lib/app-utils/console-routes.js +9 -3
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +23 -1
- package/src/lib/app-utils/contacts.js +21 -5
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/cross-pool-desync.d.ts +39 -0
- package/src/lib/app-utils/cross-pool-desync.js +47 -0
- package/src/lib/app-utils/cross-pool-desync.js.map +1 -0
- package/src/lib/app-utils/csv-upload.d.ts +29 -0
- package/src/lib/app-utils/csv-upload.js +29 -0
- package/src/lib/app-utils/csv-upload.js.map +1 -0
- package/src/lib/app-utils/dataset-models.d.ts +5 -5
- package/src/lib/app-utils/dataset-models.js +19 -2
- package/src/lib/app-utils/dataset-models.js.map +1 -1
- package/src/lib/app-utils/datasets.d.ts +65 -11
- package/src/lib/app-utils/datasets.js +74 -11
- package/src/lib/app-utils/datasets.js.map +1 -1
- package/src/lib/app-utils/docs-deflection.d.ts +6 -6
- package/src/lib/app-utils/docs-deflection.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +12 -6
- package/src/lib/app-utils/docs-help.generated.js +21 -6
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.d.ts +2 -2
- package/src/lib/app-utils/docs-index.generated.js +514 -318
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/docs-retrieval.d.ts +3 -3
- package/src/lib/app-utils/docs-retrieval.js +14 -10
- package/src/lib/app-utils/docs-retrieval.js.map +1 -1
- package/src/lib/app-utils/duplicate-resource.d.ts +4 -1
- package/src/lib/app-utils/duplicate-resource.js +7 -4
- package/src/lib/app-utils/duplicate-resource.js.map +1 -1
- package/src/lib/app-utils/dynamic-list-rule.d.ts +1 -1
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
- package/src/lib/app-utils/element-ui.d.ts +1 -1
- package/src/lib/app-utils/element-ui.js +1 -1
- package/src/lib/app-utils/element-ui.js.map +1 -1
- package/src/lib/app-utils/email-starter-policy.js +3 -3
- package/src/lib/app-utils/email-starter-policy.js.map +1 -1
- package/src/lib/app-utils/entity-picker-context.d.ts +10 -5
- package/src/lib/app-utils/entity-picker-context.js.map +1 -1
- package/src/lib/app-utils/error-beacon.d.ts +75 -6
- package/src/lib/app-utils/error-beacon.js +160 -21
- package/src/lib/app-utils/error-beacon.js.map +1 -1
- package/src/lib/app-utils/expand-repeatables.d.ts +10 -10
- package/src/lib/app-utils/expand-repeatables.js +10 -10
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/host-events.d.ts +82 -0
- package/src/lib/app-utils/host-events.js +62 -0
- package/src/lib/app-utils/host-events.js.map +1 -0
- package/src/lib/app-utils/margin-utilization.d.ts +4 -69
- package/src/lib/app-utils/margin-utilization.js +82 -69
- package/src/lib/app-utils/margin-utilization.js.map +1 -1
- package/src/lib/app-utils/node-definition-sanitizer.d.ts +16 -16
- package/src/lib/app-utils/node-definition-sanitizer.js +24 -20
- package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
- package/src/lib/app-utils/node-interactions.d.ts +7 -7
- package/src/lib/app-utils/node-interactions.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +78 -47
- package/src/lib/app-utils/notifications.js +63 -44
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/payments-configured.d.ts +8 -21
- package/src/lib/app-utils/payments-configured.js +8 -23
- package/src/lib/app-utils/payments-configured.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +43 -128
- package/src/lib/app-utils/plan-entitlements.js +145 -304
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-brand.d.ts +10 -0
- package/src/lib/app-utils/platform-brand.js +14 -3
- package/src/lib/app-utils/platform-brand.js.map +1 -1
- package/src/lib/app-utils/plugin-api-rate-limit.d.ts +7 -1
- package/src/lib/app-utils/plugin-api-rate-limit.js +11 -3
- package/src/lib/app-utils/plugin-api-rate-limit.js.map +1 -1
- package/src/lib/app-utils/plugin-bundle-checks.js +2 -2
- package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-host-events.generated.js +169 -0
- package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -0
- package/src/lib/app-utils/plugin-release-flags.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js +106 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -0
- package/src/lib/app-utils/realm-host-surface.generated.js +2 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/release-flags.d.ts +14 -5
- package/src/lib/app-utils/release-flags.js +21 -125
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/repeat-sources.d.ts +9 -0
- package/src/lib/app-utils/repeat-sources.js.map +1 -1
- package/src/lib/app-utils/retained-refusals.d.ts +73 -0
- package/src/lib/app-utils/retained-refusals.js +186 -0
- package/src/lib/app-utils/retained-refusals.js.map +1 -0
- package/src/lib/app-utils/scope-tokens.d.ts +27 -0
- package/src/lib/app-utils/scope-tokens.js +61 -0
- package/src/lib/app-utils/scope-tokens.js.map +1 -1
- package/src/lib/app-utils/screen-link-context.js +3 -3
- package/src/lib/app-utils/screen-link-context.js.map +1 -1
- package/src/lib/app-utils/screen-route.js +1 -1
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/seo-audit.d.ts +132 -0
- package/src/lib/app-utils/seo-audit.js +246 -0
- package/src/lib/app-utils/seo-audit.js.map +1 -0
- package/src/lib/app-utils/seo-keywords.d.ts +48 -0
- package/src/lib/app-utils/seo-keywords.js +60 -0
- package/src/lib/app-utils/seo-keywords.js.map +1 -0
- package/src/lib/app-utils/seo-page-facts.d.ts +66 -0
- package/src/lib/app-utils/seo-page-facts.js +155 -0
- package/src/lib/app-utils/seo-page-facts.js.map +1 -0
- package/src/lib/app-utils/seo-site-scan.d.ts +126 -0
- package/src/lib/app-utils/seo-site-scan.js +162 -0
- package/src/lib/app-utils/seo-site-scan.js.map +1 -0
- package/src/lib/app-utils/seo-title-variables.js +1 -1
- package/src/lib/app-utils/seo-title-variables.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +6 -9
- package/src/lib/app-utils/server.js +17 -27
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-interactions.d.ts +501 -0
- package/src/lib/app-utils/site-interactions.js +554 -0
- package/src/lib/app-utils/site-interactions.js.map +1 -0
- package/src/lib/app-utils/{marketplace-theme.d.ts → site-theme.d.ts} +13 -2
- package/src/lib/app-utils/{marketplace-theme.js → site-theme.js} +2 -2
- package/src/lib/app-utils/site-theme.js.map +1 -0
- package/src/lib/app-utils/sitemap.d.ts +4 -3
- package/src/lib/app-utils/sitemap.js +9 -7
- package/src/lib/app-utils/sitemap.js.map +1 -1
- package/src/lib/app-utils/stale-build-error.d.ts +51 -0
- package/src/lib/app-utils/stale-build-error.js +100 -1
- package/src/lib/app-utils/stale-build-error.js.map +1 -1
- package/src/lib/app-utils/tax-jurisdiction-figures.d.ts +118 -0
- package/src/lib/app-utils/tax-jurisdiction-figures.js +123 -0
- package/src/lib/app-utils/tax-jurisdiction-figures.js.map +1 -0
- package/src/lib/app-utils/theme-library.d.ts +10 -3
- package/src/lib/app-utils/theme-library.js +13 -4
- package/src/lib/app-utils/theme-library.js.map +1 -1
- package/src/lib/app-utils/{campaign-attribution.d.ts → utm-attribution.d.ts} +11 -6
- package/src/lib/app-utils/{campaign-attribution.js → utm-attribution.js} +13 -8
- package/src/lib/app-utils/utm-attribution.js.map +1 -0
- package/src/lib/app-utils/{campaign-forwarding.d.ts → utm-forwarding.d.ts} +14 -14
- package/src/lib/app-utils/{campaign-forwarding.js → utm-forwarding.js} +26 -26
- package/src/lib/app-utils/utm-forwarding.js.map +1 -0
- package/src/lib/app-utils/video-object.js +6 -5
- package/src/lib/app-utils/video-object.js.map +1 -1
- package/src/lib/app-utils/visitor-consent.d.ts +41 -164
- package/src/lib/app-utils/visitor-consent.js +66 -277
- package/src/lib/app-utils/visitor-consent.js.map +1 -1
- package/src/lib/app-utils/web-vitals-rum.d.ts +7 -7
- package/src/lib/app-utils/web-vitals-rum.js +31 -63
- package/src/lib/app-utils/web-vitals-rum.js.map +1 -1
- package/src/lib/app-utils/where-used.d.ts +11 -6
- package/src/lib/app-utils/where-used.js +3 -8
- package/src/lib/app-utils/where-used.js.map +1 -1
- package/src/lib/foundation/definitions/components.types.d.ts +1 -1
- package/src/lib/foundation/definitions/components.types.js +1 -1
- package/src/lib/foundation/definitions/components.types.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +9 -135
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +1 -3
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +11 -5
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +16 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js +20 -0
- package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
- package/src/lib/foundation/index.d.ts +1 -1
- package/src/lib/foundation/index.js.map +1 -1
- package/src/lib/plugin-manager/besigner-documents.d.ts +96 -0
- package/src/lib/plugin-manager/besigner-documents.js +61 -0
- package/src/lib/plugin-manager/besigner-documents.js.map +1 -0
- package/src/lib/plugin-manager/computed-variables.d.ts +71 -0
- package/src/lib/plugin-manager/computed-variables.js +74 -0
- package/src/lib/plugin-manager/computed-variables.js.map +1 -0
- package/src/lib/plugin-manager/custom-fields.d.ts +29 -2
- package/src/lib/plugin-manager/custom-fields.js +47 -1
- package/src/lib/plugin-manager/custom-fields.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +292 -20
- package/src/lib/plugin-manager/feature-plugins.js +105 -17
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +123 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +784 -10
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js +25 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contributions.d.ts +1 -1
- package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-declarations-repair.d.ts +36 -0
- package/src/lib/plugin-manager/plugin-declarations-repair.js +64 -0
- package/src/lib/plugin-manager/plugin-declarations-repair.js.map +1 -0
- package/src/lib/plugin-manager/plugin-dependents.d.ts +101 -0
- package/src/lib/plugin-manager/plugin-dependents.js +75 -0
- package/src/lib/plugin-manager/plugin-dependents.js.map +1 -0
- package/src/lib/plugin-manager/plugin-distribution.d.ts +55 -0
- package/src/lib/plugin-manager/plugin-distribution.js +45 -0
- package/src/lib/plugin-manager/plugin-distribution.js.map +1 -0
- package/src/lib/plugin-manager/plugin-host-collections.d.ts +17 -0
- package/src/lib/plugin-manager/plugin-host-collections.js +6 -0
- package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
- package/src/lib/plugin-manager/plugin-host-resources.d.ts +133 -0
- package/src/lib/plugin-manager/plugin-host-resources.js +52 -0
- package/src/lib/plugin-manager/plugin-host-resources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-org-erasure.d.ts +28 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js +61 -2
- package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -1
- package/src/lib/plugin-manager/plugin-plan-entitlements.d.ts +123 -0
- package/src/lib/plugin-manager/plugin-plan-entitlements.js +101 -0
- package/src/lib/plugin-manager/plugin-plan-entitlements.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-index.d.ts +95 -0
- package/src/lib/plugin-manager/plugin-record-index.js +51 -0
- package/src/lib/plugin-manager/plugin-record-index.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-written.d.ts +58 -0
- package/src/lib/plugin-manager/plugin-record-written.js +42 -0
- package/src/lib/plugin-manager/plugin-record-written.js.map +1 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.d.ts +213 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.js +194 -0
- package/src/lib/plugin-manager/plugin-revenue-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-bundle.d.ts +145 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js +78 -0
- package/src/lib/plugin-manager/plugin-site-bundle.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-cache.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-site-cache.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-export.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-site-export.js +40 -0
- package/src/lib/plugin-manager/plugin-site-export.js.map +1 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.d.ts +88 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.js +29 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.js.map +1 -0
- package/src/lib/plugin-manager/plugin-tax-profile.d.ts +20 -4
- package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
- package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +255 -0
- package/src/lib/plugin-manager/plugin-tax-return-sources.js +191 -0
- package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-template-sources.d.ts +75 -0
- package/src/lib/plugin-manager/plugin-template-sources.js +37 -0
- package/src/lib/plugin-manager/plugin-template-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +107 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +107 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -0
- package/src/lib/plugin-manager/plugin-zones.d.ts +4 -5
- package/src/lib/plugin-manager/plugin-zones.js.map +1 -1
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +2 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
- package/src/lib/plugin-manager/record-captured-contact.d.ts +1 -14
- package/src/lib/plugin-manager/record-captured-contact.js +10 -32
- package/src/lib/plugin-manager/record-captured-contact.js.map +1 -1
- package/src/lib/plugin-manager/repeat-rows.d.ts +124 -0
- package/src/lib/plugin-manager/repeat-rows.js +108 -0
- package/src/lib/plugin-manager/repeat-rows.js.map +1 -0
- package/src/lib/plugin-manager/submission-record-target.d.ts +144 -0
- package/src/lib/plugin-manager/submission-record-target.js +125 -0
- package/src/lib/plugin-manager/submission-record-target.js.map +1 -0
- package/src/lib/plugin-manager/video-embed-provider.d.ts +108 -0
- package/src/lib/plugin-manager/video-embed-provider.js +165 -0
- package/src/lib/plugin-manager/video-embed-provider.js.map +1 -0
- package/src/lib/types/nodes.d.ts +2 -2
- package/src/lib/types/nodes.js.map +1 -1
- package/src/lib/app-utils/campaign-attribution.js.map +0 -1
- package/src/lib/app-utils/campaign-forwarding.js.map +0 -1
- package/src/lib/app-utils/contact-holder.d.ts +0 -68
- package/src/lib/app-utils/contact-holder.js +0 -129
- package/src/lib/app-utils/contact-holder.js.map +0 -1
- package/src/lib/app-utils/contact-merge.d.ts +0 -136
- package/src/lib/app-utils/contact-merge.js +0 -412
- package/src/lib/app-utils/contact-merge.js.map +0 -1
- package/src/lib/app-utils/crm-csv.d.ts +0 -239
- package/src/lib/app-utils/crm-csv.js +0 -467
- package/src/lib/app-utils/crm-csv.js.map +0 -1
- package/src/lib/app-utils/crm-digest.d.ts +0 -185
- package/src/lib/app-utils/crm-digest.js +0 -272
- package/src/lib/app-utils/crm-digest.js.map +0 -1
- package/src/lib/app-utils/form-dataset-binding.d.ts +0 -50
- package/src/lib/app-utils/form-dataset-binding.js +0 -74
- package/src/lib/app-utils/form-dataset-binding.js.map +0 -1
- package/src/lib/app-utils/list-import.d.ts +0 -158
- package/src/lib/app-utils/list-import.js +0 -332
- package/src/lib/app-utils/list-import.js.map +0 -1
- package/src/lib/app-utils/marketplace-listing-visibility.d.ts +0 -122
- package/src/lib/app-utils/marketplace-listing-visibility.js +0 -147
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +0 -1
- package/src/lib/app-utils/marketplace-overrides.js.map +0 -1
- package/src/lib/app-utils/marketplace-provenance.js.map +0 -1
- package/src/lib/app-utils/marketplace-theme.js.map +0 -1
- package/src/lib/app-utils/marketplace-update-state.d.ts +0 -119
- package/src/lib/app-utils/marketplace-update-state.js +0 -132
- package/src/lib/app-utils/marketplace-update-state.js.map +0 -1
- package/src/lib/app-utils/publisher-agreement.d.ts +0 -289
- package/src/lib/app-utils/publisher-agreement.js +0 -321
- package/src/lib/app-utils/publisher-agreement.js.map +0 -1
- package/src/lib/app-utils/wistia-embed.d.ts +0 -104
- package/src/lib/app-utils/wistia-embed.js +0 -138
- package/src/lib/app-utils/wistia-embed.js.map +0 -1
- package/src/lib/app-utils/workflows.d.ts +0 -136
- package/src/lib/app-utils/workflows.js +0 -346
- package/src/lib/app-utils/workflows.js.map +0 -1
- package/src/lib/plugin-manager/commerce-zone-props.d.ts +0 -172
- package/src/lib/plugin-manager/commerce-zone-props.js +0 -31
- package/src/lib/plugin-manager/commerce-zone-props.js.map +0 -1
|
@@ -18,6 +18,11 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
* Where an account came from (AGL-1731) — the campaign contract between the
|
|
19
19
|
* marketing site's links and the console's signup.
|
|
20
20
|
*
|
|
21
|
+
* Named for what it parses, the web's UTM labels. The `campaign` in
|
|
22
|
+
* `utm_campaign` is the label an ad push carries; it is not the Marketing
|
|
23
|
+
* plugin's campaign, and nothing here reads one. What a site's own visitors
|
|
24
|
+
* carry to the moment they identify is `campaign-touch.ts`'s.
|
|
25
|
+
*
|
|
21
26
|
* Until this existed a paid click, an organic visit and a partner link
|
|
22
27
|
* arrived indistinguishable: `sign_up` carried `method` and nothing else, so
|
|
23
28
|
* no report could say which spend produced a customer. That is tolerable
|
|
@@ -68,14 +73,14 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
68
73
|
* site's links, the stored wire form and this parser all have to spell them
|
|
69
74
|
* identically — two spellings would read as two dimensions in GA4 and split
|
|
70
75
|
* every report in half.
|
|
71
|
-
*/ export const
|
|
76
|
+
*/ export const UTM_QUERY_KEYS = [
|
|
72
77
|
'utm_source',
|
|
73
78
|
'utm_medium',
|
|
74
79
|
'utm_campaign'
|
|
75
80
|
];
|
|
76
81
|
/** Longest value stored or sent, matching `sanitizeEventParams`'s own cap. */ const MAX_VALUE_LENGTH = 100;
|
|
77
82
|
/** The same shape test the event sanitizer uses, applied to the stored exit too. */ const EMAIL_SHAPED = /[^\s@]+@[^\s@]+\.[^\s@]+/;
|
|
78
|
-
/** Which `
|
|
83
|
+
/** Which `UtmAttribution` field each URL key fills. */ const FIELD_FOR_KEY = {
|
|
79
84
|
utm_source: 'source',
|
|
80
85
|
utm_medium: 'medium',
|
|
81
86
|
utm_campaign: 'campaign'
|
|
@@ -113,10 +118,10 @@ function rawValue(params, key) {
|
|
|
113
118
|
* Null rather than `{}` so a caller can branch on "was there a campaign at
|
|
114
119
|
* all". An empty object written to an account would read as "arrived from
|
|
115
120
|
* nowhere, confirmed" instead of "never asked", and those are different facts.
|
|
116
|
-
*/ export function
|
|
121
|
+
*/ export function parseUtmAttribution(params) {
|
|
117
122
|
if (!params) return null;
|
|
118
123
|
const attribution = {};
|
|
119
|
-
for (const key of
|
|
124
|
+
for (const key of UTM_QUERY_KEYS){
|
|
120
125
|
const value = scrubValue(rawValue(params, key));
|
|
121
126
|
if (value) attribution[FIELD_FOR_KEY[key]] = value;
|
|
122
127
|
}
|
|
@@ -125,10 +130,10 @@ function rawValue(params, key) {
|
|
|
125
130
|
/**
|
|
126
131
|
* The canonical query string for a campaign — the ONLY place the stored form
|
|
127
132
|
* is written, so it cannot drift from what the parser reads back.
|
|
128
|
-
*/ export function
|
|
133
|
+
*/ export function utmAttributionQuery(attribution) {
|
|
129
134
|
if (!attribution) return '';
|
|
130
135
|
const params = new URLSearchParams();
|
|
131
|
-
for (const key of
|
|
136
|
+
for (const key of UTM_QUERY_KEYS){
|
|
132
137
|
const value = attribution[FIELD_FOR_KEY[key]];
|
|
133
138
|
if (value) params.set(key, value);
|
|
134
139
|
}
|
|
@@ -148,7 +153,7 @@ function rawValue(params, key) {
|
|
|
148
153
|
* `campaign_source: undefined` on every unattributed signup would survive the
|
|
149
154
|
* sanitizer as an absence but read, to whoever writes the report, as a
|
|
150
155
|
* dimension that is populated sometimes and broken the rest of the time.
|
|
151
|
-
*/ export function
|
|
156
|
+
*/ export function utmEventParams(attribution) {
|
|
152
157
|
if (!attribution) return {};
|
|
153
158
|
return _extends({}, attribution.source ? {
|
|
154
159
|
campaign_source: attribution.source
|
|
@@ -159,4 +164,4 @@ function rawValue(params, key) {
|
|
|
159
164
|
} : {});
|
|
160
165
|
}
|
|
161
166
|
|
|
162
|
-
//# sourceMappingURL=
|
|
167
|
+
//# sourceMappingURL=utm-attribution.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/utm-attribution.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Where an account came from (AGL-1731) — the campaign contract between the\n * marketing site's links and the console's signup.\n *\n * Named for what it parses, the web's UTM labels. The `campaign` in\n * `utm_campaign` is the label an ad push carries; it is not the Marketing\n * plugin's campaign, and nothing here reads one. What a site's own visitors\n * carry to the moment they identify is `campaign-touch.ts`'s.\n *\n * Until this existed a paid click, an organic visit and a partner link\n * arrived indistinguishable: `sign_up` carried `method` and nothing else, so\n * no report could say which spend produced a customer. That is tolerable\n * while nothing is being spent and not tolerable the day advertising starts,\n * because the measurement has to exist BEFORE the traffic — a signup that\n * lands unattributed is unattributed forever.\n *\n * ## Why an allowlist, and why exactly three\n *\n * `utm_source`, `utm_medium` and `utm_campaign` are the three axes an ad\n * report is read on: which channel, what kind of placement, which push. They\n * are also the three the marketing site can be told to emit without further\n * decisions.\n *\n * `utm_term` and `utm_content` are deliberately OUT, matching the refusal the\n * tenant's own first-party collector already made for its visitors\n * (`apps/tenant/app/api/analytics/collect/route.ts`): keyword- and\n * variant-level labels multiply cardinality without answering a question\n * anyone asks of a signup, and a keyword string is the most likely of the\n * five to carry something a person typed. `gclid` and bare `ref` are out for\n * that same cardinality reason. Not for a privacy one: the property runs ads\n * personalization ON in every region, and Google's own auto-tagging already\n * carries the click identifier into it, so re-collecting `gclid` here would\n * duplicate a field the platform owns rather than withhold anything.\n *\n * The allowlist is the privacy mechanism, not a convenience. A parser that\n * copied \"the campaign-ish parameters\" would be one marketing link away from\n * storing `?email=` against a person, and the standing rule is that personal\n * data never travels in a URL parameter in the first place.\n *\n * ## Two exits, and only one of them is already guarded\n *\n * A parsed campaign leaves this process twice: onto the GA4 `sign_up` hit,\n * and into `users/{uid}` so revenue can be attributed to it months later.\n * Only the first passes `sanitizeEventParams`. The Firestore write does not,\n * so the scrub — email shape refused, length capped, blanks dropped — lives\n * HERE, on the way in, and both exits inherit it.\n *\n * ## Why the wire form is what gets stored\n *\n * Same contract as the AGL-1535 plan intent it rides beside: the stored value\n * is the query string, re-parsed by this parser on the way out. `users/{uid}`\n * is owner-writable, so a stored field is a client-FORGEABLE field. Re-parsing\n * means a hand-edited document can claim no more than a hand-edited URL could,\n * which is a marketing label and nothing that touches entitlement.\n */\n\n/**\n * The URL parameter names, in report order. Exported because the marketing\n * site's links, the stored wire form and this parser all have to spell them\n * identically — two spellings would read as two dimensions in GA4 and split\n * every report in half.\n */\nexport const UTM_QUERY_KEYS = [\n 'utm_source',\n 'utm_medium',\n 'utm_campaign',\n] as const\n\n/** A campaign, with every field optional — a partial one is still worth having. */\nexport interface UtmAttribution {\n /** `utm_source` — the channel: `google`, `hn`, a partner's name. */\n source?: string\n /** `utm_medium` — the kind of placement: `cpc`, `email`, `referral`. */\n medium?: string\n /** `utm_campaign` — which push: `sept-launch`. */\n campaign?: string\n}\n\n/** Longest value stored or sent, matching `sanitizeEventParams`'s own cap. */\nconst MAX_VALUE_LENGTH = 100\n\n/** The same shape test the event sanitizer uses, applied to the stored exit too. */\nconst EMAIL_SHAPED = /[^\\s@]+@[^\\s@]+\\.[^\\s@]+/\n\n/** Which `UtmAttribution` field each URL key fills. */\nconst FIELD_FOR_KEY: Record<\n (typeof UTM_QUERY_KEYS)[number],\n keyof UtmAttribution\n> = {\n utm_source: 'source',\n utm_medium: 'medium',\n utm_campaign: 'campaign',\n}\n\n/** What a Server Component holds before it has a `URLSearchParams`. */\nexport type UtmParamSource =\n | URLSearchParams\n | Record<string, string | string[] | undefined>\n | null\n | undefined\n\nfunction rawValue(params: UtmParamSource, key: string): string | null {\n if (!params) return null\n if (typeof (params as URLSearchParams).get === 'function') {\n return (params as URLSearchParams).get(key)\n }\n const value = (params as Record<string, string | string[] | undefined>)[key]\n // A repeated parameter arrives as an array. The first one wins rather than\n // being joined: `utm_medium=cpc&utm_medium=organic` is a malformed link, and\n // inventing `cpc,organic` would put a value in the reports that no campaign\n // ever set.\n if (Array.isArray(value)) return value.length ? value[0] : null\n return typeof value === 'string' ? value : null\n}\n\n/**\n * Reduce one raw parameter to something storable, or null to drop it.\n *\n * Dropping the single value rather than the whole campaign is deliberate: a\n * mail-merge link that puts an address in `utm_source` still names a real\n * `utm_campaign`, and keeping the half that is safe beats losing the\n * attribution to protect the half that is not.\n */\nfunction scrubValue(value: string | null): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n if (!trimmed) return null\n if (EMAIL_SHAPED.test(trimmed)) return null\n return trimmed.slice(0, MAX_VALUE_LENGTH)\n}\n\n/**\n * Read the campaign off a signup URL, or null when it names none.\n *\n * Null rather than `{}` so a caller can branch on \"was there a campaign at\n * all\". An empty object written to an account would read as \"arrived from\n * nowhere, confirmed\" instead of \"never asked\", and those are different facts.\n */\nexport function parseUtmAttribution(\n params: UtmParamSource,\n): UtmAttribution | null {\n if (!params) return null\n const attribution: UtmAttribution = {}\n for (const key of UTM_QUERY_KEYS) {\n const value = scrubValue(rawValue(params, key))\n if (value) attribution[FIELD_FOR_KEY[key]] = value\n }\n return Object.keys(attribution).length ? attribution : null\n}\n\n/**\n * The canonical query string for a campaign — the ONLY place the stored form\n * is written, so it cannot drift from what the parser reads back.\n */\nexport function utmAttributionQuery(\n attribution: UtmAttribution | null | undefined,\n): string {\n if (!attribution) return ''\n const params = new URLSearchParams()\n for (const key of UTM_QUERY_KEYS) {\n const value = attribution[FIELD_FOR_KEY[key]]\n if (value) params.set(key, value)\n }\n return params.toString()\n}\n\n/**\n * The campaign as GA4 event parameters, ready to spread into `sign_up`.\n *\n * Renamed off the `utm_` spellings on purpose. These are OUR event parameters,\n * registered as custom dimensions on the property (see `docs/ANALYTICS.md`);\n * the `utm_` names belong to GA's own automatic campaign collection, and\n * shipping a custom parameter under a name the platform also owns is how a\n * dimension ends up meaning two things.\n *\n * Returns an empty object — never keys with `undefined` values — so that\n * spreading it into an organic signup's params adds nothing at all. A\n * `campaign_source: undefined` on every unattributed signup would survive the\n * sanitizer as an absence but read, to whoever writes the report, as a\n * dimension that is populated sometimes and broken the rest of the time.\n */\nexport function utmEventParams(\n attribution: UtmAttribution | null | undefined,\n): {\n campaign_source?: string\n campaign_medium?: string\n campaign_name?: string\n} {\n if (!attribution) return {}\n return {\n ...(attribution.source ? { campaign_source: attribution.source } : {}),\n ...(attribution.medium ? { campaign_medium: attribution.medium } : {}),\n ...(attribution.campaign ? { campaign_name: attribution.campaign } : {}),\n }\n}\n"],"names":["UTM_QUERY_KEYS","MAX_VALUE_LENGTH","EMAIL_SHAPED","FIELD_FOR_KEY","utm_source","utm_medium","utm_campaign","rawValue","params","key","get","value","Array","isArray","length","scrubValue","trimmed","trim","test","slice","parseUtmAttribution","attribution","Object","keys","utmAttributionQuery","URLSearchParams","set","toString","utmEventParams","source","campaign_source","medium","campaign_medium","campaign","campaign_name"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsDC,GAED;;;;;CAKC,GACD,OAAO,MAAMA,iBAAiB;IAC5B;IACA;IACA;CACD,CAAS;AAYV,4EAA4E,GAC5E,MAAMC,mBAAmB;AAEzB,kFAAkF,GAClF,MAAMC,eAAe;AAErB,qDAAqD,GACrD,MAAMC,gBAGF;IACFC,YAAY;IACZC,YAAY;IACZC,cAAc;AAChB;AASA,SAASC,SAASC,MAAsB,EAAEC,GAAW;IACnD,IAAI,CAACD,QAAQ,OAAO;IACpB,IAAI,OAAO,AAACA,OAA2BE,GAAG,KAAK,YAAY;QACzD,OAAO,AAACF,OAA2BE,GAAG,CAACD;IACzC;IACA,MAAME,QAAQ,AAACH,MAAwD,CAACC,IAAI;IAC5E,2EAA2E;IAC3E,6EAA6E;IAC7E,4EAA4E;IAC5E,YAAY;IACZ,IAAIG,MAAMC,OAAO,CAACF,QAAQ,OAAOA,MAAMG,MAAM,GAAGH,KAAK,CAAC,EAAE,GAAG;IAC3D,OAAO,OAAOA,UAAU,WAAWA,QAAQ;AAC7C;AAEA;;;;;;;CAOC,GACD,SAASI,WAAWJ,KAAoB;IACtC,IAAI,OAAOA,UAAU,UAAU,OAAO;IACtC,MAAMK,UAAUL,MAAMM,IAAI;IAC1B,IAAI,CAACD,SAAS,OAAO;IACrB,IAAId,aAAagB,IAAI,CAACF,UAAU,OAAO;IACvC,OAAOA,QAAQG,KAAK,CAAC,GAAGlB;AAC1B;AAEA;;;;;;CAMC,GACD,OAAO,SAASmB,oBACdZ,MAAsB;IAEtB,IAAI,CAACA,QAAQ,OAAO;IACpB,MAAMa,cAA8B,CAAC;IACrC,KAAK,MAAMZ,OAAOT,eAAgB;QAChC,MAAMW,QAAQI,WAAWR,SAASC,QAAQC;QAC1C,IAAIE,OAAOU,WAAW,CAAClB,aAAa,CAACM,IAAI,CAAC,GAAGE;IAC/C;IACA,OAAOW,OAAOC,IAAI,CAACF,aAAaP,MAAM,GAAGO,cAAc;AACzD;AAEA;;;CAGC,GACD,OAAO,SAASG,oBACdH,WAA8C;IAE9C,IAAI,CAACA,aAAa,OAAO;IACzB,MAAMb,SAAS,IAAIiB;IACnB,KAAK,MAAMhB,OAAOT,eAAgB;QAChC,MAAMW,QAAQU,WAAW,CAAClB,aAAa,CAACM,IAAI,CAAC;QAC7C,IAAIE,OAAOH,OAAOkB,GAAG,CAACjB,KAAKE;IAC7B;IACA,OAAOH,OAAOmB,QAAQ;AACxB;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,eACdP,WAA8C;IAM9C,IAAI,CAACA,aAAa,OAAO,CAAC;IAC1B,OAAO,aACDA,YAAYQ,MAAM,GAAG;QAAEC,iBAAiBT,YAAYQ,MAAM;IAAC,IAAI,CAAC,GAChER,YAAYU,MAAM,GAAG;QAAEC,iBAAiBX,YAAYU,MAAM;IAAC,IAAI,CAAC,GAChEV,YAAYY,QAAQ,GAAG;QAAEC,eAAeb,YAAYY,QAAQ;IAAC,IAAI,CAAC;AAE1E"}
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
|
-
* Carry the
|
|
18
|
+
* Carry the visit's UTM labels across the domain hop (AGL-1731).
|
|
19
19
|
*
|
|
20
|
-
* `
|
|
20
|
+
* `utm-attribution.ts` is the READER half of this contract and it works.
|
|
21
21
|
* This is the half that was missing, and without it the reader was fed
|
|
22
22
|
* nothing: `aglyn.com` is a tenant published site and `app.aglyn.com` is the
|
|
23
23
|
* console, a real cross-origin hop, and no code anywhere put a `utm_*`
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
* it. There is no default, no inference from the referrer, and no
|
|
96
96
|
* `utm_source=direct` — a signup with no campaign must reach the console with
|
|
97
97
|
* no campaign, so that "arrived from nowhere" and "never asked" stay the
|
|
98
|
-
* distinct facts `
|
|
98
|
+
* distinct facts `utm-attribution.ts` keeps them.
|
|
99
99
|
*
|
|
100
100
|
* Only links to OUR OWN console origin are touched, and the origin is supplied
|
|
101
101
|
* by the caller rather than written here — a self-host install points
|
|
@@ -115,14 +115,14 @@
|
|
|
115
115
|
* is the exact failure AGL-1731 is about, and it is worse than a few
|
|
116
116
|
* mislabelled rows that GA's own Hostname dimension can already separate.
|
|
117
117
|
*/
|
|
118
|
-
import { type
|
|
118
|
+
import { type UtmAttribution } from './utm-attribution';
|
|
119
119
|
/**
|
|
120
120
|
* Where the first touch of this visit is held. `sessionStorage`, so it dies
|
|
121
121
|
* with the tab: a campaign is a fact about one visit, and a value that
|
|
122
122
|
* outlived the visit would start attributing next week's organic return to
|
|
123
123
|
* this week's ad.
|
|
124
124
|
*/
|
|
125
|
-
export declare const
|
|
125
|
+
export declare const UTM_VISIT_STORAGE_KEY = "aglyn:campaign";
|
|
126
126
|
/**
|
|
127
127
|
* What a read of the first-touch store produced.
|
|
128
128
|
*
|
|
@@ -134,9 +134,9 @@ export declare const CAMPAIGN_VISIT_STORAGE_KEY = "aglyn:campaign";
|
|
|
134
134
|
* treats it differently: it falls through to the live URL instead of
|
|
135
135
|
* concluding anything.
|
|
136
136
|
*/
|
|
137
|
-
export type
|
|
137
|
+
export type VisitUtmRead = {
|
|
138
138
|
status: 'campaign';
|
|
139
|
-
campaign:
|
|
139
|
+
campaign: UtmAttribution;
|
|
140
140
|
} | {
|
|
141
141
|
status: 'none';
|
|
142
142
|
} | {
|
|
@@ -150,7 +150,7 @@ export type VisitCampaignRead = {
|
|
|
150
150
|
* string can claim no more than an inbound URL could — three allowlisted keys,
|
|
151
151
|
* trimmed, email shapes refused, capped.
|
|
152
152
|
*/
|
|
153
|
-
export declare function
|
|
153
|
+
export declare function readVisitUtm(): VisitUtmRead;
|
|
154
154
|
/**
|
|
155
155
|
* Remember the campaign on the current URL as this visit's first touch.
|
|
156
156
|
*
|
|
@@ -161,7 +161,7 @@ export declare function readVisitCampaign(): VisitCampaignRead;
|
|
|
161
161
|
*
|
|
162
162
|
* Returns what is now remembered so a caller can assert on it.
|
|
163
163
|
*/
|
|
164
|
-
export declare function
|
|
164
|
+
export declare function rememberVisitUtm(search?: string): UtmAttribution | null;
|
|
165
165
|
/**
|
|
166
166
|
* Tell this module what the visitor's analytics consent state is.
|
|
167
167
|
*
|
|
@@ -171,7 +171,7 @@ export declare function rememberVisitCampaign(search?: string): CampaignAttribut
|
|
|
171
171
|
* visitor who changes their mind mid-visit should not leave the thing they
|
|
172
172
|
* withdrew consent for sitting on their device.
|
|
173
173
|
*/
|
|
174
|
-
export declare function
|
|
174
|
+
export declare function setUtmForwardingConsent(allowed: boolean | null): void;
|
|
175
175
|
/**
|
|
176
176
|
* The campaign to forward, first touch preferred.
|
|
177
177
|
*
|
|
@@ -179,7 +179,7 @@ export declare function setCampaignForwardingConsent(allowed: boolean | null): v
|
|
|
179
179
|
* which is the point of carrying that state at all: a broken store degrades
|
|
180
180
|
* this to tier 1 rather than to silence.
|
|
181
181
|
*/
|
|
182
|
-
export declare function
|
|
182
|
+
export declare function utmToForward(search?: string): UtmAttribution | null;
|
|
183
183
|
/**
|
|
184
184
|
* The href a console-bound link should carry for this visitor, or null to
|
|
185
185
|
* leave it exactly as authored.
|
|
@@ -194,7 +194,7 @@ export declare function campaignToForward(search?: string): CampaignAttribution
|
|
|
194
194
|
* row describes an event that never happened. Everything else on the href —
|
|
195
195
|
* `plan`, `interval`, the AGL-1535 intent — is preserved untouched.
|
|
196
196
|
*/
|
|
197
|
-
export declare function
|
|
197
|
+
export declare function decorateUtmHref(href: string, baseHref: string, consoleOrigin: string, campaign: UtmAttribution | null): string | null;
|
|
198
198
|
/**
|
|
199
199
|
* Start forwarding the campaign onto console-bound links on this page.
|
|
200
200
|
*
|
|
@@ -213,8 +213,8 @@ export declare function decorateCampaignHref(href: string, baseHref: string, con
|
|
|
213
213
|
* Returns an uninstall function for tests and for a caller that owns the page
|
|
214
214
|
* lifecycle.
|
|
215
215
|
*/
|
|
216
|
-
export declare function
|
|
216
|
+
export declare function installUtmForwarding(options: {
|
|
217
217
|
consoleOrigin: string;
|
|
218
218
|
}): () => void;
|
|
219
219
|
/** Test seam — removes both listeners, forgets them, and forgets consent. */
|
|
220
|
-
export declare function
|
|
220
|
+
export declare function resetUtmForwarding(): void;
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ /**
|
|
17
|
-
* Carry the
|
|
17
|
+
* Carry the visit's UTM labels across the domain hop (AGL-1731).
|
|
18
18
|
*
|
|
19
|
-
* `
|
|
19
|
+
* `utm-attribution.ts` is the READER half of this contract and it works.
|
|
20
20
|
* This is the half that was missing, and without it the reader was fed
|
|
21
21
|
* nothing: `aglyn.com` is a tenant published site and `app.aglyn.com` is the
|
|
22
22
|
* console, a real cross-origin hop, and no code anywhere put a `utm_*`
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
* it. There is no default, no inference from the referrer, and no
|
|
95
95
|
* `utm_source=direct` — a signup with no campaign must reach the console with
|
|
96
96
|
* no campaign, so that "arrived from nowhere" and "never asked" stay the
|
|
97
|
-
* distinct facts `
|
|
97
|
+
* distinct facts `utm-attribution.ts` keeps them.
|
|
98
98
|
*
|
|
99
99
|
* Only links to OUR OWN console origin are touched, and the origin is supplied
|
|
100
100
|
* by the caller rather than written here — a self-host install points
|
|
@@ -113,13 +113,13 @@
|
|
|
113
113
|
* set it would forward NOTHING while looking perfectly healthy. A silent zero
|
|
114
114
|
* is the exact failure AGL-1731 is about, and it is worse than a few
|
|
115
115
|
* mislabelled rows that GA's own Hostname dimension can already separate.
|
|
116
|
-
*/ import {
|
|
116
|
+
*/ import { UTM_QUERY_KEYS, utmAttributionQuery, parseUtmAttribution } from "./utm-attribution.js";
|
|
117
117
|
/**
|
|
118
118
|
* Where the first touch of this visit is held. `sessionStorage`, so it dies
|
|
119
119
|
* with the tab: a campaign is a fact about one visit, and a value that
|
|
120
120
|
* outlived the visit would start attributing next week's organic return to
|
|
121
121
|
* this week's ad.
|
|
122
|
-
*/ export const
|
|
122
|
+
*/ export const UTM_VISIT_STORAGE_KEY = 'aglyn:campaign';
|
|
123
123
|
/**
|
|
124
124
|
* Whether the visitor has granted analytics storage.
|
|
125
125
|
*
|
|
@@ -149,7 +149,7 @@ function sessionStore() {
|
|
|
149
149
|
* `sessionStorage` is writable by anything running on the page, so the stored
|
|
150
150
|
* string can claim no more than an inbound URL could — three allowlisted keys,
|
|
151
151
|
* trimmed, email shapes refused, capped.
|
|
152
|
-
*/ export function
|
|
152
|
+
*/ export function readVisitUtm() {
|
|
153
153
|
if (storageConsent !== true) return {
|
|
154
154
|
status: 'none'
|
|
155
155
|
};
|
|
@@ -159,7 +159,7 @@ function sessionStore() {
|
|
|
159
159
|
};
|
|
160
160
|
let raw;
|
|
161
161
|
try {
|
|
162
|
-
raw = store.getItem(
|
|
162
|
+
raw = store.getItem(UTM_VISIT_STORAGE_KEY);
|
|
163
163
|
} catch (unused) {
|
|
164
164
|
return {
|
|
165
165
|
status: 'unreadable'
|
|
@@ -168,7 +168,7 @@ function sessionStore() {
|
|
|
168
168
|
if (!raw) return {
|
|
169
169
|
status: 'none'
|
|
170
170
|
};
|
|
171
|
-
const campaign =
|
|
171
|
+
const campaign = parseUtmAttribution(new URLSearchParams(raw));
|
|
172
172
|
return campaign ? {
|
|
173
173
|
status: 'campaign',
|
|
174
174
|
campaign
|
|
@@ -185,17 +185,17 @@ function sessionStore() {
|
|
|
185
185
|
* quietly turn this into last-touch.
|
|
186
186
|
*
|
|
187
187
|
* Returns what is now remembered so a caller can assert on it.
|
|
188
|
-
*/ export function
|
|
189
|
-
const existing =
|
|
188
|
+
*/ export function rememberVisitUtm(search) {
|
|
189
|
+
const existing = readVisitUtm();
|
|
190
190
|
if (existing.status === 'campaign') return existing.campaign;
|
|
191
191
|
if (storageConsent !== true) return null;
|
|
192
192
|
const store = sessionStore();
|
|
193
193
|
if (!store) return null;
|
|
194
194
|
const source = typeof search === 'string' ? search : typeof window === 'undefined' ? '' : window.location.search;
|
|
195
|
-
const campaign =
|
|
195
|
+
const campaign = parseUtmAttribution(new URLSearchParams(source));
|
|
196
196
|
if (!campaign) return null;
|
|
197
197
|
try {
|
|
198
|
-
store.setItem(
|
|
198
|
+
store.setItem(UTM_VISIT_STORAGE_KEY, utmAttributionQuery(campaign));
|
|
199
199
|
} catch (unused) {
|
|
200
200
|
// A store that refuses the write costs the walk from the landing page to
|
|
201
201
|
// the pricing page, never the click itself — tier 1 still carries a
|
|
@@ -212,17 +212,17 @@ function sessionStore() {
|
|
|
212
212
|
* effect immediately: the stored value is dropped, not merely ignored. A
|
|
213
213
|
* visitor who changes their mind mid-visit should not leave the thing they
|
|
214
214
|
* withdrew consent for sitting on their device.
|
|
215
|
-
*/ export function
|
|
215
|
+
*/ export function setUtmForwardingConsent(allowed) {
|
|
216
216
|
storageConsent = allowed === true ? true : allowed === false ? false : null;
|
|
217
217
|
if (storageConsent === true) {
|
|
218
|
-
|
|
218
|
+
rememberVisitUtm();
|
|
219
219
|
return;
|
|
220
220
|
}
|
|
221
221
|
if (storageConsent === false) {
|
|
222
222
|
const store = sessionStore();
|
|
223
223
|
if (!store) return;
|
|
224
224
|
try {
|
|
225
|
-
store.removeItem(
|
|
225
|
+
store.removeItem(UTM_VISIT_STORAGE_KEY);
|
|
226
226
|
} catch (unused) {
|
|
227
227
|
// Nothing else to try, and a failed cleanup must not break the page.
|
|
228
228
|
}
|
|
@@ -234,11 +234,11 @@ function sessionStore() {
|
|
|
234
234
|
* The `unreadable` case falls through to the live URL rather than to nothing,
|
|
235
235
|
* which is the point of carrying that state at all: a broken store degrades
|
|
236
236
|
* this to tier 1 rather than to silence.
|
|
237
|
-
*/ export function
|
|
238
|
-
const stored =
|
|
237
|
+
*/ export function utmToForward(search) {
|
|
238
|
+
const stored = readVisitUtm();
|
|
239
239
|
if (stored.status === 'campaign') return stored.campaign;
|
|
240
240
|
const live = typeof search === 'string' ? search : typeof window === 'undefined' ? '' : window.location.search;
|
|
241
|
-
return
|
|
241
|
+
return parseUtmAttribution(new URLSearchParams(live));
|
|
242
242
|
}
|
|
243
243
|
function normalizeOrigin(value) {
|
|
244
244
|
if (!value) return '';
|
|
@@ -261,7 +261,7 @@ function normalizeOrigin(value) {
|
|
|
261
261
|
* three must not produce `source=hn` married to the author's `campaign=`: that
|
|
262
262
|
* row describes an event that never happened. Everything else on the href —
|
|
263
263
|
* `plan`, `interval`, the AGL-1535 intent — is preserved untouched.
|
|
264
|
-
*/ export function
|
|
264
|
+
*/ export function decorateUtmHref(href, baseHref, consoleOrigin, campaign) {
|
|
265
265
|
if (!campaign) return null;
|
|
266
266
|
const origin = normalizeOrigin(consoleOrigin);
|
|
267
267
|
if (!origin) return null;
|
|
@@ -272,8 +272,8 @@ function normalizeOrigin(value) {
|
|
|
272
272
|
return null;
|
|
273
273
|
}
|
|
274
274
|
if (url.origin !== origin) return null;
|
|
275
|
-
for (const key of
|
|
276
|
-
const query = new URLSearchParams(
|
|
275
|
+
for (const key of UTM_QUERY_KEYS)url.searchParams.delete(key);
|
|
276
|
+
const query = new URLSearchParams(utmAttributionQuery(campaign));
|
|
277
277
|
query.forEach((value, key)=>url.searchParams.set(key, value));
|
|
278
278
|
const next = url.toString();
|
|
279
279
|
return next === href ? null : next;
|
|
@@ -295,7 +295,7 @@ function normalizeOrigin(value) {
|
|
|
295
295
|
*
|
|
296
296
|
* Returns an uninstall function for tests and for a caller that owns the page
|
|
297
297
|
* lifecycle.
|
|
298
|
-
*/ export function
|
|
298
|
+
*/ export function installUtmForwarding(options) {
|
|
299
299
|
if (typeof document === 'undefined' || typeof window === 'undefined') {
|
|
300
300
|
return ()=>undefined;
|
|
301
301
|
}
|
|
@@ -308,7 +308,7 @@ function normalizeOrigin(value) {
|
|
|
308
308
|
if (!target || typeof target.closest !== 'function') return;
|
|
309
309
|
const anchor = target.closest('a[href]');
|
|
310
310
|
if (!anchor) return;
|
|
311
|
-
const decorated =
|
|
311
|
+
const decorated = decorateUtmHref(anchor.getAttribute('href') || '', window.location.href, origin, utmToForward());
|
|
312
312
|
if (decorated) anchor.setAttribute('href', decorated);
|
|
313
313
|
} catch (unused) {
|
|
314
314
|
// Attribution never breaks a navigation. A link that throws here would
|
|
@@ -318,9 +318,9 @@ function normalizeOrigin(value) {
|
|
|
318
318
|
installedHandler = onActivate;
|
|
319
319
|
document.addEventListener('pointerdown', onActivate, true);
|
|
320
320
|
document.addEventListener('click', onActivate, true);
|
|
321
|
-
return
|
|
321
|
+
return resetUtmForwarding;
|
|
322
322
|
}
|
|
323
|
-
/** Test seam — removes both listeners, forgets them, and forgets consent. */ export function
|
|
323
|
+
/** Test seam — removes both listeners, forgets them, and forgets consent. */ export function resetUtmForwarding() {
|
|
324
324
|
if (installedHandler && typeof document !== 'undefined') {
|
|
325
325
|
document.removeEventListener('pointerdown', installedHandler, true);
|
|
326
326
|
document.removeEventListener('click', installedHandler, true);
|
|
@@ -329,4 +329,4 @@ function normalizeOrigin(value) {
|
|
|
329
329
|
storageConsent = null;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
//# sourceMappingURL=
|
|
332
|
+
//# sourceMappingURL=utm-forwarding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/utm-forwarding.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Carry the visit's UTM labels across the domain hop (AGL-1731).\n *\n * `utm-attribution.ts` is the READER half of this contract and it works.\n * This is the half that was missing, and without it the reader was fed\n * nothing: `aglyn.com` is a tenant published site and `app.aglyn.com` is the\n * console, a real cross-origin hop, and no code anywhere put a `utm_*`\n * parameter on a console-bound link. So `users/{uid}.signupCampaign` was\n * written for exactly one population — visitors whose CTA href had a campaign\n * typed into it by hand.\n *\n * A hand-typed one is worse than none. It is STATIC: `utm_source=google`\n * authored onto the pricing button attributes every clicker to Google whether\n * they arrived from Google, from Hacker News or from a bookmark. Confidently\n * wrong attribution reaches the same spend decisions as absent attribution and\n * is much harder to notice, because the report looks populated.\n *\n * What is needed instead is PER-VISITOR forwarding: the landing page copies\n * the campaign off its own inbound URL onto the console-bound href, at click\n * time, for that one visitor.\n *\n * ## Why a delegated listener and not `AppLink`\n *\n * The same reason `analytics-link-clicks.ts` gives, and it is the reason that\n * decides this file's shape. The CTAs on `aglyn.com` are authored besigner\n * content, not repo files; rich-text and Custom HTML anchors are written with\n * `dangerouslySetInnerHTML` and are plain DOM anchors with no React handler at\n * all. A prop on `AppLink` would miss them, and reaching them from `AppLink`\n * would mean `useSearchParams` in a component the console renders on every\n * page — a dynamic-rendering hazard on statically generated surfaces, paid on\n * every route to fix a link on one of them. A capture-phase listener on\n * `document` sees every anchor, costs one listener, and renders nothing.\n *\n * ## The two tiers, and why the storage tier is the SECOND one\n *\n * A visitor rarely signs up from the page the ad landed on. They land on\n * `/?utm_source=google&...`, read `/pricing`, then click. By then the campaign\n * is off the address bar, so reading the live URL at click time alone would\n * capture only the minority who convert without navigating.\n *\n * - **Tier 1 — the live URL.** Read `window.location.search` at click time.\n * This touches NO storage: the parameters are already in the page the\n * visitor asked for. Nothing is written to their device, so there is nothing\n * here to consent to, and it works for every visitor including one who has\n * declined analytics.\n * - **Tier 2 — the first touch of the visit**, in `sessionStorage`, which is\n * what survives the walk from the landing page to the pricing page.\n *\n * Tier 2 WINS when both exist, because first touch is the question being\n * asked. Last touch — re-deriving the campaign from whatever URL happens to be\n * current at signup — answers a different question and would disagree with\n * GA4's own session attribution.\n *\n * ## Why tier 2 is gated on analytics consent, and tier 1 is not\n *\n * Tier 2 writes to the visitor's device for an analytics purpose, so it is\n * `analytics_storage` in the consent-mode sense and it waits for the same\n * grant the GA tag waits for (AGL-1498). The gate is not re-implemented here\n * and no consent record is read here: the caller already computes\n * `analyticsAllowed` for the tag and hands the same boolean down, so there is\n * one gate and it cannot drift from itself.\n *\n * It is deliberately NOT gated on advertising storage. Nothing here reads or\n * writes an advertising identifier, sets a cookie, or leaves anything that\n * outlives the tab, and AGL-1649 has advertising denied with no route for a\n * host to grant it — gating on a permission that cannot currently be given\n * would be a feature that is dead by construction rather than one that is off.\n *\n * **Consent unresolved is not consent denied, and both are falsy.** With\n * `strictNullChecks` off repo-wide that distinction is the sort that\n * evaporates, so it is carried as an explicit tri-state: until the caller has\n * actually resolved the visitor's state, tier 2 neither reads nor writes.\n * Failing closed here costs an attribution; failing open would write to a\n * device before the visitor answered.\n *\n * ## What is never done\n *\n * A value is only ever forwarded if this visitor literally arrived carrying\n * it. There is no default, no inference from the referrer, and no\n * `utm_source=direct` — a signup with no campaign must reach the console with\n * no campaign, so that \"arrived from nowhere\" and \"never asked\" stay the\n * distinct facts `utm-attribution.ts` keeps them.\n *\n * Only links to OUR OWN console origin are touched, and the origin is supplied\n * by the caller rather than written here — a self-host install points\n * `NEXT_PUBLIC_CONSOLE_URL` somewhere else entirely. Decorating any other host\n * would post our campaign labels to a third party, which is a data leak dressed\n * up as attribution.\n *\n * ## One accepted consequence\n *\n * The tenant runtime serves `aglyn.com` AND every customer's published site,\n * so a customer site that links to our signup forwards ITS OWN inbound\n * campaign — an agency partner's `utm_source=their-newsletter` would land in our\n * acquisition report describing their spend, not ours. It is not gated on\n * \"is this the operator's marketing host\" deliberately: that gate needs a\n * second configured origin to compare against, and a deployment that forgot to\n * set it would forward NOTHING while looking perfectly healthy. A silent zero\n * is the exact failure AGL-1731 is about, and it is worse than a few\n * mislabelled rows that GA's own Hostname dimension can already separate.\n */\n\nimport {\n UTM_QUERY_KEYS,\n utmAttributionQuery,\n parseUtmAttribution,\n type UtmAttribution,\n} from './utm-attribution'\n\n/**\n * Where the first touch of this visit is held. `sessionStorage`, so it dies\n * with the tab: a campaign is a fact about one visit, and a value that\n * outlived the visit would start attributing next week's organic return to\n * this week's ad.\n */\nexport const UTM_VISIT_STORAGE_KEY = 'aglyn:campaign'\n\n/**\n * What a read of the first-touch store produced.\n *\n * Three states rather than \"a campaign or null\", and the third is the whole\n * point. `sessionStorage` throws outright in some privacy modes, and a\n * `catch` that returned null would report \"this visitor named no campaign\" —\n * indistinguishable from an organic arrival, which is how an unreadable source\n * becomes a measured zero. `unreadable` is a different answer and the caller\n * treats it differently: it falls through to the live URL instead of\n * concluding anything.\n */\nexport type VisitUtmRead =\n | { status: 'campaign'; campaign: UtmAttribution }\n | { status: 'none' }\n | { status: 'unreadable' }\n\n/**\n * Whether the visitor has granted analytics storage.\n *\n * `null` means NOT YET RESOLVED, and is not the same as `false`. The consent\n * state is settled client-side after hydration, so there is a window in every\n * pageview where the answer is genuinely unknown, and treating it as a denial\n * would be as wrong as treating it as a grant. Tier 2 does nothing in it.\n */\nlet storageConsent: boolean | null = null\n\n/** The one listener pair on `document`, or null. Held, not flagged — the same\n * reason `analytics-link-clicks.ts` holds its own: a boolean lets a caller\n * install over a listener that is still attached, and re-entrant decoration is\n * harder to see than a double-counted event. */\nlet installedHandler: ((event: Event) => void) | null = null\n\nfunction sessionStore(): Storage | null {\n if (typeof window === 'undefined') return null\n // Touching the property itself can throw, not just its methods.\n try {\n return window.sessionStorage ?? null\n } catch {\n return null\n }\n}\n\n/**\n * The campaign this visit began with, as far as the store can say.\n *\n * Re-parsed through the same allowlist that wrote it rather than trusted:\n * `sessionStorage` is writable by anything running on the page, so the stored\n * string can claim no more than an inbound URL could — three allowlisted keys,\n * trimmed, email shapes refused, capped.\n */\nexport function readVisitUtm(): VisitUtmRead {\n if (storageConsent !== true) return { status: 'none' }\n const store = sessionStore()\n if (!store) return { status: 'unreadable' }\n let raw: string | null\n try {\n raw = store.getItem(UTM_VISIT_STORAGE_KEY)\n } catch {\n return { status: 'unreadable' }\n }\n if (!raw) return { status: 'none' }\n const campaign = parseUtmAttribution(new URLSearchParams(raw))\n return campaign ? { status: 'campaign', campaign } : { status: 'none' }\n}\n\n/**\n * Remember the campaign on the current URL as this visit's first touch.\n *\n * First touch means FIRST: an existing entry is never replaced. A second\n * campaign arriving later in the same visit is a link the visitor followed\n * after we already knew where they came from, and letting it overwrite would\n * quietly turn this into last-touch.\n *\n * Returns what is now remembered so a caller can assert on it.\n */\nexport function rememberVisitUtm(search?: string): UtmAttribution | null {\n const existing = readVisitUtm()\n if (existing.status === 'campaign') return existing.campaign\n if (storageConsent !== true) return null\n const store = sessionStore()\n if (!store) return null\n const source =\n typeof search === 'string'\n ? search\n : typeof window === 'undefined'\n ? ''\n : window.location.search\n const campaign = parseUtmAttribution(new URLSearchParams(source))\n if (!campaign) return null\n try {\n store.setItem(UTM_VISIT_STORAGE_KEY, utmAttributionQuery(campaign))\n } catch {\n // A store that refuses the write costs the walk from the landing page to\n // the pricing page, never the click itself — tier 1 still carries a\n // visitor who converts without navigating.\n return null\n }\n return campaign\n}\n\n/**\n * Tell this module what the visitor's analytics consent state is.\n *\n * Called by whoever already computed it for the tag, on every render, so a\n * grant seeds the first touch the moment it is given and a WITHDRAWAL takes\n * effect immediately: the stored value is dropped, not merely ignored. A\n * visitor who changes their mind mid-visit should not leave the thing they\n * withdrew consent for sitting on their device.\n */\nexport function setUtmForwardingConsent(allowed: boolean | null): void {\n storageConsent = allowed === true ? true : allowed === false ? false : null\n if (storageConsent === true) {\n rememberVisitUtm()\n return\n }\n if (storageConsent === false) {\n const store = sessionStore()\n if (!store) return\n try {\n store.removeItem(UTM_VISIT_STORAGE_KEY)\n } catch {\n // Nothing else to try, and a failed cleanup must not break the page.\n }\n }\n}\n\n/**\n * The campaign to forward, first touch preferred.\n *\n * The `unreadable` case falls through to the live URL rather than to nothing,\n * which is the point of carrying that state at all: a broken store degrades\n * this to tier 1 rather than to silence.\n */\nexport function utmToForward(search?: string): UtmAttribution | null {\n const stored = readVisitUtm()\n if (stored.status === 'campaign') return stored.campaign\n const live =\n typeof search === 'string'\n ? search\n : typeof window === 'undefined'\n ? ''\n : window.location.search\n return parseUtmAttribution(new URLSearchParams(live))\n}\n\nfunction normalizeOrigin(value: string | null | undefined): string {\n if (!value) return ''\n try {\n return new URL(value).origin\n } catch {\n return ''\n }\n}\n\n/**\n * The href a console-bound link should carry for this visitor, or null to\n * leave it exactly as authored.\n *\n * Null — rather than \"the href unchanged\" — so that a caller cannot write back\n * a value it did not mean to change, and so a test can tell \"decided not to\n * touch this\" from \"touched it and produced the same string\".\n *\n * The three keys are replaced WHOLESALE, never merged key by key. A visitor\n * who arrived with only `utm_source=hn` clicking a button authored with all\n * three must not produce `source=hn` married to the author's `campaign=`: that\n * row describes an event that never happened. Everything else on the href —\n * `plan`, `interval`, the AGL-1535 intent — is preserved untouched.\n */\nexport function decorateUtmHref(\n href: string,\n baseHref: string,\n consoleOrigin: string,\n campaign: UtmAttribution | null,\n): string | null {\n if (!campaign) return null\n const origin = normalizeOrigin(consoleOrigin)\n if (!origin) return null\n let url: URL\n try {\n url = new URL(href, baseHref)\n } catch {\n return null\n }\n if (url.origin !== origin) return null\n for (const key of UTM_QUERY_KEYS) url.searchParams.delete(key)\n const query = new URLSearchParams(utmAttributionQuery(campaign))\n query.forEach((value, key) => url.searchParams.set(key, value))\n const next = url.toString()\n return next === href ? null : next\n}\n\n/**\n * Start forwarding the campaign onto console-bound links on this page.\n *\n * Idempotent and safe to call during render, the same contract as\n * `installLinkClickTracking` and for the same AGL-1550 reason: an effect only\n * runs if React commits, and a page that renders without committing would\n * otherwise ship links with no campaign on them.\n *\n * Both `pointerdown` and `click` are listened for, in capture phase.\n * `pointerdown` is what covers a middle-click, a modifier-click into a new\n * tab, and \"Copy link address\" from the context menu — all of which read the\n * href without ever firing `click`. `click` is what covers keyboard\n * activation, which fires no pointer event at all. Decoration is idempotent,\n * so a plain left click doing both is not a problem.\n *\n * Returns an uninstall function for tests and for a caller that owns the page\n * lifecycle.\n */\nexport function installUtmForwarding(options: {\n consoleOrigin: string\n}): () => void {\n if (typeof document === 'undefined' || typeof window === 'undefined') {\n return () => undefined\n }\n if (installedHandler) return () => undefined\n const origin = normalizeOrigin(options?.consoleOrigin)\n if (!origin) return () => undefined\n\n const onActivate = (event: Event) => {\n try {\n const target = event.target as Element | null\n if (!target || typeof target.closest !== 'function') return\n const anchor = target.closest('a[href]')\n if (!anchor) return\n const decorated = decorateUtmHref(\n anchor.getAttribute('href') || '',\n window.location.href,\n origin,\n utmToForward(),\n )\n if (decorated) anchor.setAttribute('href', decorated)\n } catch {\n // Attribution never breaks a navigation. A link that throws here would\n // be a link that does not work, which costs more than the campaign does.\n }\n }\n\n installedHandler = onActivate\n document.addEventListener('pointerdown', onActivate, true)\n document.addEventListener('click', onActivate, true)\n return resetUtmForwarding\n}\n\n/** Test seam — removes both listeners, forgets them, and forgets consent. */\nexport function resetUtmForwarding(): void {\n if (installedHandler && typeof document !== 'undefined') {\n document.removeEventListener('pointerdown', installedHandler, true)\n document.removeEventListener('click', installedHandler, true)\n }\n installedHandler = null\n storageConsent = null\n}\n"],"names":["UTM_QUERY_KEYS","utmAttributionQuery","parseUtmAttribution","UTM_VISIT_STORAGE_KEY","storageConsent","installedHandler","sessionStore","window","sessionStorage","readVisitUtm","status","store","raw","getItem","campaign","URLSearchParams","rememberVisitUtm","search","existing","source","location","setItem","setUtmForwardingConsent","allowed","removeItem","utmToForward","stored","live","normalizeOrigin","value","URL","origin","decorateUtmHref","href","baseHref","consoleOrigin","url","key","searchParams","delete","query","forEach","set","next","toString","installUtmForwarding","options","document","undefined","onActivate","event","target","closest","anchor","decorated","getAttribute","setAttribute","addEventListener","resetUtmForwarding","removeEventListener"],"mappings":"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"}
|
|
@@ -48,10 +48,10 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
48
48
|
* eligible. `description` is one Google recommends rather than requires, so a
|
|
49
49
|
* blank one costs the block that field and nothing more.
|
|
50
50
|
*/ import { NODE_ROOT_ID } from "../canvas-manager/canvas-manager.js";
|
|
51
|
+
import { videoEmbedUrl } from "../plugin-manager/video-embed-provider.js";
|
|
51
52
|
import { collectDescendantIds } from "./compose-reusable-components.js";
|
|
52
53
|
import { videoDurationIso8601 } from "./media-metadata.js";
|
|
53
54
|
import { MEDIA_CDN_POSTER_WIDTH, absoluteMediaSrc, videoPosterSrc } from "./media-ref.js";
|
|
54
|
-
import { wistiaEmbedUrl } from "./wistia-embed.js";
|
|
55
55
|
/** Component id of the Video element. Persisted in documents; never renamed. */ export const VIDEO_COMPONENT_ID = 'video';
|
|
56
56
|
/** Milliseconds in a second — the unit `videoDurationIso8601` takes. */ const MS_PER_SECOND = 1000;
|
|
57
57
|
/** A trimmed string, or nothing. Blank is never a value here. */ const text = (value)=>{
|
|
@@ -150,10 +150,11 @@ import { wistiaEmbedUrl } from "./wistia-embed.js";
|
|
|
150
150
|
// All three, or nothing. See the module note: a block missing one of these
|
|
151
151
|
// is an error a search console reports, not a smaller win.
|
|
152
152
|
if (!name || !uploadDate || !thumbnailUrl) return undefined;
|
|
153
|
-
// A
|
|
154
|
-
// as the player page and never as `contentUrl`: the link an
|
|
155
|
-
// is
|
|
156
|
-
|
|
153
|
+
// A hosted player's link names a player, not a file (AGL-2826), so it is
|
|
154
|
+
// published as the player page and never as `contentUrl`: the link an
|
|
155
|
+
// author pasted is the host's media page, whose bytes are HTML. Which hosts
|
|
156
|
+
// those are is declared by plugins (`video-embed-provider.ts`).
|
|
157
|
+
const embedUrl = videoEmbedUrl(props['src']);
|
|
157
158
|
const contentUrl = embedUrl ? undefined : absoluteMediaSrc(typeof props['src'] === 'string' ? props['src'] : undefined, {
|
|
158
159
|
hostId,
|
|
159
160
|
origin
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/video-object.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * `VideoObject` structured data for a published page (AGL-2747).\n *\n * ## Why the page's NODES are the input\n *\n * Every other block `buildJsonLd` emits is derived from a screen, a collection\n * entry or the host document — metadata about the page. A video is not\n * metadata about anything: it is a thing an author placed *in* the page, and\n * the only record that it is there is the node.\n *\n * The route already walks that map twice for exactly this kind of question\n * (`pageAnimationAssets`, `deferLazyPanelNodes`) and commerce walks it in a\n * site-page enricher to seed the `Product` block. So this is the same\n * mechanism, not a second one.\n *\n * ⚠️ The composed map is **denormalized and flat**: `Record<string, node>`\n * whose children are id STRINGS under `nodes`, never a nested `children`\n * array. The commerce enricher's first version recursed `children`, matched\n * nothing on any page, and shipped green because its fixture was a tree. This\n * finds Video nodes by walking the values, and then keeps only the ones the\n * page draws — see {@link pageVideoObjects}.\n *\n * ## Why an incomplete block is not emitted\n *\n * Google requires `name`, `thumbnailUrl` and `uploadDate` for a video result. A\n * `VideoObject` missing one of them is not a partial win — it is an invalid\n * rich result that a search console reports as an error against the page. So\n * the block is withheld until all three are there, the way\n * `breadcrumbListJsonLd` declines a single-crumb list. Everything else\n * (`description`, `contentUrl`, `embedUrl`, `duration`) is added when known and\n * omitted when not, because none of those changes whether the page is\n * eligible. `description` is one Google recommends rather than requires, so a\n * blank one costs the block that field and nothing more.\n */\n\nimport { NODE_ROOT_ID } from '../canvas-manager/canvas-manager'\nimport type { AglynNodeSchema, NodeId } from '../foundation'\nimport { collectDescendantIds } from './compose-reusable-components'\nimport { videoDurationIso8601 } from './media-metadata'\nimport {\n MEDIA_CDN_POSTER_WIDTH,\n absoluteMediaSrc,\n videoPosterSrc,\n} from './media-ref'\nimport { wistiaEmbedUrl } from './wistia-embed'\n\n/** Component id of the Video element. Persisted in documents; never renamed. */\nexport const VIDEO_COMPONENT_ID = 'video'\n\n/** Milliseconds in a second — the unit `videoDurationIso8601` takes. */\nconst MS_PER_SECOND = 1000\n\n/** A node as the composed map holds it — flat, children by id. */\ninterface ComposedVideoNode {\n componentId?: string\n props?: Record<string, unknown>\n resolvedProps?: Record<string, unknown>\n}\n\nexport interface VideoObjectContext {\n /** The site's public origin — a relative URL is not a `contentUrl`. */\n origin?: string | null\n /** The site being rendered, which qualifies an org-scoped media scope. */\n hostId?: string\n}\n\n/** A trimmed string, or nothing. Blank is never a value here. */\nconst text = (value: unknown): string | undefined => {\n const trimmed = typeof value === 'string' ? value.trim() : ''\n return trimmed || undefined\n}\n\n/**\n * The UTC hour a bare calendar day is published at. An author names a day,\n * not a moment, and noon UTC is still that same day in every zone from UTC-12\n * through UTC+11. Midnight UTC is the previous evening in every zone behind\n * UTC, so a video result dated from it reads a day early across the Americas.\n */\nconst CALENDAR_DAY_UTC_HOUR = 12\n\n/** A calendar day: the shape the Publication date field asks an author for. */\nconst CALENDAR_DAY = /^(\\d{4})-(\\d{2})-(\\d{2})$/\n\n/**\n * An ISO 8601 date-time that names its zone, as `Z` or an offset — the shape\n * a bound timestamp or a video host's own metadata arrives in.\n */\nconst ZONED_DATE_TIME =\n /^(\\d{4})-(\\d{2})-(\\d{2})T(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|[+-](?:0\\d|1[0-4]):[0-5]\\d)$/\n\n/**\n * The instant at `hour` UTC on the day a match's year, month and day groups\n * name, or `undefined` when that day does not exist. `Date.UTC` rolls\n * 2026-02-30 over to March 2 rather than refusing it, so a real day is one\n * whose parts survive the round trip.\n */\nconst utcDay = (match: RegExpExecArray, hour = 0): number | undefined => {\n const year = Number(match[1])\n const month = Number(match[2]) - 1\n const day = Number(match[3])\n const time = Date.UTC(year, month, day, hour)\n const date = new Date(time)\n return date.getUTCFullYear() === year &&\n date.getUTCMonth() === month &&\n date.getUTCDate() === day\n ? time\n : undefined\n}\n\n/**\n * `uploadDate` as Google reads it: an ISO 8601 date AND time, with its zone\n * (AGL-2948).\n *\n * Google types the property as a DateTime. A bare `YYYY-MM-DD` is reported\n * against the page twice, as an invalid datetime and as one missing its\n * timezone, and a time with no zone is read in Googlebot's. Converting where\n * the block is built covers every writer at once: the field, a bound\n * property, an agent, and every document already published with a bare day.\n *\n * - A calendar day becomes that day at {@link CALENDAR_DAY_UTC_HOUR}:00 UTC.\n * - A date-time that names its zone keeps its instant.\n * - Both are spelled by `toISOString()`, as `Article.datePublished` is.\n * - Anything else — a time with no zone, a day that does not exist, free\n * text — is published exactly as typed. Which zone an author meant is a\n * guess, and a confidently wrong date is harder to notice than a warning.\n */\nconst uploadDateTime = (value: string | undefined): string | undefined => {\n if (!value) return undefined\n const day = CALENDAR_DAY.exec(value)\n if (day) {\n const noon = utcDay(day, CALENDAR_DAY_UTC_HOUR)\n return noon === undefined ? value : new Date(noon).toISOString()\n }\n const zoned = ZONED_DATE_TIME.exec(value)\n if (!zoned || utcDay(zoned) === undefined) return value\n // `toISOString` throws on an invalid date, and a page render is no place\n // to find out which fraction lengths this runtime's parser accepts.\n const instant = Date.parse(value)\n return Number.isNaN(instant) ? value : new Date(instant).toISOString()\n}\n\n/**\n * The `VideoObject` for one Video node, or `undefined` when the author has not\n * given it enough to be worth publishing.\n *\n * `props` precedence is `resolvedProps ?? props` — the same rule every other\n * consumer of the composed map uses, because a node inside a repeated\n * collection carries its bound values only in the resolved copy.\n *\n * URLs go through `absoluteMediaSrc` and `mediaVariantSrc`, never through the\n * stored value: a `media:` reference published into structured data is a\n * string no crawler can fetch, which is the AGL-1343 lesson `article-json-ld`\n * pins for cover images.\n */\nexport function videoObjectJsonLd(\n node: ComposedVideoNode | null | undefined,\n context?: VideoObjectContext,\n): Record<string, unknown> | undefined {\n if (!node || node.componentId !== VIDEO_COMPONENT_ID) return undefined\n const props = node.resolvedProps ?? node.props ?? {}\n const origin = context?.origin\n const hostId = context?.hostId\n const name = text(props['title'])\n const description = text(props['description'])\n const uploadDate = uploadDateTime(text(props['uploadDate']))\n // The SAME rule the element renders with, so the thumbnail a crawler\n // fetches is the poster a visitor sees — including the refusal to derive a\n // generated poster's url unless the node records that one exists, which is\n // what keeps a 404 out of the rich result.\n const thumbnailUrl = absoluteMediaSrc(\n videoPosterSrc({\n hostId,\n poster: props['poster'],\n src: props['src'],\n generated: props['posterFromSource'],\n width: MEDIA_CDN_POSTER_WIDTH,\n }),\n { hostId, origin },\n )\n // All three, or nothing. See the module note: a block missing one of these\n // is an error a search console reports, not a smaller win.\n if (!name || !uploadDate || !thumbnailUrl) return undefined\n // A Wistia link names a player, not a file (AGL-2826), so it is published\n // as the player page and never as `contentUrl`: the link an author pasted\n // is Wistia's media page, whose bytes are HTML.\n const embedUrl = wistiaEmbedUrl(props['src'])\n const contentUrl = embedUrl\n ? undefined\n : absoluteMediaSrc(\n typeof props['src'] === 'string' ? props['src'] : undefined,\n { hostId, origin },\n )\n // Through the DAM's own formatter, in ITS unit. The node stores seconds\n // because that is what an author types; `videoDurationIso8601` takes\n // milliseconds and rounds a sub-second clip UP to `PT1S` rather than to a\n // `PT0S` that reads as \"no duration\" — which is why this converts rather\n // than keeping the second implementation it used to have.\n const seconds = Number(props['durationSeconds'])\n const duration =\n Number.isFinite(seconds) && seconds > 0\n ? videoDurationIso8601(seconds * MS_PER_SECOND)\n : undefined\n return {\n '@context': 'https://schema.org',\n '@type': 'VideoObject',\n name,\n ...(description ? { description } : {}),\n thumbnailUrl,\n uploadDate,\n // Exactly one of the two. `contentUrl` is the file, for a film served from\n // this site's media CDN or hotlinked; `embedUrl` is a PLAYER page, which\n // this element renders only for a Wistia video.\n ...(contentUrl ? { contentUrl } : {}),\n ...(embedUrl ? { embedUrl } : {}),\n ...(duration ? { duration } : {}),\n }\n}\n\n/**\n * Every publishable `VideoObject` on a page, in the composed map's own order.\n *\n * Returns `[]` — the common case — for a page with no video, a page whose\n * videos have no SEO fields filled in, and a page with no nodes at all. The\n * route then emits no extra script, which is the same shape\n * `pageAnimationAssets` uses for the same reason.\n *\n * ⚠️ Hand this the map the page actually SHIPS, not the one the loader read.\n * A video inside a withheld lazy tab panel is not in the HTML, and a\n * `VideoObject` describing a player that is not on the page is precisely the\n * mismatch a video rich result is checked for.\n *\n * ## Only the videos the page draws (AGL-2957)\n *\n * Being in the map is not being on the page. The renderer starts at\n * {@link NODE_ROOT_ID} and follows child id lists, and nothing else, so a node\n * no list names is carried in the payload and never drawn. Composition leaves\n * such nodes on purpose: `expandCollectionEntries` and `expandRepeatables`\n * point a block's list at its clones and keep the template in the map. On a\n * list page that template publishes a block named `{{entry.title}}`; on an\n * entry page, where entry tokens are substituted across the whole map, it\n * publishes a copy of the page's own film.\n *\n * So when the map has a root, a Video counts only if a child list reaches it\n * from there. Every stage that moves a subtree does so through those lists —\n * a layout slot adopts the screen's top-level ids, a reusable component or a\n * placed form hands its definition's list to the placement, a tab panel lists\n * its content — so each of those is followed by the same walk, and a panel\n * the route withheld has already lost its list. `parentId` is never followed:\n * a template keeps its parent's id after its parent stops listing it.\n *\n * A map with no root is a fragment with no entry point to measure reach from,\n * and keeps the whole-map walk.\n *\n * One rule of the renderer's is not in the map: a self-closing element draws\n * no children, and which components are self-closing is registry knowledge.\n * The editor refuses a child under one (`nodeAcceptsChildren`), so a Video\n * there is a hand-edited document rather than an authored page.\n *\n * The reach is computed only for a map that holds a Video at all, so a page\n * without one costs a single pass over its values and no walk.\n */\nexport function pageVideoObjects(\n nodes: Record<string, unknown> | null | undefined,\n context?: VideoObjectContext,\n): Record<string, unknown>[] {\n if (!nodes) return []\n const videoIds: string[] = []\n for (const id in nodes) {\n const node = nodes[id] as ComposedVideoNode | null | undefined\n if (node?.componentId === VIDEO_COMPONENT_ID) videoIds.push(id)\n }\n if (!videoIds.length) return []\n const drawn = nodes[NODE_ROOT_ID]\n ? collectDescendantIds(\n nodes as Record<NodeId, AglynNodeSchema>,\n NODE_ROOT_ID,\n )\n : undefined\n const found: Record<string, unknown>[] = []\n for (const id of videoIds) {\n if (drawn && id !== NODE_ROOT_ID && !drawn.has(id)) continue\n const block = videoObjectJsonLd(nodes[id] as ComposedVideoNode, context)\n if (block) found.push(block)\n }\n return found\n}\n"],"names":["NODE_ROOT_ID","collectDescendantIds","videoDurationIso8601","MEDIA_CDN_POSTER_WIDTH","absoluteMediaSrc","videoPosterSrc","wistiaEmbedUrl","VIDEO_COMPONENT_ID","MS_PER_SECOND","text","value","trimmed","trim","undefined","CALENDAR_DAY_UTC_HOUR","CALENDAR_DAY","ZONED_DATE_TIME","utcDay","match","hour","year","Number","month","day","time","Date","UTC","date","getUTCFullYear","getUTCMonth","getUTCDate","uploadDateTime","exec","noon","toISOString","zoned","instant","parse","isNaN","videoObjectJsonLd","node","context","componentId","props","resolvedProps","origin","hostId","name","description","uploadDate","thumbnailUrl","poster","src","generated","width","embedUrl","contentUrl","seconds","duration","isFinite","pageVideoObjects","nodes","videoIds","id","push","length","drawn","found","has","block"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/video-object.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * `VideoObject` structured data for a published page (AGL-2747).\n *\n * ## Why the page's NODES are the input\n *\n * Every other block `buildJsonLd` emits is derived from a screen, a collection\n * entry or the host document — metadata about the page. A video is not\n * metadata about anything: it is a thing an author placed *in* the page, and\n * the only record that it is there is the node.\n *\n * The route already walks that map twice for exactly this kind of question\n * (`pageAnimationAssets`, `deferLazyPanelNodes`) and commerce walks it in a\n * site-page enricher to seed the `Product` block. So this is the same\n * mechanism, not a second one.\n *\n * ⚠️ The composed map is **denormalized and flat**: `Record<string, node>`\n * whose children are id STRINGS under `nodes`, never a nested `children`\n * array. The commerce enricher's first version recursed `children`, matched\n * nothing on any page, and shipped green because its fixture was a tree. This\n * finds Video nodes by walking the values, and then keeps only the ones the\n * page draws — see {@link pageVideoObjects}.\n *\n * ## Why an incomplete block is not emitted\n *\n * Google requires `name`, `thumbnailUrl` and `uploadDate` for a video result. A\n * `VideoObject` missing one of them is not a partial win — it is an invalid\n * rich result that a search console reports as an error against the page. So\n * the block is withheld until all three are there, the way\n * `breadcrumbListJsonLd` declines a single-crumb list. Everything else\n * (`description`, `contentUrl`, `embedUrl`, `duration`) is added when known and\n * omitted when not, because none of those changes whether the page is\n * eligible. `description` is one Google recommends rather than requires, so a\n * blank one costs the block that field and nothing more.\n */\n\nimport { NODE_ROOT_ID } from '../canvas-manager/canvas-manager'\nimport type { AglynNodeSchema, NodeId } from '../foundation'\nimport { videoEmbedUrl } from '../plugin-manager/video-embed-provider'\nimport { collectDescendantIds } from './compose-reusable-components'\nimport { videoDurationIso8601 } from './media-metadata'\nimport {\n MEDIA_CDN_POSTER_WIDTH,\n absoluteMediaSrc,\n videoPosterSrc,\n} from './media-ref'\n\n/** Component id of the Video element. Persisted in documents; never renamed. */\nexport const VIDEO_COMPONENT_ID = 'video'\n\n/** Milliseconds in a second — the unit `videoDurationIso8601` takes. */\nconst MS_PER_SECOND = 1000\n\n/** A node as the composed map holds it — flat, children by id. */\ninterface ComposedVideoNode {\n componentId?: string\n props?: Record<string, unknown>\n resolvedProps?: Record<string, unknown>\n}\n\nexport interface VideoObjectContext {\n /** The site's public origin — a relative URL is not a `contentUrl`. */\n origin?: string | null\n /** The site being rendered, which qualifies an org-scoped media scope. */\n hostId?: string\n}\n\n/** A trimmed string, or nothing. Blank is never a value here. */\nconst text = (value: unknown): string | undefined => {\n const trimmed = typeof value === 'string' ? value.trim() : ''\n return trimmed || undefined\n}\n\n/**\n * The UTC hour a bare calendar day is published at. An author names a day,\n * not a moment, and noon UTC is still that same day in every zone from UTC-12\n * through UTC+11. Midnight UTC is the previous evening in every zone behind\n * UTC, so a video result dated from it reads a day early across the Americas.\n */\nconst CALENDAR_DAY_UTC_HOUR = 12\n\n/** A calendar day: the shape the Publication date field asks an author for. */\nconst CALENDAR_DAY = /^(\\d{4})-(\\d{2})-(\\d{2})$/\n\n/**\n * An ISO 8601 date-time that names its zone, as `Z` or an offset — the shape\n * a bound timestamp or a video host's own metadata arrives in.\n */\nconst ZONED_DATE_TIME =\n /^(\\d{4})-(\\d{2})-(\\d{2})T(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|[+-](?:0\\d|1[0-4]):[0-5]\\d)$/\n\n/**\n * The instant at `hour` UTC on the day a match's year, month and day groups\n * name, or `undefined` when that day does not exist. `Date.UTC` rolls\n * 2026-02-30 over to March 2 rather than refusing it, so a real day is one\n * whose parts survive the round trip.\n */\nconst utcDay = (match: RegExpExecArray, hour = 0): number | undefined => {\n const year = Number(match[1])\n const month = Number(match[2]) - 1\n const day = Number(match[3])\n const time = Date.UTC(year, month, day, hour)\n const date = new Date(time)\n return date.getUTCFullYear() === year &&\n date.getUTCMonth() === month &&\n date.getUTCDate() === day\n ? time\n : undefined\n}\n\n/**\n * `uploadDate` as Google reads it: an ISO 8601 date AND time, with its zone\n * (AGL-2948).\n *\n * Google types the property as a DateTime. A bare `YYYY-MM-DD` is reported\n * against the page twice, as an invalid datetime and as one missing its\n * timezone, and a time with no zone is read in Googlebot's. Converting where\n * the block is built covers every writer at once: the field, a bound\n * property, an agent, and every document already published with a bare day.\n *\n * - A calendar day becomes that day at {@link CALENDAR_DAY_UTC_HOUR}:00 UTC.\n * - A date-time that names its zone keeps its instant.\n * - Both are spelled by `toISOString()`, as `Article.datePublished` is.\n * - Anything else — a time with no zone, a day that does not exist, free\n * text — is published exactly as typed. Which zone an author meant is a\n * guess, and a confidently wrong date is harder to notice than a warning.\n */\nconst uploadDateTime = (value: string | undefined): string | undefined => {\n if (!value) return undefined\n const day = CALENDAR_DAY.exec(value)\n if (day) {\n const noon = utcDay(day, CALENDAR_DAY_UTC_HOUR)\n return noon === undefined ? value : new Date(noon).toISOString()\n }\n const zoned = ZONED_DATE_TIME.exec(value)\n if (!zoned || utcDay(zoned) === undefined) return value\n // `toISOString` throws on an invalid date, and a page render is no place\n // to find out which fraction lengths this runtime's parser accepts.\n const instant = Date.parse(value)\n return Number.isNaN(instant) ? value : new Date(instant).toISOString()\n}\n\n/**\n * The `VideoObject` for one Video node, or `undefined` when the author has not\n * given it enough to be worth publishing.\n *\n * `props` precedence is `resolvedProps ?? props` — the same rule every other\n * consumer of the composed map uses, because a node inside a repeated\n * collection carries its bound values only in the resolved copy.\n *\n * URLs go through `absoluteMediaSrc` and `mediaVariantSrc`, never through the\n * stored value: a `media:` reference published into structured data is a\n * string no crawler can fetch, which is the AGL-1343 lesson `article-json-ld`\n * pins for cover images.\n */\nexport function videoObjectJsonLd(\n node: ComposedVideoNode | null | undefined,\n context?: VideoObjectContext,\n): Record<string, unknown> | undefined {\n if (!node || node.componentId !== VIDEO_COMPONENT_ID) return undefined\n const props = node.resolvedProps ?? node.props ?? {}\n const origin = context?.origin\n const hostId = context?.hostId\n const name = text(props['title'])\n const description = text(props['description'])\n const uploadDate = uploadDateTime(text(props['uploadDate']))\n // The SAME rule the element renders with, so the thumbnail a crawler\n // fetches is the poster a visitor sees — including the refusal to derive a\n // generated poster's url unless the node records that one exists, which is\n // what keeps a 404 out of the rich result.\n const thumbnailUrl = absoluteMediaSrc(\n videoPosterSrc({\n hostId,\n poster: props['poster'],\n src: props['src'],\n generated: props['posterFromSource'],\n width: MEDIA_CDN_POSTER_WIDTH,\n }),\n { hostId, origin },\n )\n // All three, or nothing. See the module note: a block missing one of these\n // is an error a search console reports, not a smaller win.\n if (!name || !uploadDate || !thumbnailUrl) return undefined\n // A hosted player's link names a player, not a file (AGL-2826), so it is\n // published as the player page and never as `contentUrl`: the link an\n // author pasted is the host's media page, whose bytes are HTML. Which hosts\n // those are is declared by plugins (`video-embed-provider.ts`).\n const embedUrl = videoEmbedUrl(props['src'])\n const contentUrl = embedUrl\n ? undefined\n : absoluteMediaSrc(\n typeof props['src'] === 'string' ? props['src'] : undefined,\n { hostId, origin },\n )\n // Through the DAM's own formatter, in ITS unit. The node stores seconds\n // because that is what an author types; `videoDurationIso8601` takes\n // milliseconds and rounds a sub-second clip UP to `PT1S` rather than to a\n // `PT0S` that reads as \"no duration\" — which is why this converts rather\n // than keeping the second implementation it used to have.\n const seconds = Number(props['durationSeconds'])\n const duration =\n Number.isFinite(seconds) && seconds > 0\n ? videoDurationIso8601(seconds * MS_PER_SECOND)\n : undefined\n return {\n '@context': 'https://schema.org',\n '@type': 'VideoObject',\n name,\n ...(description ? { description } : {}),\n thumbnailUrl,\n uploadDate,\n // Exactly one of the two. `contentUrl` is the file, for a film served from\n // this site's media CDN or hotlinked; `embedUrl` is a PLAYER page, which\n // this element renders only for a declared host's link.\n ...(contentUrl ? { contentUrl } : {}),\n ...(embedUrl ? { embedUrl } : {}),\n ...(duration ? { duration } : {}),\n }\n}\n\n/**\n * Every publishable `VideoObject` on a page, in the composed map's own order.\n *\n * Returns `[]` — the common case — for a page with no video, a page whose\n * videos have no SEO fields filled in, and a page with no nodes at all. The\n * route then emits no extra script, which is the same shape\n * `pageAnimationAssets` uses for the same reason.\n *\n * ⚠️ Hand this the map the page actually SHIPS, not the one the loader read.\n * A video inside a withheld lazy tab panel is not in the HTML, and a\n * `VideoObject` describing a player that is not on the page is precisely the\n * mismatch a video rich result is checked for.\n *\n * ## Only the videos the page draws (AGL-2957)\n *\n * Being in the map is not being on the page. The renderer starts at\n * {@link NODE_ROOT_ID} and follows child id lists, and nothing else, so a node\n * no list names is carried in the payload and never drawn. Composition leaves\n * such nodes on purpose: `expandCollectionEntries` and `expandRepeatables`\n * point a block's list at its clones and keep the template in the map. On a\n * list page that template publishes a block named `{{entry.title}}`; on an\n * entry page, where entry tokens are substituted across the whole map, it\n * publishes a copy of the page's own film.\n *\n * So when the map has a root, a Video counts only if a child list reaches it\n * from there. Every stage that moves a subtree does so through those lists —\n * a layout slot adopts the screen's top-level ids, a reusable component or a\n * placed form hands its definition's list to the placement, a tab panel lists\n * its content — so each of those is followed by the same walk, and a panel\n * the route withheld has already lost its list. `parentId` is never followed:\n * a template keeps its parent's id after its parent stops listing it.\n *\n * A map with no root is a fragment with no entry point to measure reach from,\n * and keeps the whole-map walk.\n *\n * One rule of the renderer's is not in the map: a self-closing element draws\n * no children, and which components are self-closing is registry knowledge.\n * The editor refuses a child under one (`nodeAcceptsChildren`), so a Video\n * there is a hand-edited document rather than an authored page.\n *\n * The reach is computed only for a map that holds a Video at all, so a page\n * without one costs a single pass over its values and no walk.\n */\nexport function pageVideoObjects(\n nodes: Record<string, unknown> | null | undefined,\n context?: VideoObjectContext,\n): Record<string, unknown>[] {\n if (!nodes) return []\n const videoIds: string[] = []\n for (const id in nodes) {\n const node = nodes[id] as ComposedVideoNode | null | undefined\n if (node?.componentId === VIDEO_COMPONENT_ID) videoIds.push(id)\n }\n if (!videoIds.length) return []\n const drawn = nodes[NODE_ROOT_ID]\n ? collectDescendantIds(\n nodes as Record<NodeId, AglynNodeSchema>,\n NODE_ROOT_ID,\n )\n : undefined\n const found: Record<string, unknown>[] = []\n for (const id of videoIds) {\n if (drawn && id !== NODE_ROOT_ID && !drawn.has(id)) continue\n const block = videoObjectJsonLd(nodes[id] as ComposedVideoNode, context)\n if (block) found.push(block)\n }\n return found\n}\n"],"names":["NODE_ROOT_ID","videoEmbedUrl","collectDescendantIds","videoDurationIso8601","MEDIA_CDN_POSTER_WIDTH","absoluteMediaSrc","videoPosterSrc","VIDEO_COMPONENT_ID","MS_PER_SECOND","text","value","trimmed","trim","undefined","CALENDAR_DAY_UTC_HOUR","CALENDAR_DAY","ZONED_DATE_TIME","utcDay","match","hour","year","Number","month","day","time","Date","UTC","date","getUTCFullYear","getUTCMonth","getUTCDate","uploadDateTime","exec","noon","toISOString","zoned","instant","parse","isNaN","videoObjectJsonLd","node","context","componentId","props","resolvedProps","origin","hostId","name","description","uploadDate","thumbnailUrl","poster","src","generated","width","embedUrl","contentUrl","seconds","duration","isFinite","pageVideoObjects","nodes","videoIds","id","push","length","drawn","found","has","block"],"mappings":"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"}
|