@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,82 @@
|
|
|
1
|
+
import { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils';
|
|
2
|
+
import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
|
|
3
|
+
|
|
4
|
+
import type { WebsiteSiteContext } from './website-site-context';
|
|
5
|
+
|
|
6
|
+
const NOAI_ROBOTS_HEADER_VALUE = 'noai, noimageai' as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @wildo_source:part:start saas.website.astro.headers-renderer facet:layer:astro facet:family:website
|
|
10
|
+
*
|
|
11
|
+
* Render a static-host-compatible `_headers` body for every page
|
|
12
|
+
* manifest whose `seo.noaiOptOut === true`.
|
|
13
|
+
*
|
|
14
|
+
* The output format matches Cloudflare Pages / Netlify / Vercel's
|
|
15
|
+
* path-scoped `_headers` conventions:
|
|
16
|
+
*
|
|
17
|
+
* ```txt
|
|
18
|
+
* /pricing
|
|
19
|
+
* X-Robots-Tag: noai, noimageai
|
|
20
|
+
*
|
|
21
|
+
* /fr/pricing
|
|
22
|
+
* X-Robots-Tag: noai, noimageai
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function renderWebsitePageNoaiHeaders(siteContext: WebsiteSiteContext): string {
|
|
26
|
+
const flaggedPages = siteContext.rootConfig.pageManifests.filter(
|
|
27
|
+
(manifest) => manifest.seo?.noaiOptOut === true,
|
|
28
|
+
);
|
|
29
|
+
if (flaggedPages.length === 0) {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const lines: string[] = [];
|
|
34
|
+
for (const manifest of flaggedPages) {
|
|
35
|
+
appendManifestHeaders(lines, manifest, siteContext);
|
|
36
|
+
}
|
|
37
|
+
return lines.slice(0, -1).join('\n');
|
|
38
|
+
}
|
|
39
|
+
/** @wildo_source:part:end saas.website.astro.headers-renderer */
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Future blog-post companion for the same header channel. Kept small
|
|
43
|
+
* and pure so a consumer's `[slug].astro` route or `_headers`
|
|
44
|
+
* generation script can opt blog posts out of AI training without
|
|
45
|
+
* re-implementing the locale-path logic.
|
|
46
|
+
*/
|
|
47
|
+
export function renderBlogPostNoaiHeaders(params: {
|
|
48
|
+
siteContext: WebsiteSiteContext;
|
|
49
|
+
routePath: string;
|
|
50
|
+
noaiOptOut?: boolean;
|
|
51
|
+
}): string {
|
|
52
|
+
const { siteContext, routePath, noaiOptOut } = params;
|
|
53
|
+
if (noaiOptOut !== true) {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const lines: string[] = [];
|
|
58
|
+
for (const { locale } of siteContext.websiteConfig.locales) {
|
|
59
|
+
const localizedPath = buildLocalizedRoutePath({
|
|
60
|
+
locale,
|
|
61
|
+
defaultLocale: siteContext.websiteConfig.defaultLocale,
|
|
62
|
+
routePath,
|
|
63
|
+
});
|
|
64
|
+
lines.push(localizedPath, ` X-Robots-Tag: ${NOAI_ROBOTS_HEADER_VALUE}`, '');
|
|
65
|
+
}
|
|
66
|
+
return lines.slice(0, -1).join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function appendManifestHeaders(
|
|
70
|
+
lines: string[],
|
|
71
|
+
manifest: WebsitePageManifest,
|
|
72
|
+
siteContext: WebsiteSiteContext,
|
|
73
|
+
): void {
|
|
74
|
+
for (const { locale } of siteContext.websiteConfig.locales) {
|
|
75
|
+
const localizedPath = buildLocalizedRoutePath({
|
|
76
|
+
locale,
|
|
77
|
+
defaultLocale: siteContext.websiteConfig.defaultLocale,
|
|
78
|
+
routePath: String(manifest.routePath),
|
|
79
|
+
});
|
|
80
|
+
lines.push(localizedPath, ` X-Robots-Tag: ${NOAI_ROBOTS_HEADER_VALUE}`, '');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
2
|
+
|
|
3
|
+
import type { WebsiteSiteContext } from './website-site-context';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @wildo_source:part:start saas.website.astro.i18n-helper facet:layer:astro facet:family:website
|
|
7
|
+
*
|
|
8
|
+
* `buildAstroI18nConfig(siteContext)` — derives the `i18n` block consumed
|
|
9
|
+
* by `astro/config`'s `defineConfig({ i18n })` from the shared
|
|
10
|
+
* `WebsiteSiteContext`.
|
|
11
|
+
*
|
|
12
|
+
* **Why a helper and not hand-authored in `astro.config.mjs`**: a
|
|
13
|
+
* marketing site has TWO sources of truth for "which locales does this
|
|
14
|
+
* site ship?" — the `WildoMarketingWebsiteConfig.locales` array
|
|
15
|
+
* (consumed by every Astro build helper) AND the Astro routing config
|
|
16
|
+
* (consumed by the file-based router and the dev server). Letting the
|
|
17
|
+
* consumer hand-author both creates a silent-drift surface: a locale
|
|
18
|
+
* added to one but not the other ships a working dev server with a
|
|
19
|
+
* missing label pack (or vice versa). This helper makes
|
|
20
|
+
* `WildoMarketingWebsiteConfig` (the per-service `wildo.website.config.ts`)
|
|
21
|
+
* the single source of truth.
|
|
22
|
+
*
|
|
23
|
+
* **Design decisions** (each carrying its own semantic weight):
|
|
24
|
+
*
|
|
25
|
+
* - **`prefixDefaultLocale = false`**. Astro's default routing strategy
|
|
26
|
+
* is the marketing-site convention: the default locale mounts at the
|
|
27
|
+
* un-prefixed path (`/`, `/pricing`, `/contact`) and other locales
|
|
28
|
+
* live under `/<locale>/…`. Forcing prefixes for the default locale
|
|
29
|
+
* would surface a `/en/` URL alongside `/` — duplicate content from
|
|
30
|
+
* an SEO standpoint and a UX wart. Marketing teams overwhelmingly
|
|
31
|
+
* ship the default at the root.
|
|
32
|
+
*
|
|
33
|
+
* - **`redirectToDefaultLocale = false`**. The default value Astro
|
|
34
|
+
* would otherwise infer (`true`) issues a redirect from `/` to
|
|
35
|
+
* `/<defaultLocale>/`, which directly contradicts the
|
|
36
|
+
* `prefixDefaultLocale = false` convention above. Setting both
|
|
37
|
+
* explicitly makes the contract loud at the call site (and in the
|
|
38
|
+
* compiled config) instead of relying on Astro's defaults staying
|
|
39
|
+
* stable across versions.
|
|
40
|
+
*
|
|
41
|
+
* - **`fallbackType = 'redirect'` is INTENTIONALLY OMITTED**.
|
|
42
|
+
* Marketing sites typically prefer one of two strategies: ship a
|
|
43
|
+
* page in every advertised locale OR hide the un-shipped locale
|
|
44
|
+
* entirely (no entry in `locales`). The `fallback` machinery (which
|
|
45
|
+
* would redirect `/de/pricing` to `/en/pricing` when the German
|
|
46
|
+
* page is missing) creates SEO duplicate-content risk that's hard
|
|
47
|
+
* to undo once Google indexes the redirected URL. Consumers who
|
|
48
|
+
* genuinely need fallback can spread this helper's output and
|
|
49
|
+
* add `fallback: { … }` themselves.
|
|
50
|
+
*
|
|
51
|
+
* **Type rationale**: the return type uses Astro's published `i18n`
|
|
52
|
+
* shape but we declare it as a structural literal type rather than
|
|
53
|
+
* importing from `astro` so this module stays in the saas-website
|
|
54
|
+
* source closure without forcing `astro` into the React-island side of the
|
|
55
|
+
* bundle. The package's bundle-boundary checks enforce that separation.
|
|
56
|
+
* The structural type is straightforward and matches Astro 4+ — see
|
|
57
|
+
* https://docs.astro.build/en/reference/configuration-reference/#i18n.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // astro.config.mjs
|
|
61
|
+
* import { defineConfig } from 'astro/config';
|
|
62
|
+
* import { buildAstroI18nConfig } from '@wildo-ai/saas-website/astro';
|
|
63
|
+
* import { WEBSITE_SITE_CONTEXT } from './src/website-site-context';
|
|
64
|
+
*
|
|
65
|
+
* export default defineConfig({
|
|
66
|
+
* i18n: buildAstroI18nConfig(WEBSITE_SITE_CONTEXT),
|
|
67
|
+
* });
|
|
68
|
+
*/
|
|
69
|
+
export interface AstroI18nConfig {
|
|
70
|
+
/** ISO locale string Astro mounts at the un-prefixed path. */
|
|
71
|
+
defaultLocale: string;
|
|
72
|
+
/** Every locale the site ships in (default included). */
|
|
73
|
+
locales: readonly string[];
|
|
74
|
+
routing: {
|
|
75
|
+
/**
|
|
76
|
+
* `false` means the default locale is served at the root path
|
|
77
|
+
* (`/`); other locales sit under `/<locale>/`. Typed as a
|
|
78
|
+
* widened `boolean` (not a literal `false`) so consumers who
|
|
79
|
+
* spread the config and override the field downstream do not
|
|
80
|
+
* fight literal-type narrowing — the runtime still emits
|
|
81
|
+
* `false` here, the type system just allows the override.
|
|
82
|
+
*/
|
|
83
|
+
prefixDefaultLocale: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* `false` means a request for `/` is served as the default-locale
|
|
86
|
+
* landing page (no implicit redirect to `/<defaultLocale>/`). Same
|
|
87
|
+
* literal-vs-widened rationale as `prefixDefaultLocale` above.
|
|
88
|
+
*/
|
|
89
|
+
redirectToDefaultLocale: boolean;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildAstroI18nConfig(siteContext: WebsiteSiteContext): AstroI18nConfig {
|
|
94
|
+
const { websiteConfig } = siteContext;
|
|
95
|
+
/**
|
|
96
|
+
* Defensive copy: `WildoMarketingWebsiteConfig.locales` is an array
|
|
97
|
+
* of `{ locale, path }` entries; Astro's i18n config wants raw
|
|
98
|
+
* locale strings in declaration order. We extract `locale` and map
|
|
99
|
+
* through `String()` so the `AvailableLanguage` brand layer does
|
|
100
|
+
* not leak into the Astro public API surface, and we freeze the
|
|
101
|
+
* result so an Astro plugin authored against this shape cannot
|
|
102
|
+
* mutate the source-of-truth array indirectly.
|
|
103
|
+
*/
|
|
104
|
+
const locales: readonly string[] = Object.freeze(
|
|
105
|
+
websiteConfig.locales.map((entry: { locale: AvailableLanguage }) => String(entry.locale)),
|
|
106
|
+
);
|
|
107
|
+
return {
|
|
108
|
+
defaultLocale: String(websiteConfig.defaultLocale),
|
|
109
|
+
locales,
|
|
110
|
+
routing: {
|
|
111
|
+
prefixDefaultLocale: false,
|
|
112
|
+
redirectToDefaultLocale: false,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** @wildo_source:part:end saas.website.astro.i18n-helper */
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTML-escape helpers used by every Astro head-block string
|
|
3
|
+
* builder in the saas-website engine (page-head + blog-post-head, and
|
|
4
|
+
* any future per-route renderer that interpolates consumer-supplied
|
|
5
|
+
* copy into a string-output `<head>` template).
|
|
6
|
+
*
|
|
7
|
+
* # Why a shared module rather than duplicating per-renderer
|
|
8
|
+
*
|
|
9
|
+
* Marketing copy frequently contains characters (quotes, ampersands,
|
|
10
|
+
* the rare `<`) that would break the surrounding tag if interpolated
|
|
11
|
+
* raw. Two renderers escaping with two slightly-different character
|
|
12
|
+
* sets is exactly the kind of silent-drift surface this codebase
|
|
13
|
+
* forbids — one renderer might forget the `'` → `'` substitution
|
|
14
|
+
* and produce attribute breakouts in JSON-LD copy, while the other
|
|
15
|
+
* does it correctly. Centralizing both helpers here gives every
|
|
16
|
+
* renderer ONE escape contract to audit.
|
|
17
|
+
*
|
|
18
|
+
* # Why both `Attribute` and `Text` variants exist (currently identical)
|
|
19
|
+
*
|
|
20
|
+
* Conceptually distinct (an attribute escape needs `"`/`'` covered;
|
|
21
|
+
* a text-content escape technically only needs `<`/`>`/`&`). Today
|
|
22
|
+
* they share a body for convenience — the cost of over-escaping `"`
|
|
23
|
+
* inside text content is negligible (browsers render the entity
|
|
24
|
+
* correctly) and the win is one fewer code path to audit. Splitting
|
|
25
|
+
* the implementations later (when a real divergence shows up — e.g. a
|
|
26
|
+
* JSON-LD emitter that legitimately wants raw `<script type="application/ld+json">`
|
|
27
|
+
* inner-content escaping rules) is a one-file change.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export function escapeHtmlAttribute(value: string): string {
|
|
31
|
+
return value
|
|
32
|
+
.replace(/&/g, '&')
|
|
33
|
+
.replace(/</g, '<')
|
|
34
|
+
.replace(/>/g, '>')
|
|
35
|
+
.replace(/"/g, '"')
|
|
36
|
+
.replace(/'/g, ''');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function escapeHtmlText(value: string): string {
|
|
40
|
+
return escapeHtmlAttribute(value);
|
|
41
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
5
|
+
|
|
6
|
+
import type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';
|
|
7
|
+
import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
|
|
8
|
+
import type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';
|
|
9
|
+
import {
|
|
10
|
+
validateLabelPackAgainstManifest,
|
|
11
|
+
type ExpectedLabelKeysBySectionRef,
|
|
12
|
+
type LabelPack,
|
|
13
|
+
type LabelPackValidationResult,
|
|
14
|
+
} from '../schemas/validators/label-pack.validator';
|
|
15
|
+
|
|
16
|
+
import type { WebsiteSiteContext } from './website-site-context';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @wildo_source:part:start saas.website.astro.label-loader facet:layer:astro facet:family:website
|
|
20
|
+
*
|
|
21
|
+
* `label-pack-loader.ts` — synchronous, fs-backed loader for
|
|
22
|
+
* marketing-site label packs. Consumed by:
|
|
23
|
+
* - the Astro adapter (`WebsitePageBridge.astro` reads the active
|
|
24
|
+
* locale's pack at request time during SSR / pre-render),
|
|
25
|
+
* - the `wildo website audit` CLI (Phase 7) to validate every
|
|
26
|
+
* locale pack against every manifest at CI time,
|
|
27
|
+
* - any build-time hook that wants to fail fast on a missing or
|
|
28
|
+
* malformed pack.
|
|
29
|
+
*
|
|
30
|
+
* **Convention** (matches the slice plan + the validator's contract):
|
|
31
|
+
*
|
|
32
|
+
* - One file per locale: `<packsDir>/<locale>.json`
|
|
33
|
+
* - JSON object: flat key-value map, `Record<string, string>`
|
|
34
|
+
* - Keys are fully-composed `WebsiteLabelKey`s (`website.<page>.<section>.<slot>`)
|
|
35
|
+
* - Values are the localized strings
|
|
36
|
+
*
|
|
37
|
+
* **Why synchronous I/O**:
|
|
38
|
+
* - The Astro frontmatter executes at build / SSR time in a Node
|
|
39
|
+
* process (no event loop pressure to relieve), and the bridge
|
|
40
|
+
* wants the pack resolved before it emits any HTML — async would
|
|
41
|
+
* force the consumer to `await` inside an Astro frontmatter,
|
|
42
|
+
* which is supported but adds a needless yield-point on a single
|
|
43
|
+
* small JSON file.
|
|
44
|
+
* - The validator is pure and synchronous; making the loader async
|
|
45
|
+
* would add a coloring boundary for no benefit.
|
|
46
|
+
*
|
|
47
|
+
* **Failure modes — all surfaced as thrown errors with actionable
|
|
48
|
+
* messages**:
|
|
49
|
+
* - File missing — wrapped in a `[label-pack-loader] Could not read
|
|
50
|
+
* pack at <path>` error so a typo in the dir / locale is obvious.
|
|
51
|
+
* - JSON parse failure — error includes the file path and the
|
|
52
|
+
* parser's own message.
|
|
53
|
+
* - Top-level shape is not an object, or a value is not a string —
|
|
54
|
+
* error names the offending key + observed type.
|
|
55
|
+
*
|
|
56
|
+
* **Why three exports** (loader, multi-loader, validator-runner):
|
|
57
|
+
* - `loadLabelPack(packsDir, locale)` is the atomic primitive
|
|
58
|
+
* (one file → one validated `LabelPack`). Used by the bridge at
|
|
59
|
+
* request time.
|
|
60
|
+
* - `loadAllLabelPacks(packsDir, siteContext)` is the build-time
|
|
61
|
+
* batch — every locale pack at once. Used by the CLI auditor
|
|
62
|
+
* and by the build-start hook.
|
|
63
|
+
* - `validateAllLabelPacks(packsDir, siteContext, expectedKeysBySectionRef)`
|
|
64
|
+
* is the highest-level entry point: load every pack, run the
|
|
65
|
+
* validator on each, and either return the diagnostic results
|
|
66
|
+
* or throw on the first hard error. The returned shape carries
|
|
67
|
+
* per-locale diagnostics so the caller can format a CLI report.
|
|
68
|
+
*
|
|
69
|
+
* **`siteContext` parameter** — the consumer's composed
|
|
70
|
+
* `WebsiteSiteContext` (see `website-site-context.ts`). The loader
|
|
71
|
+
* reads `siteContext.websiteConfig.locales` for "which locales does
|
|
72
|
+
* the site ship?" and `siteContext.rootConfig.{pageManifests,
|
|
73
|
+
* chromeLabelKeys, skipToMainContentLabelKey}` for the manifest
|
|
74
|
+
* registry / chrome-key set the validator runs against.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load and validate a single locale's label pack. Synchronous —
|
|
79
|
+
* suitable for Astro frontmatter execution.
|
|
80
|
+
*
|
|
81
|
+
* @param packsDir Absolute or cwd-relative path to the directory
|
|
82
|
+
* containing per-locale `<locale>.json` files.
|
|
83
|
+
* @param locale The locale to load. Must be a member of
|
|
84
|
+
* `WildoMarketingWebsiteConfig.locales`.
|
|
85
|
+
* @returns A flat `LabelPack` (`Record<string, string>`) ready to
|
|
86
|
+
* feed into `WebsiteRuntimeContextValue.labelPack`.
|
|
87
|
+
* @throws When the file is missing, malformed JSON, not an object,
|
|
88
|
+
* or contains a non-string value.
|
|
89
|
+
*/
|
|
90
|
+
export function loadLabelPack(packsDir: string, locale: AvailableLanguage): LabelPack {
|
|
91
|
+
const filePath = resolve(packsDir, `${locale}.json`);
|
|
92
|
+
|
|
93
|
+
let raw: string;
|
|
94
|
+
try {
|
|
95
|
+
raw = readFileSync(filePath, 'utf-8');
|
|
96
|
+
} catch (cause) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`[label-pack-loader] Could not read label pack at ${filePath} ` +
|
|
99
|
+
`(locale="${locale}"). ` +
|
|
100
|
+
`Make sure the directory exists and contains a "${locale}.json" file. ` +
|
|
101
|
+
`Underlying error: ${(cause as Error).message}`,
|
|
102
|
+
{ cause },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let parsed: unknown;
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(raw);
|
|
109
|
+
} catch (cause) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`[label-pack-loader] Failed to parse JSON in ${filePath} ` +
|
|
112
|
+
`(locale="${locale}"). Underlying parser error: ${(cause as Error).message}`,
|
|
113
|
+
{ cause },
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return assertLabelPackShape(parsed, filePath, locale);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Batch loader — every locale declared on the site at once. The map
|
|
122
|
+
* iteration order matches the declaration order of
|
|
123
|
+
* `siteContext.websiteConfig.locales` so CLI reports come out in a
|
|
124
|
+
* deterministic, authored sequence.
|
|
125
|
+
*/
|
|
126
|
+
export function loadAllLabelPacks(
|
|
127
|
+
packsDir: string,
|
|
128
|
+
siteContext: WebsiteSiteContext,
|
|
129
|
+
): Map<AvailableLanguage, LabelPack> {
|
|
130
|
+
const packs = new Map<AvailableLanguage, LabelPack>();
|
|
131
|
+
for (const entry of siteContext.websiteConfig.locales) {
|
|
132
|
+
packs.set(entry.locale, loadLabelPack(packsDir, entry.locale));
|
|
133
|
+
}
|
|
134
|
+
return packs;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Per-locale validation result — the validator's flat output extended
|
|
139
|
+
* with the locale tag so a multi-locale report can flatten + group.
|
|
140
|
+
*/
|
|
141
|
+
export interface LocaleLabelPackValidation {
|
|
142
|
+
locale: AvailableLanguage;
|
|
143
|
+
result: LabelPackValidationResult;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Highest-level entry point: load every locale's pack and run the
|
|
148
|
+
* label-pack validator against the manifest registry.
|
|
149
|
+
*
|
|
150
|
+
* **Return shape decision** — this function does NOT throw on
|
|
151
|
+
* `missing` keys; it returns the per-locale results so the caller
|
|
152
|
+
* (the build-start hook OR the CLI auditor OR a debugging
|
|
153
|
+
* subprocess) can decide how to surface the failure (process exit
|
|
154
|
+
* code, formatted CLI output, JSON report, …). A thin `throwIfAny…`
|
|
155
|
+
* helper sits next to it for the common "fail the build on first
|
|
156
|
+
* missing key" path.
|
|
157
|
+
*/
|
|
158
|
+
export function loadAndValidateAllLabelPacks(
|
|
159
|
+
packsDir: string,
|
|
160
|
+
siteContext: WebsiteSiteContext,
|
|
161
|
+
expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,
|
|
162
|
+
): readonly LocaleLabelPackValidation[] {
|
|
163
|
+
const { rootConfig } = siteContext;
|
|
164
|
+
const packs = loadAllLabelPacks(packsDir, siteContext);
|
|
165
|
+
const manifests: readonly WebsitePageManifest[] = rootConfig.pageManifests;
|
|
166
|
+
const effectiveChromeKeys = composeEffectiveChromeLabelKeys(rootConfig);
|
|
167
|
+
|
|
168
|
+
const validations: LocaleLabelPackValidation[] = [];
|
|
169
|
+
for (const [locale, pack] of packs) {
|
|
170
|
+
validations.push({
|
|
171
|
+
locale,
|
|
172
|
+
result: validateLabelPackAgainstManifest(
|
|
173
|
+
manifests,
|
|
174
|
+
expectedKeysBySectionRef,
|
|
175
|
+
pack,
|
|
176
|
+
effectiveChromeKeys,
|
|
177
|
+
),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return validations;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Compose the chrome-label-key set the validator should treat as
|
|
185
|
+
* "expected" — `WebsiteRootConfig.chromeLabelKeys` (consumer-authored)
|
|
186
|
+
* UNION the framework-required `skipToMainContentLabelKey`.
|
|
187
|
+
*
|
|
188
|
+
* Centralized here because:
|
|
189
|
+
* - Both `loadAndValidateAllLabelPacks` (used at build time) and
|
|
190
|
+
* `buildKeyOriginMap` (used to annotate missing-key errors) need
|
|
191
|
+
* the same composed list, and forking the union into two places
|
|
192
|
+
* would be a drift hazard.
|
|
193
|
+
* - The framework owns the skip-link key (it is rendered by
|
|
194
|
+
* `WebsitePageLayout`, not the consumer's chrome), so the
|
|
195
|
+
* consumer should NOT have to remember to also list it in
|
|
196
|
+
* `chromeLabelKeys`. Auto-injection here keeps the contract
|
|
197
|
+
* minimal at the consumer's call site (just declare the key in
|
|
198
|
+
* `WebsiteRootConfig.skipToMainContentLabelKey` once + ship the
|
|
199
|
+
* translation in every locale pack) while still surfacing the key
|
|
200
|
+
* through the validator's "missing"/"orphaned" diagnostics.
|
|
201
|
+
* - De-duplication is via `Set` semantics so a consumer who
|
|
202
|
+
* defensively lists the key in both places (no-op but harmless)
|
|
203
|
+
* does not produce a duplicate-key warning downstream.
|
|
204
|
+
*/
|
|
205
|
+
function composeEffectiveChromeLabelKeys(
|
|
206
|
+
rootConfig: WebsiteRootConfig,
|
|
207
|
+
): readonly WebsiteLabelKey[] {
|
|
208
|
+
const set = new Set<WebsiteLabelKey>(rootConfig.chromeLabelKeys);
|
|
209
|
+
set.add(rootConfig.skipToMainContentLabelKey);
|
|
210
|
+
return Array.from(set);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Convenience wrapper for the build-start hook: validates every
|
|
215
|
+
* declared locale's pack and throws a SINGLE aggregated error if any
|
|
216
|
+
* locale has a hard failure (`missing` keys or `unmappedSectionRefs`).
|
|
217
|
+
* Orphaned keys are logged via `console.warn` (soft warning per the
|
|
218
|
+
* validator's contract) but do NOT throw.
|
|
219
|
+
*
|
|
220
|
+
* **Why aggregate (not first-fail)**: a multi-locale marketing site
|
|
221
|
+
* with `en` and `fr` packs both missing keys would, under a
|
|
222
|
+
* first-fail policy, force the author to fix `en`, re-run the build,
|
|
223
|
+
* THEN see `fr`'s problem. Aggregating means one build run surfaces
|
|
224
|
+
* every locale's hard errors together — material UX win for the
|
|
225
|
+
* authoring loop. The aggregated message keeps each locale's failure
|
|
226
|
+
* in its own block so the diagnostic stays scannable.
|
|
227
|
+
*
|
|
228
|
+
* **Why throw vs. return**: the build-start hook lives in
|
|
229
|
+
* `astro.config.mjs` (or a `vite-plugin`-style integration) and
|
|
230
|
+
* benefits from the shortest possible code path — a thrown error
|
|
231
|
+
* naturally aborts `astro build` with a non-zero exit code and
|
|
232
|
+
* surfaces the message in the build log. The richer
|
|
233
|
+
* `loadAndValidateAllLabelPacks` is what `wildo website audit` calls
|
|
234
|
+
* when it wants programmatic access to the per-locale results.
|
|
235
|
+
*
|
|
236
|
+
* **Key-origin annotation**: every `missing` key is tagged with the
|
|
237
|
+
* page (and, if section-scoped, the section) that declared it, so the
|
|
238
|
+
* author can grep + open the right `defineWebsiteSection` /
|
|
239
|
+
* `defineWebsitePageManifest` call site without leaving the build log.
|
|
240
|
+
*/
|
|
241
|
+
export function assertLabelPacksValid(
|
|
242
|
+
packsDir: string,
|
|
243
|
+
siteContext: WebsiteSiteContext,
|
|
244
|
+
expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,
|
|
245
|
+
): void {
|
|
246
|
+
const { rootConfig } = siteContext;
|
|
247
|
+
const validations = loadAndValidateAllLabelPacks(
|
|
248
|
+
packsDir,
|
|
249
|
+
siteContext,
|
|
250
|
+
expectedKeysBySectionRef,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Build a key → origin map ONCE per assert call (cost is bounded by
|
|
255
|
+
* the total number of declared keys across the whole site, which is
|
|
256
|
+
* typically small for a marketing surface). Enriches the missing-key
|
|
257
|
+
* error with the page/section that committed to each key — the
|
|
258
|
+
* author can grep the ref to find the site authoring call site.
|
|
259
|
+
*/
|
|
260
|
+
const keyOrigins = buildKeyOriginMap(
|
|
261
|
+
rootConfig.pageManifests,
|
|
262
|
+
expectedKeysBySectionRef,
|
|
263
|
+
composeEffectiveChromeLabelKeys(rootConfig),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const localeErrorBlocks: string[] = [];
|
|
267
|
+
|
|
268
|
+
for (const { locale, result } of validations) {
|
|
269
|
+
const blocks: string[] = [];
|
|
270
|
+
if (result.unmappedSectionRefs.length > 0) {
|
|
271
|
+
blocks.push(
|
|
272
|
+
` unmappedSectionRefs (build-pipeline bug — sections referenced by a ` +
|
|
273
|
+
`manifest but missing from the expected-keys collector): ` +
|
|
274
|
+
`[${(result.unmappedSectionRefs as unknown as readonly string[]).join(', ')}]. ` +
|
|
275
|
+
`Add every referenced section to the array passed to ` +
|
|
276
|
+
`collectExpectedLabelKeysFromSections().`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (result.missing.length > 0) {
|
|
280
|
+
const annotated = (result.missing as unknown as readonly string[]).map(
|
|
281
|
+
(key) => ` "${key}" ← ${keyOrigins.get(key) ?? 'unknown origin'}`,
|
|
282
|
+
);
|
|
283
|
+
blocks.push(
|
|
284
|
+
` missing ${result.missing.length} required key(s) in ${packsDir}/${locale}.json:\n` +
|
|
285
|
+
annotated.join('\n'),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (blocks.length > 0) {
|
|
289
|
+
localeErrorBlocks.push(`Locale "${locale}":\n${blocks.join('\n')}`);
|
|
290
|
+
}
|
|
291
|
+
if (result.orphaned.length > 0) {
|
|
292
|
+
console.warn(
|
|
293
|
+
`[label-pack-loader] Locale "${locale}": ${result.orphaned.length} ` +
|
|
294
|
+
`orphaned key(s) in ${locale}.json (declared in pack but not consumed ` +
|
|
295
|
+
`by any section or page manifest):\n ${result.orphaned.join('\n ')}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (localeErrorBlocks.length > 0) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`[label-pack-loader] Label pack validation failed for ` +
|
|
303
|
+
`${localeErrorBlocks.length} locale(s):\n\n${localeErrorBlocks.join('\n\n')}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Build a `keyString -> "page <pageRef> meta titleKey" | "section <sectionRef> in page <pageRef>"`
|
|
310
|
+
* lookup so the assert message can name the call site that committed
|
|
311
|
+
* to each missing key. Pure and small (cost is O(totalKeys) per build
|
|
312
|
+
* — bounded by the marketing surface).
|
|
313
|
+
*/
|
|
314
|
+
function buildKeyOriginMap(
|
|
315
|
+
manifests: readonly WebsitePageManifest[],
|
|
316
|
+
expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,
|
|
317
|
+
chromeLabelKeys: readonly WebsiteLabelKey[],
|
|
318
|
+
): Map<string, string> {
|
|
319
|
+
const origins = new Map<string, string>();
|
|
320
|
+
for (const manifest of manifests) {
|
|
321
|
+
const pageRefStr = manifest.ref as unknown as string;
|
|
322
|
+
const titleKey = manifest.metaLabelKeys.titleKey as unknown as string;
|
|
323
|
+
const descriptionKey = manifest.metaLabelKeys.descriptionKey as unknown as string;
|
|
324
|
+
if (!origins.has(titleKey)) {
|
|
325
|
+
origins.set(titleKey, `page "${pageRefStr}" metaLabelKeys.titleKey`);
|
|
326
|
+
}
|
|
327
|
+
if (!origins.has(descriptionKey)) {
|
|
328
|
+
origins.set(descriptionKey, `page "${pageRefStr}" metaLabelKeys.descriptionKey`);
|
|
329
|
+
}
|
|
330
|
+
for (const sectionRef of manifest.sectionRefs) {
|
|
331
|
+
const sectionRefStr = sectionRef as unknown as string;
|
|
332
|
+
const sectionKeys = expectedKeysBySectionRef.get(sectionRef);
|
|
333
|
+
if (sectionKeys === undefined) continue;
|
|
334
|
+
for (const sectionKey of sectionKeys) {
|
|
335
|
+
const keyStr = sectionKey as unknown as string;
|
|
336
|
+
if (!origins.has(keyStr)) {
|
|
337
|
+
origins.set(keyStr, `section "${sectionRefStr}" in page "${pageRefStr}"`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
for (const chromeKey of chromeLabelKeys) {
|
|
343
|
+
const keyStr = chromeKey as unknown as string;
|
|
344
|
+
if (!origins.has(keyStr)) {
|
|
345
|
+
origins.set(keyStr, `WebsiteRootConfig.chromeLabelKeys`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return origins;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Defensive shape guard. Surface the offending key + observed type
|
|
353
|
+
* so the consumer can find the malformed entry quickly. Pure
|
|
354
|
+
* validation — no transformation; the returned value is the same
|
|
355
|
+
* object reference passed in (cast to `LabelPack`) so the caller
|
|
356
|
+
* does not pay an unnecessary copy.
|
|
357
|
+
*/
|
|
358
|
+
function assertLabelPackShape(
|
|
359
|
+
parsed: unknown,
|
|
360
|
+
filePath: string,
|
|
361
|
+
locale: AvailableLanguage,
|
|
362
|
+
): LabelPack {
|
|
363
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`[label-pack-loader] ${filePath} (locale="${locale}") must contain a ` +
|
|
366
|
+
`JSON object at the top level (got ${describeJson(parsed)}).`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
370
|
+
if (typeof value !== 'string') {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`[label-pack-loader] ${filePath} (locale="${locale}"): key "${key}" ` +
|
|
373
|
+
`must map to a string (got ${describeJson(value)}).`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return parsed as LabelPack;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function describeJson(value: unknown): string {
|
|
381
|
+
if (value === null) return 'null';
|
|
382
|
+
if (Array.isArray(value)) return 'array';
|
|
383
|
+
return typeof value;
|
|
384
|
+
}
|
|
385
|
+
/** @wildo_source:part:end saas.website.astro.label-loader */
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type { WebsiteSiteContext } from './website-site-context';
|
|
5
|
+
|
|
6
|
+
export interface ReadLlmsTxtBodyOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Directory containing the hand-authored `llms.txt` file. Defaults
|
|
9
|
+
* to the conventional `./src/content` path from the website package
|
|
10
|
+
* root.
|
|
11
|
+
*/
|
|
12
|
+
contentDir?: string;
|
|
13
|
+
/**
|
|
14
|
+
* File name under `contentDir`. Exposed for tests; consumers should
|
|
15
|
+
* stick to the canonical `llms.txt`.
|
|
16
|
+
*/
|
|
17
|
+
fileName?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @wildo_source:part:start saas.website.astro.llms-txt-server facet:layer:astro facet:family:website
|
|
22
|
+
*
|
|
23
|
+
* Read the hand-authored `llms.txt` body for the marketing site.
|
|
24
|
+
*
|
|
25
|
+
* Publication policy:
|
|
26
|
+
* - `seo.llmsTxt?.enabled === false` → return `null`
|
|
27
|
+
* - `seo.llmsTxt?.enabled === true` → read the file if present, else `null`
|
|
28
|
+
* - `seo.llmsTxt === undefined` or `enabled === undefined` → auto-detect:
|
|
29
|
+
* publish when the file exists, otherwise return `null`
|
|
30
|
+
*/
|
|
31
|
+
export function readLlmsTxtBody(
|
|
32
|
+
siteContext: WebsiteSiteContext,
|
|
33
|
+
options: ReadLlmsTxtBodyOptions = {},
|
|
34
|
+
): string | null {
|
|
35
|
+
const contentDir = options.contentDir ?? './src/content';
|
|
36
|
+
const fileName = options.fileName ?? 'llms.txt';
|
|
37
|
+
const filePath = resolve(contentDir, fileName);
|
|
38
|
+
|
|
39
|
+
if (siteContext.rootConfig.seo.llmsTxt?.enabled === false) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (!existsSync(filePath)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return readFileSync(filePath, 'utf-8');
|
|
48
|
+
} catch (cause) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`[llms-txt-server] Could not read llms.txt at ${filePath}. ` +
|
|
51
|
+
`Underlying error: ${(cause as Error).message}`,
|
|
52
|
+
{ cause },
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** @wildo_source:part:end saas.website.astro.llms-txt-server */
|