@wildo-ai/saas-website 1.1.2 → 1.1.3
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 +21 -0
- package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
- package/dist/esm/astro/blog-post-bridge.js +5 -2
- package/dist/esm/astro/blog-post-bridge.js.map +1 -1
- package/dist/esm/astro/bridge-runtime.d.ts +37 -0
- package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
- package/dist/esm/astro/bridge-runtime.js +6 -2
- package/dist/esm/astro/bridge-runtime.js.map +1 -1
- package/dist/esm/astro/robots-renderer.d.ts.map +1 -1
- package/dist/esm/astro/robots-renderer.js +14 -1
- package/dist/esm/astro/robots-renderer.js.map +1 -1
- package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -1
- package/dist/esm/astro/website-page-head.renderer.js +32 -12
- package/dist/esm/astro/website-page-head.renderer.js.map +1 -1
- package/dist/esm/config/wildo-website-config.schemas.d.ts +12 -5
- package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -1
- package/dist/esm/config/wildo-website-config.schemas.js +12 -5
- package/dist/esm/config/wildo-website-config.schemas.js.map +1 -1
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +5 -5
- 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 +7 -6
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -1
- package/dist/esm/core/contexts/WebsitePageContext.d.ts +4 -3
- package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -1
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +36 -7
- 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 +3 -2
- package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -1
- package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -1
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +37 -13
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -1
- package/dist/esm/core/external-providers/frontend-provider-registry.website.js +28 -19
- package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -1
- package/dist/esm/core/external-providers/provider-scripts.website.d.ts +68 -0
- package/dist/esm/core/external-providers/provider-scripts.website.d.ts.map +1 -0
- package/dist/esm/core/external-providers/provider-scripts.website.js +99 -0
- package/dist/esm/core/external-providers/provider-scripts.website.js.map +1 -0
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +34 -0
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -0
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +78 -0
- package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -0
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -1
- package/dist/esm/core/layouts/WebsitePageLayout.js +7 -0
- package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -1
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts +19 -20
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -1
- package/dist/esm/schemas/sections/website-section-category.shared.js +19 -20
- package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/src/__tests__/bundle-isolation.test.ts +21 -0
- package/src/astro/__tests__/blog-post-bridge.test.tsx +2 -2
- package/src/astro/__tests__/bridge-runtime.test.tsx +57 -2
- package/src/astro/__tests__/robots-renderer.test.ts +16 -1
- package/src/astro/__tests__/website-page-head.renderer.test.ts +43 -0
- package/src/astro/__tests__/website-site-context.test.ts +1 -1
- package/src/astro/blog-post-bridge.tsx +26 -1
- package/src/astro/bridge-runtime.tsx +44 -1
- package/src/astro/robots-renderer.ts +14 -1
- package/src/astro/website-page-head.renderer.ts +32 -12
- package/src/config/__tests__/define-website-config.test.ts +9 -3
- package/src/config/wildo-website-config.schemas.ts +12 -5
- package/src/core/__tests__/WebsitePageLayout.test.tsx +47 -4
- package/src/core/anonymous-session/inbound-contact-form.schema.ts +7 -6
- package/src/core/contexts/WebsitePageContext.tsx +4 -3
- package/src/core/contexts/WebsiteRuntimeContext.tsx +36 -7
- package/src/core/contexts/WebsiteSectionContext.tsx +3 -2
- package/src/core/external-providers/__tests__/frontend-provider-registry.website.test.ts +79 -0
- package/src/core/external-providers/__tests__/provider-scripts.website.test.ts +146 -0
- package/src/core/external-providers/frontend-provider-registry.website.ts +53 -54
- package/src/core/external-providers/provider-scripts.website.ts +126 -0
- package/src/core/external-providers/useWebsiteProviderScripts.ts +92 -0
- package/src/core/layouts/WebsitePageLayout.tsx +11 -0
- package/src/schemas/sections/website-section-category.shared.ts +19 -20
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading what a website provider actually declared, instead of naming it.
|
|
3
|
+
*
|
|
4
|
+
* `FRONTEND_SDK` was a label on this surface. The layout resolved the analytics,
|
|
5
|
+
* error-monitoring and captcha provider refs and stamped them into `data-*`
|
|
6
|
+
* attributes on the page root; no consumer loaded anything. A provider could be
|
|
7
|
+
* declared, enabled, hydrated — and do nothing at all.
|
|
8
|
+
*
|
|
9
|
+
* This is the consumer for the DATA half of a provider's executable surface: the
|
|
10
|
+
* `scripts` it declares (#332). It serves the case a dynamic `import()` cannot,
|
|
11
|
+
* and it is the dominant case on a marketing site: a vendor that ships a
|
|
12
|
+
* global-installing `<script>` tag rather than an npm module (reCAPTCHA, tag
|
|
13
|
+
* managers, chat widgets, and the CDN distributions of most analytics SDKs).
|
|
14
|
+
*
|
|
15
|
+
* The npm half — `sdk.activate`, a dynamic import inside the provider package —
|
|
16
|
+
* needs the CODE channel, which needs this package to depend on
|
|
17
|
+
* `@wildo-ai/external-connectors-public`. That dependency is not added here; see
|
|
18
|
+
* the note in the S4a ledger. Nothing below anticipates it: a provider that
|
|
19
|
+
* declares both gets its scripts from here and its SDK from there.
|
|
20
|
+
*
|
|
21
|
+
* ## Client-side injection, and the alternative that was rejected
|
|
22
|
+
*
|
|
23
|
+
* This is an Astro static site, so the "obvious" home for a third-party tag is
|
|
24
|
+
* the built HTML `<head>`. That was rejected for now: the set of live providers
|
|
25
|
+
* is per SERVICE and per ENVIRONMENT, resolved from the materialized artifact,
|
|
26
|
+
* while the built HTML is one artifact shared across deploys of the same build.
|
|
27
|
+
* Baking the tags in would make the build environment-specific — the same class
|
|
28
|
+
* of coupling the DATA channel exists to avoid. Injecting from the client keeps
|
|
29
|
+
* one build correct everywhere, at the cost of the tag arriving after hydration.
|
|
30
|
+
* Revisit if a provider ever needs to run before first paint.
|
|
31
|
+
*/
|
|
32
|
+
import type { ProviderFrontendScriptDeclaration } from '@wildo-ai/saas-models/public-runtime';
|
|
33
|
+
import type { FrontendWebsiteProviderRegistry } from './frontend-provider-registry.website';
|
|
34
|
+
/**
|
|
35
|
+
* Marks a `<script>` this module owns, so a re-render, a client-side route
|
|
36
|
+
* change or a second layout mount finds it instead of appending a duplicate.
|
|
37
|
+
* A vendor tag loaded twice initialises twice — two analytics clients, two sets
|
|
38
|
+
* of events — which is the failure mode this attribute exists to prevent.
|
|
39
|
+
*/
|
|
40
|
+
export declare const PROVIDER_SCRIPT_MARKER_ATTRIBUTE = "data-wildo-provider-script";
|
|
41
|
+
export interface DeclaredProviderScript {
|
|
42
|
+
readonly providerRef: string;
|
|
43
|
+
readonly declaration: ProviderFrontendScriptDeclaration;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Every script declared by the providers live on this surface, in registry
|
|
47
|
+
* order, with the declaring ref kept beside each one so a failure can name it.
|
|
48
|
+
*/
|
|
49
|
+
export declare function collectDeclaredProviderScripts(registry: FrontendWebsiteProviderRegistry | undefined): ReadonlyArray<DeclaredProviderScript>;
|
|
50
|
+
export interface InjectProviderScriptsResult {
|
|
51
|
+
/** Refs whose script this call added. */
|
|
52
|
+
readonly injected: ReadonlyArray<string>;
|
|
53
|
+
/** Refs whose script was already present, so nothing was added. */
|
|
54
|
+
readonly alreadyPresent: ReadonlyArray<string>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Adds each declared script to the document, once.
|
|
58
|
+
*
|
|
59
|
+
* Idempotent by `src`: a declaration whose element is already in the document is
|
|
60
|
+
* skipped. Callers may therefore run this on every mount without tracking state.
|
|
61
|
+
*
|
|
62
|
+
* `async` defaults to TRUE when the declaration says nothing. A third-party tag
|
|
63
|
+
* that blocks parsing is the single most common way an analytics provider costs
|
|
64
|
+
* a marketing site its paint metrics, and a provider author who has not thought
|
|
65
|
+
* about it should get the safe answer rather than the blocking one.
|
|
66
|
+
*/
|
|
67
|
+
export declare function injectProviderScripts(scripts: ReadonlyArray<DeclaredProviderScript>, targetDocument: Document): InjectProviderScriptsResult;
|
|
68
|
+
//# sourceMappingURL=provider-scripts.website.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-scripts.website.d.ts","sourceRoot":"","sources":["../../../../../src/core/external-providers/provider-scripts.website.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,sCAAsC,CAAC;AAC9F,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,sCAAsC,CAAC;AAE5F;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,+BAA+B,CAAC;AAE7E,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,iCAAiC,CAAC;CACzD;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,+BAA+B,GAAG,SAAS,GACpD,aAAa,CAAC,sBAAsB,CAAC,CAUvC;AAED,MAAM,WAAW,2BAA2B;IAC1C,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACzC,mEAAmE;IACnE,QAAQ,CAAC,cAAc,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAC9C,cAAc,EAAE,QAAQ,GACvB,2BAA2B,CAsC7B"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading what a website provider actually declared, instead of naming it.
|
|
3
|
+
*
|
|
4
|
+
* `FRONTEND_SDK` was a label on this surface. The layout resolved the analytics,
|
|
5
|
+
* error-monitoring and captcha provider refs and stamped them into `data-*`
|
|
6
|
+
* attributes on the page root; no consumer loaded anything. A provider could be
|
|
7
|
+
* declared, enabled, hydrated — and do nothing at all.
|
|
8
|
+
*
|
|
9
|
+
* This is the consumer for the DATA half of a provider's executable surface: the
|
|
10
|
+
* `scripts` it declares (#332). It serves the case a dynamic `import()` cannot,
|
|
11
|
+
* and it is the dominant case on a marketing site: a vendor that ships a
|
|
12
|
+
* global-installing `<script>` tag rather than an npm module (reCAPTCHA, tag
|
|
13
|
+
* managers, chat widgets, and the CDN distributions of most analytics SDKs).
|
|
14
|
+
*
|
|
15
|
+
* The npm half — `sdk.activate`, a dynamic import inside the provider package —
|
|
16
|
+
* needs the CODE channel, which needs this package to depend on
|
|
17
|
+
* `@wildo-ai/external-connectors-public`. That dependency is not added here; see
|
|
18
|
+
* the note in the S4a ledger. Nothing below anticipates it: a provider that
|
|
19
|
+
* declares both gets its scripts from here and its SDK from there.
|
|
20
|
+
*
|
|
21
|
+
* ## Client-side injection, and the alternative that was rejected
|
|
22
|
+
*
|
|
23
|
+
* This is an Astro static site, so the "obvious" home for a third-party tag is
|
|
24
|
+
* the built HTML `<head>`. That was rejected for now: the set of live providers
|
|
25
|
+
* is per SERVICE and per ENVIRONMENT, resolved from the materialized artifact,
|
|
26
|
+
* while the built HTML is one artifact shared across deploys of the same build.
|
|
27
|
+
* Baking the tags in would make the build environment-specific — the same class
|
|
28
|
+
* of coupling the DATA channel exists to avoid. Injecting from the client keeps
|
|
29
|
+
* one build correct everywhere, at the cost of the tag arriving after hydration.
|
|
30
|
+
* Revisit if a provider ever needs to run before first paint.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Marks a `<script>` this module owns, so a re-render, a client-side route
|
|
34
|
+
* change or a second layout mount finds it instead of appending a duplicate.
|
|
35
|
+
* A vendor tag loaded twice initialises twice — two analytics clients, two sets
|
|
36
|
+
* of events — which is the failure mode this attribute exists to prevent.
|
|
37
|
+
*/
|
|
38
|
+
export const PROVIDER_SCRIPT_MARKER_ATTRIBUTE = 'data-wildo-provider-script';
|
|
39
|
+
/**
|
|
40
|
+
* Every script declared by the providers live on this surface, in registry
|
|
41
|
+
* order, with the declaring ref kept beside each one so a failure can name it.
|
|
42
|
+
*/
|
|
43
|
+
export function collectDeclaredProviderScripts(registry) {
|
|
44
|
+
if (registry === undefined)
|
|
45
|
+
return [];
|
|
46
|
+
const collected = [];
|
|
47
|
+
for (const provider of registry.modules.values()) {
|
|
48
|
+
for (const declaration of provider.scripts ?? []) {
|
|
49
|
+
collected.push({ providerRef: provider.metadata.ref, declaration });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return collected;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Adds each declared script to the document, once.
|
|
56
|
+
*
|
|
57
|
+
* Idempotent by `src`: a declaration whose element is already in the document is
|
|
58
|
+
* skipped. Callers may therefore run this on every mount without tracking state.
|
|
59
|
+
*
|
|
60
|
+
* `async` defaults to TRUE when the declaration says nothing. A third-party tag
|
|
61
|
+
* that blocks parsing is the single most common way an analytics provider costs
|
|
62
|
+
* a marketing site its paint metrics, and a provider author who has not thought
|
|
63
|
+
* about it should get the safe answer rather than the blocking one.
|
|
64
|
+
*/
|
|
65
|
+
export function injectProviderScripts(scripts, targetDocument) {
|
|
66
|
+
const injected = [];
|
|
67
|
+
const alreadyPresent = [];
|
|
68
|
+
for (const { providerRef, declaration } of scripts) {
|
|
69
|
+
// Matched by reading the attribute rather than by a selector carrying the
|
|
70
|
+
// URL: a `src` can contain characters a CSS selector would have to escape,
|
|
71
|
+
// and an escaping bug here would show up as a duplicated vendor tag — two
|
|
72
|
+
// analytics clients, two sets of events — rather than as an error.
|
|
73
|
+
const existing = [
|
|
74
|
+
...targetDocument.querySelectorAll(`script[${PROVIDER_SCRIPT_MARKER_ATTRIBUTE}]`),
|
|
75
|
+
].some((element) => element.getAttribute('src') === declaration.src);
|
|
76
|
+
if (existing) {
|
|
77
|
+
alreadyPresent.push(providerRef);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const element = targetDocument.createElement('script');
|
|
81
|
+
element.src = declaration.src;
|
|
82
|
+
element.setAttribute(PROVIDER_SCRIPT_MARKER_ATTRIBUTE, providerRef);
|
|
83
|
+
if (declaration.integrity !== undefined) {
|
|
84
|
+
element.integrity = declaration.integrity;
|
|
85
|
+
}
|
|
86
|
+
if (declaration.crossOrigin !== undefined) {
|
|
87
|
+
element.crossOrigin = declaration.crossOrigin;
|
|
88
|
+
}
|
|
89
|
+
// Both flags are set explicitly rather than left to the element's defaults:
|
|
90
|
+
// a dynamically created script is `async` by default, so `defer: true` alone
|
|
91
|
+
// would be silently ignored without the paired `async = false`.
|
|
92
|
+
element.async = declaration.async ?? declaration.defer !== true;
|
|
93
|
+
element.defer = declaration.defer ?? false;
|
|
94
|
+
targetDocument.head.appendChild(element);
|
|
95
|
+
injected.push(providerRef);
|
|
96
|
+
}
|
|
97
|
+
return { injected, alreadyPresent };
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=provider-scripts.website.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-scripts.website.js","sourceRoot":"","sources":["../../../../../src/core/external-providers/provider-scripts.website.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,4BAA4B,CAAC;AAO7E;;;GAGG;AACH,MAAM,UAAU,8BAA8B,CAC5C,QAAqD;IAErD,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAEtC,MAAM,SAAS,GAA6B,EAAE,CAAC;IAC/C,KAAK,MAAM,QAAQ,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QACjD,KAAK,MAAM,WAAW,IAAI,QAAQ,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;YACjD,SAAS,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AASD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAA8C,EAC9C,cAAwB;IAExB,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,cAAc,GAAa,EAAE,CAAC;IAEpC,KAAK,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,IAAI,OAAO,EAAE,CAAC;QACnD,0EAA0E;QAC1E,2EAA2E;QAC3E,0EAA0E;QAC1E,mEAAmE;QACnE,MAAM,QAAQ,GAAG;YACf,GAAG,cAAc,CAAC,gBAAgB,CAAC,UAAU,gCAAgC,GAAG,CAAC;SAClF,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,GAAG,CAAC,CAAC;QACrE,IAAI,QAAQ,EAAE,CAAC;YACb,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YACjC,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,cAAc,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QACvD,OAAO,CAAC,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC;QAC9B,OAAO,CAAC,YAAY,CAAC,gCAAgC,EAAE,WAAW,CAAC,CAAC;QAEpE,IAAI,WAAW,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YACxC,OAAO,CAAC,SAAS,GAAG,WAAW,CAAC,SAAS,CAAC;QAC5C,CAAC;QACD,IAAI,WAAW,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YAC1C,OAAO,CAAC,WAAW,GAAG,WAAW,CAAC,WAAW,CAAC;QAChD,CAAC;QACD,4EAA4E;QAC5E,6EAA6E;QAC7E,gEAAgE;QAChE,OAAO,CAAC,KAAK,GAAG,WAAW,CAAC,KAAK,IAAI,WAAW,CAAC,KAAK,KAAK,IAAI,CAAC;QAChE,OAAO,CAAC,KAAK,GAAG,WAAW,CAAC,KAAK,IAAI,KAAK,CAAC;QAE3C,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACzC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;AACtC,CAAC","sourcesContent":["/**\n * Loading what a website provider actually declared, instead of naming it.\n *\n * `FRONTEND_SDK` was a label on this surface. The layout resolved the analytics,\n * error-monitoring and captcha provider refs and stamped them into `data-*`\n * attributes on the page root; no consumer loaded anything. A provider could be\n * declared, enabled, hydrated — and do nothing at all.\n *\n * This is the consumer for the DATA half of a provider's executable surface: the\n * `scripts` it declares (#332). It serves the case a dynamic `import()` cannot,\n * and it is the dominant case on a marketing site: a vendor that ships a\n * global-installing `<script>` tag rather than an npm module (reCAPTCHA, tag\n * managers, chat widgets, and the CDN distributions of most analytics SDKs).\n *\n * The npm half — `sdk.activate`, a dynamic import inside the provider package —\n * needs the CODE channel, which needs this package to depend on\n * `@wildo-ai/external-connectors-public`. That dependency is not added here; see\n * the note in the S4a ledger. Nothing below anticipates it: a provider that\n * declares both gets its scripts from here and its SDK from there.\n *\n * ## Client-side injection, and the alternative that was rejected\n *\n * This is an Astro static site, so the \"obvious\" home for a third-party tag is\n * the built HTML `<head>`. That was rejected for now: the set of live providers\n * is per SERVICE and per ENVIRONMENT, resolved from the materialized artifact,\n * while the built HTML is one artifact shared across deploys of the same build.\n * Baking the tags in would make the build environment-specific — the same class\n * of coupling the DATA channel exists to avoid. Injecting from the client keeps\n * one build correct everywhere, at the cost of the tag arriving after hydration.\n * Revisit if a provider ever needs to run before first paint.\n */\n\nimport type { ProviderFrontendScriptDeclaration } from '@wildo-ai/saas-models/public-runtime';\nimport type { FrontendWebsiteProviderRegistry } from './frontend-provider-registry.website';\n\n/**\n * Marks a `<script>` this module owns, so a re-render, a client-side route\n * change or a second layout mount finds it instead of appending a duplicate.\n * A vendor tag loaded twice initialises twice — two analytics clients, two sets\n * of events — which is the failure mode this attribute exists to prevent.\n */\nexport const PROVIDER_SCRIPT_MARKER_ATTRIBUTE = 'data-wildo-provider-script';\n\nexport interface DeclaredProviderScript {\n readonly providerRef: string;\n readonly declaration: ProviderFrontendScriptDeclaration;\n}\n\n/**\n * Every script declared by the providers live on this surface, in registry\n * order, with the declaring ref kept beside each one so a failure can name it.\n */\nexport function collectDeclaredProviderScripts(\n registry: FrontendWebsiteProviderRegistry | undefined,\n): ReadonlyArray<DeclaredProviderScript> {\n if (registry === undefined) return [];\n\n const collected: DeclaredProviderScript[] = [];\n for (const provider of registry.modules.values()) {\n for (const declaration of provider.scripts ?? []) {\n collected.push({ providerRef: provider.metadata.ref, declaration });\n }\n }\n return collected;\n}\n\nexport interface InjectProviderScriptsResult {\n /** Refs whose script this call added. */\n readonly injected: ReadonlyArray<string>;\n /** Refs whose script was already present, so nothing was added. */\n readonly alreadyPresent: ReadonlyArray<string>;\n}\n\n/**\n * Adds each declared script to the document, once.\n *\n * Idempotent by `src`: a declaration whose element is already in the document is\n * skipped. Callers may therefore run this on every mount without tracking state.\n *\n * `async` defaults to TRUE when the declaration says nothing. A third-party tag\n * that blocks parsing is the single most common way an analytics provider costs\n * a marketing site its paint metrics, and a provider author who has not thought\n * about it should get the safe answer rather than the blocking one.\n */\nexport function injectProviderScripts(\n scripts: ReadonlyArray<DeclaredProviderScript>,\n targetDocument: Document,\n): InjectProviderScriptsResult {\n const injected: string[] = [];\n const alreadyPresent: string[] = [];\n\n for (const { providerRef, declaration } of scripts) {\n // Matched by reading the attribute rather than by a selector carrying the\n // URL: a `src` can contain characters a CSS selector would have to escape,\n // and an escaping bug here would show up as a duplicated vendor tag — two\n // analytics clients, two sets of events — rather than as an error.\n const existing = [\n ...targetDocument.querySelectorAll(`script[${PROVIDER_SCRIPT_MARKER_ATTRIBUTE}]`),\n ].some((element) => element.getAttribute('src') === declaration.src);\n if (existing) {\n alreadyPresent.push(providerRef);\n continue;\n }\n\n const element = targetDocument.createElement('script');\n element.src = declaration.src;\n element.setAttribute(PROVIDER_SCRIPT_MARKER_ATTRIBUTE, providerRef);\n\n if (declaration.integrity !== undefined) {\n element.integrity = declaration.integrity;\n }\n if (declaration.crossOrigin !== undefined) {\n element.crossOrigin = declaration.crossOrigin;\n }\n // Both flags are set explicitly rather than left to the element's defaults:\n // a dynamically created script is `async` by default, so `defer: true` alone\n // would be silently ignored without the paired `async = false`.\n element.async = declaration.async ?? declaration.defer !== true;\n element.defer = declaration.defer ?? false;\n\n targetDocument.head.appendChild(element);\n injected.push(providerRef);\n }\n\n return { injected, alreadyPresent };\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The website's executable-surface hooks: load what the live providers declared.
|
|
3
|
+
*
|
|
4
|
+
* Two mechanisms, one per kind of vendor. `useWebsiteProviderScripts` adds the
|
|
5
|
+
* `<script>` tags a provider declared — the case a dynamic `import()` cannot
|
|
6
|
+
* serve, and the dominant one on a marketing site. `useWebsiteProviderSdks`
|
|
7
|
+
* activates the npm SDK of any provider that ships one, which reaches this
|
|
8
|
+
* bundle through the CODE channel.
|
|
9
|
+
*
|
|
10
|
+
* Both are effect-only, and that is not incidental: this is an Astro site, so
|
|
11
|
+
* page modules execute in Node at build time. Injecting into a document that
|
|
12
|
+
* does not exist would throw, and starting a vendor SDK there would run a live
|
|
13
|
+
* analytics client inside a build.
|
|
14
|
+
*
|
|
15
|
+
* SDK activation comes from `@wildo-ai/external-connectors-public/website`, which
|
|
16
|
+
* this package now depends on. The script injector beside it does NOT yet, and
|
|
17
|
+
* that is a timing artifact rather than a decision: it belongs in the same
|
|
18
|
+
* package so the technical doc can use one copy — a second copy of a DOM
|
|
19
|
+
* mutation eventually disagrees about idempotence, which is how a vendor tag
|
|
20
|
+
* gets loaded twice. Moving it needs that package's `dist` to carry it, and no
|
|
21
|
+
* watcher is running in this tree; consolidating it is a one-step follow-up, not
|
|
22
|
+
* a design question.
|
|
23
|
+
*/
|
|
24
|
+
import type { FrontendWebsiteProviderRegistry } from './frontend-provider-registry.website';
|
|
25
|
+
export declare function useWebsiteProviderScripts(registry: FrontendWebsiteProviderRegistry | undefined): void;
|
|
26
|
+
/**
|
|
27
|
+
* Loads and starts the npm SDK of every live provider that declares one.
|
|
28
|
+
*
|
|
29
|
+
* Unlike a script tag, an SDK activation IS undone on unmount where the vendor
|
|
30
|
+
* offers a teardown — an analytics client that cannot be stopped cannot honour
|
|
31
|
+
* a consent withdrawal, and accumulates one instance per view transition.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useWebsiteProviderSdks(registry: FrontendWebsiteProviderRegistry | undefined): void;
|
|
34
|
+
//# sourceMappingURL=useWebsiteProviderScripts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useWebsiteProviderScripts.d.ts","sourceRoot":"","sources":["../../../../../src/core/external-providers/useWebsiteProviderScripts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAYH,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,sCAAsC,CAAC;AAE5F,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,+BAA+B,GAAG,SAAS,GACpD,IAAI,CAaN;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,+BAA+B,GAAG,SAAS,GACpD,IAAI,CA6BN"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The website's executable-surface hooks: load what the live providers declared.
|
|
3
|
+
*
|
|
4
|
+
* Two mechanisms, one per kind of vendor. `useWebsiteProviderScripts` adds the
|
|
5
|
+
* `<script>` tags a provider declared — the case a dynamic `import()` cannot
|
|
6
|
+
* serve, and the dominant one on a marketing site. `useWebsiteProviderSdks`
|
|
7
|
+
* activates the npm SDK of any provider that ships one, which reaches this
|
|
8
|
+
* bundle through the CODE channel.
|
|
9
|
+
*
|
|
10
|
+
* Both are effect-only, and that is not incidental: this is an Astro site, so
|
|
11
|
+
* page modules execute in Node at build time. Injecting into a document that
|
|
12
|
+
* does not exist would throw, and starting a vendor SDK there would run a live
|
|
13
|
+
* analytics client inside a build.
|
|
14
|
+
*
|
|
15
|
+
* SDK activation comes from `@wildo-ai/external-connectors-public/website`, which
|
|
16
|
+
* this package now depends on. The script injector beside it does NOT yet, and
|
|
17
|
+
* that is a timing artifact rather than a decision: it belongs in the same
|
|
18
|
+
* package so the technical doc can use one copy — a second copy of a DOM
|
|
19
|
+
* mutation eventually disagrees about idempotence, which is how a vendor tag
|
|
20
|
+
* gets loaded twice. Moving it needs that package's `dist` to carry it, and no
|
|
21
|
+
* watcher is running in this tree; consolidating it is a one-step follow-up, not
|
|
22
|
+
* a design question.
|
|
23
|
+
*/
|
|
24
|
+
import { useEffect } from 'react';
|
|
25
|
+
import { activateFrontendProviderSdks, } from '@wildo-ai/external-connectors-public/website';
|
|
26
|
+
import { collectDeclaredProviderScripts, injectProviderScripts, } from './provider-scripts.website.js';
|
|
27
|
+
import { AppFrontendType } from '@wildo-ai/saas-models/public-runtime';
|
|
28
|
+
export function useWebsiteProviderScripts(registry) {
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (typeof document === 'undefined')
|
|
31
|
+
return;
|
|
32
|
+
const scripts = collectDeclaredProviderScripts(registry);
|
|
33
|
+
if (scripts.length === 0)
|
|
34
|
+
return;
|
|
35
|
+
injectProviderScripts(scripts, document);
|
|
36
|
+
// Deliberately no teardown. A vendor tag installs a global and often starts
|
|
37
|
+
// work immediately; removing the element undoes neither, so a cleanup that
|
|
38
|
+
// removed it would only make the next mount inject a SECOND copy of an
|
|
39
|
+
// already-running script. Idempotence by `src` is the real guard.
|
|
40
|
+
}, [registry]);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Loads and starts the npm SDK of every live provider that declares one.
|
|
44
|
+
*
|
|
45
|
+
* Unlike a script tag, an SDK activation IS undone on unmount where the vendor
|
|
46
|
+
* offers a teardown — an analytics client that cannot be stopped cannot honour
|
|
47
|
+
* a consent withdrawal, and accumulates one instance per view transition.
|
|
48
|
+
*/
|
|
49
|
+
export function useWebsiteProviderSdks(registry) {
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (registry === undefined)
|
|
52
|
+
return;
|
|
53
|
+
let unmounted = false;
|
|
54
|
+
let activated;
|
|
55
|
+
void activateFrontendProviderSdks({
|
|
56
|
+
surface: AppFrontendType.STATIC_WEBSITE,
|
|
57
|
+
modules: registry.modules.values(),
|
|
58
|
+
onDiagnostic: (diagnostic) => {
|
|
59
|
+
// The website runtime has no logger service, and a provider that cannot
|
|
60
|
+
// start is a developer-facing fact that must not be silent.
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.warn(`[wildo-provider-sdk] ${diagnostic.kind}: ${diagnostic.message}`);
|
|
63
|
+
},
|
|
64
|
+
}).then((result) => {
|
|
65
|
+
activated = result;
|
|
66
|
+
// Activation is asynchronous and unmount is not: if the surface went away
|
|
67
|
+
// while a vendor bundle was still downloading, stop what we just started
|
|
68
|
+
// rather than leaving a client nobody can reach.
|
|
69
|
+
if (unmounted)
|
|
70
|
+
void result.deactivateAll();
|
|
71
|
+
});
|
|
72
|
+
return () => {
|
|
73
|
+
unmounted = true;
|
|
74
|
+
void activated?.deactivateAll();
|
|
75
|
+
};
|
|
76
|
+
}, [registry]);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=useWebsiteProviderScripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useWebsiteProviderScripts.js","sourceRoot":"","sources":["../../../../../src/core/external-providers/useWebsiteProviderScripts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EACL,4BAA4B,GAE7B,MAAM,8CAA8C,CAAC;AACtD,OAAO,EACL,8BAA8B,EAC9B,qBAAqB,GACtB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AAGvE,MAAM,UAAU,yBAAyB,CACvC,QAAqD;IAErD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,QAAQ,KAAK,WAAW;YAAE,OAAO;QAE5C,MAAM,OAAO,GAAG,8BAA8B,CAAC,QAAQ,CAAC,CAAC;QACzD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAEjC,qBAAqB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACzC,4EAA4E;QAC5E,2EAA2E;QAC3E,uEAAuE;QACvE,kEAAkE;IACpE,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CACpC,QAAqD;IAErD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO;QAEnC,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,IAAI,SAAoD,CAAC;QAEzD,KAAK,4BAA4B,CAAC;YAChC,OAAO,EAAE,eAAe,CAAC,cAAc;YACvC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE;YAClC,YAAY,EAAE,CAAC,UAAU,EAAE,EAAE;gBAC3B,wEAAwE;gBACxE,4DAA4D;gBAC5D,sCAAsC;gBACtC,OAAO,CAAC,IAAI,CAAC,wBAAwB,UAAU,CAAC,IAAI,KAAK,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC;YACjF,CAAC;SACF,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YACjB,SAAS,GAAG,MAAM,CAAC;YACnB,0EAA0E;YAC1E,yEAAyE;YACzE,iDAAiD;YACjD,IAAI,SAAS;gBAAE,KAAK,MAAM,CAAC,aAAa,EAAE,CAAC;QAC7C,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,IAAI,CAAC;YACjB,KAAK,SAAS,EAAE,aAAa,EAAE,CAAC;QAClC,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AACjB,CAAC","sourcesContent":["/**\n * The website's executable-surface hooks: load what the live providers declared.\n *\n * Two mechanisms, one per kind of vendor. `useWebsiteProviderScripts` adds the\n * `<script>` tags a provider declared — the case a dynamic `import()` cannot\n * serve, and the dominant one on a marketing site. `useWebsiteProviderSdks`\n * activates the npm SDK of any provider that ships one, which reaches this\n * bundle through the CODE channel.\n *\n * Both are effect-only, and that is not incidental: this is an Astro site, so\n * page modules execute in Node at build time. Injecting into a document that\n * does not exist would throw, and starting a vendor SDK there would run a live\n * analytics client inside a build.\n *\n * SDK activation comes from `@wildo-ai/external-connectors-public/website`, which\n * this package now depends on. The script injector beside it does NOT yet, and\n * that is a timing artifact rather than a decision: it belongs in the same\n * package so the technical doc can use one copy — a second copy of a DOM\n * mutation eventually disagrees about idempotence, which is how a vendor tag\n * gets loaded twice. Moving it needs that package's `dist` to carry it, and no\n * watcher is running in this tree; consolidating it is a one-step follow-up, not\n * a design question.\n */\n\nimport { useEffect } from 'react';\nimport {\n activateFrontendProviderSdks,\n type ActivatedFrontendProviderSdks,\n} from '@wildo-ai/external-connectors-public/website';\nimport {\n collectDeclaredProviderScripts,\n injectProviderScripts,\n} from './provider-scripts.website';\nimport { AppFrontendType } from '@wildo-ai/saas-models/public-runtime';\nimport type { FrontendWebsiteProviderRegistry } from './frontend-provider-registry.website';\n\nexport function useWebsiteProviderScripts(\n registry: FrontendWebsiteProviderRegistry | undefined,\n): void {\n useEffect(() => {\n if (typeof document === 'undefined') return;\n\n const scripts = collectDeclaredProviderScripts(registry);\n if (scripts.length === 0) return;\n\n injectProviderScripts(scripts, document);\n // Deliberately no teardown. A vendor tag installs a global and often starts\n // work immediately; removing the element undoes neither, so a cleanup that\n // removed it would only make the next mount inject a SECOND copy of an\n // already-running script. Idempotence by `src` is the real guard.\n }, [registry]);\n}\n\n/**\n * Loads and starts the npm SDK of every live provider that declares one.\n *\n * Unlike a script tag, an SDK activation IS undone on unmount where the vendor\n * offers a teardown — an analytics client that cannot be stopped cannot honour\n * a consent withdrawal, and accumulates one instance per view transition.\n */\nexport function useWebsiteProviderSdks(\n registry: FrontendWebsiteProviderRegistry | undefined,\n): void {\n useEffect(() => {\n if (registry === undefined) return;\n\n let unmounted = false;\n let activated: ActivatedFrontendProviderSdks | undefined;\n\n void activateFrontendProviderSdks({\n surface: AppFrontendType.STATIC_WEBSITE,\n modules: registry.modules.values(),\n onDiagnostic: (diagnostic) => {\n // The website runtime has no logger service, and a provider that cannot\n // start is a developer-facing fact that must not be silent.\n // eslint-disable-next-line no-console\n console.warn(`[wildo-provider-sdk] ${diagnostic.kind}: ${diagnostic.message}`);\n },\n }).then((result) => {\n activated = result;\n // Activation is asynchronous and unmount is not: if the surface went away\n // while a vendor bundle was still downloading, stop what we just started\n // rather than leaving a client nobody can reach.\n if (unmounted) void result.deactivateAll();\n });\n\n return () => {\n unmounted = true;\n void activated?.deactivateAll();\n };\n }, [registry]);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebsitePageLayout.d.ts","sourceRoot":"","sources":["../../../../../src/core/layouts/WebsitePageLayout.tsx"],"names":[],"mappings":"AAAA,OAAc,EAAW,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"WebsitePageLayout.d.ts","sourceRoot":"","sources":["../../../../../src/core/layouts/WebsitePageLayout.tsx"],"names":[],"mappings":"AAAA,OAAc,EAAW,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAWvD,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,qCAAqC,CAAC;AA6F7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,OAAO,EAAE,cAAc,GAAG,MAAM,CAAC;IACjC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAC1B;;OAEG;IACH,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IAC1B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,MAAM,EACN,MAAM,EACN,SAAS,EACT,aAAa,EACb,QAAQ,GACT,EAAE,sBAAsB,GAAG,SAAS,CA6HpC;yBApIe,iBAAiB;;;AAsIjC,gEAAgE"}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
3
|
import { BUILTIN_PROVIDER_CAPABILITY } from '@wildo-ai/saas-models/public-runtime';
|
|
4
4
|
import { WebsitePageContextProvider } from '../contexts/WebsitePageContext.js';
|
|
5
|
+
import { useWebsiteProviderScripts, useWebsiteProviderSdks, } from '../external-providers/useWebsiteProviderScripts.js';
|
|
5
6
|
import { useWebsiteRuntime } from '../contexts/useWebsiteRuntime.js';
|
|
6
7
|
import { useWebsiteLabelByKey } from '../hooks/useWebsiteLabel.js';
|
|
7
8
|
import { WebsitePageRefSchema, } from '../../schemas/refs/page-ref.schemas.js';
|
|
@@ -130,6 +131,12 @@ export function WebsitePageLayout({ pageRef, header, footer, className, mainClas
|
|
|
130
131
|
const FooterComponent = runtime.navigation.footerComponent;
|
|
131
132
|
return _jsx(FooterComponent, {});
|
|
132
133
|
}, [footer, runtime.navigation.footerComponent]);
|
|
134
|
+
// Load whatever the live providers declared. Until this landed, the surface
|
|
135
|
+
// resolved provider refs and stamped them into the attributes below and
|
|
136
|
+
// stopped there — a provider could be declared, enabled and hydrated without
|
|
137
|
+
// anything of it ever running.
|
|
138
|
+
useWebsiteProviderScripts(runtime.frontendProviderRegistry);
|
|
139
|
+
useWebsiteProviderSdks(runtime.frontendProviderRegistry);
|
|
133
140
|
const providerRootAttributes = useMemo(() => {
|
|
134
141
|
const attrs = {};
|
|
135
142
|
const productAnalyticsProviderRef = runtime.frontendProviderRegistry
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebsitePageLayout.js","sourceRoot":"","sources":["../../../../../src/core/layouts/WebsitePageLayout.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useMemo, type ReactNode } from 'react';\nimport { BUILTIN_PROVIDER_CAPABILITY } from '@wildo-ai/saas-models/public-runtime';\n\nimport { WebsitePageContextProvider } from '../contexts/WebsitePageContext';\nimport type { WebsitePageContextValue } from '../contexts/WebsitePageContext';\nimport { useWebsiteRuntime } from '../contexts/useWebsiteRuntime';\nimport { useWebsiteLabelByKey } from '../hooks/useWebsiteLabel';\nimport {\n WebsitePageRefSchema,\n type WebsitePageRef,\n} from '../../schemas/refs/page-ref.schemas';\n\n/**\n * Stable DOM id stamped on the `<main>` element so the framework's\n * skip-link `<a href=\"#main-content\">` can move keyboard focus past\n * the chrome on every page (Phase 6 audit fix M-α). Co-located here\n * (rather than exposed as a prop) because:\n * - The skip-link target is a framework concern — exposing it as a\n * consumer-tunable id would invite drift between the link's href\n * and the main element's id.\n * - Screen readers announce the heading inside `<main id=\"...\">` on\n * focus; a stable, framework-owned id keeps that affordance\n * deterministic across every page on every site.\n */\nconst MAIN_CONTENT_DOM_ID = 'main-content';\n\n/**\n * Inline style for the skip link's \"visually hidden by default,\n * visible on focus\" pattern. Inlined (NOT shipped as a CSS class)\n * because:\n * - The framework intentionally avoids shipping any CSS file the\n * consumer must opt in to — the only stylesheet emitted is the\n * `:root { --website-*: … }` design-token block, and the rest of\n * the styling surface lives in consumer code (see Phase 5\n * `renderWebsiteDesignTokensCss`). Inlining keeps the skip link\n * self-sufficient and side-effect-free.\n * - The \"skip-link\" pattern is a small, well-known a11y idiom; the\n * handful of style declarations here do not benefit from being\n * reorganized into a class with a custom-property hook.\n *\n * The \"focused\" variant uses BOTH `:focus` AND `:focus-visible`\n * pseudo-classes via a scoped `<style>` tag inside the layout output.\n * We cannot apply pseudo-class styles via a `style` attribute, but\n * emitting a tiny scoped block once at the layout root is safe (one\n * block per page render, ~120 bytes) and avoids importing a CSS-in-JS\n * runtime.\n *\n * **Why both selectors and not just `:focus-visible`** (Phase 6 fourth\n * audit fix M-Η): `:focus-visible` is the modern UA-heuristic-driven\n * selector that hides focus rings on mouse clicks, and it is the\n * RIGHT default for buttons / links. For a skip link though we WANT\n * the affordance to appear on EVERY focus event — the link is\n * invisible by default and a keyboard-only user (the only intended\n * audience) is the only one who can reach it. Older browsers that\n * predate `:focus-visible` (and Safari < 15.4 with the feature\n * disabled) would skip the rule entirely with a `:focus-visible`-only\n * declaration, leaving the keyboard user looking at an apparently\n * empty page when they pressed Tab. Pairing `:focus, :focus-visible`\n * is the documented WCAG 2.1 fallback pattern.\n */\nconst SKIP_LINK_HIDDEN_STYLE: React.CSSProperties = {\n position: 'absolute',\n top: 0,\n left: 0,\n width: '1px',\n height: '1px',\n padding: 0,\n margin: '-1px',\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * Single scoped style block for the skip link's `:focus-visible`\n * affordance. Targets the `data-website-skip-link` attribute (NOT a\n * class) so consumer Tailwind / utility classes cannot accidentally\n * shadow it. Stays trivially small to keep payload under the\n * \"framework rendering\" implicit budget.\n */\nconst SKIP_LINK_FOCUS_CSS = `\n[data-website-skip-link]:focus,\n[data-website-skip-link]:focus-visible {\n position: fixed !important;\n top: 0.5rem !important;\n left: 0.5rem !important;\n width: auto !important;\n height: auto !important;\n padding: 0.75rem 1rem !important;\n margin: 0 !important;\n overflow: visible !important;\n clip: auto !important;\n white-space: normal !important;\n background: var(--website-colors-background, #fff) !important;\n color: var(--website-colors-foreground, #000) !important;\n border: 2px solid var(--website-colors-primary, #000) !important;\n border-radius: var(--website-radii-md, 0.375rem) !important;\n z-index: 9999 !important;\n text-decoration: none !important;\n}\n`.trim();\n\n/**\n * @wildo_source:part:start saas.website.page-layout.component facet:layer:core facet:family:website\n *\n * `WebsitePageLayout` — the per-page structural primitive.\n *\n * **Three responsibilities** (each carrying its own semantic weight):\n *\n * 1. **Provides `WebsitePageContext`** — registers the active page\n * ref so child sections (`<WebsiteSection>`) and label hooks\n * (`useWebsiteLabel`) can derive label keys without re-typing\n * the page ref at every leaf.\n * 2. **Renders the site-wide chrome** — pulls\n * `headerComponent` and `footerComponent` from the runtime\n * navigation config and wraps the page's main content in the\n * site's standard header/main/footer scaffold. Per-page\n * overrides (`header={…}` / `footer={…}` props) take precedence\n * over the runtime defaults so individual pages can opt into a\n * custom chrome (eg. a campaign landing page running with\n * header-only minimal chrome).\n * 3. **Stamps DOM landmarks** — emits `data-website-page-ref` on\n * the layout root for analytics / SEO crawlers / overlay\n * tooling that needs to identify the active page from the DOM\n * without booting React. When the website facet hydrates\n * frontend-provider bootstraps, the same root also carries the\n * active public provider refs for analytics / browser-monitoring /\n * captcha so non-React tooling can detect the configured vendor\n * without reverse-engineering app code.\n *\n * **Why semantic `<header>` / `<main>` / `<footer>` elements**:\n * marketing-site SEO scoring penalizes pages that ship a single\n * unstructured `<div>` tree. Using semantic landmarks costs nothing\n * and improves crawler accessibility AND screen-reader\n * navigability — both are first-class concerns for a public\n * marketing surface.\n *\n * **Why this lives in `core/`** (not `components/`): the layout is\n * structural framework machinery (it owns the page-context\n * provisioning + the navigation slot resolution), not a styled\n * primitive. Designers override `header` / `footer` via runtime\n * config or per-page props, NOT by replacing this component.\n */\nexport interface WebsitePageLayoutProps {\n /**\n * The active page's ref. Accepts either an already-branded\n * `WebsitePageRef` or a raw string — the layout parses through\n * `WebsitePageRefSchema` so a malformed ref fails loud at render.\n *\n * Typically sourced from the page's `WebsitePageManifest.ref`\n * (eg. `<WebsitePageLayout pageRef={LANDING_PAGE_MANIFEST.ref}>`).\n */\n pageRef: WebsitePageRef | string;\n /**\n * Per-page header override. When supplied, takes precedence over\n * `WebsiteRuntimeContext.designTokens` navigation config's\n * `headerComponent`. Pass `null` explicitly to suppress the header\n * entirely (campaign landing pages without site-wide nav).\n */\n header?: ReactNode | null;\n /**\n * Per-page footer override. Same semantics as `header`.\n */\n footer?: ReactNode | null;\n /**\n * Optional className applied to the root `<div data-website-page-ref=\"…\">`\n * wrapper. Use for page-level layout tweaks (eg. `min-h-screen` on\n * landing pages).\n */\n className?: string;\n /**\n * Optional className applied to the inner `<main>` element. Use for\n * vertical-rhythm overrides on long-scroll landing pages.\n */\n mainClassName?: string;\n /**\n * The page's section composition. Typically a sequence of\n * `<WebsiteSection>` wrappers around concrete section components.\n */\n children: ReactNode;\n}\n\nexport function WebsitePageLayout({\n pageRef,\n header,\n footer,\n className,\n mainClassName,\n children,\n}: WebsitePageLayoutProps): ReactNode {\n const runtime = useWebsiteRuntime();\n\n /**\n * Defensive parse: callers should pass a `WebsitePageManifest.ref`\n * (already branded), but accepting raw strings + parsing here means\n * a hand-typed call site can't smuggle a malformed ref into the\n * label-key derivation chain.\n */\n const safePageRef = useMemo<WebsitePageRef>(\n () => WebsitePageRefSchema.parse(pageRef),\n [pageRef],\n );\n\n const pageContextValue = useMemo<WebsitePageContextValue>(\n () => ({ pageRef: safePageRef }),\n [safePageRef],\n );\n\n /**\n * Header resolution rules:\n * - `header === null` => explicit suppression\n * - `header !== undefined` => per-page override (any ReactNode)\n * - `header === undefined` => fall back to the runtime navigation\n * config's `headerComponent`\n *\n * The fallback is the common path: every page renders the same\n * site-wide chrome. The override path is the escape hatch (campaign\n * landing pages with custom header layouts).\n */\n const resolvedHeader = useMemo<ReactNode>(() => {\n if (header === null) return null;\n if (header !== undefined) return header;\n const HeaderComponent = runtime.navigation.headerComponent;\n return <HeaderComponent />;\n }, [header, runtime.navigation.headerComponent]);\n\n const resolvedFooter = useMemo<ReactNode>(() => {\n if (footer === null) return null;\n if (footer !== undefined) return footer;\n const FooterComponent = runtime.navigation.footerComponent;\n return <FooterComponent />;\n }, [footer, runtime.navigation.footerComponent]);\n\n const providerRootAttributes = useMemo<Record<string, string>>(() => {\n const attrs: Record<string, string> = {};\n const productAnalyticsProviderRef = runtime.frontendProviderRegistry\n ?.findProviderByProviderCapability(\n BUILTIN_PROVIDER_CAPABILITY.FRONTEND_PRODUCT_ANALYTICS,\n )\n ?.metadata.ref;\n const errorMonitoringProviderRef = runtime.frontendProviderRegistry\n ?.findProviderByProviderCapability(\n BUILTIN_PROVIDER_CAPABILITY.FRONTEND_ERROR_MONITORING,\n )\n ?.metadata.ref;\n const captchaProviderRef = runtime.frontendProviderRegistry\n ?.findProviderByProviderCapability(\n BUILTIN_PROVIDER_CAPABILITY.FRONTEND_CAPTCHA,\n )\n ?.metadata.ref;\n\n if (productAnalyticsProviderRef) {\n attrs['data-website-product-analytics-provider-ref'] = productAnalyticsProviderRef;\n }\n if (errorMonitoringProviderRef) {\n attrs['data-website-error-monitoring-provider-ref'] = errorMonitoringProviderRef;\n }\n if (captchaProviderRef) {\n attrs['data-website-captcha-provider-ref'] = captchaProviderRef;\n }\n\n return attrs;\n }, [runtime.frontendProviderRegistry]);\n\n /**\n * Resolved skip-link label. Reads through the runtime label pack\n * via the standard `useWebsiteLabelByKey` hook so a missing\n * translation surfaces the same diagnostic shape as any other\n * missing chrome label. The framework guarantees the key reaches\n * the bridge (see `pickLabelsForPage`) and the consumer's\n * label-pack validator (see `composeEffectiveChromeLabelKeys` in\n * `label-pack-loader.ts`) ensures it is present in every locale\n * pack — there is therefore no silent-fallback path that would\n * ship an English skip link to a non-EN visitor.\n */\n const skipLinkLabel = useWebsiteLabelByKey(runtime.skipToMainContentLabelKey);\n\n return (\n <WebsitePageContextProvider value={pageContextValue}>\n <div\n data-website-page-ref={safePageRef as unknown as string}\n className={className}\n {...providerRootAttributes}\n >\n {/*\n * Skip link rendered as the FIRST focusable element on the\n * page so a keyboard / AT user can bypass the chrome on every\n * navigation (WCAG 2.1 §2.4.1 \"Bypass Blocks\", Level A).\n * Visually hidden by default; revealed on focus via the\n * scoped `<style>` block below.\n */}\n <style>{SKIP_LINK_FOCUS_CSS}</style>\n <a\n href={`#${MAIN_CONTENT_DOM_ID}`}\n data-website-skip-link=\"\"\n style={SKIP_LINK_HIDDEN_STYLE}\n >\n {skipLinkLabel}\n </a>\n {resolvedHeader !== null ? <header>{resolvedHeader}</header> : null}\n <main id={MAIN_CONTENT_DOM_ID} className={mainClassName} tabIndex={-1}>\n {children}\n </main>\n {resolvedFooter !== null ? <footer>{resolvedFooter}</footer> : null}\n </div>\n </WebsitePageContextProvider>\n );\n}\nWebsitePageLayout.displayName = 'WebsitePageLayout';\n/** @wildo_source:part:end saas.website.page-layout.component */\n"]}
|
|
1
|
+
{"version":3,"file":"WebsitePageLayout.js","sourceRoot":"","sources":["../../../../../src/core/layouts/WebsitePageLayout.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useMemo, type ReactNode } from 'react';\nimport { BUILTIN_PROVIDER_CAPABILITY } from '@wildo-ai/saas-models/public-runtime';\n\nimport { WebsitePageContextProvider } from '../contexts/WebsitePageContext';\nimport {\n useWebsiteProviderScripts,\n useWebsiteProviderSdks,\n} from '../external-providers/useWebsiteProviderScripts';\nimport type { WebsitePageContextValue } from '../contexts/WebsitePageContext';\nimport { useWebsiteRuntime } from '../contexts/useWebsiteRuntime';\nimport { useWebsiteLabelByKey } from '../hooks/useWebsiteLabel';\nimport {\n WebsitePageRefSchema,\n type WebsitePageRef,\n} from '../../schemas/refs/page-ref.schemas';\n\n/**\n * Stable DOM id stamped on the `<main>` element so the framework's\n * skip-link `<a href=\"#main-content\">` can move keyboard focus past\n * the chrome on every page (Phase 6 audit fix M-α). Co-located here\n * (rather than exposed as a prop) because:\n * - The skip-link target is a framework concern — exposing it as a\n * consumer-tunable id would invite drift between the link's href\n * and the main element's id.\n * - Screen readers announce the heading inside `<main id=\"...\">` on\n * focus; a stable, framework-owned id keeps that affordance\n * deterministic across every page on every site.\n */\nconst MAIN_CONTENT_DOM_ID = 'main-content';\n\n/**\n * Inline style for the skip link's \"visually hidden by default,\n * visible on focus\" pattern. Inlined (NOT shipped as a CSS class)\n * because:\n * - The framework intentionally avoids shipping any CSS file the\n * consumer must opt in to — the only stylesheet emitted is the\n * `:root { --website-*: … }` design-token block, and the rest of\n * the styling surface lives in consumer code (see Phase 5\n * `renderWebsiteDesignTokensCss`). Inlining keeps the skip link\n * self-sufficient and side-effect-free.\n * - The \"skip-link\" pattern is a small, well-known a11y idiom; the\n * handful of style declarations here do not benefit from being\n * reorganized into a class with a custom-property hook.\n *\n * The \"focused\" variant uses BOTH `:focus` AND `:focus-visible`\n * pseudo-classes via a scoped `<style>` tag inside the layout output.\n * We cannot apply pseudo-class styles via a `style` attribute, but\n * emitting a tiny scoped block once at the layout root is safe (one\n * block per page render, ~120 bytes) and avoids importing a CSS-in-JS\n * runtime.\n *\n * **Why both selectors and not just `:focus-visible`** (Phase 6 fourth\n * audit fix M-Η): `:focus-visible` is the modern UA-heuristic-driven\n * selector that hides focus rings on mouse clicks, and it is the\n * RIGHT default for buttons / links. For a skip link though we WANT\n * the affordance to appear on EVERY focus event — the link is\n * invisible by default and a keyboard-only user (the only intended\n * audience) is the only one who can reach it. Older browsers that\n * predate `:focus-visible` (and Safari < 15.4 with the feature\n * disabled) would skip the rule entirely with a `:focus-visible`-only\n * declaration, leaving the keyboard user looking at an apparently\n * empty page when they pressed Tab. Pairing `:focus, :focus-visible`\n * is the documented WCAG 2.1 fallback pattern.\n */\nconst SKIP_LINK_HIDDEN_STYLE: React.CSSProperties = {\n position: 'absolute',\n top: 0,\n left: 0,\n width: '1px',\n height: '1px',\n padding: 0,\n margin: '-1px',\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * Single scoped style block for the skip link's `:focus-visible`\n * affordance. Targets the `data-website-skip-link` attribute (NOT a\n * class) so consumer Tailwind / utility classes cannot accidentally\n * shadow it. Stays trivially small to keep payload under the\n * \"framework rendering\" implicit budget.\n */\nconst SKIP_LINK_FOCUS_CSS = `\n[data-website-skip-link]:focus,\n[data-website-skip-link]:focus-visible {\n position: fixed !important;\n top: 0.5rem !important;\n left: 0.5rem !important;\n width: auto !important;\n height: auto !important;\n padding: 0.75rem 1rem !important;\n margin: 0 !important;\n overflow: visible !important;\n clip: auto !important;\n white-space: normal !important;\n background: var(--website-colors-background, #fff) !important;\n color: var(--website-colors-foreground, #000) !important;\n border: 2px solid var(--website-colors-primary, #000) !important;\n border-radius: var(--website-radii-md, 0.375rem) !important;\n z-index: 9999 !important;\n text-decoration: none !important;\n}\n`.trim();\n\n/**\n * @wildo_source:part:start saas.website.page-layout.component facet:layer:core facet:family:website\n *\n * `WebsitePageLayout` — the per-page structural primitive.\n *\n * **Three responsibilities** (each carrying its own semantic weight):\n *\n * 1. **Provides `WebsitePageContext`** — registers the active page\n * ref so child sections (`<WebsiteSection>`) and label hooks\n * (`useWebsiteLabel`) can derive label keys without re-typing\n * the page ref at every leaf.\n * 2. **Renders the site-wide chrome** — pulls\n * `headerComponent` and `footerComponent` from the runtime\n * navigation config and wraps the page's main content in the\n * site's standard header/main/footer scaffold. Per-page\n * overrides (`header={…}` / `footer={…}` props) take precedence\n * over the runtime defaults so individual pages can opt into a\n * custom chrome (eg. a campaign landing page running with\n * header-only minimal chrome).\n * 3. **Stamps DOM landmarks** — emits `data-website-page-ref` on\n * the layout root for analytics / SEO crawlers / overlay\n * tooling that needs to identify the active page from the DOM\n * without booting React. When the website facet hydrates\n * frontend-provider bootstraps, the same root also carries the\n * active public provider refs for analytics / browser-monitoring /\n * captcha so non-React tooling can detect the configured vendor\n * without reverse-engineering app code.\n *\n * **Why semantic `<header>` / `<main>` / `<footer>` elements**:\n * marketing-site SEO scoring penalizes pages that ship a single\n * unstructured `<div>` tree. Using semantic landmarks costs nothing\n * and improves crawler accessibility AND screen-reader\n * navigability — both are first-class concerns for a public\n * marketing surface.\n *\n * **Why this lives in `core/`** (not `components/`): the layout is\n * structural framework machinery (it owns the page-context\n * provisioning + the navigation slot resolution), not a styled\n * primitive. Designers override `header` / `footer` via runtime\n * config or per-page props, NOT by replacing this component.\n */\nexport interface WebsitePageLayoutProps {\n /**\n * The active page's ref. Accepts either an already-branded\n * `WebsitePageRef` or a raw string — the layout parses through\n * `WebsitePageRefSchema` so a malformed ref fails loud at render.\n *\n * Typically sourced from the page's `WebsitePageManifest.ref`\n * (eg. `<WebsitePageLayout pageRef={LANDING_PAGE_MANIFEST.ref}>`).\n */\n pageRef: WebsitePageRef | string;\n /**\n * Per-page header override. When supplied, takes precedence over\n * `WebsiteRuntimeContext.designTokens` navigation config's\n * `headerComponent`. Pass `null` explicitly to suppress the header\n * entirely (campaign landing pages without site-wide nav).\n */\n header?: ReactNode | null;\n /**\n * Per-page footer override. Same semantics as `header`.\n */\n footer?: ReactNode | null;\n /**\n * Optional className applied to the root `<div data-website-page-ref=\"…\">`\n * wrapper. Use for page-level layout tweaks (eg. `min-h-screen` on\n * landing pages).\n */\n className?: string;\n /**\n * Optional className applied to the inner `<main>` element. Use for\n * vertical-rhythm overrides on long-scroll landing pages.\n */\n mainClassName?: string;\n /**\n * The page's section composition. Typically a sequence of\n * `<WebsiteSection>` wrappers around concrete section components.\n */\n children: ReactNode;\n}\n\nexport function WebsitePageLayout({\n pageRef,\n header,\n footer,\n className,\n mainClassName,\n children,\n}: WebsitePageLayoutProps): ReactNode {\n const runtime = useWebsiteRuntime();\n\n /**\n * Defensive parse: callers should pass a `WebsitePageManifest.ref`\n * (already branded), but accepting raw strings + parsing here means\n * a hand-typed call site can't smuggle a malformed ref into the\n * label-key derivation chain.\n */\n const safePageRef = useMemo<WebsitePageRef>(\n () => WebsitePageRefSchema.parse(pageRef),\n [pageRef],\n );\n\n const pageContextValue = useMemo<WebsitePageContextValue>(\n () => ({ pageRef: safePageRef }),\n [safePageRef],\n );\n\n /**\n * Header resolution rules:\n * - `header === null` => explicit suppression\n * - `header !== undefined` => per-page override (any ReactNode)\n * - `header === undefined` => fall back to the runtime navigation\n * config's `headerComponent`\n *\n * The fallback is the common path: every page renders the same\n * site-wide chrome. The override path is the escape hatch (campaign\n * landing pages with custom header layouts).\n */\n const resolvedHeader = useMemo<ReactNode>(() => {\n if (header === null) return null;\n if (header !== undefined) return header;\n const HeaderComponent = runtime.navigation.headerComponent;\n return <HeaderComponent />;\n }, [header, runtime.navigation.headerComponent]);\n\n const resolvedFooter = useMemo<ReactNode>(() => {\n if (footer === null) return null;\n if (footer !== undefined) return footer;\n const FooterComponent = runtime.navigation.footerComponent;\n return <FooterComponent />;\n }, [footer, runtime.navigation.footerComponent]);\n\n // Load whatever the live providers declared. Until this landed, the surface\n // resolved provider refs and stamped them into the attributes below and\n // stopped there — a provider could be declared, enabled and hydrated without\n // anything of it ever running.\n useWebsiteProviderScripts(runtime.frontendProviderRegistry);\n useWebsiteProviderSdks(runtime.frontendProviderRegistry);\n\n const providerRootAttributes = useMemo<Record<string, string>>(() => {\n const attrs: Record<string, string> = {};\n const productAnalyticsProviderRef = runtime.frontendProviderRegistry\n ?.findProviderByProviderCapability(\n BUILTIN_PROVIDER_CAPABILITY.FRONTEND_PRODUCT_ANALYTICS,\n )\n ?.metadata.ref;\n const errorMonitoringProviderRef = runtime.frontendProviderRegistry\n ?.findProviderByProviderCapability(\n BUILTIN_PROVIDER_CAPABILITY.FRONTEND_ERROR_MONITORING,\n )\n ?.metadata.ref;\n const captchaProviderRef = runtime.frontendProviderRegistry\n ?.findProviderByProviderCapability(\n BUILTIN_PROVIDER_CAPABILITY.FRONTEND_CAPTCHA,\n )\n ?.metadata.ref;\n\n if (productAnalyticsProviderRef) {\n attrs['data-website-product-analytics-provider-ref'] = productAnalyticsProviderRef;\n }\n if (errorMonitoringProviderRef) {\n attrs['data-website-error-monitoring-provider-ref'] = errorMonitoringProviderRef;\n }\n if (captchaProviderRef) {\n attrs['data-website-captcha-provider-ref'] = captchaProviderRef;\n }\n\n return attrs;\n }, [runtime.frontendProviderRegistry]);\n\n /**\n * Resolved skip-link label. Reads through the runtime label pack\n * via the standard `useWebsiteLabelByKey` hook so a missing\n * translation surfaces the same diagnostic shape as any other\n * missing chrome label. The framework guarantees the key reaches\n * the bridge (see `pickLabelsForPage`) and the consumer's\n * label-pack validator (see `composeEffectiveChromeLabelKeys` in\n * `label-pack-loader.ts`) ensures it is present in every locale\n * pack — there is therefore no silent-fallback path that would\n * ship an English skip link to a non-EN visitor.\n */\n const skipLinkLabel = useWebsiteLabelByKey(runtime.skipToMainContentLabelKey);\n\n return (\n <WebsitePageContextProvider value={pageContextValue}>\n <div\n data-website-page-ref={safePageRef as unknown as string}\n className={className}\n {...providerRootAttributes}\n >\n {/*\n * Skip link rendered as the FIRST focusable element on the\n * page so a keyboard / AT user can bypass the chrome on every\n * navigation (WCAG 2.1 §2.4.1 \"Bypass Blocks\", Level A).\n * Visually hidden by default; revealed on focus via the\n * scoped `<style>` block below.\n */}\n <style>{SKIP_LINK_FOCUS_CSS}</style>\n <a\n href={`#${MAIN_CONTENT_DOM_ID}`}\n data-website-skip-link=\"\"\n style={SKIP_LINK_HIDDEN_STYLE}\n >\n {skipLinkLabel}\n </a>\n {resolvedHeader !== null ? <header>{resolvedHeader}</header> : null}\n <main id={MAIN_CONTENT_DOM_ID} className={mainClassName} tabIndex={-1}>\n {children}\n </main>\n {resolvedFooter !== null ? <footer>{resolvedFooter}</footer> : null}\n </div>\n </WebsitePageContextProvider>\n );\n}\nWebsitePageLayout.displayName = 'WebsitePageLayout';\n/** @wildo_source:part:end saas.website.page-layout.component */\n"]}
|
|
@@ -7,33 +7,31 @@ import { z } from 'zod';
|
|
|
7
7
|
*
|
|
8
8
|
* The category is NOT a layout-shape constraint and NOT a CSS class; the
|
|
9
9
|
* framework ships zero concrete sections and never inspects
|
|
10
|
-
* the category at render time. The category exists for
|
|
10
|
+
* the category at render time. The category exists for two orthogonal
|
|
11
11
|
* non-render concerns:
|
|
12
12
|
*
|
|
13
|
-
* 1. **
|
|
14
|
-
* SEO library reads `WebsiteSectionDefinition.category` to decide
|
|
15
|
-
* which JSON-LD vocabulary the section's `getStructuredData()` hook
|
|
16
|
-
* is allowed to emit. e.g. `PRICING` may emit `Product` /
|
|
17
|
-
* `AggregateOffer`; `FAQ` may emit `FAQPage`; `HERO` may emit
|
|
18
|
-
* `Organization` / `WebSite`. Mismatches are surfaced as build-time
|
|
19
|
-
* warnings.
|
|
20
|
-
* 2. **Authoring guidance**: each value has matching application-builder
|
|
13
|
+
* 1. **Authoring guidance**: each value has matching application-builder
|
|
21
14
|
* guidance. When introducing a new category, provide its public authoring
|
|
22
15
|
* method before asking an application to use it.
|
|
23
|
-
*
|
|
16
|
+
* 2. **Analytics labelling**: the runtime emits
|
|
24
17
|
* `data-section-category="…"` on the `<WebsiteSection>` wrapper so
|
|
25
18
|
* analytics tooling can group section impressions
|
|
26
19
|
* without parsing className conventions.
|
|
27
20
|
*
|
|
21
|
+
* **It does NOT gate structured data.** Nothing dispatches JSON-LD by category: the structured-data
|
|
22
|
+
* renderer receives only `sectionRef` and `getStructuredData`, the reconciliation contract carries
|
|
23
|
+
* no category at all, and no build-time warning compares the two. A section may return any
|
|
24
|
+
* vocabulary from `getStructuredData()` whatever its category says, so choose the category for what
|
|
25
|
+
* it does do (guidance and analytics grouping) and validate your JSON-LD yourself.
|
|
26
|
+
*
|
|
28
27
|
* **Closed vocabulary on purpose**: a free-string `category` would
|
|
29
|
-
* defeat the skill
|
|
30
|
-
* need a wholly-novel section shape use the `RAW` advanced path
|
|
28
|
+
* defeat the skill mapping and would make the analytics attribute unaggregatable.
|
|
29
|
+
* Consumers who need a wholly-novel section shape use the `RAW` advanced path
|
|
31
30
|
* (`website-factory-advanced` skill) which bypasses the category
|
|
32
31
|
* vocabulary entirely.
|
|
33
32
|
*
|
|
34
|
-
* **Adding a value**: requires (a) a new entry here
|
|
35
|
-
* `website_section_<value>` skill stub
|
|
36
|
-
* structured-data dispatch entry in the SEO library. Removing a value
|
|
33
|
+
* **Adding a value**: requires (a) a new entry here and (b) a matching
|
|
34
|
+
* `website_section_<value>` skill stub. Removing a value
|
|
37
35
|
* is a labelled rename — every consumer's
|
|
38
36
|
* `defineWebsiteSection({ category: … })` declaration breaks at compile
|
|
39
37
|
* time, which is the desired loud failure.
|
|
@@ -50,11 +48,12 @@ export declare enum WebsiteSectionCategory {
|
|
|
50
48
|
FOOTER_CTA = "footer-cta",
|
|
51
49
|
/**
|
|
52
50
|
* Escape hatch for sections that don't fit any standard category
|
|
53
|
-
* (third-party widgets, custom one-off marketing pieces).
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
51
|
+
* (third-party widgets, custom one-off marketing pieces). Authors should
|
|
52
|
+
* prefer a concrete category whenever possible, so the section receives the
|
|
53
|
+
* matching authoring guidance and groups with its peers in analytics.
|
|
54
|
+
*
|
|
55
|
+
* It makes no difference to structured data: no category validates
|
|
56
|
+
* `getStructuredData()`, so `RAW` opts out of nothing there.
|
|
58
57
|
*/
|
|
59
58
|
RAW = "raw"
|
|
60
59
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"website-section-category.shared.d.ts","sourceRoot":"","sources":["../../../../../src/schemas/sections/website-section-category.shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB
|
|
1
|
+
{"version":3,"file":"website-section-category.shared.d.ts","sourceRoot":"","sources":["../../../../../src/schemas/sections/website-section-category.shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,oBAAY,sBAAsB;IAChC,IAAI,SAAS;IACb,GAAG,QAAQ;IACX,uBAAuB,4BAA4B;IACnD,cAAc,mBAAmB;IACjC,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,YAAY,iBAAiB;IAC7B,GAAG,QAAQ;IACX,UAAU,eAAe;IACzB;;;;;;;;OAQG;IACH,GAAG,QAAQ;CACZ;AAED,eAAO,MAAM,4BAA4B,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,sBAAsB,CAClD,CAAC;AACjC,iEAAiE"}
|
|
@@ -7,33 +7,31 @@ import { z } from 'zod';
|
|
|
7
7
|
*
|
|
8
8
|
* The category is NOT a layout-shape constraint and NOT a CSS class; the
|
|
9
9
|
* framework ships zero concrete sections and never inspects
|
|
10
|
-
* the category at render time. The category exists for
|
|
10
|
+
* the category at render time. The category exists for two orthogonal
|
|
11
11
|
* non-render concerns:
|
|
12
12
|
*
|
|
13
|
-
* 1. **
|
|
14
|
-
* SEO library reads `WebsiteSectionDefinition.category` to decide
|
|
15
|
-
* which JSON-LD vocabulary the section's `getStructuredData()` hook
|
|
16
|
-
* is allowed to emit. e.g. `PRICING` may emit `Product` /
|
|
17
|
-
* `AggregateOffer`; `FAQ` may emit `FAQPage`; `HERO` may emit
|
|
18
|
-
* `Organization` / `WebSite`. Mismatches are surfaced as build-time
|
|
19
|
-
* warnings.
|
|
20
|
-
* 2. **Authoring guidance**: each value has matching application-builder
|
|
13
|
+
* 1. **Authoring guidance**: each value has matching application-builder
|
|
21
14
|
* guidance. When introducing a new category, provide its public authoring
|
|
22
15
|
* method before asking an application to use it.
|
|
23
|
-
*
|
|
16
|
+
* 2. **Analytics labelling**: the runtime emits
|
|
24
17
|
* `data-section-category="…"` on the `<WebsiteSection>` wrapper so
|
|
25
18
|
* analytics tooling can group section impressions
|
|
26
19
|
* without parsing className conventions.
|
|
27
20
|
*
|
|
21
|
+
* **It does NOT gate structured data.** Nothing dispatches JSON-LD by category: the structured-data
|
|
22
|
+
* renderer receives only `sectionRef` and `getStructuredData`, the reconciliation contract carries
|
|
23
|
+
* no category at all, and no build-time warning compares the two. A section may return any
|
|
24
|
+
* vocabulary from `getStructuredData()` whatever its category says, so choose the category for what
|
|
25
|
+
* it does do (guidance and analytics grouping) and validate your JSON-LD yourself.
|
|
26
|
+
*
|
|
28
27
|
* **Closed vocabulary on purpose**: a free-string `category` would
|
|
29
|
-
* defeat the skill
|
|
30
|
-
* need a wholly-novel section shape use the `RAW` advanced path
|
|
28
|
+
* defeat the skill mapping and would make the analytics attribute unaggregatable.
|
|
29
|
+
* Consumers who need a wholly-novel section shape use the `RAW` advanced path
|
|
31
30
|
* (`website-factory-advanced` skill) which bypasses the category
|
|
32
31
|
* vocabulary entirely.
|
|
33
32
|
*
|
|
34
|
-
* **Adding a value**: requires (a) a new entry here
|
|
35
|
-
* `website_section_<value>` skill stub
|
|
36
|
-
* structured-data dispatch entry in the SEO library. Removing a value
|
|
33
|
+
* **Adding a value**: requires (a) a new entry here and (b) a matching
|
|
34
|
+
* `website_section_<value>` skill stub. Removing a value
|
|
37
35
|
* is a labelled rename — every consumer's
|
|
38
36
|
* `defineWebsiteSection({ category: … })` declaration breaks at compile
|
|
39
37
|
* time, which is the desired loud failure.
|
|
@@ -51,11 +49,12 @@ export var WebsiteSectionCategory;
|
|
|
51
49
|
WebsiteSectionCategory["FOOTER_CTA"] = "footer-cta";
|
|
52
50
|
/**
|
|
53
51
|
* Escape hatch for sections that don't fit any standard category
|
|
54
|
-
* (third-party widgets, custom one-off marketing pieces).
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
52
|
+
* (third-party widgets, custom one-off marketing pieces). Authors should
|
|
53
|
+
* prefer a concrete category whenever possible, so the section receives the
|
|
54
|
+
* matching authoring guidance and groups with its peers in analytics.
|
|
55
|
+
*
|
|
56
|
+
* It makes no difference to structured data: no category validates
|
|
57
|
+
* `getStructuredData()`, so `RAW` opts out of nothing there.
|
|
59
58
|
*/
|
|
60
59
|
WebsiteSectionCategory["RAW"] = "raw";
|
|
61
60
|
})(WebsiteSectionCategory || (WebsiteSectionCategory = {}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"website-section-category.shared.js","sourceRoot":"","sources":["../../../../../src/schemas/sections/website-section-category.shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB
|
|
1
|
+
{"version":3,"file":"website-section-category.shared.js","sourceRoot":"","sources":["../../../../../src/schemas/sections/website-section-category.shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,CAAN,IAAY,sBAoBX;AApBD,WAAY,sBAAsB;IAChC,uCAAa,CAAA;IACb,qCAAW,CAAA;IACX,6EAAmD,CAAA;IACnD,2DAAiC,CAAA;IACjC,6CAAmB,CAAA;IACnB,6CAAmB,CAAA;IACnB,uDAA6B,CAAA;IAC7B,qCAAW,CAAA;IACX,mDAAyB,CAAA;IACzB;;;;;;;;OAQG;IACH,qCAAW,CAAA;AACb,CAAC,EApBW,sBAAsB,KAAtB,sBAAsB,QAoBjC;AAED,MAAM,CAAC,MAAM,4BAA4B,GACvC,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;AACjC,iEAAiE","sourcesContent":["import { z } from 'zod';\n\n/**\n * @wildo_source:part:start saas.website.sections.category-enum facet:layer:shared facet:family:website\n *\n * `WebsiteSectionCategory` — closed vocabulary tagging the **structural\n * intent** of a section.\n *\n * The category is NOT a layout-shape constraint and NOT a CSS class; the\n * framework ships zero concrete sections and never inspects\n * the category at render time. The category exists for two orthogonal\n * non-render concerns:\n *\n * 1. **Authoring guidance**: each value has matching application-builder\n * guidance. When introducing a new category, provide its public authoring\n * method before asking an application to use it.\n * 2. **Analytics labelling**: the runtime emits\n * `data-section-category=\"…\"` on the `<WebsiteSection>` wrapper so\n * analytics tooling can group section impressions\n * without parsing className conventions.\n *\n * **It does NOT gate structured data.** Nothing dispatches JSON-LD by category: the structured-data\n * renderer receives only `sectionRef` and `getStructuredData`, the reconciliation contract carries\n * no category at all, and no build-time warning compares the two. A section may return any\n * vocabulary from `getStructuredData()` whatever its category says, so choose the category for what\n * it does do (guidance and analytics grouping) and validate your JSON-LD yourself.\n *\n * **Closed vocabulary on purpose**: a free-string `category` would\n * defeat the skill mapping and would make the analytics attribute unaggregatable.\n * Consumers who need a wholly-novel section shape use the `RAW` advanced path\n * (`website-factory-advanced` skill) which bypasses the category\n * vocabulary entirely.\n *\n * **Adding a value**: requires (a) a new entry here and (b) a matching\n * `website_section_<value>` skill stub. Removing a value\n * is a labelled rename — every consumer's\n * `defineWebsiteSection({ category: … })` declaration breaks at compile\n * time, which is the desired loud failure.\n */\nexport enum WebsiteSectionCategory {\n HERO = 'hero',\n CTA = 'cta',\n FEATURE_BENEFIT_SUMMARY = 'feature-benefit-summary',\n FEATURE_DETAIL = 'feature-detail',\n PRICING = 'pricing',\n CONTACT = 'contact',\n SOCIAL_PROOF = 'social-proof',\n FAQ = 'faq',\n FOOTER_CTA = 'footer-cta',\n /**\n * Escape hatch for sections that don't fit any standard category\n * (third-party widgets, custom one-off marketing pieces). Authors should\n * prefer a concrete category whenever possible, so the section receives the\n * matching authoring guidance and groups with its peers in analytics.\n *\n * It makes no difference to structured data: no category validates\n * `getStructuredData()`, so `RAW` opts out of nothing there.\n */\n RAW = 'raw',\n}\n\nexport const WebsiteSectionCategorySchema: z.ZodEnum<typeof WebsiteSectionCategory> =\n z.enum(WebsiteSectionCategory);\n/** @wildo_source:part:end saas.website.sections.category-enum */\n"]}
|