@wildo-ai/saas-website 1.1.1
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/LICENSE +34 -0
- package/dist/esm/.builder.pid +9 -0
- package/dist/esm/astro/blog-post-bridge.d.ts +324 -0
- package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -0
- package/dist/esm/astro/blog-post-bridge.js +57 -0
- package/dist/esm/astro/blog-post-bridge.js.map +1 -0
- package/dist/esm/astro/blog-post-head.renderer.d.ts +129 -0
- package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -0
- package/dist/esm/astro/blog-post-head.renderer.js +139 -0
- package/dist/esm/astro/blog-post-head.renderer.js.map +1 -0
- package/dist/esm/astro/bridge-runtime.d.ts +224 -0
- package/dist/esm/astro/bridge-runtime.d.ts.map +1 -0
- package/dist/esm/astro/bridge-runtime.js +30 -0
- package/dist/esm/astro/bridge-runtime.js.map +1 -0
- package/dist/esm/astro/collect-expected-label-keys.d.ts +65 -0
- package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -0
- package/dist/esm/astro/collect-expected-label-keys.js +101 -0
- package/dist/esm/astro/collect-expected-label-keys.js.map +1 -0
- package/dist/esm/astro/headers-renderer.d.ts +32 -0
- package/dist/esm/astro/headers-renderer.d.ts.map +1 -0
- package/dist/esm/astro/headers-renderer.js +64 -0
- package/dist/esm/astro/headers-renderer.js.map +1 -0
- package/dist/esm/astro/i18n-routing.helper.d.ts +91 -0
- package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -0
- package/dist/esm/astro/i18n-routing.helper.js +23 -0
- package/dist/esm/astro/i18n-routing.helper.js.map +1 -0
- package/dist/esm/astro/internal/html-escape.d.ts +31 -0
- package/dist/esm/astro/internal/html-escape.d.ts.map +1 -0
- package/dist/esm/astro/internal/html-escape.js +40 -0
- package/dist/esm/astro/internal/html-escape.js.map +1 -0
- package/dist/esm/astro/label-pack-loader.d.ts +134 -0
- package/dist/esm/astro/label-pack-loader.d.ts.map +1 -0
- package/dist/esm/astro/label-pack-loader.js +298 -0
- package/dist/esm/astro/label-pack-loader.js.map +1 -0
- package/dist/esm/astro/llms-txt-server.d.ts +28 -0
- package/dist/esm/astro/llms-txt-server.d.ts.map +1 -0
- package/dist/esm/astro/llms-txt-server.js +33 -0
- package/dist/esm/astro/llms-txt-server.js.map +1 -0
- package/dist/esm/astro/pick-labels-for-page.d.ts +63 -0
- package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -0
- package/dist/esm/astro/pick-labels-for-page.js +73 -0
- package/dist/esm/astro/pick-labels-for-page.js.map +1 -0
- package/dist/esm/astro/robots-renderer.d.ts +85 -0
- package/dist/esm/astro/robots-renderer.d.ts.map +1 -0
- package/dist/esm/astro/robots-renderer.js +157 -0
- package/dist/esm/astro/robots-renderer.js.map +1 -0
- package/dist/esm/astro/sitemap-coverage.d.ts +101 -0
- package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -0
- package/dist/esm/astro/sitemap-coverage.js +67 -0
- package/dist/esm/astro/sitemap-coverage.js.map +1 -0
- package/dist/esm/astro/structured-data.renderer.d.ts +21 -0
- package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -0
- package/dist/esm/astro/structured-data.renderer.js +142 -0
- package/dist/esm/astro/structured-data.renderer.js.map +1 -0
- package/dist/esm/astro/website-page-head.renderer.d.ts +93 -0
- package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -0
- package/dist/esm/astro/website-page-head.renderer.js +184 -0
- package/dist/esm/astro/website-page-head.renderer.js.map +1 -0
- package/dist/esm/astro/website-page-runtime.helper.d.ts +112 -0
- package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -0
- package/dist/esm/astro/website-page-runtime.helper.js +60 -0
- package/dist/esm/astro/website-page-runtime.helper.js.map +1 -0
- package/dist/esm/astro/website-site-context.d.ts +126 -0
- package/dist/esm/astro/website-site-context.d.ts.map +1 -0
- package/dist/esm/astro/website-site-context.js +37 -0
- package/dist/esm/astro/website-site-context.js.map +1 -0
- package/dist/esm/astro-island.d.ts +56 -0
- package/dist/esm/astro-island.d.ts.map +1 -0
- package/dist/esm/astro-island.js +61 -0
- package/dist/esm/astro-island.js.map +1 -0
- package/dist/esm/astro.d.ts +151 -0
- package/dist/esm/astro.d.ts.map +1 -0
- package/dist/esm/astro.js +151 -0
- package/dist/esm/astro.js.map +1 -0
- package/dist/esm/companion-exports.d.ts +38 -0
- package/dist/esm/companion-exports.d.ts.map +1 -0
- package/dist/esm/companion-exports.js +38 -0
- package/dist/esm/companion-exports.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteButton.d.ts +73 -0
- package/dist/esm/components/low-level/WebsiteButton.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteButton.js +68 -0
- package/dist/esm/components/low-level/WebsiteButton.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteHeading.d.ts +78 -0
- package/dist/esm/components/low-level/WebsiteHeading.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteHeading.js +41 -0
- package/dist/esm/components/low-level/WebsiteHeading.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteImage.d.ts +73 -0
- package/dist/esm/components/low-level/WebsiteImage.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteImage.js +21 -0
- package/dist/esm/components/low-level/WebsiteImage.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.d.ts +90 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.js +47 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.d.ts +99 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.js +68 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteLink.d.ts +50 -0
- package/dist/esm/components/low-level/WebsiteLink.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteLink.js +22 -0
- package/dist/esm/components/low-level/WebsiteLink.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteText.d.ts +57 -0
- package/dist/esm/components/low-level/WebsiteText.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteText.js +57 -0
- package/dist/esm/components/low-level/WebsiteText.js.map +1 -0
- package/dist/esm/config/define-website-config.d.ts +83 -0
- package/dist/esm/config/define-website-config.d.ts.map +1 -0
- package/dist/esm/config/define-website-config.js +126 -0
- package/dist/esm/config/define-website-config.js.map +1 -0
- package/dist/esm/config/index.d.ts +38 -0
- package/dist/esm/config/index.d.ts.map +1 -0
- package/dist/esm/config/index.js +38 -0
- package/dist/esm/config/index.js.map +1 -0
- package/dist/esm/config/load-website-config.d.ts +136 -0
- package/dist/esm/config/load-website-config.d.ts.map +1 -0
- package/dist/esm/config/load-website-config.js +177 -0
- package/dist/esm/config/load-website-config.js.map +1 -0
- package/dist/esm/config/wildo-website-config.schemas.d.ts +251 -0
- package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -0
- package/dist/esm/config/wildo-website-config.schemas.js +302 -0
- package/dist/esm/config/wildo-website-config.schemas.js.map +1 -0
- package/dist/esm/config-loader.d.ts +35 -0
- package/dist/esm/config-loader.d.ts.map +1 -0
- package/dist/esm/config-loader.js +35 -0
- package/dist/esm/config-loader.js.map +1 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +38 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.js +77 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +41 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +58 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +81 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +177 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -0
- package/dist/esm/core/contexts/WebsitePageContext.d.ts +40 -0
- package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -0
- package/dist/esm/core/contexts/WebsitePageContext.js +8 -0
- package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +162 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.js +22 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.d.ts +47 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.js +8 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -0
- package/dist/esm/core/contexts/useWebsitePage.d.ts +13 -0
- package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -0
- package/dist/esm/core/contexts/useWebsitePage.js +22 -0
- package/dist/esm/core/contexts/useWebsitePage.js.map +1 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +19 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -0
- package/dist/esm/core/contexts/useWebsiteSection.d.ts +12 -0
- package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -0
- package/dist/esm/core/contexts/useWebsiteSection.js +22 -0
- package/dist/esm/core/contexts/useWebsiteSection.js.map +1 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +17 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.js +23 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -0
- package/dist/esm/core/factories/define-website-page-manifest.d.ts +70 -0
- package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -0
- package/dist/esm/core/factories/define-website-page-manifest.js +51 -0
- package/dist/esm/core/factories/define-website-page-manifest.js.map +1 -0
- package/dist/esm/core/factories/define-website-section.d.ts +101 -0
- package/dist/esm/core/factories/define-website-section.d.ts.map +1 -0
- package/dist/esm/core/factories/define-website-section.js +71 -0
- package/dist/esm/core/factories/define-website-section.js.map +1 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts +23 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.js +25 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.js.map +1 -0
- package/dist/esm/core/hooks/useWebsiteLabel.d.ts +68 -0
- package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -0
- package/dist/esm/core/hooks/useWebsiteLabel.js +103 -0
- package/dist/esm/core/hooks/useWebsiteLabel.js.map +1 -0
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts +87 -0
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -0
- package/dist/esm/core/layouts/WebsitePageLayout.js +170 -0
- package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -0
- package/dist/esm/core/layouts/WebsiteSection.d.ts +91 -0
- package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -0
- package/dist/esm/core/layouts/WebsiteSection.js +26 -0
- package/dist/esm/core/layouts/WebsiteSection.js.map +1 -0
- package/dist/esm/core/routing/internal-routing.utils.d.ts +65 -0
- package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -0
- package/dist/esm/core/routing/internal-routing.utils.js +14 -0
- package/dist/esm/core/routing/internal-routing.utils.js.map +1 -0
- package/dist/esm/index.d.ts +129 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +129 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/mdx/BlogPost.d.ts +193 -0
- package/dist/esm/mdx/BlogPost.d.ts.map +1 -0
- package/dist/esm/mdx/BlogPost.js +161 -0
- package/dist/esm/mdx/BlogPost.js.map +1 -0
- package/dist/esm/mdx/blog-post-collection.config.d.ts +139 -0
- package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -0
- package/dist/esm/mdx/blog-post-collection.config.js +27 -0
- package/dist/esm/mdx/blog-post-collection.config.js.map +1 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.d.ts +12 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.js +13 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.js.map +1 -0
- package/dist/esm/mdx/index.d.ts +22 -0
- package/dist/esm/mdx/index.d.ts.map +1 -0
- package/dist/esm/mdx/index.js +22 -0
- package/dist/esm/mdx/index.js.map +1 -0
- package/dist/esm/mdx/website-mdx-provider.d.ts +90 -0
- package/dist/esm/mdx/website-mdx-provider.d.ts.map +1 -0
- package/dist/esm/mdx/website-mdx-provider.js +8 -0
- package/dist/esm/mdx/website-mdx-provider.js.map +1 -0
- package/dist/esm/mdx.d.ts +38 -0
- package/dist/esm/mdx.d.ts.map +1 -0
- package/dist/esm/mdx.js +38 -0
- package/dist/esm/mdx.js.map +1 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts +75 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.js +85 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +120 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +152 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts +73 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.js +76 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.js.map +1 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts +209 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.js +218 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +431 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +438 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/refs/page-ref.schemas.d.ts +40 -0
- package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/refs/page-ref.schemas.js +43 -0
- package/dist/esm/schemas/refs/page-ref.schemas.js.map +1 -0
- package/dist/esm/schemas/refs/section-ref.schemas.d.ts +41 -0
- package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/refs/section-ref.schemas.js +44 -0
- package/dist/esm/schemas/refs/section-ref.schemas.js.map +1 -0
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts +63 -0
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -0
- package/dist/esm/schemas/sections/website-section-category.shared.js +64 -0
- package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +114 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +69 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +100 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +355 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +58 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js +2 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/validators/label-pack.validator.d.ts +144 -0
- package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -0
- package/dist/esm/schemas/validators/label-pack.validator.js +100 -0
- package/dist/esm/schemas/validators/label-pack.validator.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +143 -0
- package/src/__tests__/bundle-isolation.test.ts +607 -0
- package/src/astro/__tests__/blog-post-bridge.test.tsx +288 -0
- package/src/astro/__tests__/blog-post-head.renderer.test.ts +249 -0
- package/src/astro/__tests__/bridge-runtime.test.tsx +198 -0
- package/src/astro/__tests__/collect-expected-label-keys.test.ts +69 -0
- package/src/astro/__tests__/headers-renderer.test.ts +143 -0
- package/src/astro/__tests__/i18n-routing.helper.test.ts +116 -0
- package/src/astro/__tests__/label-pack-loader.test.ts +305 -0
- package/src/astro/__tests__/llms-txt-server.test.ts +127 -0
- package/src/astro/__tests__/pick-labels-for-page.test.ts +60 -0
- package/src/astro/__tests__/robots-renderer.test.ts +228 -0
- package/src/astro/__tests__/sitemap-coverage.test.ts +230 -0
- package/src/astro/__tests__/structured-data.renderer.test.ts +172 -0
- package/src/astro/__tests__/website-page-head.renderer.test.ts +424 -0
- package/src/astro/__tests__/website-page-runtime.helper.test.ts +157 -0
- package/src/astro/__tests__/website-site-context.test.ts +121 -0
- package/src/astro/blog-post-bridge.tsx +404 -0
- package/src/astro/blog-post-head.renderer.ts +293 -0
- package/src/astro/bridge-runtime.tsx +270 -0
- package/src/astro/collect-expected-label-keys.ts +113 -0
- package/src/astro/headers-renderer.ts +82 -0
- package/src/astro/i18n-routing.helper.ts +116 -0
- package/src/astro/internal/html-escape.ts +41 -0
- package/src/astro/label-pack-loader.ts +385 -0
- package/src/astro/llms-txt-server.ts +56 -0
- package/src/astro/pick-labels-for-page.ts +85 -0
- package/src/astro/robots-renderer.ts +171 -0
- package/src/astro/sitemap-coverage.ts +186 -0
- package/src/astro/structured-data.renderer.ts +171 -0
- package/src/astro/website-page-head.renderer.ts +279 -0
- package/src/astro/website-page-runtime.helper.ts +188 -0
- package/src/astro/website-site-context.ts +136 -0
- package/src/astro-island.ts +61 -0
- package/src/astro.ts +151 -0
- package/src/companion-exports.ts +38 -0
- package/src/components/low-level/WebsiteButton.tsx +118 -0
- package/src/components/low-level/WebsiteHeading.tsx +96 -0
- package/src/components/low-level/WebsiteImage.tsx +96 -0
- package/src/components/low-level/WebsiteInternalButton.tsx +172 -0
- package/src/components/low-level/WebsiteInternalLink.tsx +193 -0
- package/src/components/low-level/WebsiteLink.tsx +71 -0
- package/src/components/low-level/WebsiteText.tsx +75 -0
- package/src/components/low-level/__tests__/WebsiteInternalButton.test.tsx +169 -0
- package/src/components/low-level/__tests__/WebsiteInternalLink.test.tsx +202 -0
- package/src/components/low-level/__tests__/primitives.test.tsx +141 -0
- package/src/config/__tests__/define-website-config.test.ts +231 -0
- package/src/config/__tests__/load-website-config.test.ts +215 -0
- package/src/config/define-website-config.ts +134 -0
- package/src/config/index.ts +38 -0
- package/src/config/load-website-config.ts +229 -0
- package/src/config/wildo-website-config.schemas.ts +322 -0
- package/src/config-loader.ts +35 -0
- package/src/core/__tests__/WebsitePageLayout.test.tsx +184 -0
- package/src/core/__tests__/WebsiteSection.test.tsx +85 -0
- package/src/core/__tests__/contexts.test.tsx +133 -0
- package/src/core/__tests__/core-boundary.test.ts +85 -0
- package/src/core/__tests__/define-website-page-manifest.test.tsx +111 -0
- package/src/core/__tests__/define-website-section.test.tsx +89 -0
- package/src/core/__tests__/useWebsiteDesignTokens.test.tsx +41 -0
- package/src/core/__tests__/useWebsiteLabel.test.tsx +117 -0
- package/src/core/anonymous-session/InboundContactForm.tsx +157 -0
- package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +44 -0
- package/src/core/anonymous-session/inbound-contact-form.schema.ts +62 -0
- package/src/core/anonymous-session/website-anonymous-session-client.ts +194 -0
- package/src/core/contexts/WebsitePageContext.tsx +52 -0
- package/src/core/contexts/WebsiteRuntimeContext.tsx +175 -0
- package/src/core/contexts/WebsiteSectionContext.tsx +59 -0
- package/src/core/contexts/useWebsitePage.ts +28 -0
- package/src/core/contexts/useWebsiteRuntime.ts +34 -0
- package/src/core/contexts/useWebsiteSection.ts +28 -0
- package/src/core/external-providers/frontend-provider-registry.website.ts +62 -0
- package/src/core/factories/define-website-page-manifest.ts +102 -0
- package/src/core/factories/define-website-section.ts +128 -0
- package/src/core/hooks/useWebsiteDesignTokens.ts +26 -0
- package/src/core/hooks/useWebsiteLabel.ts +121 -0
- package/src/core/layouts/WebsitePageLayout.tsx +311 -0
- package/src/core/layouts/WebsiteSection.tsx +139 -0
- package/src/core/routing/__tests__/internal-routing.utils.test.ts +134 -0
- package/src/core/routing/internal-routing.utils.ts +77 -0
- package/src/index.ts +132 -0
- package/src/mdx/BlogPost.tsx +296 -0
- package/src/mdx/__tests__/BlogPost.test.tsx +218 -0
- package/src/mdx/__tests__/blog-post-collection.config.test.ts +127 -0
- package/src/mdx/__tests__/website-mdx-provider.test.tsx +82 -0
- package/src/mdx/blog-post-collection.config.ts +177 -0
- package/src/mdx/blog-post-frontmatter-source.ts +31 -0
- package/src/mdx/index.ts +39 -0
- package/src/mdx/website-mdx-provider.tsx +95 -0
- package/src/mdx.ts +38 -0
- package/src/schemas/__tests__/blog-frontmatter.test.ts +50 -0
- package/src/schemas/__tests__/companion-exports-react-free.test.ts +51 -0
- package/src/schemas/__tests__/design-tokens.test.ts +163 -0
- package/src/schemas/__tests__/label-key.test.ts +46 -0
- package/src/schemas/__tests__/label-pack-validator.test.ts +146 -0
- package/src/schemas/__tests__/page-manifest.test.ts +66 -0
- package/src/schemas/__tests__/refs.test.ts +37 -0
- package/src/schemas/__tests__/root-config.test.ts +292 -0
- package/src/schemas/__tests__/section-definition.test.ts +47 -0
- package/src/schemas/__tests__/structured-data-reconciliation.test.ts +543 -0
- package/src/schemas/blog/blog-post-frontmatter.shared.schemas.ts +111 -0
- package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +166 -0
- package/src/schemas/label-keys/website-label-key.schemas.ts +88 -0
- package/src/schemas/manifests/website-page-manifest.shared.schemas.ts +266 -0
- package/src/schemas/manifests/website-root-config.shared.schemas.ts +504 -0
- package/src/schemas/refs/page-ref.schemas.ts +49 -0
- package/src/schemas/refs/section-ref.schemas.ts +50 -0
- package/src/schemas/sections/website-section-category.shared.ts +65 -0
- package/src/schemas/sections/website-section-definition.shared.schemas.ts +120 -0
- package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +579 -0
- package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +62 -0
- package/src/schemas/validators/label-pack.validator.ts +201 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured-data.renderer.js","sourceRoot":"","sources":["../../../../src/astro/structured-data.renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n WebsiteStructuredDataPayload,\n WebsiteStructuredDataReturn,\n} from '../schemas/structured-data/website-structured-data.shared.schemas';\nimport type { WebsiteSectionDefinition } from '../schemas/sections/website-section-definition.shared.schemas';\n\n/**\n * @wildo_source:part:start saas.website.astro.structured-data-renderer facet:layer:astro facet:family:website\n *\n * `renderPageStructuredData()` — collects every section's\n * `getStructuredData()` return + any explicit page-level payloads,\n * normalizes them, dedupes by `@id`, and emits one\n * `<script type=\"application/ld+json\">` block per surviving payload.\n *\n * **Why one `<script>` per payload (NOT a single `@graph` array)**:\n *\n * - Google's structured-data testing tool, Bing webmaster tools, and\n * the Schema.org reference parser all accept multiple sibling\n * `<script type=\"application/ld+json\">` blocks. Emitting them as\n * separate blocks lets each payload be diff-friendly in the\n * page source AND lets per-payload removal happen on a future\n * AEO opt-out without churning the whole graph.\n * - `@graph` is technically valid but harder to author — every\n * payload would need to share `@context` with the wrapper, and\n * per-section authors would have to coordinate to merge their\n * payloads into one shared graph. The \"one script per payload\"\n * pattern keeps section authoring fully local.\n *\n * **Why dedupe by `@id` (last-wins)**: a page may have a section\n * emit a `Product` payload AND another section emit a `Review`\n * referencing the same `Product` via `@id`. Without dedupe, the\n * `Product` would emit twice (once standalone, once referenced).\n * Last-wins because the section's own emission is closer to the\n * truth than a reference; the dedupe is best-effort and harmless\n * if `@id` is absent (the payload always emits).\n *\n * **`@context` injection**: when a payload omits `@context`, the\n * renderer injects `'https://schema.org'`. Consumers wanting a\n * custom vocabulary supply `@context` themselves and the renderer\n * passes it through unchanged.\n *\n * **HTML-safe serialization**: JSON-LD payloads are serialized via\n * `JSON.stringify` with the closing `</` escaped to `<\\/` (and the\n * `<!` opener escaped to `<\\!`) to prevent the payload from\n * accidentally closing the surrounding `<script>` tag if the\n * payload contains the string `</script>` (e.g. a blog post about\n * structured data). This is the canonical safe-serialization\n * pattern (see https://html.spec.whatwg.org/#restrictions-for-contents-of-script-elements).\n */\n\nconst SCHEMA_ORG_DEFAULT_CONTEXT = 'https://schema.org' as const;\n\nexport interface RenderPageStructuredDataOptions {\n /**\n * Section definitions whose `getStructuredData()` (when present)\n * contributes payloads to the page. Sections without\n * `getStructuredData` are skipped silently.\n */\n sections?: readonly Pick<WebsiteSectionDefinition, 'sectionRef' | 'getStructuredData'>[];\n /**\n * Optional page-level payloads contributed by the page-shell\n * itself (typical: `BreadcrumbList`, `Organization`, `WebSite`).\n * Composed AFTER section payloads in the emission order so a\n * section's `@id`-keyed payload wins over a page-level one with\n * the same `@id` (last-wins; see preamble).\n */\n pagePayloads?: readonly WebsiteStructuredDataPayload[];\n}\n\nexport function renderPageStructuredData(opts: RenderPageStructuredDataOptions): string {\n const collected: WebsiteStructuredDataPayload[] = [];\n\n if (opts.sections) {\n for (const section of opts.sections) {\n if (!section.getStructuredData) continue;\n const value: WebsiteStructuredDataReturn = section.getStructuredData();\n collectInto(collected, value);\n }\n }\n\n if (opts.pagePayloads) {\n for (const payload of opts.pagePayloads) {\n collected.push(payload);\n }\n }\n\n if (collected.length === 0) return '';\n\n /**\n * Dedupe by `@id` (last-wins). Payloads without `@id` always\n * survive. We walk LAST-to-FIRST so the LAST occurrence of an\n * `@id` is the kept one (later writes overwrite earlier ones\n * naturally with this loop direction + reverse at the end).\n */\n const seen = new Set<string>();\n const deduped: WebsiteStructuredDataPayload[] = [];\n for (let i = collected.length - 1; i >= 0; i -= 1) {\n const payload = collected[i];\n if (payload === undefined) continue;\n const id = payload['@id'];\n if (typeof id === 'string') {\n if (seen.has(id)) continue;\n seen.add(id);\n }\n deduped.push(payload);\n }\n deduped.reverse();\n\n const lines: string[] = [];\n for (const payload of deduped) {\n const withContext: Record<string, unknown> =\n payload['@context'] === undefined\n ? { '@context': SCHEMA_ORG_DEFAULT_CONTEXT, ...payload }\n : { ...payload };\n const json = serializeForScriptTag(withContext);\n lines.push(`<script type=\"application/ld+json\">${json}</script>`);\n }\n return lines.join('\\n');\n}\n\n/**\n * Normalize a `getStructuredData()` return value into a flat array\n * the renderer iterates. Handles the four legal shapes\n * (`null` / `undefined` / single payload / readonly array).\n *\n * Mutates the `into` array in place — the function is private to\n * this module and the in-place mutation avoids one allocation per\n * section (sections compose linearly, the cost adds up on long\n * pages).\n */\nfunction collectInto(\n into: WebsiteStructuredDataPayload[],\n value: WebsiteStructuredDataReturn,\n): void {\n if (value === null || value === undefined) return;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== null && item !== undefined) into.push(item);\n }\n return;\n }\n /**\n * Single-payload shape — narrow the readonly-array branch above\n * by elimination. `Array.isArray` returns `false` for readonly\n * arrays in some TS configs but TRUE at runtime; this branch\n * runs only for non-array `WebsiteStructuredDataPayload`.\n */\n into.push(value as WebsiteStructuredDataPayload);\n}\n\n/**\n * Serialize a payload for emission inside a `<script>` tag.\n *\n * `JSON.stringify` alone is NOT safe inside `<script>` tags:\n *\n * - A payload field containing the literal string `</script>`\n * (rare but possible on a blog post about HTML / structured\n * data) would close the script tag prematurely and inject the\n * remainder as page HTML.\n * - A payload field containing `<!--` similarly opens an HTML\n * comment that breaks the surrounding parsing context.\n *\n * Both are mitigated by escaping `<` to `\\u003c` inside the JSON\n * payload — the JSON parser treats `\\u003c` as identical to `<`,\n * so the consumed payload is unchanged, but the HTML parser sees\n * no `<` characters and never gets confused.\n */\nfunction serializeForScriptTag(payload: Record<string, unknown>): string {\n return JSON.stringify(payload).replace(/</g, '\\\\u003c');\n}\n/** @wildo_source:part:end saas.website.astro.structured-data-renderer */\n"]}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
2
|
+
import type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';
|
|
3
|
+
import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
|
|
4
|
+
import type { LabelPack } from '../schemas/validators/label-pack.validator';
|
|
5
|
+
import type { WebsiteSiteContext } from './website-site-context';
|
|
6
|
+
/**
|
|
7
|
+
* @wildo_source:part:start saas.website.astro.head-renderer facet:layer:astro facet:family:website
|
|
8
|
+
*
|
|
9
|
+
* Head-block renderers — pure string builders the consumer's per-page
|
|
10
|
+
* `.astro` file injects via `<Fragment set:html={...}/>` /
|
|
11
|
+
* `<style set:html={...}></style>`. Two functions:
|
|
12
|
+
*
|
|
13
|
+
* - `renderWebsitePageHeadHtml(opts)` — emits the complete SEO
|
|
14
|
+
* `<head>` block: `<title>`, meta description, full Open Graph
|
|
15
|
+
* card (`og:title` / `og:description` / `og:type` / `og:url` /
|
|
16
|
+
* `og:site_name` / `og:locale` / `og:locale:alternate` / optional
|
|
17
|
+
* `og:image`), Twitter card mirrors, `<link rel="canonical">`,
|
|
18
|
+
* and one `<link rel="alternate" hreflang>` per declared locale
|
|
19
|
+
* (plus an `x-default` for Google's i18n discoverability). Every
|
|
20
|
+
* URL is ABSOLUTE (composed from `websiteConfig.publicOrigin` +
|
|
21
|
+
* the page's `routePath`) — relative hreflang/canonical
|
|
22
|
+
* annotations are not honored consistently across crawlers
|
|
23
|
+
* (Phase 6 audit findings H-B / H-C / M-A).
|
|
24
|
+
* - `renderWebsiteDesignTokensCss(tokens)` — flattens the design-token
|
|
25
|
+
* bundle into `--website-*` custom properties scoped to `:root`.
|
|
26
|
+
* Server-static — zero island JS.
|
|
27
|
+
*
|
|
28
|
+
* **Why pure string builders** (rather than React components or Astro
|
|
29
|
+
* components):
|
|
30
|
+
*
|
|
31
|
+
* 1. **Avoids the `.astro` distribution problem.** A `.astro` shell
|
|
32
|
+
* cannot be re-exported from a TS subpath barrel and `tsc` does
|
|
33
|
+
* not copy `.astro` files to `dist/`. Pure TS sidesteps both.
|
|
34
|
+
* 2. **Avoids the React-island boundary.** Rendering head content
|
|
35
|
+
* via React would require either (a) a server-only React subtree
|
|
36
|
+
* (extra moving part) or (b) hydrating the `<head>` on the client
|
|
37
|
+
* (waste of bundle size for static-build content).
|
|
38
|
+
* 3. **Vitest-friendly.** Pure functions returning strings are
|
|
39
|
+
* trivially asserted against without an Astro test harness.
|
|
40
|
+
*
|
|
41
|
+
* **HTML escaping**: every consumer-supplied string (title,
|
|
42
|
+
* description, OG image URL, hreflang values) is escaped with
|
|
43
|
+
* `escapeHtmlAttribute` / `escapeHtmlText` before interpolation into
|
|
44
|
+
* the emitted markup. Marketing copy frequently contains characters
|
|
45
|
+
* (quotes, ampersands, the rare `<`) that would break the surrounding
|
|
46
|
+
* tag if interpolated raw — escaping is non-negotiable for a public
|
|
47
|
+
* surface.
|
|
48
|
+
*
|
|
49
|
+
* **Failure modes** — `renderWebsitePageHeadHtml` throws when:
|
|
50
|
+
* - `manifest.metaLabelKeys.titleKey` is absent from the pack
|
|
51
|
+
* - `manifest.metaLabelKeys.descriptionKey` is absent from the pack
|
|
52
|
+
* Both throw with the locale + key + actionable next step (`Run
|
|
53
|
+
* \`wildo website audit\` or add the entry to <pack-path>.json`).
|
|
54
|
+
*/
|
|
55
|
+
export interface RenderWebsitePageHeadHtmlOptions {
|
|
56
|
+
manifest: WebsitePageManifest;
|
|
57
|
+
/**
|
|
58
|
+
* Composed `WebsiteSiteContext` — the renderer reads
|
|
59
|
+
* `websiteConfig.{defaultLocale, locales, publicOrigin}` for the
|
|
60
|
+
* URL/locale surface and `rootConfig.seo.{siteName, defaultOgImageUrl}`
|
|
61
|
+
* for the SEO branding fallback.
|
|
62
|
+
*/
|
|
63
|
+
siteContext: WebsiteSiteContext;
|
|
64
|
+
locale: AvailableLanguage;
|
|
65
|
+
labelPack: LabelPack;
|
|
66
|
+
}
|
|
67
|
+
export declare function renderWebsitePageHeadHtml(opts: RenderWebsitePageHeadHtmlOptions): string;
|
|
68
|
+
/**
|
|
69
|
+
* Flatten `WebsiteDesignTokens` to a `:root { … }` block of CSS custom
|
|
70
|
+
* properties. Naming convention: `--website-<group>-<token>` for nested
|
|
71
|
+
* groups (`colors.primary` → `--website-colors-primary`); top-level
|
|
72
|
+
* string-valued tokens (none today, future-proofed) emit as
|
|
73
|
+
* `--website-<token>`.
|
|
74
|
+
*
|
|
75
|
+
* **Why scoped to `:root`** rather than `<WebsitePageLayout>`'s root
|
|
76
|
+
* div: marketing pages frequently embed third-party widgets (live-chat,
|
|
77
|
+
* analytics overlays) that paint outside the React tree. Scoping
|
|
78
|
+
* tokens to `:root` makes them visible to those widgets too. The
|
|
79
|
+
* design-token surface is small enough that the cost of a broader
|
|
80
|
+
* scope (token-name collisions with consumer-authored `:root` rules)
|
|
81
|
+
* is bounded by the `--website-` prefix.
|
|
82
|
+
*/
|
|
83
|
+
export declare function renderWebsiteDesignTokensCss(tokens: WebsiteDesignTokens): string;
|
|
84
|
+
/**
|
|
85
|
+
* @wildo_source:part:end saas.website.astro.head-renderer
|
|
86
|
+
*
|
|
87
|
+
* HTML escape helpers moved to `./internal/html-escape.ts` (Phase 7)
|
|
88
|
+
* so the new `blog-post-head.renderer.ts` sibling can share the same
|
|
89
|
+
* escape contract instead of redefining it. See that module's preamble
|
|
90
|
+
* for the framework-mindset rationale (one escape contract per engine,
|
|
91
|
+
* not one per renderer).
|
|
92
|
+
*/
|
|
93
|
+
//# sourceMappingURL=website-page-head.renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"website-page-head.renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/website-page-head.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAG9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+DAA+D,CAAC;AACzG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2DAA2D,CAAC;AACrG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAG5E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B;;;;;OAKG;IACH,WAAW,EAAE,kBAAkB,CAAC;IAChC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,gCAAgC,GAAG,MAAM,CAmKxF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAehF;AAED;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils.js';
|
|
2
|
+
import { escapeHtmlAttribute, escapeHtmlText } from './internal/html-escape.js';
|
|
3
|
+
export function renderWebsitePageHeadHtml(opts) {
|
|
4
|
+
const { manifest, siteContext, locale, labelPack } = opts;
|
|
5
|
+
const { rootConfig, websiteConfig } = siteContext;
|
|
6
|
+
const titleKey = manifest.metaLabelKeys.titleKey;
|
|
7
|
+
const descriptionKey = manifest.metaLabelKeys.descriptionKey;
|
|
8
|
+
const title = labelPack[titleKey];
|
|
9
|
+
const description = labelPack[descriptionKey];
|
|
10
|
+
if (title === undefined) {
|
|
11
|
+
throw new Error(`[renderWebsitePageHeadHtml] Label pack for locale "${locale}" is missing ` +
|
|
12
|
+
`the page title key "${titleKey}" (page ref="${manifest.ref}"). ` +
|
|
13
|
+
`Run \`wildo website audit\` or add the entry to your <locale>.json pack.`);
|
|
14
|
+
}
|
|
15
|
+
if (description === undefined) {
|
|
16
|
+
throw new Error(`[renderWebsitePageHeadHtml] Label pack for locale "${locale}" is missing ` +
|
|
17
|
+
`the page description key "${descriptionKey}" (page ref="${manifest.ref}"). ` +
|
|
18
|
+
`Run \`wildo website audit\` or add the entry to your <locale>.json pack.`);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* OG image precedence: per-page `manifest.metaLabelKeys.ogImageUrl`
|
|
22
|
+
* wins; falls back to the site-level `seo.defaultOgImageUrl` so
|
|
23
|
+
* marketing pages without a hero image still produce a richer
|
|
24
|
+
* social-card (Phase 6 audit fix M-A). When neither is present,
|
|
25
|
+
* the Twitter card kind degrades to the smaller `summary` and the
|
|
26
|
+
* `og:image` / `twitter:image` lines are omitted entirely.
|
|
27
|
+
*/
|
|
28
|
+
const ogImageUrl = manifest.metaLabelKeys.ogImageUrl ?? rootConfig.seo.defaultOgImageUrl;
|
|
29
|
+
const titleAttr = escapeHtmlAttribute(title);
|
|
30
|
+
const descriptionAttr = escapeHtmlAttribute(description);
|
|
31
|
+
const titleText = escapeHtmlText(title);
|
|
32
|
+
/**
|
|
33
|
+
* Absolute URL builder. `prefixDefaultLocale: false` means the
|
|
34
|
+
* default locale lives at the locale-neutral path; every other
|
|
35
|
+
* locale sits under `/<locale>/<routePath>`. `x-default` always
|
|
36
|
+
* resolves to the default-locale path.
|
|
37
|
+
*
|
|
38
|
+
* Every URL emitted here is FULLY-QUALIFIED (composed against
|
|
39
|
+
* `websiteConfig.publicOrigin`) — Google's hreflang spec requires
|
|
40
|
+
* absolute URLs, and `<link rel="canonical">` relies on absolute
|
|
41
|
+
* URLs to survive cross-host fetches (preview deployments, CDN
|
|
42
|
+
* edges). See `WildoMarketingWebsiteConfigSchema.publicOrigin`
|
|
43
|
+
* JSDoc for the full rationale (incl. trailing-slash rejection).
|
|
44
|
+
*/
|
|
45
|
+
const routePath = manifest.routePath;
|
|
46
|
+
const siteOrigin = websiteConfig.publicOrigin;
|
|
47
|
+
const defaultLocale = websiteConfig.defaultLocale;
|
|
48
|
+
const currentLocaleStr = String(locale);
|
|
49
|
+
/**
|
|
50
|
+
* Compose the absolute URL for `(locale, routePath)` by delegating
|
|
51
|
+
* the locale-prefix decision to the shared `buildLocalizedRoutePath`
|
|
52
|
+
* helper — same routing rule that powers `WebsiteInternalLink`. This
|
|
53
|
+
* guarantees that hreflang/canonical URLs and the chrome's internal
|
|
54
|
+
* links can never disagree about how a locale is prefixed.
|
|
55
|
+
*/
|
|
56
|
+
function buildAbsoluteUrl(forLocale) {
|
|
57
|
+
const path = buildLocalizedRoutePath({
|
|
58
|
+
locale: forLocale,
|
|
59
|
+
defaultLocale,
|
|
60
|
+
routePath,
|
|
61
|
+
});
|
|
62
|
+
return `${siteOrigin}${path}`;
|
|
63
|
+
}
|
|
64
|
+
const canonicalUrl = buildAbsoluteUrl(locale);
|
|
65
|
+
const hreflangLines = [];
|
|
66
|
+
for (const entry of websiteConfig.locales) {
|
|
67
|
+
const localeStr = String(entry.locale);
|
|
68
|
+
hreflangLines.push(`<link rel="alternate" hreflang="${escapeHtmlAttribute(localeStr)}" href="${escapeHtmlAttribute(buildAbsoluteUrl(entry.locale))}" />`);
|
|
69
|
+
}
|
|
70
|
+
hreflangLines.push(`<link rel="alternate" hreflang="x-default" href="${escapeHtmlAttribute(buildAbsoluteUrl(defaultLocale))}" />`);
|
|
71
|
+
/**
|
|
72
|
+
* `og:image` + `twitter:image` are emitted only when an image URL
|
|
73
|
+
* is available (page-level override OR site-level fallback).
|
|
74
|
+
* Twitter card type also flips between `summary` (no image) and
|
|
75
|
+
* `summary_large_image` (image present).
|
|
76
|
+
*/
|
|
77
|
+
const ogImageLines = [];
|
|
78
|
+
if (ogImageUrl !== undefined) {
|
|
79
|
+
const ogImageAttr = escapeHtmlAttribute(ogImageUrl);
|
|
80
|
+
ogImageLines.push(`<meta property="og:image" content="${ogImageAttr}" />`, `<meta name="twitter:image" content="${ogImageAttr}" />`);
|
|
81
|
+
}
|
|
82
|
+
const twitterCardKind = ogImageUrl !== undefined ? 'summary_large_image' : 'summary';
|
|
83
|
+
/**
|
|
84
|
+
* `og:locale:alternate` — one per non-current declared locale. Open
|
|
85
|
+
* Graph spec: the `og:locale` field carries the page's primary
|
|
86
|
+
* locale; alternates declare the other locales the page is
|
|
87
|
+
* available in. Emitted in declaration order.
|
|
88
|
+
*
|
|
89
|
+
* Locale format: we emit the bare language code (`en`, `fr`) per
|
|
90
|
+
* the AvailableLanguage atom shape. Open Graph technically
|
|
91
|
+
* recommends `language_TERRITORY` (e.g. `en_US`), but Facebook /
|
|
92
|
+
* LinkedIn / Slack accept the bare language code in practice. The
|
|
93
|
+
* future fix when the platform grows territory-tagged locales is to
|
|
94
|
+
* map at this site (no consumer change required).
|
|
95
|
+
*/
|
|
96
|
+
const ogLocaleAlternateLines = [];
|
|
97
|
+
for (const entry of websiteConfig.locales) {
|
|
98
|
+
const localeStr = String(entry.locale);
|
|
99
|
+
if (localeStr === currentLocaleStr)
|
|
100
|
+
continue;
|
|
101
|
+
ogLocaleAlternateLines.push(`<meta property="og:locale:alternate" content="${escapeHtmlAttribute(localeStr)}" />`);
|
|
102
|
+
}
|
|
103
|
+
const siteNameAttr = escapeHtmlAttribute(rootConfig.seo.siteName);
|
|
104
|
+
const canonicalAttr = escapeHtmlAttribute(canonicalUrl);
|
|
105
|
+
const ogLocaleAttr = escapeHtmlAttribute(currentLocaleStr);
|
|
106
|
+
/**
|
|
107
|
+
* AI-training opt-out (D-AG3) — `<meta name="robots" content="noai, noimageai">`
|
|
108
|
+
* is emitted ONLY when the page manifest opts in via
|
|
109
|
+
* `seo.noaiOptOut: true`. Default is OPT-IN to AI training (the
|
|
110
|
+
* marketing-site default). This emission pairs with the sibling
|
|
111
|
+
* `_headers` entry produced by `renderWebsitePageNoaiHeaders()`
|
|
112
|
+
* for crawlers that don't honor in-document meta.
|
|
113
|
+
*
|
|
114
|
+
* Format: `noai` (block AI training on text) and `noimageai`
|
|
115
|
+
* (block AI training on images) are the two robots directives the
|
|
116
|
+
* major AI crawlers (GPTBot, ClaudeBot, Google-Extended, etc.)
|
|
117
|
+
* recognise. Emitting them as a single comma-separated `content`
|
|
118
|
+
* value is the canonical robots-meta shape.
|
|
119
|
+
*/
|
|
120
|
+
const noaiMetaLines = [];
|
|
121
|
+
if (manifest.seo?.noaiOptOut === true) {
|
|
122
|
+
noaiMetaLines.push(`<meta name="robots" content="noai, noimageai" />`);
|
|
123
|
+
}
|
|
124
|
+
return [
|
|
125
|
+
`<title>${titleText}</title>`,
|
|
126
|
+
`<meta name="description" content="${descriptionAttr}" />`,
|
|
127
|
+
`<link rel="canonical" href="${canonicalAttr}" />`,
|
|
128
|
+
`<meta property="og:title" content="${titleAttr}" />`,
|
|
129
|
+
`<meta property="og:description" content="${descriptionAttr}" />`,
|
|
130
|
+
`<meta property="og:type" content="website" />`,
|
|
131
|
+
`<meta property="og:url" content="${canonicalAttr}" />`,
|
|
132
|
+
`<meta property="og:site_name" content="${siteNameAttr}" />`,
|
|
133
|
+
`<meta property="og:locale" content="${ogLocaleAttr}" />`,
|
|
134
|
+
...ogLocaleAlternateLines,
|
|
135
|
+
...ogImageLines,
|
|
136
|
+
`<meta name="twitter:card" content="${twitterCardKind}" />`,
|
|
137
|
+
`<meta name="twitter:title" content="${titleAttr}" />`,
|
|
138
|
+
`<meta name="twitter:description" content="${descriptionAttr}" />`,
|
|
139
|
+
...noaiMetaLines,
|
|
140
|
+
...hreflangLines,
|
|
141
|
+
].join('\n');
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Flatten `WebsiteDesignTokens` to a `:root { … }` block of CSS custom
|
|
145
|
+
* properties. Naming convention: `--website-<group>-<token>` for nested
|
|
146
|
+
* groups (`colors.primary` → `--website-colors-primary`); top-level
|
|
147
|
+
* string-valued tokens (none today, future-proofed) emit as
|
|
148
|
+
* `--website-<token>`.
|
|
149
|
+
*
|
|
150
|
+
* **Why scoped to `:root`** rather than `<WebsitePageLayout>`'s root
|
|
151
|
+
* div: marketing pages frequently embed third-party widgets (live-chat,
|
|
152
|
+
* analytics overlays) that paint outside the React tree. Scoping
|
|
153
|
+
* tokens to `:root` makes them visible to those widgets too. The
|
|
154
|
+
* design-token surface is small enough that the cost of a broader
|
|
155
|
+
* scope (token-name collisions with consumer-authored `:root` rules)
|
|
156
|
+
* is bounded by the `--website-` prefix.
|
|
157
|
+
*/
|
|
158
|
+
export function renderWebsiteDesignTokensCss(tokens) {
|
|
159
|
+
const lines = [':root {'];
|
|
160
|
+
for (const [groupName, groupValue] of Object.entries(tokens)) {
|
|
161
|
+
if (groupValue !== null && typeof groupValue === 'object' && !Array.isArray(groupValue)) {
|
|
162
|
+
for (const [tokenName, tokenValue] of Object.entries(groupValue)) {
|
|
163
|
+
if (typeof tokenValue === 'string') {
|
|
164
|
+
lines.push(` --website-${groupName}-${tokenName}: ${tokenValue};`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (typeof groupValue === 'string') {
|
|
169
|
+
lines.push(` --website-${groupName}: ${groupValue};`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
lines.push('}');
|
|
173
|
+
return lines.join('\n');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* @wildo_source:part:end saas.website.astro.head-renderer
|
|
177
|
+
*
|
|
178
|
+
* HTML escape helpers moved to `./internal/html-escape.ts` (Phase 7)
|
|
179
|
+
* so the new `blog-post-head.renderer.ts` sibling can share the same
|
|
180
|
+
* escape contract instead of redefining it. See that module's preamble
|
|
181
|
+
* for the framework-mindset rationale (one escape contract per engine,
|
|
182
|
+
* not one per renderer).
|
|
183
|
+
*/
|
|
184
|
+
//# sourceMappingURL=website-page-head.renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"website-page-head.renderer.js","sourceRoot":"","sources":["../../../../src/astro/website-page-head.renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils';\nimport type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';\nimport type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';\nimport type { LabelPack } from '../schemas/validators/label-pack.validator';\n\nimport { escapeHtmlAttribute, escapeHtmlText } from './internal/html-escape';\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.head-renderer facet:layer:astro facet:family:website\n *\n * Head-block renderers — pure string builders the consumer's per-page\n * `.astro` file injects via `<Fragment set:html={...}/>` /\n * `<style set:html={...}></style>`. Two functions:\n *\n * - `renderWebsitePageHeadHtml(opts)` — emits the complete SEO\n * `<head>` block: `<title>`, meta description, full Open Graph\n * card (`og:title` / `og:description` / `og:type` / `og:url` /\n * `og:site_name` / `og:locale` / `og:locale:alternate` / optional\n * `og:image`), Twitter card mirrors, `<link rel=\"canonical\">`,\n * and one `<link rel=\"alternate\" hreflang>` per declared locale\n * (plus an `x-default` for Google's i18n discoverability). Every\n * URL is ABSOLUTE (composed from `websiteConfig.publicOrigin` +\n * the page's `routePath`) — relative hreflang/canonical\n * annotations are not honored consistently across crawlers\n * (Phase 6 audit findings H-B / H-C / M-A).\n * - `renderWebsiteDesignTokensCss(tokens)` — flattens the design-token\n * bundle into `--website-*` custom properties scoped to `:root`.\n * Server-static — zero island JS.\n *\n * **Why pure string builders** (rather than React components or Astro\n * components):\n *\n * 1. **Avoids the `.astro` distribution problem.** A `.astro` shell\n * cannot be re-exported from a TS subpath barrel and `tsc` does\n * not copy `.astro` files to `dist/`. Pure TS sidesteps both.\n * 2. **Avoids the React-island boundary.** Rendering head content\n * via React would require either (a) a server-only React subtree\n * (extra moving part) or (b) hydrating the `<head>` on the client\n * (waste of bundle size for static-build content).\n * 3. **Vitest-friendly.** Pure functions returning strings are\n * trivially asserted against without an Astro test harness.\n *\n * **HTML escaping**: every consumer-supplied string (title,\n * description, OG image URL, hreflang values) is escaped with\n * `escapeHtmlAttribute` / `escapeHtmlText` before interpolation into\n * the emitted markup. Marketing copy frequently contains characters\n * (quotes, ampersands, the rare `<`) that would break the surrounding\n * tag if interpolated raw — escaping is non-negotiable for a public\n * surface.\n *\n * **Failure modes** — `renderWebsitePageHeadHtml` throws when:\n * - `manifest.metaLabelKeys.titleKey` is absent from the pack\n * - `manifest.metaLabelKeys.descriptionKey` is absent from the pack\n * Both throw with the locale + key + actionable next step (`Run\n * \\`wildo website audit\\` or add the entry to <pack-path>.json`).\n */\n\nexport interface RenderWebsitePageHeadHtmlOptions {\n manifest: WebsitePageManifest;\n /**\n * Composed `WebsiteSiteContext` — the renderer reads\n * `websiteConfig.{defaultLocale, locales, publicOrigin}` for the\n * URL/locale surface and `rootConfig.seo.{siteName, defaultOgImageUrl}`\n * for the SEO branding fallback.\n */\n siteContext: WebsiteSiteContext;\n locale: AvailableLanguage;\n labelPack: LabelPack;\n}\n\nexport function renderWebsitePageHeadHtml(opts: RenderWebsitePageHeadHtmlOptions): string {\n const { manifest, siteContext, locale, labelPack } = opts;\n const { rootConfig, websiteConfig } = siteContext;\n\n const titleKey = manifest.metaLabelKeys.titleKey as unknown as string;\n const descriptionKey = manifest.metaLabelKeys.descriptionKey as unknown as string;\n const title = labelPack[titleKey];\n const description = labelPack[descriptionKey];\n\n if (title === undefined) {\n throw new Error(\n `[renderWebsitePageHeadHtml] Label pack for locale \"${locale}\" is missing ` +\n `the page title key \"${titleKey}\" (page ref=\"${manifest.ref as unknown as string}\"). ` +\n `Run \\`wildo website audit\\` or add the entry to your <locale>.json pack.`,\n );\n }\n if (description === undefined) {\n throw new Error(\n `[renderWebsitePageHeadHtml] Label pack for locale \"${locale}\" is missing ` +\n `the page description key \"${descriptionKey}\" (page ref=\"${manifest.ref as unknown as string}\"). ` +\n `Run \\`wildo website audit\\` or add the entry to your <locale>.json pack.`,\n );\n }\n\n /**\n * OG image precedence: per-page `manifest.metaLabelKeys.ogImageUrl`\n * wins; falls back to the site-level `seo.defaultOgImageUrl` so\n * marketing pages without a hero image still produce a richer\n * social-card (Phase 6 audit fix M-A). When neither is present,\n * the Twitter card kind degrades to the smaller `summary` and the\n * `og:image` / `twitter:image` lines are omitted entirely.\n */\n const ogImageUrl = manifest.metaLabelKeys.ogImageUrl ?? rootConfig.seo.defaultOgImageUrl;\n const titleAttr = escapeHtmlAttribute(title);\n const descriptionAttr = escapeHtmlAttribute(description);\n const titleText = escapeHtmlText(title);\n\n /**\n * Absolute URL builder. `prefixDefaultLocale: false` means the\n * default locale lives at the locale-neutral path; every other\n * locale sits under `/<locale>/<routePath>`. `x-default` always\n * resolves to the default-locale path.\n *\n * Every URL emitted here is FULLY-QUALIFIED (composed against\n * `websiteConfig.publicOrigin`) — Google's hreflang spec requires\n * absolute URLs, and `<link rel=\"canonical\">` relies on absolute\n * URLs to survive cross-host fetches (preview deployments, CDN\n * edges). See `WildoMarketingWebsiteConfigSchema.publicOrigin`\n * JSDoc for the full rationale (incl. trailing-slash rejection).\n */\n const routePath = manifest.routePath as unknown as string;\n const siteOrigin = websiteConfig.publicOrigin;\n const defaultLocale = websiteConfig.defaultLocale;\n const currentLocaleStr = String(locale);\n\n /**\n * Compose the absolute URL for `(locale, routePath)` by delegating\n * the locale-prefix decision to the shared `buildLocalizedRoutePath`\n * helper — same routing rule that powers `WebsiteInternalLink`. This\n * guarantees that hreflang/canonical URLs and the chrome's internal\n * links can never disagree about how a locale is prefixed.\n */\n function buildAbsoluteUrl(forLocale: AvailableLanguage): string {\n const path = buildLocalizedRoutePath({\n locale: forLocale,\n defaultLocale,\n routePath,\n });\n return `${siteOrigin}${path}`;\n }\n\n const canonicalUrl = buildAbsoluteUrl(locale);\n\n const hreflangLines: string[] = [];\n for (const entry of websiteConfig.locales) {\n const localeStr = String(entry.locale);\n hreflangLines.push(\n `<link rel=\"alternate\" hreflang=\"${escapeHtmlAttribute(localeStr)}\" href=\"${escapeHtmlAttribute(buildAbsoluteUrl(entry.locale))}\" />`,\n );\n }\n hreflangLines.push(\n `<link rel=\"alternate\" hreflang=\"x-default\" href=\"${escapeHtmlAttribute(buildAbsoluteUrl(defaultLocale))}\" />`,\n );\n\n /**\n * `og:image` + `twitter:image` are emitted only when an image URL\n * is available (page-level override OR site-level fallback).\n * Twitter card type also flips between `summary` (no image) and\n * `summary_large_image` (image present).\n */\n const ogImageLines: string[] = [];\n if (ogImageUrl !== undefined) {\n const ogImageAttr = escapeHtmlAttribute(ogImageUrl);\n ogImageLines.push(\n `<meta property=\"og:image\" content=\"${ogImageAttr}\" />`,\n `<meta name=\"twitter:image\" content=\"${ogImageAttr}\" />`,\n );\n }\n const twitterCardKind = ogImageUrl !== undefined ? 'summary_large_image' : 'summary';\n\n /**\n * `og:locale:alternate` — one per non-current declared locale. Open\n * Graph spec: the `og:locale` field carries the page's primary\n * locale; alternates declare the other locales the page is\n * available in. Emitted in declaration order.\n *\n * Locale format: we emit the bare language code (`en`, `fr`) per\n * the AvailableLanguage atom shape. Open Graph technically\n * recommends `language_TERRITORY` (e.g. `en_US`), but Facebook /\n * LinkedIn / Slack accept the bare language code in practice. The\n * future fix when the platform grows territory-tagged locales is to\n * map at this site (no consumer change required).\n */\n const ogLocaleAlternateLines: string[] = [];\n for (const entry of websiteConfig.locales) {\n const localeStr = String(entry.locale);\n if (localeStr === currentLocaleStr) continue;\n ogLocaleAlternateLines.push(\n `<meta property=\"og:locale:alternate\" content=\"${escapeHtmlAttribute(localeStr)}\" />`,\n );\n }\n\n const siteNameAttr = escapeHtmlAttribute(rootConfig.seo.siteName);\n const canonicalAttr = escapeHtmlAttribute(canonicalUrl);\n const ogLocaleAttr = escapeHtmlAttribute(currentLocaleStr);\n\n /**\n * AI-training opt-out (D-AG3) — `<meta name=\"robots\" content=\"noai, noimageai\">`\n * is emitted ONLY when the page manifest opts in via\n * `seo.noaiOptOut: true`. Default is OPT-IN to AI training (the\n * marketing-site default). This emission pairs with the sibling\n * `_headers` entry produced by `renderWebsitePageNoaiHeaders()`\n * for crawlers that don't honor in-document meta.\n *\n * Format: `noai` (block AI training on text) and `noimageai`\n * (block AI training on images) are the two robots directives the\n * major AI crawlers (GPTBot, ClaudeBot, Google-Extended, etc.)\n * recognise. Emitting them as a single comma-separated `content`\n * value is the canonical robots-meta shape.\n */\n const noaiMetaLines: string[] = [];\n if (manifest.seo?.noaiOptOut === true) {\n noaiMetaLines.push(`<meta name=\"robots\" content=\"noai, noimageai\" />`);\n }\n\n return [\n `<title>${titleText}</title>`,\n `<meta name=\"description\" content=\"${descriptionAttr}\" />`,\n `<link rel=\"canonical\" href=\"${canonicalAttr}\" />`,\n `<meta property=\"og:title\" content=\"${titleAttr}\" />`,\n `<meta property=\"og:description\" content=\"${descriptionAttr}\" />`,\n `<meta property=\"og:type\" content=\"website\" />`,\n `<meta property=\"og:url\" content=\"${canonicalAttr}\" />`,\n `<meta property=\"og:site_name\" content=\"${siteNameAttr}\" />`,\n `<meta property=\"og:locale\" content=\"${ogLocaleAttr}\" />`,\n ...ogLocaleAlternateLines,\n ...ogImageLines,\n `<meta name=\"twitter:card\" content=\"${twitterCardKind}\" />`,\n `<meta name=\"twitter:title\" content=\"${titleAttr}\" />`,\n `<meta name=\"twitter:description\" content=\"${descriptionAttr}\" />`,\n ...noaiMetaLines,\n ...hreflangLines,\n ].join('\\n');\n}\n\n/**\n * Flatten `WebsiteDesignTokens` to a `:root { … }` block of CSS custom\n * properties. Naming convention: `--website-<group>-<token>` for nested\n * groups (`colors.primary` → `--website-colors-primary`); top-level\n * string-valued tokens (none today, future-proofed) emit as\n * `--website-<token>`.\n *\n * **Why scoped to `:root`** rather than `<WebsitePageLayout>`'s root\n * div: marketing pages frequently embed third-party widgets (live-chat,\n * analytics overlays) that paint outside the React tree. Scoping\n * tokens to `:root` makes them visible to those widgets too. The\n * design-token surface is small enough that the cost of a broader\n * scope (token-name collisions with consumer-authored `:root` rules)\n * is bounded by the `--website-` prefix.\n */\nexport function renderWebsiteDesignTokensCss(tokens: WebsiteDesignTokens): string {\n const lines: string[] = [':root {'];\n for (const [groupName, groupValue] of Object.entries(tokens) as [string, unknown][]) {\n if (groupValue !== null && typeof groupValue === 'object' && !Array.isArray(groupValue)) {\n for (const [tokenName, tokenValue] of Object.entries(groupValue) as [string, unknown][]) {\n if (typeof tokenValue === 'string') {\n lines.push(` --website-${groupName}-${tokenName}: ${tokenValue};`);\n }\n }\n } else if (typeof groupValue === 'string') {\n lines.push(` --website-${groupName}: ${groupValue};`);\n }\n }\n lines.push('}');\n return lines.join('\\n');\n}\n\n/**\n * @wildo_source:part:end saas.website.astro.head-renderer\n *\n * HTML escape helpers moved to `./internal/html-escape.ts` (Phase 7)\n * so the new `blog-post-head.renderer.ts` sibling can share the same\n * escape contract instead of redefining it. See that module's preamble\n * for the framework-mindset rationale (one escape contract per engine,\n * not one per renderer).\n */\n"]}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
2
|
+
import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
|
|
3
|
+
import type { LabelPack } from '../schemas/validators/label-pack.validator';
|
|
4
|
+
import type { WebsiteSiteContext } from './website-site-context';
|
|
5
|
+
/**
|
|
6
|
+
* @wildo_source:part:start saas.website.astro.runtime-helper facet:layer:astro facet:family:website
|
|
7
|
+
*
|
|
8
|
+
* `loadWebsitePageRuntime(opts)` — the per-request entry point a
|
|
9
|
+
* consumer's `src/pages/<page>.astro` calls inside its frontmatter to
|
|
10
|
+
* resolve the active locale and load the matching label pack. Returns
|
|
11
|
+
* the two primitive values (`locale`, `labelPack`) that travel as
|
|
12
|
+
* Astro-island props to `WebsitePageBridgeIsland` (see
|
|
13
|
+
* `bridge-runtime.tsx`).
|
|
14
|
+
*
|
|
15
|
+
* **Why a single helper** (instead of two — locale resolution + pack
|
|
16
|
+
* load — invoked separately in every page's frontmatter): the locale
|
|
17
|
+
* fallback rule (`Astro.currentLocale ?? websiteConfig.defaultLocale`)
|
|
18
|
+
* MUST stay aligned with the `prefixDefaultLocale: false` convention
|
|
19
|
+
* encoded by `buildAstroI18nConfig`. Centralizing the rule here means
|
|
20
|
+
* a future change (e.g. honoring an `Accept-Language` header before
|
|
21
|
+
* the static default-locale fallback) lands in one place rather than
|
|
22
|
+
* being duplicated across every consumer page's frontmatter.
|
|
23
|
+
*
|
|
24
|
+
* **Why this lives in a `.ts` helper, NOT in a `.astro` shell**: a shipped
|
|
25
|
+
* `WebsitePageBridge.astro` cannot cross-host the page/header/footer
|
|
26
|
+
* `ComponentType` references through the React-island serialization
|
|
27
|
+
* boundary (Astro serializes island props via `devalue`-style JSON;
|
|
28
|
+
* function refs do not survive). Keeping the per-request work in a
|
|
29
|
+
* pure TS helper means the consumer authors their own thin per-page
|
|
30
|
+
* `.astro` file that statically imports each page's bridge island —
|
|
31
|
+
* the component reference is then a JSX tag in the consumer's
|
|
32
|
+
* `.astro` template, which Astro's compiler resolves at build time
|
|
33
|
+
* (no prop serialization).
|
|
34
|
+
*
|
|
35
|
+
* **Failure modes** (all surfaced as thrown errors with actionable
|
|
36
|
+
* messages):
|
|
37
|
+
* - `astroLocale` is provided but is not a member of
|
|
38
|
+
* `siteContext.websiteConfig.locales`: throws — Astro's i18n
|
|
39
|
+
* routing should never present a locale outside the declared set;
|
|
40
|
+
* if it does, `astro.config.mjs` is out of sync with
|
|
41
|
+
* `WildoMarketingWebsiteConfig`.
|
|
42
|
+
* - Label pack file missing / malformed: surfaced by
|
|
43
|
+
* `loadLabelPack` (filesystem error message + locale + path).
|
|
44
|
+
*
|
|
45
|
+
* @param opts.manifest The page manifest the consumer is
|
|
46
|
+
* rendering. Read by `pickLabelsForPage`
|
|
47
|
+
* to filter the loaded label pack down to
|
|
48
|
+
* the keys this page actually consumes
|
|
49
|
+
* (the page's own `website.<ref>.*` slice
|
|
50
|
+
* plus the shared `website.chrome.*`
|
|
51
|
+
* slice). Without this filter every page
|
|
52
|
+
* would serialize the whole site's pack
|
|
53
|
+
* into its `<astro-island props=…>`
|
|
54
|
+
* attribute — see
|
|
55
|
+
* `pick-labels-for-page.ts` for the full
|
|
56
|
+
* rationale.
|
|
57
|
+
* @param opts.siteContext The composed `WebsiteSiteContext`. Read
|
|
58
|
+
* for `websiteConfig.{defaultLocale,
|
|
59
|
+
* locales}` (locale resolution) and
|
|
60
|
+
* `rootConfig.skipToMainContentLabelKey`
|
|
61
|
+
* (chrome key carried into the per-page
|
|
62
|
+
* pack slice).
|
|
63
|
+
* @param opts.labelPacksDir Directory holding `<locale>.json` packs.
|
|
64
|
+
* Resolved relative to the cwd Astro runs
|
|
65
|
+
* in (typically the project root).
|
|
66
|
+
* @param opts.astroLocale Whatever `Astro.currentLocale` returned
|
|
67
|
+
* at the consumer's call site.
|
|
68
|
+
* `string | undefined` — Astro returns
|
|
69
|
+
* `undefined` for the un-prefixed
|
|
70
|
+
* default-locale path.
|
|
71
|
+
*/
|
|
72
|
+
export interface LoadWebsitePageRuntimeOptions {
|
|
73
|
+
/** The page manifest the consumer is rendering (carried for forward-compat). */
|
|
74
|
+
manifest: WebsitePageManifest;
|
|
75
|
+
/** The site's composed `WebsiteSiteContext` — single source of truth. */
|
|
76
|
+
siteContext: WebsiteSiteContext;
|
|
77
|
+
/** Directory holding `<locale>.json` label-pack files. */
|
|
78
|
+
labelPacksDir: string;
|
|
79
|
+
/** The value of `Astro.currentLocale` at the call site. */
|
|
80
|
+
astroLocale: string | undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Result of `loadWebsitePageRuntime` — the two primitive values that
|
|
84
|
+
* travel as Astro-island props to the page's bridge island.
|
|
85
|
+
*
|
|
86
|
+
* Both values are JSON-serializable so they cleanly cross Astro's
|
|
87
|
+
* client-island hydration boundary. Function refs (page component,
|
|
88
|
+
* header / footer component) are NOT carried here on purpose; they
|
|
89
|
+
* are closed over by the per-page `createWebsitePageBridgeIsland(...)`
|
|
90
|
+
* call so they never need to cross a serialization boundary. See the
|
|
91
|
+
* file-level JSDoc above and `bridge-runtime.tsx` for the redesign
|
|
92
|
+
* rationale.
|
|
93
|
+
*/
|
|
94
|
+
export interface WebsitePageRuntime {
|
|
95
|
+
/** The resolved locale (Astro's value with default-locale fallback). */
|
|
96
|
+
locale: AvailableLanguage;
|
|
97
|
+
/**
|
|
98
|
+
* The site's default locale (mirrors `websiteConfig.defaultLocale`).
|
|
99
|
+
* Returned alongside `locale` so the consumer can pass both to the
|
|
100
|
+
* bridge island in one symmetric block (`{ locale, defaultLocale }`)
|
|
101
|
+
* — the island then publishes both into `WebsiteRuntimeContext`,
|
|
102
|
+
* which `WebsiteInternalLink` reads to compute locale-prefixed paths.
|
|
103
|
+
* Re-exporting it here avoids forcing every
|
|
104
|
+
* consumer page to import the website config separately.
|
|
105
|
+
*/
|
|
106
|
+
defaultLocale: AvailableLanguage;
|
|
107
|
+
/** The loaded + shape-validated label pack for the active locale. */
|
|
108
|
+
labelPack: LabelPack;
|
|
109
|
+
}
|
|
110
|
+
export declare function loadWebsitePageRuntime(opts: LoadWebsitePageRuntimeOptions): WebsitePageRuntime;
|
|
111
|
+
/** @wildo_source:part:end saas.website.astro.runtime-helper */
|
|
112
|
+
//# sourceMappingURL=website-page-runtime.helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"website-page-runtime.helper.d.ts","sourceRoot":"","sources":["../../../../src/astro/website-page-runtime.helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAGzE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2DAA2D,CAAC;AACrG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAI5E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,gFAAgF;IAChF,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,yEAAyE;IACzE,WAAW,EAAE,kBAAkB,CAAC;IAChC,0DAA0D;IAC1D,aAAa,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IACjC,wEAAwE;IACxE,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;;;;OAQG;IACH,aAAa,EAAE,iBAAiB,CAAC;IACjC,qEAAqE;IACrE,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,6BAA6B,GAClC,kBAAkB,CAqCpB;AA+BD,+DAA+D"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { loadLabelPack } from './label-pack-loader.js';
|
|
2
|
+
import { pickLabelsForPage } from './pick-labels-for-page.js';
|
|
3
|
+
export function loadWebsitePageRuntime(opts) {
|
|
4
|
+
const { manifest, siteContext, labelPacksDir, astroLocale } = opts;
|
|
5
|
+
const { rootConfig, websiteConfig } = siteContext;
|
|
6
|
+
/**
|
|
7
|
+
* Locale resolution — the `prefixDefaultLocale: false` convention
|
|
8
|
+
* means Astro serves the default locale at the un-prefixed path
|
|
9
|
+
* (`/`, `/pricing`, …) and reports `undefined` from
|
|
10
|
+
* `Astro.currentLocale` for those requests. Coalesce here to the
|
|
11
|
+
* website config's default rather than letting `undefined` leak
|
|
12
|
+
* into the label-pack lookup.
|
|
13
|
+
*/
|
|
14
|
+
const resolvedLocale = astroLocale === undefined
|
|
15
|
+
? websiteConfig.defaultLocale
|
|
16
|
+
: assertKnownLocale(astroLocale, websiteConfig);
|
|
17
|
+
const fullPack = loadLabelPack(labelPacksDir, resolvedLocale);
|
|
18
|
+
/**
|
|
19
|
+
* Filter to the keys this page can ever read at runtime — page-own
|
|
20
|
+
* slice + shared chrome slice. The full pack is loaded once (one
|
|
21
|
+
* fs read per request), then sliced; the slimmed result is what
|
|
22
|
+
* crosses the SSR → client serialization boundary inside
|
|
23
|
+
* `<astro-island props=…>`. Sites with O(100) keys per page on
|
|
24
|
+
* O(10) pages save material payload on every HTML response — the
|
|
25
|
+
* untrimmed default would have served every page the entire site's
|
|
26
|
+
* pack on every locale.
|
|
27
|
+
*/
|
|
28
|
+
const labelPack = pickLabelsForPage(fullPack, manifest, [
|
|
29
|
+
rootConfig.skipToMainContentLabelKey,
|
|
30
|
+
]);
|
|
31
|
+
return {
|
|
32
|
+
locale: resolvedLocale,
|
|
33
|
+
defaultLocale: websiteConfig.defaultLocale,
|
|
34
|
+
labelPack,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Defensive: Astro's i18n routing should only ever present a locale
|
|
39
|
+
* declared on `WildoMarketingWebsiteConfig.locales`. If it does not,
|
|
40
|
+
* the consumer's `astro.config.mjs` is out of sync with the website
|
|
41
|
+
* config — surface that loudly here rather than letting an unknown
|
|
42
|
+
* locale trigger a `[label-pack-loader] Could not read pack` error
|
|
43
|
+
* that would imply a content-authoring fault (missing JSON file)
|
|
44
|
+
* rather than a configuration fault (Astro routing mismatch).
|
|
45
|
+
*/
|
|
46
|
+
function assertKnownLocale(astroLocale, websiteConfig) {
|
|
47
|
+
for (const entry of websiteConfig.locales) {
|
|
48
|
+
if (entry.locale === astroLocale) {
|
|
49
|
+
return entry.locale;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const declared = websiteConfig.locales.map((entry) => entry.locale);
|
|
53
|
+
throw new Error(`[loadWebsitePageRuntime] Astro presented locale "${astroLocale}" but ` +
|
|
54
|
+
`WildoMarketingWebsiteConfig.locales declares only [${declared.join(', ')}]. ` +
|
|
55
|
+
`Either add "${astroLocale}" to wildo.website.config.ts's locales or update ` +
|
|
56
|
+
`astro.config.mjs to consume buildAstroI18nConfig(WEBSITE_SITE_CONTEXT) so ` +
|
|
57
|
+
`both sides stay in sync.`);
|
|
58
|
+
}
|
|
59
|
+
/** @wildo_source:part:end saas.website.astro.runtime-helper */
|
|
60
|
+
//# sourceMappingURL=website-page-runtime.helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"website-page-runtime.helper.js","sourceRoot":"","sources":["../../../../src/astro/website-page-runtime.helper.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AA8G3D,MAAM,UAAU,sBAAsB,CACpC,IAAmC;IAEnC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACnE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,WAAW,CAAC;IAElD;;;;;;;OAOG;IACH,MAAM,cAAc,GAClB,WAAW,KAAK,SAAS;QACvB,CAAC,CAAC,aAAa,CAAC,aAAa;QAC7B,CAAC,CAAC,iBAAiB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAEpD,MAAM,QAAQ,GAAG,aAAa,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC;IAC9D;;;;;;;;;OASG;IACH,MAAM,SAAS,GAAG,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE;QACtD,UAAU,CAAC,yBAAyB;KACrC,CAAC,CAAC;IAEH,OAAO;QACL,MAAM,EAAE,cAAc;QACtB,aAAa,EAAE,aAAa,CAAC,aAAa;QAC1C,SAAS;KACV,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,iBAAiB,CACxB,WAAmB,EACnB,aAA0C;IAE1C,KAAK,MAAM,KAAK,IAAI,aAAa,CAAC,OAAO,EAAE,CAAC;QAC1C,IAAK,KAAK,CAAC,MAA4B,KAAK,WAAW,EAAE,CAAC;YACxD,OAAO,KAAK,CAAC,MAAM,CAAC;QACtB,CAAC;IACH,CAAC;IACD,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,GAAG,CACxC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAA2B,CAC7C,CAAC;IACF,MAAM,IAAI,KAAK,CACb,oDAAoD,WAAW,QAAQ;QACrE,sDAAsD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;QAC9E,eAAe,WAAW,mDAAmD;QAC7E,4EAA4E;QAC5E,0BAA0B,CAC7B,CAAC;AACJ,CAAC;AACD,+DAA+D","sourcesContent":["import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport type { WildoMarketingWebsiteConfig } from '../config/wildo-website-config.schemas';\nimport type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';\nimport type { LabelPack } from '../schemas/validators/label-pack.validator';\n\nimport { loadLabelPack } from './label-pack-loader';\nimport { pickLabelsForPage } from './pick-labels-for-page';\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.runtime-helper facet:layer:astro facet:family:website\n *\n * `loadWebsitePageRuntime(opts)` — the per-request entry point a\n * consumer's `src/pages/<page>.astro` calls inside its frontmatter to\n * resolve the active locale and load the matching label pack. Returns\n * the two primitive values (`locale`, `labelPack`) that travel as\n * Astro-island props to `WebsitePageBridgeIsland` (see\n * `bridge-runtime.tsx`).\n *\n * **Why a single helper** (instead of two — locale resolution + pack\n * load — invoked separately in every page's frontmatter): the locale\n * fallback rule (`Astro.currentLocale ?? websiteConfig.defaultLocale`)\n * MUST stay aligned with the `prefixDefaultLocale: false` convention\n * encoded by `buildAstroI18nConfig`. Centralizing the rule here means\n * a future change (e.g. honoring an `Accept-Language` header before\n * the static default-locale fallback) lands in one place rather than\n * being duplicated across every consumer page's frontmatter.\n *\n * **Why this lives in a `.ts` helper, NOT in a `.astro` shell**: a shipped\n * `WebsitePageBridge.astro` cannot cross-host the page/header/footer\n * `ComponentType` references through the React-island serialization\n * boundary (Astro serializes island props via `devalue`-style JSON;\n * function refs do not survive). Keeping the per-request work in a\n * pure TS helper means the consumer authors their own thin per-page\n * `.astro` file that statically imports each page's bridge island —\n * the component reference is then a JSX tag in the consumer's\n * `.astro` template, which Astro's compiler resolves at build time\n * (no prop serialization).\n *\n * **Failure modes** (all surfaced as thrown errors with actionable\n * messages):\n * - `astroLocale` is provided but is not a member of\n * `siteContext.websiteConfig.locales`: throws — Astro's i18n\n * routing should never present a locale outside the declared set;\n * if it does, `astro.config.mjs` is out of sync with\n * `WildoMarketingWebsiteConfig`.\n * - Label pack file missing / malformed: surfaced by\n * `loadLabelPack` (filesystem error message + locale + path).\n *\n * @param opts.manifest The page manifest the consumer is\n * rendering. Read by `pickLabelsForPage`\n * to filter the loaded label pack down to\n * the keys this page actually consumes\n * (the page's own `website.<ref>.*` slice\n * plus the shared `website.chrome.*`\n * slice). Without this filter every page\n * would serialize the whole site's pack\n * into its `<astro-island props=…>`\n * attribute — see\n * `pick-labels-for-page.ts` for the full\n * rationale.\n * @param opts.siteContext The composed `WebsiteSiteContext`. Read\n * for `websiteConfig.{defaultLocale,\n * locales}` (locale resolution) and\n * `rootConfig.skipToMainContentLabelKey`\n * (chrome key carried into the per-page\n * pack slice).\n * @param opts.labelPacksDir Directory holding `<locale>.json` packs.\n * Resolved relative to the cwd Astro runs\n * in (typically the project root).\n * @param opts.astroLocale Whatever `Astro.currentLocale` returned\n * at the consumer's call site.\n * `string | undefined` — Astro returns\n * `undefined` for the un-prefixed\n * default-locale path.\n */\nexport interface LoadWebsitePageRuntimeOptions {\n /** The page manifest the consumer is rendering (carried for forward-compat). */\n manifest: WebsitePageManifest;\n /** The site's composed `WebsiteSiteContext` — single source of truth. */\n siteContext: WebsiteSiteContext;\n /** Directory holding `<locale>.json` label-pack files. */\n labelPacksDir: string;\n /** The value of `Astro.currentLocale` at the call site. */\n astroLocale: string | undefined;\n}\n\n/**\n * Result of `loadWebsitePageRuntime` — the two primitive values that\n * travel as Astro-island props to the page's bridge island.\n *\n * Both values are JSON-serializable so they cleanly cross Astro's\n * client-island hydration boundary. Function refs (page component,\n * header / footer component) are NOT carried here on purpose; they\n * are closed over by the per-page `createWebsitePageBridgeIsland(...)`\n * call so they never need to cross a serialization boundary. See the\n * file-level JSDoc above and `bridge-runtime.tsx` for the redesign\n * rationale.\n */\nexport interface WebsitePageRuntime {\n /** The resolved locale (Astro's value with default-locale fallback). */\n locale: AvailableLanguage;\n /**\n * The site's default locale (mirrors `websiteConfig.defaultLocale`).\n * Returned alongside `locale` so the consumer can pass both to the\n * bridge island in one symmetric block (`{ locale, defaultLocale }`)\n * — the island then publishes both into `WebsiteRuntimeContext`,\n * which `WebsiteInternalLink` reads to compute locale-prefixed paths.\n * Re-exporting it here avoids forcing every\n * consumer page to import the website config separately.\n */\n defaultLocale: AvailableLanguage;\n /** The loaded + shape-validated label pack for the active locale. */\n labelPack: LabelPack;\n}\n\nexport function loadWebsitePageRuntime(\n opts: LoadWebsitePageRuntimeOptions,\n): WebsitePageRuntime {\n const { manifest, siteContext, labelPacksDir, astroLocale } = opts;\n const { rootConfig, websiteConfig } = siteContext;\n\n /**\n * Locale resolution — the `prefixDefaultLocale: false` convention\n * means Astro serves the default locale at the un-prefixed path\n * (`/`, `/pricing`, …) and reports `undefined` from\n * `Astro.currentLocale` for those requests. Coalesce here to the\n * website config's default rather than letting `undefined` leak\n * into the label-pack lookup.\n */\n const resolvedLocale: AvailableLanguage =\n astroLocale === undefined\n ? websiteConfig.defaultLocale\n : assertKnownLocale(astroLocale, websiteConfig);\n\n const fullPack = loadLabelPack(labelPacksDir, resolvedLocale);\n /**\n * Filter to the keys this page can ever read at runtime — page-own\n * slice + shared chrome slice. The full pack is loaded once (one\n * fs read per request), then sliced; the slimmed result is what\n * crosses the SSR → client serialization boundary inside\n * `<astro-island props=…>`. Sites with O(100) keys per page on\n * O(10) pages save material payload on every HTML response — the\n * untrimmed default would have served every page the entire site's\n * pack on every locale.\n */\n const labelPack = pickLabelsForPage(fullPack, manifest, [\n rootConfig.skipToMainContentLabelKey,\n ]);\n\n return {\n locale: resolvedLocale,\n defaultLocale: websiteConfig.defaultLocale,\n labelPack,\n };\n}\n\n/**\n * Defensive: Astro's i18n routing should only ever present a locale\n * declared on `WildoMarketingWebsiteConfig.locales`. If it does not,\n * the consumer's `astro.config.mjs` is out of sync with the website\n * config — surface that loudly here rather than letting an unknown\n * locale trigger a `[label-pack-loader] Could not read pack` error\n * that would imply a content-authoring fault (missing JSON file)\n * rather than a configuration fault (Astro routing mismatch).\n */\nfunction assertKnownLocale(\n astroLocale: string,\n websiteConfig: WildoMarketingWebsiteConfig,\n): AvailableLanguage {\n for (const entry of websiteConfig.locales) {\n if ((entry.locale as unknown as string) === astroLocale) {\n return entry.locale;\n }\n }\n const declared = websiteConfig.locales.map(\n (entry) => entry.locale as unknown as string,\n );\n throw new Error(\n `[loadWebsitePageRuntime] Astro presented locale \"${astroLocale}\" but ` +\n `WildoMarketingWebsiteConfig.locales declares only [${declared.join(', ')}]. ` +\n `Either add \"${astroLocale}\" to wildo.website.config.ts's locales or update ` +\n `astro.config.mjs to consume buildAstroI18nConfig(WEBSITE_SITE_CONTEXT) so ` +\n `both sides stay in sync.`,\n );\n}\n/** @wildo_source:part:end saas.website.astro.runtime-helper */\n"]}
|