@wildo-ai/saas-website 1.1.5 → 1.1.7
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/dist/esm/astro/blog-post-bridge.d.ts +9 -0
- package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
- package/dist/esm/astro/blog-post-bridge.js +10 -5
- package/dist/esm/astro/blog-post-bridge.js.map +1 -1
- package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -1
- package/dist/esm/astro/bridge-runtime.d.ts +25 -6
- package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
- package/dist/esm/astro/bridge-runtime.js +10 -6
- package/dist/esm/astro/bridge-runtime.js.map +1 -1
- package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -1
- package/dist/esm/astro/headers-renderer.d.ts.map +1 -1
- package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -1
- package/dist/esm/astro/label-pack-loader.d.ts +4 -4
- package/dist/esm/astro/label-pack-loader.d.ts.map +1 -1
- package/dist/esm/astro/label-pack-loader.js +9 -4
- package/dist/esm/astro/label-pack-loader.js.map +1 -1
- package/dist/esm/astro/llms-txt-server.d.ts.map +1 -1
- package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -1
- package/dist/esm/astro/robots-renderer.d.ts +9 -8
- package/dist/esm/astro/robots-renderer.d.ts.map +1 -1
- package/dist/esm/astro/robots-renderer.js +15 -15
- package/dist/esm/astro/robots-renderer.js.map +1 -1
- package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -1
- package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -1
- package/dist/esm/astro/website-page-head.renderer.d.ts +1 -1
- package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -1
- package/dist/esm/astro/website-page-head.renderer.js +2 -1
- package/dist/esm/astro/website-page-head.renderer.js.map +1 -1
- package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -1
- package/dist/esm/astro/website-site-context.d.ts +5 -2
- package/dist/esm/astro/website-site-context.d.ts.map +1 -1
- package/dist/esm/astro/website-site-context.js.map +1 -1
- package/dist/esm/astro-island.d.ts.map +1 -1
- package/dist/esm/astro.d.ts.map +1 -1
- package/dist/esm/companion-exports.d.ts +8 -15
- package/dist/esm/companion-exports.d.ts.map +1 -1
- package/dist/esm/companion-exports.js +8 -15
- package/dist/esm/companion-exports.js.map +1 -1
- package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -1
- package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -1
- package/dist/esm/config/define-website-config.d.ts +3 -2
- package/dist/esm/config/define-website-config.d.ts.map +1 -1
- package/dist/esm/config/define-website-config.js +10 -10
- package/dist/esm/config/define-website-config.js.map +1 -1
- package/dist/esm/config/index.d.ts.map +1 -1
- package/dist/esm/config/load-website-config.d.ts +6 -0
- package/dist/esm/config/load-website-config.d.ts.map +1 -1
- package/dist/esm/config/load-website-config.js +18 -9
- package/dist/esm/config/load-website-config.js.map +1 -1
- package/dist/esm/config/load-website-pricing-catalog.d.ts +13 -0
- package/dist/esm/config/load-website-pricing-catalog.d.ts.map +1 -0
- package/dist/esm/config/load-website-pricing-catalog.js +34 -0
- package/dist/esm/config/load-website-pricing-catalog.js.map +1 -0
- package/dist/esm/config/load-website-site-context.d.ts +53 -0
- package/dist/esm/config/load-website-site-context.d.ts.map +1 -0
- package/dist/esm/config/load-website-site-context.js +98 -0
- package/dist/esm/config/load-website-site-context.js.map +1 -0
- package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -1
- package/dist/esm/config-loader.d.ts +2 -0
- package/dist/esm/config-loader.d.ts.map +1 -1
- package/dist/esm/config-loader.js +2 -0
- package/dist/esm/config-loader.js.map +1 -1
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +26 -6
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/InboundContactForm.js +51 -33
- package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -1
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +11 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +15 -4
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -1
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +14 -2
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -1
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +34 -4
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -1
- package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts +36 -0
- package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/website-submission-challenge.js +69 -0
- package/dist/esm/core/anonymous-session/website-submission-challenge.js.map +1 -0
- package/dist/esm/core/consent/WebsiteConsentBanner.d.ts.map +1 -1
- package/dist/esm/core/consent/WebsiteConsentBanner.js +11 -10
- package/dist/esm/core/consent/WebsiteConsentBanner.js.map +1 -1
- package/dist/esm/core/consent/WebsiteConsentContext.d.ts.map +1 -1
- package/dist/esm/core/consent/WebsiteConsentGate.d.ts.map +1 -1
- package/dist/esm/core/consent/useWebsiteConsent.d.ts.map +1 -1
- package/dist/esm/core/consent/website-consent-storage.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +25 -5
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -1
- package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -1
- package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -1
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +18 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -1
- package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -2
- package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -1
- package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -1
- package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts +57 -0
- package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts.map +1 -0
- package/dist/esm/core/external-providers/WebsiteProviderComponent.js +56 -0
- package/dist/esm/core/external-providers/WebsiteProviderComponent.js.map +1 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -1
- package/dist/esm/core/external-providers/provider-component-registry.website.d.ts +63 -0
- package/dist/esm/core/external-providers/provider-component-registry.website.d.ts.map +1 -0
- package/dist/esm/core/external-providers/provider-component-registry.website.js +68 -0
- package/dist/esm/core/external-providers/provider-component-registry.website.js.map +1 -0
- package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts +36 -0
- package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts.map +1 -0
- package/dist/esm/core/external-providers/useWebsiteProviderComponent.js +59 -0
- package/dist/esm/core/external-providers/useWebsiteProviderComponent.js.map +1 -0
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +15 -5
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -1
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +39 -24
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -1
- package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts +11 -0
- package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts.map +1 -0
- package/dist/esm/core/external-providers/website-provider-sdk-activations.js +14 -0
- package/dist/esm/core/external-providers/website-provider-sdk-activations.js.map +1 -0
- package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -1
- package/dist/esm/core/factories/define-website-section.d.ts +3 -3
- package/dist/esm/core/factories/define-website-section.d.ts.map +1 -1
- package/dist/esm/core/factories/define-website-section.js +3 -3
- package/dist/esm/core/factories/define-website-section.js.map +1 -1
- package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -1
- package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -1
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -1
- package/dist/esm/core/layouts/WebsitePageLayout.js +10 -7
- package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -1
- package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -1
- package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -1
- package/dist/esm/index.d.ts +30 -65
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +30 -65
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/mdx/BlogPost.d.ts.map +1 -1
- package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -1
- package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -1
- package/dist/esm/mdx/index.d.ts.map +1 -1
- package/dist/esm/mdx.d.ts.map +1 -1
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +35 -6
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +39 -5
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -1
- package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts +32 -0
- package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js +36 -0
- package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js.map +1 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +9 -8
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +8 -7
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -1
- package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -1
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +14 -7
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +14 -7
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -1
- package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts +57 -0
- package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts.map +1 -0
- package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js +76 -0
- package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js.map +1 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +23 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -1
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +20 -2
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -1
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +7 -5
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -1
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -1
- package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/__tests__/bundle-isolation.test.ts +19 -22
- package/src/astro/__tests__/blog-post-bridge.test.tsx +18 -2
- package/src/astro/__tests__/bridge-runtime.test.tsx +21 -3
- package/src/astro/__tests__/label-pack-loader.test.ts +7 -4
- package/src/astro/blog-post-bridge.tsx +19 -5
- package/src/astro/bridge-runtime.tsx +35 -11
- package/src/astro/label-pack-loader.ts +8 -4
- package/src/astro/robots-renderer.ts +15 -15
- package/src/astro/website-page-head.renderer.ts +2 -2
- package/src/astro/website-site-context.ts +5 -2
- package/src/companion-exports.ts +8 -15
- package/src/config/__tests__/website-build-origins.test.ts +52 -0
- package/src/config/define-website-config.ts +10 -10
- package/src/config/load-website-config.ts +25 -14
- package/src/config/load-website-pricing-catalog.ts +52 -0
- package/src/config/load-website-site-context.ts +145 -0
- package/src/config-loader.ts +2 -0
- package/src/core/__tests__/contexts.test.tsx +20 -1
- package/src/core/__tests__/website-consent.test.tsx +2 -1
- package/src/core/anonymous-session/InboundContactForm.tsx +100 -45
- package/src/core/anonymous-session/__tests__/InboundContactForm.labels.test.tsx +108 -0
- package/src/core/anonymous-session/__tests__/InboundContactForm.submission-challenge.test.tsx +139 -0
- package/src/core/anonymous-session/inbound-contact-form.schema.ts +16 -4
- package/src/core/anonymous-session/website-anonymous-session-client.ts +39 -4
- package/src/core/anonymous-session/website-submission-challenge.ts +99 -0
- package/src/core/consent/WebsiteConsentBanner.tsx +11 -10
- package/src/core/contexts/WebsiteRuntimeContext.tsx +25 -5
- package/src/core/contexts/useWebsiteRuntime.ts +33 -2
- package/src/core/external-providers/WebsiteProviderComponent.tsx +153 -0
- package/src/core/external-providers/__tests__/frontend-provider-registry.website.test.ts +4 -0
- package/src/core/external-providers/__tests__/website-provider-component.test.tsx +238 -0
- package/src/core/external-providers/__tests__/website-provider-sdks.test.tsx +72 -0
- package/src/core/external-providers/provider-component-registry.website.ts +115 -0
- package/src/core/external-providers/useWebsiteProviderComponent.ts +76 -0
- package/src/core/external-providers/useWebsiteProviderScripts.ts +56 -37
- package/src/core/external-providers/website-provider-sdk-activations.ts +23 -0
- package/src/core/factories/define-website-section.ts +3 -3
- package/src/core/layouts/WebsitePageLayout.tsx +11 -6
- package/src/index.ts +31 -65
- package/src/schemas/__tests__/design-token-css-property.test.ts +72 -0
- package/src/schemas/__tests__/pricing-offer-structured-data.test.ts +122 -0
- package/src/schemas/__tests__/structured-data-reconciliation.test.ts +35 -0
- package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +45 -5
- package/src/schemas/label-keys/__tests__/scaffolded-consent-label-pack.parity.test.ts +12 -0
- package/src/schemas/label-keys/website-provider-component-label-keys.schemas.ts +40 -0
- package/src/schemas/manifests/website-root-config.shared.schemas.ts +10 -9
- package/src/schemas/sections/website-section-definition.shared.schemas.ts +14 -7
- package/src/schemas/structured-data/pricing-offer-structured-data.shared.ts +144 -0
- package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +46 -1
- package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +7 -5
|
@@ -9,7 +9,8 @@ import type { WebsiteStructuredDataReturn } from '../structured-data/website-str
|
|
|
9
9
|
* `core/define-website-section.tsx` module in Phase 2 because it carries
|
|
10
10
|
* React types in its inputs). The shape is declared in `schemas/`
|
|
11
11
|
* because it is the **observable contract** the page-manifest
|
|
12
|
-
* registration, label-pack validator, and
|
|
12
|
+
* registration, the label-pack validator, `renderPageStructuredData` and the
|
|
13
|
+
* structured-data audit depend on.
|
|
13
14
|
*
|
|
14
15
|
* **Shape rationale** (each field carries semantic weight — see
|
|
15
16
|
* `design-philosophy.mdc`):
|
|
@@ -19,7 +20,8 @@ import type { WebsiteStructuredDataReturn } from '../structured-data/website-str
|
|
|
19
20
|
* (a section can be reused with the same `sectionRef` across pages
|
|
20
21
|
* when its label-key root is intentionally shared).
|
|
21
22
|
* - `category` — see `WebsiteSectionCategory` JSDoc; tags the section
|
|
22
|
-
* for
|
|
23
|
+
* for skill-mapping and analytics labelling. It does NOT select the
|
|
24
|
+
* section's structured data.
|
|
23
25
|
* - `expectedLabelKeys` — the **fully-composed** label keys the section
|
|
24
26
|
* commits to consuming. The plan originally proposed leaf-relative
|
|
25
27
|
* keys (`'headline'`) resolved at runtime against the
|
|
@@ -32,9 +34,13 @@ import type { WebsiteStructuredDataReturn } from '../structured-data/website-str
|
|
|
32
34
|
* graph — `wildo website audit` can list a page's required keys
|
|
33
35
|
* without booting React.
|
|
34
36
|
* 3. The leaf-relative ergonomics (`useWebsiteLabel('headline')`)
|
|
35
|
-
* survive
|
|
36
|
-
*
|
|
37
|
-
* `expectedLabelKeys
|
|
37
|
+
* survive — the hook composes the leaf with the page+section
|
|
38
|
+
* context and looks the result up in the active pack. It does
|
|
39
|
+
* NOT check the key against `expectedLabelKeys`: a leaf the
|
|
40
|
+
* section never declared renders its key (with a console warning)
|
|
41
|
+
* and is caught by nothing until a reviewer reads the page. The
|
|
42
|
+
* build and `wildo website audit` check the pack against the
|
|
43
|
+
* DECLARED keys, so declare every slot the component reads.
|
|
38
44
|
* - `Component` — the React component that renders the section. Typed
|
|
39
45
|
* as `ComponentType<unknown>` here because schema-layer cannot lock
|
|
40
46
|
* the component's prop shape (props vary per section). The factory
|
|
@@ -44,8 +50,9 @@ import type { WebsiteStructuredDataReturn } from '../structured-data/website-str
|
|
|
44
50
|
* - `getStructuredData?` — optional pure function the SEO/AEO/GEO
|
|
45
51
|
* builder library invokes at static-build time to materialize JSON-LD
|
|
46
52
|
* for the section. The return type is intentionally `unknown` here;
|
|
47
|
-
*
|
|
48
|
-
* `
|
|
53
|
+
* nothing narrows it by `category` — the section returns whatever
|
|
54
|
+
* vocabulary it emits, and `wildo website audit` reconciles that
|
|
55
|
+
* against the application's Schema.org catalog.
|
|
49
56
|
*
|
|
50
57
|
* **Why not `z.custom<ComponentType>(…)`**: a `z.custom` predicate
|
|
51
58
|
* cannot meaningfully validate "is a React component" beyond
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"website-section-definition.shared.schemas.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"website-section-definition.shared.schemas.d.ts","sourceRoot":"","sources":["../../../../src/schemas/sections/website-section-definition.shared.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2DAA2D,CAAC;AAG7G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,QAAA,MAAM,+BAA+B;;;;;;iBAMnC,CAAC;AAEH,eAAO,MAAM,8BAA8B,EAAE,OAAO,+BACnB,CAAC;AAElC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,wBAAyB,SAAQ,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC;IAC/F,SAAS,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC;IAC9B;;;;;;;;;;;;;;;;;OAiBG;IACH,iBAAiB,CAAC,EAAE,MAAM,2BAA2B,CAAC;CACvD;;AACD,qEAAqE"}
|
|
@@ -10,7 +10,8 @@ import { WebsiteSectionCategorySchema } from './website-section-category.shared.
|
|
|
10
10
|
* `core/define-website-section.tsx` module in Phase 2 because it carries
|
|
11
11
|
* React types in its inputs). The shape is declared in `schemas/`
|
|
12
12
|
* because it is the **observable contract** the page-manifest
|
|
13
|
-
* registration, label-pack validator, and
|
|
13
|
+
* registration, the label-pack validator, `renderPageStructuredData` and the
|
|
14
|
+
* structured-data audit depend on.
|
|
14
15
|
*
|
|
15
16
|
* **Shape rationale** (each field carries semantic weight — see
|
|
16
17
|
* `design-philosophy.mdc`):
|
|
@@ -20,7 +21,8 @@ import { WebsiteSectionCategorySchema } from './website-section-category.shared.
|
|
|
20
21
|
* (a section can be reused with the same `sectionRef` across pages
|
|
21
22
|
* when its label-key root is intentionally shared).
|
|
22
23
|
* - `category` — see `WebsiteSectionCategory` JSDoc; tags the section
|
|
23
|
-
* for
|
|
24
|
+
* for skill-mapping and analytics labelling. It does NOT select the
|
|
25
|
+
* section's structured data.
|
|
24
26
|
* - `expectedLabelKeys` — the **fully-composed** label keys the section
|
|
25
27
|
* commits to consuming. The plan originally proposed leaf-relative
|
|
26
28
|
* keys (`'headline'`) resolved at runtime against the
|
|
@@ -33,9 +35,13 @@ import { WebsiteSectionCategorySchema } from './website-section-category.shared.
|
|
|
33
35
|
* graph — `wildo website audit` can list a page's required keys
|
|
34
36
|
* without booting React.
|
|
35
37
|
* 3. The leaf-relative ergonomics (`useWebsiteLabel('headline')`)
|
|
36
|
-
* survive
|
|
37
|
-
*
|
|
38
|
-
* `expectedLabelKeys
|
|
38
|
+
* survive — the hook composes the leaf with the page+section
|
|
39
|
+
* context and looks the result up in the active pack. It does
|
|
40
|
+
* NOT check the key against `expectedLabelKeys`: a leaf the
|
|
41
|
+
* section never declared renders its key (with a console warning)
|
|
42
|
+
* and is caught by nothing until a reviewer reads the page. The
|
|
43
|
+
* build and `wildo website audit` check the pack against the
|
|
44
|
+
* DECLARED keys, so declare every slot the component reads.
|
|
39
45
|
* - `Component` — the React component that renders the section. Typed
|
|
40
46
|
* as `ComponentType<unknown>` here because schema-layer cannot lock
|
|
41
47
|
* the component's prop shape (props vary per section). The factory
|
|
@@ -45,8 +51,9 @@ import { WebsiteSectionCategorySchema } from './website-section-category.shared.
|
|
|
45
51
|
* - `getStructuredData?` — optional pure function the SEO/AEO/GEO
|
|
46
52
|
* builder library invokes at static-build time to materialize JSON-LD
|
|
47
53
|
* for the section. The return type is intentionally `unknown` here;
|
|
48
|
-
*
|
|
49
|
-
* `
|
|
54
|
+
* nothing narrows it by `category` — the section returns whatever
|
|
55
|
+
* vocabulary it emits, and `wildo website audit` reconciles that
|
|
56
|
+
* against the application's Schema.org catalog.
|
|
50
57
|
*
|
|
51
58
|
* **Why not `z.custom<ComponentType>(…)`**: a `z.custom` predicate
|
|
52
59
|
* cannot meaningfully validate "is a React component" beyond
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"website-section-definition.shared.schemas.js","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"website-section-definition.shared.schemas.js","sourceRoot":"","sources":["../../../../src/schemas/sections/website-section-definition.shared.schemas.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAChF,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AAEjF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,MAAM,+BAA+B,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,UAAU,EAAE,uBAAuB;IACnC,QAAQ,EAAE,4BAA4B;IACtC,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,QAAQ,EAAE;IAC5D,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE;IACtB,iBAAiB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;CAC1C,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,8BAA8B,GACzC,+BAA+B,CAAC;AA+ClC,qEAAqE","sourcesContent":["import type { ComponentType } from 'react';\nimport { z } from 'zod';\n\nimport { WebsiteLabelKeySchema } from '../label-keys/website-label-key.schemas';\nimport { WebsiteSectionRefSchema } from '../refs/section-ref.schemas';\nimport type { WebsiteStructuredDataReturn } from '../structured-data/website-structured-data.shared.schemas';\nimport { WebsiteSectionCategorySchema } from './website-section-category.shared';\n\n/**\n * @wildo_source:part:start saas.website.sections.definition-schema facet:layer:shared facet:family:website\n *\n * `WebsiteSectionDefinition` — the runtime contract returned by the\n * `defineWebsiteSection({ … })` factory (authored as a separate\n * `core/define-website-section.tsx` module in Phase 2 because it carries\n * React types in its inputs). The shape is declared in `schemas/`\n * because it is the **observable contract** the page-manifest\n * registration, the label-pack validator, `renderPageStructuredData` and the\n * structured-data audit depend on.\n *\n * **Shape rationale** (each field carries semantic weight — see\n * `design-philosophy.mdc`):\n *\n * - `sectionRef` — the section's identity inside its hosting page.\n * Uniqueness is enforced **per page** at manifest registration time\n * (a section can be reused with the same `sectionRef` across pages\n * when its label-key root is intentionally shared).\n * - `category` — see `WebsiteSectionCategory` JSDoc; tags the section\n * for skill-mapping and analytics labelling. It does NOT select the\n * section's structured data.\n * - `expectedLabelKeys` — the **fully-composed** label keys the section\n * commits to consuming. The plan originally proposed leaf-relative\n * keys (`'headline'`) resolved at runtime against the\n * page+section context, but storing the fully-composed key on the\n * definition has three concrete benefits:\n * 1. The label-pack validator (`validateLabelPackAgainstManifest`)\n * runs as a pure function with no need to walk the JSX tree at\n * build time to discover the leaf-to-key resolution.\n * 2. The contract is auditable from outside the React render\n * graph — `wildo website audit` can list a page's required keys\n * without booting React.\n * 3. The leaf-relative ergonomics (`useWebsiteLabel('headline')`)\n * survive — the hook composes the leaf with the page+section\n * context and looks the result up in the active pack. It does\n * NOT check the key against `expectedLabelKeys`: a leaf the\n * section never declared renders its key (with a console warning)\n * and is caught by nothing until a reviewer reads the page. The\n * build and `wildo website audit` check the pack against the\n * DECLARED keys, so declare every slot the component reads.\n * - `Component` — the React component that renders the section. Typed\n * as `ComponentType<unknown>` here because schema-layer cannot lock\n * the component's prop shape (props vary per section). The factory\n * function (`defineWebsiteSection<TProps>(…)`) preserves the precise\n * prop type at the consumer call site via generics; this schema only\n * guards the runtime shape of \"is callable as a React component\".\n * - `getStructuredData?` — optional pure function the SEO/AEO/GEO\n * builder library invokes at static-build time to materialize JSON-LD\n * for the section. The return type is intentionally `unknown` here;\n * nothing narrows it by `category` — the section returns whatever\n * vocabulary it emits, and `wildo website audit` reconciles that\n * against the application's Schema.org catalog.\n *\n * **Why not `z.custom<ComponentType>(…)`**: a `z.custom` predicate\n * cannot meaningfully validate \"is a React component\" beyond\n * `typeof === 'function'`, and the false-positive cost (any function\n * passes) is too high for the value (none — the consumer either\n * imports a real component or fails at JSX render time with a clearer\n * error). Using `z.unknown()` + a TS-side type cast keeps the runtime\n * cost zero and the type-safety contract entirely on the TS side\n * (where it actually has teeth via the factory's generic).\n */\nconst _WebsiteSectionDefinitionSchema = z.object({\n sectionRef: WebsiteSectionRefSchema,\n category: WebsiteSectionCategorySchema,\n expectedLabelKeys: z.array(WebsiteLabelKeySchema).readonly(),\n Component: z.unknown(),\n getStructuredData: z.unknown().optional(),\n});\n\nexport const WebsiteSectionDefinitionSchema: typeof _WebsiteSectionDefinitionSchema =\n _WebsiteSectionDefinitionSchema;\n\n/**\n * Runtime shape of `WebsiteSectionDefinition`. Note: `Component` and\n * `getStructuredData` are typed precisely on the factory return type\n * (`defineWebsiteSection<TProps>` in Phase 2); the schema-inferred\n * type is the lowest common denominator used at the persistence /\n * validation boundary only.\n *\n * **Why `ComponentType<any>` and NOT `ComponentType<unknown>`**:\n * React component props are CONTRAVARIANT in `ComponentType<T>` —\n * a narrower `ComponentType<{ foo: string }>` is NOT assignable to\n * `ComponentType<unknown>` (because a caller could pass an `unknown`\n * the narrower component cannot accept). This made every consumer\n * who built sections via `defineWebsiteSection<TProps>(…)` unable\n * to feed the resulting `TypedWebsiteSectionDefinition<TProps>` to\n * helpers typed against the bare `WebsiteSectionDefinition` —\n * collectors, validators, and registries. Using `any` here is the\n * exact escape hatch the React typings themselves use for\n * heterogeneous component containers (`React.forwardRef`,\n * `React.lazy`'s internal component slot, etc.) and matches the\n * runtime contract: the framework NEVER instantiates these\n * components directly with arbitrary props — the consumer's JSX\n * supplies the right props at the call site.\n */\nexport interface WebsiteSectionDefinition extends z.infer<typeof _WebsiteSectionDefinitionSchema> {\n Component: ComponentType<any>;\n /**\n * Optional pure function the SEO/AEO/GEO renderer invokes at\n * static-build time to materialize JSON-LD for the section.\n *\n * **Why typed as `WebsiteStructuredDataReturn` (not `unknown`)** —\n * a typed return shape gives section authors autocomplete on\n * `@type` / `@id` / `@context` (the framework-owned fields) while\n * keeping all other fields free-form (Schema.org has hundreds of\n * optional fields per entity; locking them in TS would be both\n * impractical and brittle as Schema.org evolves). The companion\n * catalog reconciliation surface catches missing-required and\n * deprecated-field-used at the SEMANTIC layer (warning-only per\n * D-AG4), so the runtime type stays loose.\n *\n * Returns `null`, `undefined`, or `[]` when the section has no\n * structured-data contribution for the current state — the\n * renderer short-circuits all three.\n */\n getStructuredData?: () => WebsiteStructuredDataReturn;\n}\n/** @wildo_source:part:end saas.website.sections.definition-schema */\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type PublicPricingCatalog } from '@wildo-ai/saas-models/public-runtime';
|
|
2
|
+
import type { WebsiteStructuredDataPayload } from './website-structured-data.shared.schemas';
|
|
3
|
+
/**
|
|
4
|
+
* @wildo_source:part:start saas.website.pricing-offer-structured-data facet:layer:core facet:family:website facet:audience:app-developer
|
|
5
|
+
*
|
|
6
|
+
* `buildWebsitePricingOfferStructuredData` — the `AggregateOffer` a pricing page publishes, DERIVED from
|
|
7
|
+
* the application's public pricing catalogue (#1835) instead of typed by hand beside it.
|
|
8
|
+
*
|
|
9
|
+
* ## What it publishes, and what it refuses to
|
|
10
|
+
*
|
|
11
|
+
* A search engine shows an offer's `price` as the price of the offer, so an offer is published only
|
|
12
|
+
* when that is TRUE:
|
|
13
|
+
*
|
|
14
|
+
* - the plan is on sale (`availability: AVAILABLE`) — an upcoming plan cannot be bought;
|
|
15
|
+
* - its price is `LISTED` — a starting price is a floor, and an on-request plan has no price;
|
|
16
|
+
* - its published price states the whole offer for its unit: a flat or per-unit amount, never an
|
|
17
|
+
* indicative amount of a tiered, graduated or package price.
|
|
18
|
+
*
|
|
19
|
+
* Each published offer uses the plan's DEFAULT price (the one the catalogue marks) and carries a
|
|
20
|
+
* `UnitPriceSpecification` saying what the number is FOR: its recurrence as `billingDuration` (ISO 8601
|
|
21
|
+
* `P1M` / `P1Y`) and, for a per-unit price, a `referenceQuantity` of one of the page's unit (`member`)
|
|
22
|
+
* — so "$4 per member per month" is never read as "$4".
|
|
23
|
+
*
|
|
24
|
+
* One `AggregateOffer` per (currency, amount basis, recurrence): a range is only meaningful over prices
|
|
25
|
+
* that measure the same thing, so a flat price and a per-member price, or a monthly and a yearly one, are
|
|
26
|
+
* never folded into one `lowPrice` / `highPrice`. EVERY group's `@id` is `aggregateOfferIdPrefix` suffixed with
|
|
27
|
+
* what distinguishes it (`-per-unit-1-month-usd`), so an aggregate's identity depends only on what it
|
|
28
|
+
* aggregates: adding, removing or reordering a plan in ANOTHER group never moves it. Returns `[]` when
|
|
29
|
+
* nothing may be published — a page with only on-request plans publishes no price at all, which is the
|
|
30
|
+
* true statement.
|
|
31
|
+
*/
|
|
32
|
+
export interface BuildWebsitePricingOfferStructuredDataInput {
|
|
33
|
+
readonly catalog: PublicPricingCatalog;
|
|
34
|
+
/** The `@id` of the entity the offers price — typically the page's `SoftwareApplication`. */
|
|
35
|
+
readonly itemOfferedId: string;
|
|
36
|
+
/**
|
|
37
|
+
* The prefix of every `AggregateOffer` `@id` (`https://…/pricing#plans`); each group appends what it
|
|
38
|
+
* aggregates, e.g. `#plans-flat-1-month-usd`.
|
|
39
|
+
*/
|
|
40
|
+
readonly aggregateOfferIdPrefix: string;
|
|
41
|
+
/** The URL of the page the offers are made on; each offer's URL is this plus `#plan-<key>`. */
|
|
42
|
+
readonly url: string;
|
|
43
|
+
/**
|
|
44
|
+
* The plan's name in the page's locale. Names are text a person reads, so they come from the page's
|
|
45
|
+
* label pack; a plan the callback does not name is published without one rather than with its key.
|
|
46
|
+
*/
|
|
47
|
+
readonly planName?: (planKey: string) => string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* What ONE unit of a per-unit price is, in the page's locale (`member`, `seat`) — published as the
|
|
50
|
+
* offer's `referenceQuantity.unitText`. Required when any published offer is per-unit: omitting it
|
|
51
|
+
* would state a per-member price as a whole price, so the builder refuses instead.
|
|
52
|
+
*/
|
|
53
|
+
readonly perUnitName?: string;
|
|
54
|
+
}
|
|
55
|
+
export declare function buildWebsitePricingOfferStructuredData(input: BuildWebsitePricingOfferStructuredDataInput): readonly WebsiteStructuredDataPayload[];
|
|
56
|
+
/** @wildo_source:part:end saas.website.pricing-offer-structured-data */
|
|
57
|
+
//# sourceMappingURL=pricing-offer-structured-data.shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing-offer-structured-data.shared.d.ts","sourceRoot":"","sources":["../../../../src/schemas/structured-data/pricing-offer-structured-data.shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,oBAAoB,EAG1B,MAAM,sCAAsC,CAAC;AAE9C,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,0CAA0C,CAAC;AAE7F;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,2CAA2C;IAC1D,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;IACvC,6FAA6F;IAC7F,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,+FAA+F;IAC/F,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC5D;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAgB,sCAAsC,CAAC,KAAK,EAAE,2CAA2C,GAAG,SAAS,4BAA4B,EAAE,CAmDlJ;AA0BD,wEAAwE"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ProductAvailability, ProductPriceDisclosure, PublicPricingAmountBasis, PublicPricingRecurrenceUnit, publicPricingAmountDecimal, } from '@wildo-ai/saas-models/public-runtime';
|
|
2
|
+
export function buildWebsitePricingOfferStructuredData(input) {
|
|
3
|
+
const published = input.catalog.plans.flatMap((plan) => {
|
|
4
|
+
const price = publishablePrice(plan);
|
|
5
|
+
return price === undefined ? [] : [{ plan, price }];
|
|
6
|
+
});
|
|
7
|
+
if (published.some(({ price }) => price.amountBasis === PublicPricingAmountBasis.PER_UNIT) && !input.perUnitName) {
|
|
8
|
+
throw new Error('[buildWebsitePricingOfferStructuredData] a published plan is priced per unit, so the page must name the unit '
|
|
9
|
+
+ '(`perUnitName`, e.g. "member"); without it the offer would state a per-unit price as the whole price.');
|
|
10
|
+
}
|
|
11
|
+
const groupKeyOf = (price) => `${price.currency}|${price.amountBasis}|${price.recurrence.unit}|${price.recurrence.count}`;
|
|
12
|
+
const groupKeys = [...new Set(published.map(({ price }) => groupKeyOf(price)))];
|
|
13
|
+
return groupKeys.map((groupKey) => {
|
|
14
|
+
const offers = published.filter(({ price }) => groupKeyOf(price) === groupKey);
|
|
15
|
+
const sample = offers[0].price;
|
|
16
|
+
const amounts = offers.map(({ price }) => publicPricingAmountDecimal(price));
|
|
17
|
+
const numeric = amounts.map(Number);
|
|
18
|
+
return {
|
|
19
|
+
'@type': 'AggregateOffer',
|
|
20
|
+
'@id': `${input.aggregateOfferIdPrefix}-${aggregateSuffix(sample)}`,
|
|
21
|
+
itemOffered: { '@id': input.itemOfferedId },
|
|
22
|
+
priceCurrency: sample.currency,
|
|
23
|
+
lowPrice: amounts[numeric.indexOf(Math.min(...numeric))],
|
|
24
|
+
highPrice: amounts[numeric.indexOf(Math.max(...numeric))],
|
|
25
|
+
offerCount: offers.length,
|
|
26
|
+
url: input.url,
|
|
27
|
+
offers: offers.map(({ plan, price }) => {
|
|
28
|
+
const name = input.planName?.(plan.key);
|
|
29
|
+
const amount = publicPricingAmountDecimal(price);
|
|
30
|
+
return {
|
|
31
|
+
'@type': 'Offer',
|
|
32
|
+
...(name === undefined ? {} : { name }),
|
|
33
|
+
price: amount,
|
|
34
|
+
priceCurrency: price.currency,
|
|
35
|
+
availability: 'https://schema.org/InStock',
|
|
36
|
+
url: `${input.url}#plan-${plan.key}`,
|
|
37
|
+
priceSpecification: {
|
|
38
|
+
'@type': 'UnitPriceSpecification',
|
|
39
|
+
price: amount,
|
|
40
|
+
priceCurrency: price.currency,
|
|
41
|
+
...(price.recurrence.unit === PublicPricingRecurrenceUnit.ONE_TIME ? {} : { billingDuration: isoDurationOf(price) }),
|
|
42
|
+
...(price.amountBasis === PublicPricingAmountBasis.PER_UNIT
|
|
43
|
+
? { referenceQuantity: { '@type': 'QuantitativeValue', value: 1, unitText: input.perUnitName } }
|
|
44
|
+
: {}),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** What distinguishes an aggregate's `@id`: its basis, its recurrence and its currency — exactly its group key. */
|
|
52
|
+
function aggregateSuffix(price) {
|
|
53
|
+
return `${price.amountBasis}-${price.recurrence.count}-${price.recurrence.unit}-${price.currency}`.toLowerCase().replace(/_/gu, '-');
|
|
54
|
+
}
|
|
55
|
+
/** The plan's default price, when publishing it as the offer's price would be true. */
|
|
56
|
+
function publishablePrice(plan) {
|
|
57
|
+
if (plan.availability !== ProductAvailability.AVAILABLE)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (plan.priceDisclosure !== ProductPriceDisclosure.LISTED)
|
|
60
|
+
return undefined;
|
|
61
|
+
const price = plan.prices.find((candidate) => candidate.isDefault) ?? plan.prices[0];
|
|
62
|
+
if (price === undefined || price.amountBasis === PublicPricingAmountBasis.VARIABLE)
|
|
63
|
+
return undefined;
|
|
64
|
+
return price;
|
|
65
|
+
}
|
|
66
|
+
/** Total over the recurring units: a member added to the vocabulary is a compile error here until it is mapped. */
|
|
67
|
+
const ISO_DURATION_DESIGNATOR = {
|
|
68
|
+
[PublicPricingRecurrenceUnit.MONTH]: 'M',
|
|
69
|
+
[PublicPricingRecurrenceUnit.YEAR]: 'Y',
|
|
70
|
+
};
|
|
71
|
+
function isoDurationOf(price) {
|
|
72
|
+
const unit = price.recurrence.unit;
|
|
73
|
+
return `P${price.recurrence.count}${ISO_DURATION_DESIGNATOR[unit]}`;
|
|
74
|
+
}
|
|
75
|
+
/** @wildo_source:part:end saas.website.pricing-offer-structured-data */
|
|
76
|
+
//# sourceMappingURL=pricing-offer-structured-data.shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing-offer-structured-data.shared.js","sourceRoot":"","sources":["../../../../src/schemas/structured-data/pricing-offer-structured-data.shared.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ProductAvailability,\n ProductPriceDisclosure,\n PublicPricingAmountBasis,\n PublicPricingRecurrenceUnit,\n publicPricingAmountDecimal,\n type PublicPricingCatalog,\n type PublicPricingPlan,\n type PublicPricingPrice,\n} from '@wildo-ai/saas-models/public-runtime';\n\nimport type { WebsiteStructuredDataPayload } from './website-structured-data.shared.schemas';\n\n/**\n * @wildo_source:part:start saas.website.pricing-offer-structured-data facet:layer:core facet:family:website facet:audience:app-developer\n *\n * `buildWebsitePricingOfferStructuredData` — the `AggregateOffer` a pricing page publishes, DERIVED from\n * the application's public pricing catalogue (#1835) instead of typed by hand beside it.\n *\n * ## What it publishes, and what it refuses to\n *\n * A search engine shows an offer's `price` as the price of the offer, so an offer is published only\n * when that is TRUE:\n *\n * - the plan is on sale (`availability: AVAILABLE`) — an upcoming plan cannot be bought;\n * - its price is `LISTED` — a starting price is a floor, and an on-request plan has no price;\n * - its published price states the whole offer for its unit: a flat or per-unit amount, never an\n * indicative amount of a tiered, graduated or package price.\n *\n * Each published offer uses the plan's DEFAULT price (the one the catalogue marks) and carries a\n * `UnitPriceSpecification` saying what the number is FOR: its recurrence as `billingDuration` (ISO 8601\n * `P1M` / `P1Y`) and, for a per-unit price, a `referenceQuantity` of one of the page's unit (`member`)\n * — so \"$4 per member per month\" is never read as \"$4\".\n *\n * One `AggregateOffer` per (currency, amount basis, recurrence): a range is only meaningful over prices\n * that measure the same thing, so a flat price and a per-member price, or a monthly and a yearly one, are\n * never folded into one `lowPrice` / `highPrice`. EVERY group's `@id` is `aggregateOfferIdPrefix` suffixed with\n * what distinguishes it (`-per-unit-1-month-usd`), so an aggregate's identity depends only on what it\n * aggregates: adding, removing or reordering a plan in ANOTHER group never moves it. Returns `[]` when\n * nothing may be published — a page with only on-request plans publishes no price at all, which is the\n * true statement.\n */\nexport interface BuildWebsitePricingOfferStructuredDataInput {\n readonly catalog: PublicPricingCatalog;\n /** The `@id` of the entity the offers price — typically the page's `SoftwareApplication`. */\n readonly itemOfferedId: string;\n /**\n * The prefix of every `AggregateOffer` `@id` (`https://…/pricing#plans`); each group appends what it\n * aggregates, e.g. `#plans-flat-1-month-usd`.\n */\n readonly aggregateOfferIdPrefix: string;\n /** The URL of the page the offers are made on; each offer's URL is this plus `#plan-<key>`. */\n readonly url: string;\n /**\n * The plan's name in the page's locale. Names are text a person reads, so they come from the page's\n * label pack; a plan the callback does not name is published without one rather than with its key.\n */\n readonly planName?: (planKey: string) => string | undefined;\n /**\n * What ONE unit of a per-unit price is, in the page's locale (`member`, `seat`) — published as the\n * offer's `referenceQuantity.unitText`. Required when any published offer is per-unit: omitting it\n * would state a per-member price as a whole price, so the builder refuses instead.\n */\n readonly perUnitName?: string;\n}\n\nexport function buildWebsitePricingOfferStructuredData(input: BuildWebsitePricingOfferStructuredDataInput): readonly WebsiteStructuredDataPayload[] {\n const published = input.catalog.plans.flatMap((plan) => {\n const price = publishablePrice(plan);\n return price === undefined ? [] : [{ plan, price }];\n });\n if (published.some(({ price }) => price.amountBasis === PublicPricingAmountBasis.PER_UNIT) && !input.perUnitName) {\n throw new Error(\n '[buildWebsitePricingOfferStructuredData] a published plan is priced per unit, so the page must name the unit '\n + '(`perUnitName`, e.g. \"member\"); without it the offer would state a per-unit price as the whole price.',\n );\n }\n const groupKeyOf = (price: PublicPricingPrice): string =>\n `${price.currency}|${price.amountBasis}|${price.recurrence.unit}|${price.recurrence.count}`;\n const groupKeys = [...new Set(published.map(({ price }) => groupKeyOf(price)))];\n return groupKeys.map((groupKey) => {\n const offers = published.filter(({ price }) => groupKeyOf(price) === groupKey);\n const sample = offers[0]!.price;\n const amounts = offers.map(({ price }) => publicPricingAmountDecimal(price));\n const numeric = amounts.map(Number);\n return {\n '@type': 'AggregateOffer',\n '@id': `${input.aggregateOfferIdPrefix}-${aggregateSuffix(sample)}`,\n itemOffered: { '@id': input.itemOfferedId },\n priceCurrency: sample.currency,\n lowPrice: amounts[numeric.indexOf(Math.min(...numeric))],\n highPrice: amounts[numeric.indexOf(Math.max(...numeric))],\n offerCount: offers.length,\n url: input.url,\n offers: offers.map(({ plan, price }) => {\n const name = input.planName?.(plan.key);\n const amount = publicPricingAmountDecimal(price);\n return {\n '@type': 'Offer',\n ...(name === undefined ? {} : { name }),\n price: amount,\n priceCurrency: price.currency,\n availability: 'https://schema.org/InStock',\n url: `${input.url}#plan-${plan.key}`,\n priceSpecification: {\n '@type': 'UnitPriceSpecification',\n price: amount,\n priceCurrency: price.currency,\n ...(price.recurrence.unit === PublicPricingRecurrenceUnit.ONE_TIME ? {} : { billingDuration: isoDurationOf(price) }),\n ...(price.amountBasis === PublicPricingAmountBasis.PER_UNIT\n ? { referenceQuantity: { '@type': 'QuantitativeValue', value: 1, unitText: input.perUnitName } }\n : {}),\n },\n };\n }),\n };\n });\n}\n\n/** What distinguishes an aggregate's `@id`: its basis, its recurrence and its currency — exactly its group key. */\nfunction aggregateSuffix(price: PublicPricingPrice): string {\n return `${price.amountBasis}-${price.recurrence.count}-${price.recurrence.unit}-${price.currency}`.toLowerCase().replace(/_/gu, '-');\n}\n\n/** The plan's default price, when publishing it as the offer's price would be true. */\nfunction publishablePrice(plan: PublicPricingPlan): PublicPricingPrice | undefined {\n if (plan.availability !== ProductAvailability.AVAILABLE) return undefined;\n if (plan.priceDisclosure !== ProductPriceDisclosure.LISTED) return undefined;\n const price = plan.prices.find((candidate) => candidate.isDefault) ?? plan.prices[0];\n if (price === undefined || price.amountBasis === PublicPricingAmountBasis.VARIABLE) return undefined;\n return price;\n}\n\n/** Total over the recurring units: a member added to the vocabulary is a compile error here until it is mapped. */\nconst ISO_DURATION_DESIGNATOR: Record<Exclude<PublicPricingRecurrenceUnit, PublicPricingRecurrenceUnit.ONE_TIME>, string> = {\n [PublicPricingRecurrenceUnit.MONTH]: 'M',\n [PublicPricingRecurrenceUnit.YEAR]: 'Y',\n};\n\nfunction isoDurationOf(price: PublicPricingPrice): string {\n const unit = price.recurrence.unit as Exclude<PublicPricingRecurrenceUnit, PublicPricingRecurrenceUnit.ONE_TIME>;\n return `P${price.recurrence.count}${ISO_DURATION_DESIGNATOR[unit]}`;\n}\n/** @wildo_source:part:end saas.website.pricing-offer-structured-data */\n"]}
|
|
@@ -80,6 +80,27 @@ export interface StructuredDataCrossEntityReferenceDriftFinding {
|
|
|
80
80
|
message: string;
|
|
81
81
|
}
|
|
82
82
|
export type StructuredDataReconciliationFinding = StructuredDataCatalogOverrideFinding | StructuredDataCatalogMissFinding | StructuredDataSpecViolationFinding | StructuredDataDeprecatedFieldFinding | StructuredDataCrossEntityReferenceDriftFinding;
|
|
83
|
+
/**
|
|
84
|
+
* An OPTIONAL field a payload leaves out, reported because its catalog entry opted in with
|
|
85
|
+
* `recommendOptionals: true` — the entry's statement that for this type the optional fields are
|
|
86
|
+
* where the richer search and answer-engine results come from.
|
|
87
|
+
*
|
|
88
|
+
* Deliberately NOT a finding. A finding says the payload is wrong (a missing required field, an
|
|
89
|
+
* unknown type, a deprecated field) and `--strict` escalates every finding to a failure. A payload
|
|
90
|
+
* without its optional fields is valid; reporting it through the same channel would fail strict CI for
|
|
91
|
+
* following the vocabulary. So recommendations travel beside the findings, are shown as information,
|
|
92
|
+
* and never change an audit's outcome.
|
|
93
|
+
*/
|
|
94
|
+
export interface StructuredDataOptionalFieldRecommendation {
|
|
95
|
+
pageRef: string;
|
|
96
|
+
routePath: string;
|
|
97
|
+
sectionRef: string;
|
|
98
|
+
entityType: string;
|
|
99
|
+
entityId?: string;
|
|
100
|
+
/** The entry's optional fields this payload does not carry, in catalog order. */
|
|
101
|
+
missingOptionalFields: readonly string[];
|
|
102
|
+
message: string;
|
|
103
|
+
}
|
|
83
104
|
export interface StructuredDataReconciliationPageResult {
|
|
84
105
|
pageRef: string;
|
|
85
106
|
routePath: string;
|
|
@@ -88,6 +109,8 @@ export interface StructuredDataReconciliationPageResult {
|
|
|
88
109
|
}
|
|
89
110
|
export interface StructuredDataReconciliationResult {
|
|
90
111
|
findings: StructuredDataReconciliationFinding[];
|
|
112
|
+
/** Optional-field recommendations from entries that opted in — information, never a finding. */
|
|
113
|
+
recommendations: StructuredDataOptionalFieldRecommendation[];
|
|
91
114
|
pages: StructuredDataReconciliationPageResult[];
|
|
92
115
|
}
|
|
93
116
|
export interface ReconcileStructuredDataOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"structured-data-reconciliation.shared.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"structured-data-reconciliation.shared.d.ts","sourceRoot":"","sources":["../../../../src/schemas/structured-data/structured-data-reconciliation.shared.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export function reconcileStructuredDataAgainstCatalog(options) {
|
|
2
2
|
const findings = [];
|
|
3
|
+
const recommendations = [];
|
|
3
4
|
const pages = [];
|
|
4
5
|
const sectionsByRef = new Map(options.sections.map((section) => [section.sectionRef, section]));
|
|
5
6
|
const pagePayloadsByPageRef = new Map();
|
|
@@ -138,7 +139,7 @@ export function reconcileStructuredDataAgainstCatalog(options) {
|
|
|
138
139
|
}
|
|
139
140
|
const routeFindingStart = findings.length;
|
|
140
141
|
for (const payloadContext of payloadContexts) {
|
|
141
|
-
validatePayloadAgainstCatalog(payloadContext, catalogByType, declaredEntityIds, findings);
|
|
142
|
+
validatePayloadAgainstCatalog(payloadContext, catalogByType, declaredEntityIds, findings, recommendations);
|
|
142
143
|
}
|
|
143
144
|
pages.push({
|
|
144
145
|
pageRef: page.ref,
|
|
@@ -164,6 +165,7 @@ export function reconcileStructuredDataAgainstCatalog(options) {
|
|
|
164
165
|
}
|
|
165
166
|
return {
|
|
166
167
|
findings,
|
|
168
|
+
recommendations,
|
|
167
169
|
pages,
|
|
168
170
|
};
|
|
169
171
|
}
|
|
@@ -215,7 +217,7 @@ function collectNormalizedPayloadContexts(input) {
|
|
|
215
217
|
});
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
|
-
function validatePayloadAgainstCatalog(payloadContext, catalogByType, declaredEntityIds, findings) {
|
|
220
|
+
function validatePayloadAgainstCatalog(payloadContext, catalogByType, declaredEntityIds, findings, recommendations) {
|
|
219
221
|
const { payload, pageRef, routePath, sectionRef } = payloadContext;
|
|
220
222
|
const entityType = payload['@type'];
|
|
221
223
|
const entityId = typeof payload['@id'] === 'string' ? payload['@id'] : undefined;
|
|
@@ -249,6 +251,22 @@ function validatePayloadAgainstCatalog(payloadContext, catalogByType, declaredEn
|
|
|
249
251
|
});
|
|
250
252
|
}
|
|
251
253
|
}
|
|
254
|
+
if (catalogEntry.recommendOptionals === true) {
|
|
255
|
+
const missingOptionalFields = (catalogEntry.optionalFields ?? []).filter((field) => !hasPresentFieldValue(payload, field));
|
|
256
|
+
if (missingOptionalFields.length > 0) {
|
|
257
|
+
recommendations.push({
|
|
258
|
+
pageRef,
|
|
259
|
+
routePath,
|
|
260
|
+
sectionRef,
|
|
261
|
+
entityType,
|
|
262
|
+
entityId,
|
|
263
|
+
missingOptionalFields,
|
|
264
|
+
message: `Structured-data payload @type="${entityType}" emitted by section "${sectionRef}" on page "${pageRef}" ` +
|
|
265
|
+
`could carry ${missingOptionalFields.map((field) => `"${field}"`).join(', ')} — optional, and the catalog ` +
|
|
266
|
+
'recommends them for richer search and answer-engine results.',
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
252
270
|
for (const deprecatedField of catalogEntry.deprecatedFields ?? []) {
|
|
253
271
|
if (!hasPresentFieldValue(payload, deprecatedField.field)) {
|
|
254
272
|
continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"structured-data-reconciliation.shared.js","sourceRoot":"","sources":["../../../../../src/schemas/structured-data/structured-data-reconciliation.shared.ts"],"names":[],"mappings":"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","sourcesContent":["export interface StructuredDataDeprecatedField {\n field: string;\n replacedBy: string | null;\n deprecatedSince: string;\n}\n\nexport interface StructuredDataCatalogEntry {\n '@type': string;\n requiredFields: readonly string[];\n optionalFields?: readonly string[];\n deprecatedFields?: readonly StructuredDataDeprecatedField[];\n recommendOptionals?: boolean;\n}\n\nexport interface StructuredDataPayload {\n '@type': string;\n '@id'?: string;\n [key: string]: unknown;\n}\n\nexport interface StructuredDataSectionDefinitionLike {\n sectionRef: string;\n getStructuredData?: () => unknown;\n}\n\nexport interface StructuredDataPageManifestLike {\n ref: string;\n routePath: string;\n sectionRefs: readonly string[];\n}\n\nexport interface StructuredDataPagePayloadLike {\n pageRef: string;\n routePath?: string;\n sourceRef?: string;\n payloads: unknown;\n}\n\nexport type StructuredDataSpecViolationCode =\n | 'invalid-payload-shape'\n | 'missing-entity-type'\n | 'missing-required-field'\n | 'missing-section-definition'\n | 'orphan-page-payload';\n\nexport interface StructuredDataCatalogOverrideFinding {\n kind: 'catalog-override';\n entityType: string;\n count: number;\n message: string;\n}\n\nexport interface StructuredDataCatalogMissFinding {\n kind: 'catalog-miss';\n pageRef: string;\n routePath: string;\n sectionRef: string;\n entityType: string;\n entityId?: string;\n message: string;\n}\n\nexport interface StructuredDataSpecViolationFinding {\n kind: 'spec-violation';\n code: StructuredDataSpecViolationCode;\n pageRef: string;\n routePath: string;\n sectionRef: string;\n entityType?: string;\n entityId?: string;\n field?: string;\n message: string;\n}\n\nexport interface StructuredDataDeprecatedFieldFinding {\n kind: 'deprecated-field';\n pageRef: string;\n routePath: string;\n sectionRef: string;\n entityType: string;\n entityId?: string;\n field: string;\n replacedBy: string | null;\n deprecatedSince: string;\n message: string;\n}\n\nexport interface StructuredDataCrossEntityReferenceDriftFinding {\n kind: 'cross-entity-reference-drift';\n pageRef: string;\n routePath: string;\n sectionRef: string;\n entityType: string;\n entityId?: string;\n missingReferenceId: string;\n message: string;\n}\n\nexport type StructuredDataReconciliationFinding =\n | StructuredDataCatalogOverrideFinding\n | StructuredDataCatalogMissFinding\n | StructuredDataSpecViolationFinding\n | StructuredDataDeprecatedFieldFinding\n | StructuredDataCrossEntityReferenceDriftFinding;\n\nexport interface StructuredDataReconciliationPageResult {\n pageRef: string;\n routePath: string;\n payloadCount: number;\n findingCount: number;\n}\n\nexport interface StructuredDataReconciliationResult {\n findings: StructuredDataReconciliationFinding[];\n pages: StructuredDataReconciliationPageResult[];\n}\n\nexport interface ReconcileStructuredDataOptions {\n catalog: readonly StructuredDataCatalogEntry[];\n pages: readonly StructuredDataPageManifestLike[];\n sections: readonly StructuredDataSectionDefinitionLike[];\n pagePayloads?: readonly StructuredDataPagePayloadLike[];\n}\n\ninterface ReconciliationPayloadContext {\n pageRef: string;\n routePath: string;\n sectionRef: string;\n payload: StructuredDataPayload;\n}\n\nexport function reconcileStructuredDataAgainstCatalog(\n options: ReconcileStructuredDataOptions,\n): StructuredDataReconciliationResult {\n const findings: StructuredDataReconciliationFinding[] = [];\n const pages: StructuredDataReconciliationPageResult[] = [];\n const sectionsByRef = new Map(\n options.sections.map((section) => [section.sectionRef, section] as const),\n );\n const pagePayloadsByPageRef = new Map<string, StructuredDataPagePayloadLike[]>();\n for (const pagePayload of options.pagePayloads ?? []) {\n const pagePayloads = pagePayloadsByPageRef.get(pagePayload.pageRef) ?? [];\n pagePayloads.push(pagePayload);\n pagePayloadsByPageRef.set(pagePayload.pageRef, pagePayloads);\n }\n const catalogByType = new Map<string, StructuredDataCatalogEntry>();\n const catalogCounts = new Map<string, number>();\n\n for (const entry of options.catalog) {\n catalogByType.set(entry['@type'], entry);\n catalogCounts.set(entry['@type'], (catalogCounts.get(entry['@type']) ?? 0) + 1);\n }\n\n for (const [entityType, count] of catalogCounts.entries()) {\n if (count > 1) {\n findings.push({\n kind: 'catalog-override',\n entityType,\n count,\n message:\n `Merged catalog contains ${count} entries for @type=\"${entityType}\". ` +\n 'Replacement semantics are last-wins; duplicate array entries indicate an unresolved override.',\n });\n }\n }\n\n for (const page of options.pages) {\n const pageFindingStart = findings.length;\n const sectionPayloadContexts: ReconciliationPayloadContext[] = [];\n const isTemplatedRoute = page.routePath.includes('[');\n\n for (const sectionRef of page.sectionRefs) {\n const section = sectionsByRef.get(sectionRef);\n if (!section) {\n findings.push({\n kind: 'spec-violation',\n code: 'missing-section-definition',\n pageRef: page.ref,\n routePath: page.routePath,\n sectionRef,\n message:\n `Page \"${page.ref}\" references sectionRef \"${sectionRef}\" but no matching section ` +\n 'definition was supplied to the structured-data reconciliation harness.',\n });\n continue;\n }\n\n if (!section.getStructuredData) {\n continue;\n }\n\n const rawStructuredData = section.getStructuredData();\n const normalizedPayloads = normalizeStructuredDataReturn(rawStructuredData);\n\n if (normalizedPayloads === null) {\n findings.push({\n kind: 'spec-violation',\n code: 'invalid-payload-shape',\n pageRef: page.ref,\n routePath: page.routePath,\n sectionRef,\n message:\n `Section \"${sectionRef}\" on page \"${page.ref}\" returned an invalid ` +\n '`getStructuredData()` shape. Expected object, array, null, or undefined.',\n });\n continue;\n }\n\n for (const candidate of normalizedPayloads) {\n if (!isPlainObject(candidate)) {\n findings.push({\n kind: 'spec-violation',\n code: 'invalid-payload-shape',\n pageRef: page.ref,\n routePath: page.routePath,\n sectionRef,\n message:\n `Section \"${sectionRef}\" on page \"${page.ref}\" returned a non-object structured-data payload.`,\n });\n continue;\n }\n\n if (typeof candidate['@type'] !== 'string' || candidate['@type'].trim().length === 0) {\n findings.push({\n kind: 'spec-violation',\n code: 'missing-entity-type',\n pageRef: page.ref,\n routePath: page.routePath,\n sectionRef,\n entityId: typeof candidate['@id'] === 'string' ? candidate['@id'] : undefined,\n message:\n `Section \"${sectionRef}\" on page \"${page.ref}\" returned a structured-data payload ` +\n 'without a non-empty string `@type`.',\n });\n continue;\n }\n\n sectionPayloadContexts.push({\n pageRef: page.ref,\n routePath: page.routePath,\n sectionRef,\n payload: candidate as StructuredDataPayload,\n });\n }\n }\n\n const contextsByRoutePath = new Map<string, ReconciliationPayloadContext[]>();\n const collectionFindingCountByRoutePath = new Map<string, number>();\n const pagePayloadsForPage = pagePayloadsByPageRef.get(page.ref) ?? [];\n pagePayloadsByPageRef.delete(page.ref);\n const shouldEmitManifestResult =\n (sectionPayloadContexts.length > 0 && (!isTemplatedRoute || pagePayloadsForPage.length === 0))\n || findings.length > pageFindingStart\n || pagePayloadsForPage.length === 0;\n if (shouldEmitManifestResult) {\n const manifestPayloadContexts =\n isTemplatedRoute && pagePayloadsForPage.length > 0\n ? []\n : [...sectionPayloadContexts];\n contextsByRoutePath.set(page.routePath, manifestPayloadContexts);\n collectionFindingCountByRoutePath.set(page.routePath, findings.length - pageFindingStart);\n }\n\n for (const pagePayload of pagePayloadsForPage) {\n const routePath = pagePayload.routePath ?? page.routePath;\n const payloadContexts = contextsByRoutePath.get(routePath) ?? [];\n if (\n payloadContexts.length === 0\n && isTemplatedRoute\n && sectionPayloadContexts.length > 0\n ) {\n payloadContexts.push(\n ...sectionPayloadContexts.map((context) => ({\n ...context,\n routePath,\n })),\n );\n }\n contextsByRoutePath.set(routePath, payloadContexts);\n const collectionFindingStart = findings.length;\n collectNormalizedPayloadContexts({\n rawStructuredData: pagePayload.payloads,\n pageRef: page.ref,\n routePath,\n sectionRef: pagePayload.sourceRef ?? `page:${page.ref}`,\n findings,\n into: payloadContexts,\n });\n collectionFindingCountByRoutePath.set(\n routePath,\n (collectionFindingCountByRoutePath.get(routePath) ?? 0)\n + findings.length - collectionFindingStart,\n );\n }\n\n for (const [routePath, payloadContexts] of contextsByRoutePath.entries()) {\n const declaredEntityIds = new Set<string>();\n for (const payloadContext of payloadContexts) {\n collectDeclaredEntityIds(payloadContext.payload, declaredEntityIds);\n }\n\n const routeFindingStart = findings.length;\n for (const payloadContext of payloadContexts) {\n validatePayloadAgainstCatalog(payloadContext, catalogByType, declaredEntityIds, findings);\n }\n\n pages.push({\n pageRef: page.ref,\n routePath,\n payloadCount: payloadContexts.length,\n findingCount:\n (collectionFindingCountByRoutePath.get(routePath) ?? 0)\n + findings.length - routeFindingStart,\n });\n }\n }\n\n for (const [pageRef, pagePayloads] of pagePayloadsByPageRef.entries()) {\n for (const pagePayload of pagePayloads) {\n findings.push({\n kind: 'spec-violation',\n code: 'orphan-page-payload',\n pageRef,\n routePath: pagePayload.routePath ?? '',\n sectionRef: pagePayload.sourceRef ?? `page:${pageRef}`,\n message:\n `Structured-data page payload \"${pagePayload.sourceRef ?? `page:${pageRef}`}\" ` +\n `references pageRef \"${pageRef}\" but no matching page manifest was supplied.`,\n });\n }\n }\n\n return {\n findings,\n pages,\n };\n}\n\nfunction collectNormalizedPayloadContexts(input: {\n rawStructuredData: unknown;\n pageRef: string;\n routePath: string;\n sectionRef: string;\n findings: StructuredDataReconciliationFinding[];\n into: ReconciliationPayloadContext[];\n}): void {\n const {\n rawStructuredData,\n pageRef,\n routePath,\n sectionRef,\n findings,\n into,\n } = input;\n const normalizedPayloads = normalizeStructuredDataReturn(rawStructuredData);\n\n if (normalizedPayloads === null) {\n findings.push({\n kind: 'spec-violation',\n code: 'invalid-payload-shape',\n pageRef,\n routePath,\n sectionRef,\n message:\n `Structured-data source \"${sectionRef}\" on page \"${pageRef}\" returned an invalid ` +\n 'structured-data shape. Expected object, array, null, or undefined.',\n });\n return;\n }\n\n for (const candidate of normalizedPayloads) {\n if (!isPlainObject(candidate)) {\n findings.push({\n kind: 'spec-violation',\n code: 'invalid-payload-shape',\n pageRef,\n routePath,\n sectionRef,\n message:\n `Structured-data source \"${sectionRef}\" on page \"${pageRef}\" returned a non-object structured-data payload.`,\n });\n continue;\n }\n\n if (typeof candidate['@type'] !== 'string' || candidate['@type'].trim().length === 0) {\n findings.push({\n kind: 'spec-violation',\n code: 'missing-entity-type',\n pageRef,\n routePath,\n sectionRef,\n entityId: typeof candidate['@id'] === 'string' ? candidate['@id'] : undefined,\n message:\n `Structured-data source \"${sectionRef}\" on page \"${pageRef}\" returned a structured-data payload ` +\n 'without a non-empty string `@type`.',\n });\n continue;\n }\n\n into.push({\n pageRef,\n routePath,\n sectionRef,\n payload: candidate as StructuredDataPayload,\n });\n }\n}\n\nfunction validatePayloadAgainstCatalog(\n payloadContext: ReconciliationPayloadContext,\n catalogByType: ReadonlyMap<string, StructuredDataCatalogEntry>,\n declaredEntityIds: ReadonlySet<string>,\n findings: StructuredDataReconciliationFinding[],\n): void {\n const { payload, pageRef, routePath, sectionRef } = payloadContext;\n const entityType = payload['@type'];\n const entityId = typeof payload['@id'] === 'string' ? payload['@id'] : undefined;\n const catalogEntry = catalogByType.get(entityType);\n\n if (!catalogEntry) {\n findings.push({\n kind: 'catalog-miss',\n pageRef,\n routePath,\n sectionRef,\n entityType,\n entityId,\n message:\n `Structured-data payload @type=\"${entityType}\" emitted by section \"${sectionRef}\" ` +\n `on page \"${pageRef}\" does not exist in the merged catalog.`,\n });\n return;\n }\n\n for (const requiredField of catalogEntry.requiredFields) {\n if (!hasPresentFieldValue(payload, requiredField)) {\n findings.push({\n kind: 'spec-violation',\n code: 'missing-required-field',\n pageRef,\n routePath,\n sectionRef,\n entityType,\n entityId,\n field: requiredField,\n message:\n `Structured-data payload @type=\"${entityType}\" emitted by section \"${sectionRef}\" ` +\n `on page \"${pageRef}\" is missing required field \"${requiredField}\".`,\n });\n }\n }\n\n for (const deprecatedField of catalogEntry.deprecatedFields ?? []) {\n if (!hasPresentFieldValue(payload, deprecatedField.field)) {\n continue;\n }\n\n findings.push({\n kind: 'deprecated-field',\n pageRef,\n routePath,\n sectionRef,\n entityType,\n entityId,\n field: deprecatedField.field,\n replacedBy: deprecatedField.replacedBy,\n deprecatedSince: deprecatedField.deprecatedSince,\n message:\n deprecatedField.replacedBy === null\n ? `Structured-data payload @type=\"${entityType}\" emitted by section \"${sectionRef}\" ` +\n `on page \"${pageRef}\" uses deprecated field \"${deprecatedField.field}\" ` +\n `(deprecated since ${deprecatedField.deprecatedSince}).`\n : `Structured-data payload @type=\"${entityType}\" emitted by section \"${sectionRef}\" ` +\n `on page \"${pageRef}\" uses deprecated field \"${deprecatedField.field}\". ` +\n `Use \"${deprecatedField.replacedBy}\" instead ` +\n `(deprecated since ${deprecatedField.deprecatedSince}).`,\n });\n }\n\n const referencedIds = new Set<string>();\n collectReferencedEntityIds(payload, referencedIds, true);\n for (const referencedId of referencedIds) {\n if (declaredEntityIds.has(referencedId)) {\n continue;\n }\n\n findings.push({\n kind: 'cross-entity-reference-drift',\n pageRef,\n routePath,\n sectionRef,\n entityType,\n entityId,\n missingReferenceId: referencedId,\n message:\n `Structured-data payload @type=\"${entityType}\" emitted by section \"${sectionRef}\" ` +\n `on page \"${pageRef}\" references @id \"${referencedId}\" but no emitted entity on the page defines it.`,\n });\n }\n}\n\nfunction normalizeStructuredDataReturn(value: unknown): unknown[] | null {\n if (value === null || value === undefined) {\n return [];\n }\n\n if (Array.isArray(value)) {\n return value.filter((item) => item !== null && item !== undefined);\n }\n\n if (isPlainObject(value)) {\n return [value];\n }\n\n return null;\n}\n\nfunction hasPresentFieldValue(payload: StructuredDataPayload, fieldName: string): boolean {\n if (!Object.prototype.hasOwnProperty.call(payload, fieldName)) {\n return false;\n }\n\n const value = payload[fieldName];\n return value !== null && value !== undefined;\n}\n\nfunction collectDeclaredEntityIds(value: unknown, into: Set<string>): void {\n if (Array.isArray(value)) {\n for (const item of value) {\n collectDeclaredEntityIds(item, into);\n }\n return;\n }\n\n if (!isPlainObject(value)) {\n return;\n }\n\n const entityId = typeof value['@id'] === 'string' ? value['@id'] : null;\n const entityType = typeof value['@type'] === 'string' ? value['@type'] : null;\n if (entityId && entityType) {\n into.add(entityId);\n }\n\n for (const child of Object.values(value)) {\n collectDeclaredEntityIds(child, into);\n }\n}\n\nfunction collectReferencedEntityIds(\n value: unknown,\n into: Set<string>,\n isRootPayload: boolean,\n): void {\n if (Array.isArray(value)) {\n for (const item of value) {\n collectReferencedEntityIds(item, into, false);\n }\n return;\n }\n\n if (!isPlainObject(value)) {\n return;\n }\n\n const entityId = typeof value['@id'] === 'string' ? value['@id'] : null;\n const entityType = typeof value['@type'] === 'string' ? value['@type'] : null;\n if (!isRootPayload && entityId && !entityType) {\n into.add(entityId);\n }\n\n for (const child of Object.values(value)) {\n collectReferencedEntityIds(child, into, false);\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"structured-data-reconciliation.shared.js","sourceRoot":"","sources":["../../../../src/schemas/structured-data/structured-data-reconciliation.shared.ts"],"names":[],"mappings":"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","sourcesContent":["export interface StructuredDataDeprecatedField {\n field: string;\n replacedBy: string | null;\n deprecatedSince: string;\n}\n\nexport interface StructuredDataCatalogEntry {\n '@type': string;\n requiredFields: readonly string[];\n optionalFields?: readonly string[];\n deprecatedFields?: readonly StructuredDataDeprecatedField[];\n recommendOptionals?: boolean;\n}\n\nexport interface StructuredDataPayload {\n '@type': string;\n '@id'?: string;\n [key: string]: unknown;\n}\n\nexport interface StructuredDataSectionDefinitionLike {\n sectionRef: string;\n getStructuredData?: () => unknown;\n}\n\nexport interface StructuredDataPageManifestLike {\n ref: string;\n routePath: string;\n sectionRefs: readonly string[];\n}\n\nexport interface StructuredDataPagePayloadLike {\n pageRef: string;\n routePath?: string;\n sourceRef?: string;\n payloads: unknown;\n}\n\nexport type StructuredDataSpecViolationCode =\n | 'invalid-payload-shape'\n | 'missing-entity-type'\n | 'missing-required-field'\n | 'missing-section-definition'\n | 'orphan-page-payload';\n\nexport interface StructuredDataCatalogOverrideFinding {\n kind: 'catalog-override';\n entityType: string;\n count: number;\n message: string;\n}\n\nexport interface StructuredDataCatalogMissFinding {\n kind: 'catalog-miss';\n pageRef: string;\n routePath: string;\n sectionRef: string;\n entityType: string;\n entityId?: string;\n message: string;\n}\n\nexport interface StructuredDataSpecViolationFinding {\n kind: 'spec-violation';\n code: StructuredDataSpecViolationCode;\n pageRef: string;\n routePath: string;\n sectionRef: string;\n entityType?: string;\n entityId?: string;\n field?: string;\n message: string;\n}\n\nexport interface StructuredDataDeprecatedFieldFinding {\n kind: 'deprecated-field';\n pageRef: string;\n routePath: string;\n sectionRef: string;\n entityType: string;\n entityId?: string;\n field: string;\n replacedBy: string | null;\n deprecatedSince: string;\n message: string;\n}\n\nexport interface StructuredDataCrossEntityReferenceDriftFinding {\n kind: 'cross-entity-reference-drift';\n pageRef: string;\n routePath: string;\n sectionRef: string;\n entityType: string;\n entityId?: string;\n missingReferenceId: string;\n message: string;\n}\n\nexport type StructuredDataReconciliationFinding =\n | StructuredDataCatalogOverrideFinding\n | StructuredDataCatalogMissFinding\n | StructuredDataSpecViolationFinding\n | StructuredDataDeprecatedFieldFinding\n | StructuredDataCrossEntityReferenceDriftFinding;\n\n/**\n * An OPTIONAL field a payload leaves out, reported because its catalog entry opted in with\n * `recommendOptionals: true` — the entry's statement that for this type the optional fields are\n * where the richer search and answer-engine results come from.\n *\n * Deliberately NOT a finding. A finding says the payload is wrong (a missing required field, an\n * unknown type, a deprecated field) and `--strict` escalates every finding to a failure. A payload\n * without its optional fields is valid; reporting it through the same channel would fail strict CI for\n * following the vocabulary. So recommendations travel beside the findings, are shown as information,\n * and never change an audit's outcome.\n */\nexport interface StructuredDataOptionalFieldRecommendation {\n pageRef: string;\n routePath: string;\n sectionRef: string;\n entityType: string;\n entityId?: string;\n /** The entry's optional fields this payload does not carry, in catalog order. */\n missingOptionalFields: readonly string[];\n message: string;\n}\n\nexport interface StructuredDataReconciliationPageResult {\n pageRef: string;\n routePath: string;\n payloadCount: number;\n findingCount: number;\n}\n\nexport interface StructuredDataReconciliationResult {\n findings: StructuredDataReconciliationFinding[];\n /** Optional-field recommendations from entries that opted in — information, never a finding. */\n recommendations: StructuredDataOptionalFieldRecommendation[];\n pages: StructuredDataReconciliationPageResult[];\n}\n\nexport interface ReconcileStructuredDataOptions {\n catalog: readonly StructuredDataCatalogEntry[];\n pages: readonly StructuredDataPageManifestLike[];\n sections: readonly StructuredDataSectionDefinitionLike[];\n pagePayloads?: readonly StructuredDataPagePayloadLike[];\n}\n\ninterface ReconciliationPayloadContext {\n pageRef: string;\n routePath: string;\n sectionRef: string;\n payload: StructuredDataPayload;\n}\n\nexport function reconcileStructuredDataAgainstCatalog(\n options: ReconcileStructuredDataOptions,\n): StructuredDataReconciliationResult {\n const findings: StructuredDataReconciliationFinding[] = [];\n const recommendations: StructuredDataOptionalFieldRecommendation[] = [];\n const pages: StructuredDataReconciliationPageResult[] = [];\n const sectionsByRef = new Map(\n options.sections.map((section) => [section.sectionRef, section] as const),\n );\n const pagePayloadsByPageRef = new Map<string, StructuredDataPagePayloadLike[]>();\n for (const pagePayload of options.pagePayloads ?? []) {\n const pagePayloads = pagePayloadsByPageRef.get(pagePayload.pageRef) ?? [];\n pagePayloads.push(pagePayload);\n pagePayloadsByPageRef.set(pagePayload.pageRef, pagePayloads);\n }\n const catalogByType = new Map<string, StructuredDataCatalogEntry>();\n const catalogCounts = new Map<string, number>();\n\n for (const entry of options.catalog) {\n catalogByType.set(entry['@type'], entry);\n catalogCounts.set(entry['@type'], (catalogCounts.get(entry['@type']) ?? 0) + 1);\n }\n\n for (const [entityType, count] of catalogCounts.entries()) {\n if (count > 1) {\n findings.push({\n kind: 'catalog-override',\n entityType,\n count,\n message:\n `Merged catalog contains ${count} entries for @type=\"${entityType}\". ` +\n 'Replacement semantics are last-wins; duplicate array entries indicate an unresolved override.',\n });\n }\n }\n\n for (const page of options.pages) {\n const pageFindingStart = findings.length;\n const sectionPayloadContexts: ReconciliationPayloadContext[] = [];\n const isTemplatedRoute = page.routePath.includes('[');\n\n for (const sectionRef of page.sectionRefs) {\n const section = sectionsByRef.get(sectionRef);\n if (!section) {\n findings.push({\n kind: 'spec-violation',\n code: 'missing-section-definition',\n pageRef: page.ref,\n routePath: page.routePath,\n sectionRef,\n message:\n `Page \"${page.ref}\" references sectionRef \"${sectionRef}\" but no matching section ` +\n 'definition was supplied to the structured-data reconciliation harness.',\n });\n continue;\n }\n\n if (!section.getStructuredData) {\n continue;\n }\n\n const rawStructuredData = section.getStructuredData();\n const normalizedPayloads = normalizeStructuredDataReturn(rawStructuredData);\n\n if (normalizedPayloads === null) {\n findings.push({\n kind: 'spec-violation',\n code: 'invalid-payload-shape',\n pageRef: page.ref,\n routePath: page.routePath,\n sectionRef,\n message:\n `Section \"${sectionRef}\" on page \"${page.ref}\" returned an invalid ` +\n '`getStructuredData()` shape. Expected object, array, null, or undefined.',\n });\n continue;\n }\n\n for (const candidate of normalizedPayloads) {\n if (!isPlainObject(candidate)) {\n findings.push({\n kind: 'spec-violation',\n code: 'invalid-payload-shape',\n pageRef: page.ref,\n routePath: page.routePath,\n sectionRef,\n message:\n `Section \"${sectionRef}\" on page \"${page.ref}\" returned a non-object structured-data payload.`,\n });\n continue;\n }\n\n if (typeof candidate['@type'] !== 'string' || candidate['@type'].trim().length === 0) {\n findings.push({\n kind: 'spec-violation',\n code: 'missing-entity-type',\n pageRef: page.ref,\n routePath: page.routePath,\n sectionRef,\n entityId: typeof candidate['@id'] === 'string' ? candidate['@id'] : undefined,\n message:\n `Section \"${sectionRef}\" on page \"${page.ref}\" returned a structured-data payload ` +\n 'without a non-empty string `@type`.',\n });\n continue;\n }\n\n sectionPayloadContexts.push({\n pageRef: page.ref,\n routePath: page.routePath,\n sectionRef,\n payload: candidate as StructuredDataPayload,\n });\n }\n }\n\n const contextsByRoutePath = new Map<string, ReconciliationPayloadContext[]>();\n const collectionFindingCountByRoutePath = new Map<string, number>();\n const pagePayloadsForPage = pagePayloadsByPageRef.get(page.ref) ?? [];\n pagePayloadsByPageRef.delete(page.ref);\n const shouldEmitManifestResult =\n (sectionPayloadContexts.length > 0 && (!isTemplatedRoute || pagePayloadsForPage.length === 0))\n || findings.length > pageFindingStart\n || pagePayloadsForPage.length === 0;\n if (shouldEmitManifestResult) {\n const manifestPayloadContexts =\n isTemplatedRoute && pagePayloadsForPage.length > 0\n ? []\n : [...sectionPayloadContexts];\n contextsByRoutePath.set(page.routePath, manifestPayloadContexts);\n collectionFindingCountByRoutePath.set(page.routePath, findings.length - pageFindingStart);\n }\n\n for (const pagePayload of pagePayloadsForPage) {\n const routePath = pagePayload.routePath ?? page.routePath;\n const payloadContexts = contextsByRoutePath.get(routePath) ?? [];\n if (\n payloadContexts.length === 0\n && isTemplatedRoute\n && sectionPayloadContexts.length > 0\n ) {\n payloadContexts.push(\n ...sectionPayloadContexts.map((context) => ({\n ...context,\n routePath,\n })),\n );\n }\n contextsByRoutePath.set(routePath, payloadContexts);\n const collectionFindingStart = findings.length;\n collectNormalizedPayloadContexts({\n rawStructuredData: pagePayload.payloads,\n pageRef: page.ref,\n routePath,\n sectionRef: pagePayload.sourceRef ?? `page:${page.ref}`,\n findings,\n into: payloadContexts,\n });\n collectionFindingCountByRoutePath.set(\n routePath,\n (collectionFindingCountByRoutePath.get(routePath) ?? 0)\n + findings.length - collectionFindingStart,\n );\n }\n\n for (const [routePath, payloadContexts] of contextsByRoutePath.entries()) {\n const declaredEntityIds = new Set<string>();\n for (const payloadContext of payloadContexts) {\n collectDeclaredEntityIds(payloadContext.payload, declaredEntityIds);\n }\n\n const routeFindingStart = findings.length;\n for (const payloadContext of payloadContexts) {\n validatePayloadAgainstCatalog(payloadContext, catalogByType, declaredEntityIds, findings, recommendations);\n }\n\n pages.push({\n pageRef: page.ref,\n routePath,\n payloadCount: payloadContexts.length,\n findingCount:\n (collectionFindingCountByRoutePath.get(routePath) ?? 0)\n + findings.length - routeFindingStart,\n });\n }\n }\n\n for (const [pageRef, pagePayloads] of pagePayloadsByPageRef.entries()) {\n for (const pagePayload of pagePayloads) {\n findings.push({\n kind: 'spec-violation',\n code: 'orphan-page-payload',\n pageRef,\n routePath: pagePayload.routePath ?? '',\n sectionRef: pagePayload.sourceRef ?? `page:${pageRef}`,\n message:\n `Structured-data page payload \"${pagePayload.sourceRef ?? `page:${pageRef}`}\" ` +\n `references pageRef \"${pageRef}\" but no matching page manifest was supplied.`,\n });\n }\n }\n\n return {\n findings,\n recommendations,\n pages,\n };\n}\n\nfunction collectNormalizedPayloadContexts(input: {\n rawStructuredData: unknown;\n pageRef: string;\n routePath: string;\n sectionRef: string;\n findings: StructuredDataReconciliationFinding[];\n into: ReconciliationPayloadContext[];\n}): void {\n const {\n rawStructuredData,\n pageRef,\n routePath,\n sectionRef,\n findings,\n into,\n } = input;\n const normalizedPayloads = normalizeStructuredDataReturn(rawStructuredData);\n\n if (normalizedPayloads === null) {\n findings.push({\n kind: 'spec-violation',\n code: 'invalid-payload-shape',\n pageRef,\n routePath,\n sectionRef,\n message:\n `Structured-data source \"${sectionRef}\" on page \"${pageRef}\" returned an invalid ` +\n 'structured-data shape. Expected object, array, null, or undefined.',\n });\n return;\n }\n\n for (const candidate of normalizedPayloads) {\n if (!isPlainObject(candidate)) {\n findings.push({\n kind: 'spec-violation',\n code: 'invalid-payload-shape',\n pageRef,\n routePath,\n sectionRef,\n message:\n `Structured-data source \"${sectionRef}\" on page \"${pageRef}\" returned a non-object structured-data payload.`,\n });\n continue;\n }\n\n if (typeof candidate['@type'] !== 'string' || candidate['@type'].trim().length === 0) {\n findings.push({\n kind: 'spec-violation',\n code: 'missing-entity-type',\n pageRef,\n routePath,\n sectionRef,\n entityId: typeof candidate['@id'] === 'string' ? candidate['@id'] : undefined,\n message:\n `Structured-data source \"${sectionRef}\" on page \"${pageRef}\" returned a structured-data payload ` +\n 'without a non-empty string `@type`.',\n });\n continue;\n }\n\n into.push({\n pageRef,\n routePath,\n sectionRef,\n payload: candidate as StructuredDataPayload,\n });\n }\n}\n\nfunction validatePayloadAgainstCatalog(\n payloadContext: ReconciliationPayloadContext,\n catalogByType: ReadonlyMap<string, StructuredDataCatalogEntry>,\n declaredEntityIds: ReadonlySet<string>,\n findings: StructuredDataReconciliationFinding[],\n recommendations: StructuredDataOptionalFieldRecommendation[],\n): void {\n const { payload, pageRef, routePath, sectionRef } = payloadContext;\n const entityType = payload['@type'];\n const entityId = typeof payload['@id'] === 'string' ? payload['@id'] : undefined;\n const catalogEntry = catalogByType.get(entityType);\n\n if (!catalogEntry) {\n findings.push({\n kind: 'catalog-miss',\n pageRef,\n routePath,\n sectionRef,\n entityType,\n entityId,\n message:\n `Structured-data payload @type=\"${entityType}\" emitted by section \"${sectionRef}\" ` +\n `on page \"${pageRef}\" does not exist in the merged catalog.`,\n });\n return;\n }\n\n for (const requiredField of catalogEntry.requiredFields) {\n if (!hasPresentFieldValue(payload, requiredField)) {\n findings.push({\n kind: 'spec-violation',\n code: 'missing-required-field',\n pageRef,\n routePath,\n sectionRef,\n entityType,\n entityId,\n field: requiredField,\n message:\n `Structured-data payload @type=\"${entityType}\" emitted by section \"${sectionRef}\" ` +\n `on page \"${pageRef}\" is missing required field \"${requiredField}\".`,\n });\n }\n }\n\n if (catalogEntry.recommendOptionals === true) {\n const missingOptionalFields = (catalogEntry.optionalFields ?? []).filter((field) => !hasPresentFieldValue(payload, field));\n if (missingOptionalFields.length > 0) {\n recommendations.push({\n pageRef,\n routePath,\n sectionRef,\n entityType,\n entityId,\n missingOptionalFields,\n message:\n `Structured-data payload @type=\"${entityType}\" emitted by section \"${sectionRef}\" on page \"${pageRef}\" ` +\n `could carry ${missingOptionalFields.map((field) => `\"${field}\"`).join(', ')} — optional, and the catalog ` +\n 'recommends them for richer search and answer-engine results.',\n });\n }\n }\n\n for (const deprecatedField of catalogEntry.deprecatedFields ?? []) {\n if (!hasPresentFieldValue(payload, deprecatedField.field)) {\n continue;\n }\n\n findings.push({\n kind: 'deprecated-field',\n pageRef,\n routePath,\n sectionRef,\n entityType,\n entityId,\n field: deprecatedField.field,\n replacedBy: deprecatedField.replacedBy,\n deprecatedSince: deprecatedField.deprecatedSince,\n message:\n deprecatedField.replacedBy === null\n ? `Structured-data payload @type=\"${entityType}\" emitted by section \"${sectionRef}\" ` +\n `on page \"${pageRef}\" uses deprecated field \"${deprecatedField.field}\" ` +\n `(deprecated since ${deprecatedField.deprecatedSince}).`\n : `Structured-data payload @type=\"${entityType}\" emitted by section \"${sectionRef}\" ` +\n `on page \"${pageRef}\" uses deprecated field \"${deprecatedField.field}\". ` +\n `Use \"${deprecatedField.replacedBy}\" instead ` +\n `(deprecated since ${deprecatedField.deprecatedSince}).`,\n });\n }\n\n const referencedIds = new Set<string>();\n collectReferencedEntityIds(payload, referencedIds, true);\n for (const referencedId of referencedIds) {\n if (declaredEntityIds.has(referencedId)) {\n continue;\n }\n\n findings.push({\n kind: 'cross-entity-reference-drift',\n pageRef,\n routePath,\n sectionRef,\n entityType,\n entityId,\n missingReferenceId: referencedId,\n message:\n `Structured-data payload @type=\"${entityType}\" emitted by section \"${sectionRef}\" ` +\n `on page \"${pageRef}\" references @id \"${referencedId}\" but no emitted entity on the page defines it.`,\n });\n }\n}\n\nfunction normalizeStructuredDataReturn(value: unknown): unknown[] | null {\n if (value === null || value === undefined) {\n return [];\n }\n\n if (Array.isArray(value)) {\n return value.filter((item) => item !== null && item !== undefined);\n }\n\n if (isPlainObject(value)) {\n return [value];\n }\n\n return null;\n}\n\nfunction hasPresentFieldValue(payload: StructuredDataPayload, fieldName: string): boolean {\n if (!Object.prototype.hasOwnProperty.call(payload, fieldName)) {\n return false;\n }\n\n const value = payload[fieldName];\n return value !== null && value !== undefined;\n}\n\nfunction collectDeclaredEntityIds(value: unknown, into: Set<string>): void {\n if (Array.isArray(value)) {\n for (const item of value) {\n collectDeclaredEntityIds(item, into);\n }\n return;\n }\n\n if (!isPlainObject(value)) {\n return;\n }\n\n const entityId = typeof value['@id'] === 'string' ? value['@id'] : null;\n const entityType = typeof value['@type'] === 'string' ? value['@type'] : null;\n if (entityId && entityType) {\n into.add(entityId);\n }\n\n for (const child of Object.values(value)) {\n collectDeclaredEntityIds(child, into);\n }\n}\n\nfunction collectReferencedEntityIds(\n value: unknown,\n into: Set<string>,\n isRootPayload: boolean,\n): void {\n if (Array.isArray(value)) {\n for (const item of value) {\n collectReferencedEntityIds(item, into, false);\n }\n return;\n }\n\n if (!isPlainObject(value)) {\n return;\n }\n\n const entityId = typeof value['@id'] === 'string' ? value['@id'] : null;\n const entityType = typeof value['@type'] === 'string' ? value['@type'] : null;\n if (!isRootPayload && entityId && !entityType) {\n into.add(entityId);\n }\n\n for (const child of Object.values(value)) {\n collectReferencedEntityIds(child, into, false);\n }\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
|
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
*
|
|
17
17
|
* - `@type` REQUIRED — the Schema.org entity kind (`'Article'`,
|
|
18
18
|
* `'BlogPosting'`, `'FAQPage'`, …). Typed as a free-form `string`
|
|
19
|
-
* here (not a closed enum)
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* companion's
|
|
23
|
-
*
|
|
19
|
+
* here (not a closed enum) because this package ships to the browser
|
|
20
|
+
* and must not carry the catalog: the renderer emits whatever type a
|
|
21
|
+
* section returns. What VALIDATES the type is the Schema.org catalog,
|
|
22
|
+
* through `wildo website audit` and the companion's reconciliation —
|
|
23
|
+
* and that catalog is a closed enum an application can override but
|
|
24
|
+
* not extend, so a type outside it reports `catalog-miss` until the
|
|
25
|
+
* framework adds it.
|
|
24
26
|
* - `@id?` OPTIONAL — a stable URI identifying the entity. Used for
|
|
25
27
|
* cross-referencing on the same page (e.g. `Article.publisher`
|
|
26
28
|
* referencing the page-level `Organization` via `@id`). The
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"website-structured-data.shared.schemas.d.ts","sourceRoot":"","sources":["
|
|
1
|
+
{"version":3,"file":"website-structured-data.shared.schemas.d.ts","sourceRoot":"","sources":["../../../../src/schemas/structured-data/website-structured-data.shared.schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,2BAA2B,GACnC,4BAA4B,GAC5B,SAAS,4BAA4B,EAAE,GACvC,IAAI,GACJ,SAAS,CAAC;AACd,wEAAwE"}
|