@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,85 @@
|
|
|
1
|
+
import type { WebsiteSiteContext } from './website-site-context';
|
|
2
|
+
/**
|
|
3
|
+
* @wildo_source:part:start saas.website.astro.robots-renderer facet:layer:astro facet:family:website
|
|
4
|
+
*
|
|
5
|
+
* `renderRobotsTxt(siteContext)` — pure string builder for the
|
|
6
|
+
* marketing site's `robots.txt` body. The consumer wires a thin
|
|
7
|
+
* `src/pages/robots.txt.ts` Astro endpoint that imports this helper
|
|
8
|
+
* + `WEBSITE_SITE_CONTEXT` and returns its output as a `Response`
|
|
9
|
+
* with `Content-Type: text/plain` (see `website-seo-aeo-geo` SKILL).
|
|
10
|
+
*
|
|
11
|
+
* # Why a pure string builder (not an Astro component)
|
|
12
|
+
*
|
|
13
|
+
* Same rationale as `renderWebsitePageHeadHtml`:
|
|
14
|
+
* 1. Pure TypeScript ships through the `@wildo-ai/saas-website/astro`
|
|
15
|
+
* subpath barrel (Astro components do not).
|
|
16
|
+
* 2. Vitest-friendly — every policy permutation is asserted
|
|
17
|
+
* against the returned string without an Astro test harness.
|
|
18
|
+
* 3. The Astro endpoint stays a 3-line shim the consumer can
|
|
19
|
+
* audit at a glance.
|
|
20
|
+
*
|
|
21
|
+
* # Output shape
|
|
22
|
+
*
|
|
23
|
+
* The emitted text follows the de-facto `robots.txt` ordering
|
|
24
|
+
* (RFC 9309 §2 + Google's published guidance):
|
|
25
|
+
*
|
|
26
|
+
* 1. Per-bot blocks for `allowedCrawlers` (deny-by-default
|
|
27
|
+
* direction) — each named bot gets its own `User-agent: <bot>`
|
|
28
|
+
* + `Allow: /` block.
|
|
29
|
+
* 2. Per-bot blocks for `disallowedCrawlers` (allow-by-default
|
|
30
|
+
* direction) — each named bot gets its own `User-agent: <bot>`
|
|
31
|
+
* + `Disallow: /` block.
|
|
32
|
+
* 3. Catch-all `User-agent: *` block carrying the site-wide
|
|
33
|
+
* `policy` (`Disallow: /` if `policy === 'noindex'` OR if
|
|
34
|
+
* `allowedCrawlers` is set, otherwise nothing) and any
|
|
35
|
+
* `disallow[]` path-prefix entries.
|
|
36
|
+
* 4. `Sitemap: <publicOrigin>/sitemap-index.xml` footer when
|
|
37
|
+
* `seo.sitemap?.enabled !== false` (defaults to `true`).
|
|
38
|
+
* `@astrojs/sitemap` emits `sitemap-index.xml` regardless of
|
|
39
|
+
* whether the site has one or many sitemap files (the index
|
|
40
|
+
* is always-present per the plugin's contract), so this is
|
|
41
|
+
* the stable URL to advertise.
|
|
42
|
+
*
|
|
43
|
+
* # Mutual-exclusion invariant
|
|
44
|
+
*
|
|
45
|
+
* `WebsiteRobotsConfigSchema.superRefine` already guarantees
|
|
46
|
+
* `allowedCrawlers` and `disallowedCrawlers` are NOT both set, so
|
|
47
|
+
* step 1 and step 2 above are mutually exclusive at the input
|
|
48
|
+
* layer. The string builder is defensive (handles both being unset)
|
|
49
|
+
* but does NOT re-validate the invariant.
|
|
50
|
+
*
|
|
51
|
+
* # Why we always emit `Sitemap:` footer when `enabled !== false`
|
|
52
|
+
*
|
|
53
|
+
* The `Sitemap:` directive is independent from any `User-agent`
|
|
54
|
+
* block — it is a top-level signal to any crawler that the site
|
|
55
|
+
* publishes a sitemap. Even when the catch-all policy is
|
|
56
|
+
* `Disallow: /` (e.g. `policy === 'noindex'`), the `Sitemap:` line
|
|
57
|
+
* stays useful: a crawler that ignores `noindex` (Bing was famous
|
|
58
|
+
* for this) at least has a single canonical URL list to consult,
|
|
59
|
+
* and human readers of `robots.txt` see the publication contract
|
|
60
|
+
* intact.
|
|
61
|
+
*
|
|
62
|
+
* # Failure modes
|
|
63
|
+
*
|
|
64
|
+
* - `siteContext.websiteConfig.publicOrigin` undefined → throws.
|
|
65
|
+
* The marketing-site origin is the SINGLE source of truth for
|
|
66
|
+
* absolute URL composition (see `WildoMarketingWebsiteConfig`
|
|
67
|
+
* JSDoc); a missing origin is a configuration error, not a
|
|
68
|
+
* silent fallback opportunity.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* // examples/<app>/website/src/pages/robots.txt.ts
|
|
73
|
+
* import type { APIRoute } from 'astro';
|
|
74
|
+
* import { renderRobotsTxt } from '@wildo-ai/saas-website/astro';
|
|
75
|
+
* import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';
|
|
76
|
+
*
|
|
77
|
+
* export const GET: APIRoute = () =>
|
|
78
|
+
* new Response(renderRobotsTxt(WEBSITE_SITE_CONTEXT), {
|
|
79
|
+
* headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function renderRobotsTxt(siteContext: WebsiteSiteContext): string;
|
|
84
|
+
/** @wildo_source:part:end saas.website.astro.robots-renderer */
|
|
85
|
+
//# sourceMappingURL=robots-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots-renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/robots-renderer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,kBAAkB,GAAG,MAAM,CAkFvE;AACD,gEAAgE"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wildo_source:part:start saas.website.astro.robots-renderer facet:layer:astro facet:family:website
|
|
3
|
+
*
|
|
4
|
+
* `renderRobotsTxt(siteContext)` — pure string builder for the
|
|
5
|
+
* marketing site's `robots.txt` body. The consumer wires a thin
|
|
6
|
+
* `src/pages/robots.txt.ts` Astro endpoint that imports this helper
|
|
7
|
+
* + `WEBSITE_SITE_CONTEXT` and returns its output as a `Response`
|
|
8
|
+
* with `Content-Type: text/plain` (see `website-seo-aeo-geo` SKILL).
|
|
9
|
+
*
|
|
10
|
+
* # Why a pure string builder (not an Astro component)
|
|
11
|
+
*
|
|
12
|
+
* Same rationale as `renderWebsitePageHeadHtml`:
|
|
13
|
+
* 1. Pure TypeScript ships through the `@wildo-ai/saas-website/astro`
|
|
14
|
+
* subpath barrel (Astro components do not).
|
|
15
|
+
* 2. Vitest-friendly — every policy permutation is asserted
|
|
16
|
+
* against the returned string without an Astro test harness.
|
|
17
|
+
* 3. The Astro endpoint stays a 3-line shim the consumer can
|
|
18
|
+
* audit at a glance.
|
|
19
|
+
*
|
|
20
|
+
* # Output shape
|
|
21
|
+
*
|
|
22
|
+
* The emitted text follows the de-facto `robots.txt` ordering
|
|
23
|
+
* (RFC 9309 §2 + Google's published guidance):
|
|
24
|
+
*
|
|
25
|
+
* 1. Per-bot blocks for `allowedCrawlers` (deny-by-default
|
|
26
|
+
* direction) — each named bot gets its own `User-agent: <bot>`
|
|
27
|
+
* + `Allow: /` block.
|
|
28
|
+
* 2. Per-bot blocks for `disallowedCrawlers` (allow-by-default
|
|
29
|
+
* direction) — each named bot gets its own `User-agent: <bot>`
|
|
30
|
+
* + `Disallow: /` block.
|
|
31
|
+
* 3. Catch-all `User-agent: *` block carrying the site-wide
|
|
32
|
+
* `policy` (`Disallow: /` if `policy === 'noindex'` OR if
|
|
33
|
+
* `allowedCrawlers` is set, otherwise nothing) and any
|
|
34
|
+
* `disallow[]` path-prefix entries.
|
|
35
|
+
* 4. `Sitemap: <publicOrigin>/sitemap-index.xml` footer when
|
|
36
|
+
* `seo.sitemap?.enabled !== false` (defaults to `true`).
|
|
37
|
+
* `@astrojs/sitemap` emits `sitemap-index.xml` regardless of
|
|
38
|
+
* whether the site has one or many sitemap files (the index
|
|
39
|
+
* is always-present per the plugin's contract), so this is
|
|
40
|
+
* the stable URL to advertise.
|
|
41
|
+
*
|
|
42
|
+
* # Mutual-exclusion invariant
|
|
43
|
+
*
|
|
44
|
+
* `WebsiteRobotsConfigSchema.superRefine` already guarantees
|
|
45
|
+
* `allowedCrawlers` and `disallowedCrawlers` are NOT both set, so
|
|
46
|
+
* step 1 and step 2 above are mutually exclusive at the input
|
|
47
|
+
* layer. The string builder is defensive (handles both being unset)
|
|
48
|
+
* but does NOT re-validate the invariant.
|
|
49
|
+
*
|
|
50
|
+
* # Why we always emit `Sitemap:` footer when `enabled !== false`
|
|
51
|
+
*
|
|
52
|
+
* The `Sitemap:` directive is independent from any `User-agent`
|
|
53
|
+
* block — it is a top-level signal to any crawler that the site
|
|
54
|
+
* publishes a sitemap. Even when the catch-all policy is
|
|
55
|
+
* `Disallow: /` (e.g. `policy === 'noindex'`), the `Sitemap:` line
|
|
56
|
+
* stays useful: a crawler that ignores `noindex` (Bing was famous
|
|
57
|
+
* for this) at least has a single canonical URL list to consult,
|
|
58
|
+
* and human readers of `robots.txt` see the publication contract
|
|
59
|
+
* intact.
|
|
60
|
+
*
|
|
61
|
+
* # Failure modes
|
|
62
|
+
*
|
|
63
|
+
* - `siteContext.websiteConfig.publicOrigin` undefined → throws.
|
|
64
|
+
* The marketing-site origin is the SINGLE source of truth for
|
|
65
|
+
* absolute URL composition (see `WildoMarketingWebsiteConfig`
|
|
66
|
+
* JSDoc); a missing origin is a configuration error, not a
|
|
67
|
+
* silent fallback opportunity.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* // examples/<app>/website/src/pages/robots.txt.ts
|
|
72
|
+
* import type { APIRoute } from 'astro';
|
|
73
|
+
* import { renderRobotsTxt } from '@wildo-ai/saas-website/astro';
|
|
74
|
+
* import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';
|
|
75
|
+
*
|
|
76
|
+
* export const GET: APIRoute = () =>
|
|
77
|
+
* new Response(renderRobotsTxt(WEBSITE_SITE_CONTEXT), {
|
|
78
|
+
* headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function renderRobotsTxt(siteContext) {
|
|
83
|
+
const { rootConfig, websiteConfig } = siteContext;
|
|
84
|
+
if (!websiteConfig.publicOrigin) {
|
|
85
|
+
throw new Error('renderRobotsTxt: WildoMarketingWebsiteConfig.publicOrigin is required to compose the absolute Sitemap: URL. Author it in your service\'s `wildo.website.config.ts`.');
|
|
86
|
+
}
|
|
87
|
+
const robots = rootConfig.seo.robots;
|
|
88
|
+
const policy = robots?.policy ?? 'index';
|
|
89
|
+
const allowedCrawlers = robots?.allowedCrawlers ?? [];
|
|
90
|
+
const disallowedCrawlers = robots?.disallowedCrawlers ?? [];
|
|
91
|
+
const disallow = robots?.disallow ?? [];
|
|
92
|
+
const blocks = [];
|
|
93
|
+
/**
|
|
94
|
+
* Block 1 — per-bot `Allow: /` for the deny-by-default direction.
|
|
95
|
+
* Empty when `allowedCrawlers` is unset (the dominant case).
|
|
96
|
+
*/
|
|
97
|
+
for (const bot of allowedCrawlers) {
|
|
98
|
+
blocks.push(`User-agent: ${bot}\nAllow: /`);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Block 2 — per-bot `Disallow: /` for the allow-by-default
|
|
102
|
+
* direction. Empty when `disallowedCrawlers` is unset (the
|
|
103
|
+
* dominant case for a public marketing site).
|
|
104
|
+
*/
|
|
105
|
+
for (const bot of disallowedCrawlers) {
|
|
106
|
+
blocks.push(`User-agent: ${bot}\nDisallow: /`);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Block 3 — catch-all `User-agent: *`. Two policy-driven shapes:
|
|
110
|
+
* - `noindex` → `Disallow: /` (the site-wide closure).
|
|
111
|
+
* - `index` (default) → `Disallow:` is the spec-canonical "allow
|
|
112
|
+
* everything" line; combined with any per-path `Disallow: <path>`
|
|
113
|
+
* entries from `disallow[]`. When `allowedCrawlers` is set, the
|
|
114
|
+
* catch-all also closes the rest of the crawler universe with
|
|
115
|
+
* `Disallow: /` so the named bots are the ONLY ones permitted.
|
|
116
|
+
*/
|
|
117
|
+
const catchAllLines = ['User-agent: *'];
|
|
118
|
+
if (policy === 'noindex' || allowedCrawlers.length > 0) {
|
|
119
|
+
catchAllLines.push('Disallow: /');
|
|
120
|
+
}
|
|
121
|
+
else if (disallow.length === 0) {
|
|
122
|
+
/**
|
|
123
|
+
* Spec-canonical "allow everything" — `Disallow:` with no path.
|
|
124
|
+
* Some crawlers treat the absence of any `Disallow:` line as
|
|
125
|
+
* undefined behavior; an explicit empty Disallow is the safe
|
|
126
|
+
* shape.
|
|
127
|
+
*/
|
|
128
|
+
catchAllLines.push('Disallow:');
|
|
129
|
+
}
|
|
130
|
+
for (const path of disallow) {
|
|
131
|
+
catchAllLines.push(`Disallow: ${path}`);
|
|
132
|
+
}
|
|
133
|
+
blocks.push(catchAllLines.join('\n'));
|
|
134
|
+
/**
|
|
135
|
+
* Block 4 — `Sitemap:` footer, advertised whenever sitemap
|
|
136
|
+
* emission is enabled (the marketing-site default). See file
|
|
137
|
+
* preamble for why we always advertise `sitemap-index.xml` as
|
|
138
|
+
* the stable URL.
|
|
139
|
+
*/
|
|
140
|
+
const sitemapEnabled = rootConfig.seo.sitemap?.enabled ?? true;
|
|
141
|
+
if (sitemapEnabled) {
|
|
142
|
+
/**
|
|
143
|
+
* Trim trailing slash on the origin to avoid emitting
|
|
144
|
+
* `https://example.com//sitemap-index.xml`. `URL` constructor
|
|
145
|
+
* is overkill for a single concat; the regex is enough.
|
|
146
|
+
*/
|
|
147
|
+
const origin = websiteConfig.publicOrigin.replace(/\/+$/, '');
|
|
148
|
+
blocks.push(`Sitemap: ${origin}/sitemap-index.xml`);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Spec-conformant trailing newline. Some crawler parsers tolerate
|
|
152
|
+
* its absence; emitting it is universally safe.
|
|
153
|
+
*/
|
|
154
|
+
return `${blocks.join('\n\n')}\n`;
|
|
155
|
+
}
|
|
156
|
+
/** @wildo_source:part:end saas.website.astro.robots-renderer */
|
|
157
|
+
//# sourceMappingURL=robots-renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots-renderer.js","sourceRoot":"","sources":["../../../../src/astro/robots-renderer.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AACH,MAAM,UAAU,eAAe,CAAC,WAA+B;IAC7D,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,WAAW,CAAC;IAElD,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,qKAAqK,CACtK,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAoC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC;IACtE,MAAM,MAAM,GAAwB,MAAM,EAAE,MAAM,IAAI,OAAO,CAAC;IAC9D,MAAM,eAAe,GAAG,MAAM,EAAE,eAAe,IAAI,EAAE,CAAC;IACtD,MAAM,kBAAkB,GAAG,MAAM,EAAE,kBAAkB,IAAI,EAAE,CAAC;IAC5D,MAAM,QAAQ,GAAG,MAAM,EAAE,QAAQ,IAAI,EAAE,CAAC;IAExC,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B;;;OAGG;IACH,KAAK,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CAAC,eAAe,GAAG,YAAY,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,KAAK,MAAM,GAAG,IAAI,kBAAkB,EAAE,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC,CAAC;IACjD,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,aAAa,GAAa,CAAC,eAAe,CAAC,CAAC;IAClD,IAAI,MAAM,KAAK,SAAS,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvD,aAAa,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACpC,CAAC;SAAM,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC;;;;;WAKG;QACH,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAClC,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,aAAa,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAEtC;;;;;OAKG;IACH,MAAM,cAAc,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,IAAI,IAAI,CAAC;IAC/D,IAAI,cAAc,EAAE,CAAC;QACnB;;;;WAIG;QACH,MAAM,MAAM,GAAG,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC9D,MAAM,CAAC,IAAI,CAAC,YAAY,MAAM,oBAAoB,CAAC,CAAC;IACtD,CAAC;IAED;;;OAGG;IACH,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;AACpC,CAAC;AACD,gEAAgE","sourcesContent":["import type {\n WebsiteRobotsConfig,\n} from '../schemas/manifests/website-root-config.shared.schemas';\n\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.robots-renderer facet:layer:astro facet:family:website\n *\n * `renderRobotsTxt(siteContext)` — pure string builder for the\n * marketing site's `robots.txt` body. The consumer wires a thin\n * `src/pages/robots.txt.ts` Astro endpoint that imports this helper\n * + `WEBSITE_SITE_CONTEXT` and returns its output as a `Response`\n * with `Content-Type: text/plain` (see `website-seo-aeo-geo` SKILL).\n *\n * # Why a pure string builder (not an Astro component)\n *\n * Same rationale as `renderWebsitePageHeadHtml`:\n * 1. Pure TypeScript ships through the `@wildo-ai/saas-website/astro`\n * subpath barrel (Astro components do not).\n * 2. Vitest-friendly — every policy permutation is asserted\n * against the returned string without an Astro test harness.\n * 3. The Astro endpoint stays a 3-line shim the consumer can\n * audit at a glance.\n *\n * # Output shape\n *\n * The emitted text follows the de-facto `robots.txt` ordering\n * (RFC 9309 §2 + Google's published guidance):\n *\n * 1. Per-bot blocks for `allowedCrawlers` (deny-by-default\n * direction) — each named bot gets its own `User-agent: <bot>`\n * + `Allow: /` block.\n * 2. Per-bot blocks for `disallowedCrawlers` (allow-by-default\n * direction) — each named bot gets its own `User-agent: <bot>`\n * + `Disallow: /` block.\n * 3. Catch-all `User-agent: *` block carrying the site-wide\n * `policy` (`Disallow: /` if `policy === 'noindex'` OR if\n * `allowedCrawlers` is set, otherwise nothing) and any\n * `disallow[]` path-prefix entries.\n * 4. `Sitemap: <publicOrigin>/sitemap-index.xml` footer when\n * `seo.sitemap?.enabled !== false` (defaults to `true`).\n * `@astrojs/sitemap` emits `sitemap-index.xml` regardless of\n * whether the site has one or many sitemap files (the index\n * is always-present per the plugin's contract), so this is\n * the stable URL to advertise.\n *\n * # Mutual-exclusion invariant\n *\n * `WebsiteRobotsConfigSchema.superRefine` already guarantees\n * `allowedCrawlers` and `disallowedCrawlers` are NOT both set, so\n * step 1 and step 2 above are mutually exclusive at the input\n * layer. The string builder is defensive (handles both being unset)\n * but does NOT re-validate the invariant.\n *\n * # Why we always emit `Sitemap:` footer when `enabled !== false`\n *\n * The `Sitemap:` directive is independent from any `User-agent`\n * block — it is a top-level signal to any crawler that the site\n * publishes a sitemap. Even when the catch-all policy is\n * `Disallow: /` (e.g. `policy === 'noindex'`), the `Sitemap:` line\n * stays useful: a crawler that ignores `noindex` (Bing was famous\n * for this) at least has a single canonical URL list to consult,\n * and human readers of `robots.txt` see the publication contract\n * intact.\n *\n * # Failure modes\n *\n * - `siteContext.websiteConfig.publicOrigin` undefined → throws.\n * The marketing-site origin is the SINGLE source of truth for\n * absolute URL composition (see `WildoMarketingWebsiteConfig`\n * JSDoc); a missing origin is a configuration error, not a\n * silent fallback opportunity.\n *\n * @example\n * ```ts\n * // examples/<app>/website/src/pages/robots.txt.ts\n * import type { APIRoute } from 'astro';\n * import { renderRobotsTxt } from '@wildo-ai/saas-website/astro';\n * import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';\n *\n * export const GET: APIRoute = () =>\n * new Response(renderRobotsTxt(WEBSITE_SITE_CONTEXT), {\n * headers: { 'Content-Type': 'text/plain; charset=utf-8' },\n * });\n * ```\n */\nexport function renderRobotsTxt(siteContext: WebsiteSiteContext): string {\n const { rootConfig, websiteConfig } = siteContext;\n\n if (!websiteConfig.publicOrigin) {\n throw new Error(\n 'renderRobotsTxt: WildoMarketingWebsiteConfig.publicOrigin is required to compose the absolute Sitemap: URL. Author it in your service\\'s `wildo.website.config.ts`.',\n );\n }\n\n const robots: WebsiteRobotsConfig | undefined = rootConfig.seo.robots;\n const policy: 'index' | 'noindex' = robots?.policy ?? 'index';\n const allowedCrawlers = robots?.allowedCrawlers ?? [];\n const disallowedCrawlers = robots?.disallowedCrawlers ?? [];\n const disallow = robots?.disallow ?? [];\n\n const blocks: string[] = [];\n\n /**\n * Block 1 — per-bot `Allow: /` for the deny-by-default direction.\n * Empty when `allowedCrawlers` is unset (the dominant case).\n */\n for (const bot of allowedCrawlers) {\n blocks.push(`User-agent: ${bot}\\nAllow: /`);\n }\n\n /**\n * Block 2 — per-bot `Disallow: /` for the allow-by-default\n * direction. Empty when `disallowedCrawlers` is unset (the\n * dominant case for a public marketing site).\n */\n for (const bot of disallowedCrawlers) {\n blocks.push(`User-agent: ${bot}\\nDisallow: /`);\n }\n\n /**\n * Block 3 — catch-all `User-agent: *`. Two policy-driven shapes:\n * - `noindex` → `Disallow: /` (the site-wide closure).\n * - `index` (default) → `Disallow:` is the spec-canonical \"allow\n * everything\" line; combined with any per-path `Disallow: <path>`\n * entries from `disallow[]`. When `allowedCrawlers` is set, the\n * catch-all also closes the rest of the crawler universe with\n * `Disallow: /` so the named bots are the ONLY ones permitted.\n */\n const catchAllLines: string[] = ['User-agent: *'];\n if (policy === 'noindex' || allowedCrawlers.length > 0) {\n catchAllLines.push('Disallow: /');\n } else if (disallow.length === 0) {\n /**\n * Spec-canonical \"allow everything\" — `Disallow:` with no path.\n * Some crawlers treat the absence of any `Disallow:` line as\n * undefined behavior; an explicit empty Disallow is the safe\n * shape.\n */\n catchAllLines.push('Disallow:');\n }\n for (const path of disallow) {\n catchAllLines.push(`Disallow: ${path}`);\n }\n blocks.push(catchAllLines.join('\\n'));\n\n /**\n * Block 4 — `Sitemap:` footer, advertised whenever sitemap\n * emission is enabled (the marketing-site default). See file\n * preamble for why we always advertise `sitemap-index.xml` as\n * the stable URL.\n */\n const sitemapEnabled = rootConfig.seo.sitemap?.enabled ?? true;\n if (sitemapEnabled) {\n /**\n * Trim trailing slash on the origin to avoid emitting\n * `https://example.com//sitemap-index.xml`. `URL` constructor\n * is overkill for a single concat; the regex is enough.\n */\n const origin = websiteConfig.publicOrigin.replace(/\\/+$/, '');\n blocks.push(`Sitemap: ${origin}/sitemap-index.xml`);\n }\n\n /**\n * Spec-conformant trailing newline. Some crawler parsers tolerate\n * its absence; emitting it is universally safe.\n */\n return `${blocks.join('\\n\\n')}\\n`;\n}\n/** @wildo_source:part:end saas.website.astro.robots-renderer */\n"]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
2
|
+
import type { WebsiteSiteContext } from './website-site-context';
|
|
3
|
+
/**
|
|
4
|
+
* @wildo_source:part:start saas.website.astro.sitemap-coverage facet:layer:astro facet:family:website
|
|
5
|
+
*
|
|
6
|
+
* `enumerateMarketingPageSitemapUrls(siteContext)` — pure helper that
|
|
7
|
+
* returns every (page-manifest × locale) absolute URL the marketing
|
|
8
|
+
* site's `sitemap.xml` MUST advertise, paired with its hreflang
|
|
9
|
+
* alternates.
|
|
10
|
+
*
|
|
11
|
+
* # Why this helper exists (vs leaning entirely on `@astrojs/sitemap`)
|
|
12
|
+
*
|
|
13
|
+
* `@astrojs/sitemap` (the official Astro plugin) auto-discovers every
|
|
14
|
+
* page Astro emits and emits a `sitemap-index.xml` + sub-sitemaps at
|
|
15
|
+
* build time. That covers the dominant case (every page in
|
|
16
|
+
* `src/pages/**` ships in the sitemap). What it does NOT cover by
|
|
17
|
+
* itself:
|
|
18
|
+
*
|
|
19
|
+
* 1. **Coverage enforcement at unit-test time.** The plugin runs
|
|
20
|
+
* during `astro build`; vitest cannot exercise it without
|
|
21
|
+
* booting the full Astro pipeline. This helper is the testable
|
|
22
|
+
* surface — a regression that drops a manifest-registered route
|
|
23
|
+
* from the expected URL set fails fast in vitest, well before
|
|
24
|
+
* the `astro build` step would surface it (and even then only
|
|
25
|
+
* via end-to-end inspection of the emitted XML).
|
|
26
|
+
* 2. **Programmatic page registration.** Future framework-owned
|
|
27
|
+
* features that synthesize routes outside Astro's
|
|
28
|
+
* file-based router (e.g. data-driven landing variants) need an
|
|
29
|
+
* explicit URL list to pass into `@astrojs/sitemap`'s
|
|
30
|
+
* `customPages` config. This helper is the canonical place to
|
|
31
|
+
* compute that list — consumers spread the result into the
|
|
32
|
+
* plugin config without re-deriving the locale × route logic.
|
|
33
|
+
* 3. **Cross-check.** The same site context that drives the head
|
|
34
|
+
* renderer drives this enumeration; a divergence between
|
|
35
|
+
* "what the head renderer thinks the canonical URL set is" and
|
|
36
|
+
* "what the sitemap publishes" is a silent drift surface this
|
|
37
|
+
* helper makes auditable.
|
|
38
|
+
*
|
|
39
|
+
* # Output shape
|
|
40
|
+
*
|
|
41
|
+
* Each entry carries:
|
|
42
|
+
* - `url` — the absolute canonical URL (no trailing slash, matches
|
|
43
|
+
* `<link rel="canonical">` output of `renderWebsitePageHeadHtml`).
|
|
44
|
+
* - `pageRef` — the page-manifest `ref` (debug + grouping aid).
|
|
45
|
+
* - `routePath` — the manifest-declared `routePath` (debug + grouping aid).
|
|
46
|
+
* - `locale` — the URL's language.
|
|
47
|
+
* - `alternates` — every other locale's URL for the SAME page,
|
|
48
|
+
* emitted as `xhtml:link rel="alternate" hreflang` entries inside
|
|
49
|
+
* the sitemap (single-sitemap-with-hreflang shape, per Google's
|
|
50
|
+
* recommended sitemap format).
|
|
51
|
+
*
|
|
52
|
+
* # Routing convention (matches `buildLocalizedRoutePath`)
|
|
53
|
+
*
|
|
54
|
+
* Default locale mounts at the un-prefixed path (`/`, `/pricing`);
|
|
55
|
+
* other locales sit under `/<locale>/…`. The helper delegates to
|
|
56
|
+
* `buildLocalizedRoutePath` so a future change to the routing
|
|
57
|
+
* convention lands in one file rather than two.
|
|
58
|
+
*
|
|
59
|
+
* # What this helper does NOT enumerate
|
|
60
|
+
*
|
|
61
|
+
* - **Blog post slugs.** Blog posts share ONE page-manifest ref
|
|
62
|
+
* (`BLOG_POST_PAGE_REF`) but materialize as N URLs — one per
|
|
63
|
+
* `(slug, locale)` MDX file. The MDX file system is the
|
|
64
|
+
* source-of-truth for blog URLs, NOT the page manifest. Astro's
|
|
65
|
+
* file-based router auto-discovers each blog `[slug].astro` route
|
|
66
|
+
* and `@astrojs/sitemap` auto-publishes them. A future helper
|
|
67
|
+
* (`enumerateBlogPostSitemapUrls(...)`) can address the same
|
|
68
|
+
* coverage-enforcement gap for the blog surface specifically.
|
|
69
|
+
* - **Static `.astro` pages without a page manifest.** If a consumer
|
|
70
|
+
* authors a page directly in `src/pages/` without registering a
|
|
71
|
+
* manifest in `WebsiteRootConfig.pageManifests`, this helper will
|
|
72
|
+
* not list it (and `renderWebsitePageHeadHtml` cannot render its
|
|
73
|
+
* `<head>` either). The framework convention is "every public
|
|
74
|
+
* page has a manifest" — pages without manifests are a label-pack
|
|
75
|
+
* discipline regression and surface separately via
|
|
76
|
+
* `assertLabelPacksValid`.
|
|
77
|
+
*/
|
|
78
|
+
export interface MarketingPageSitemapEntry {
|
|
79
|
+
/** Absolute canonical URL, no trailing slash. */
|
|
80
|
+
readonly url: string;
|
|
81
|
+
/** The page-manifest `ref` (branded string in the source schema). */
|
|
82
|
+
readonly pageRef: string;
|
|
83
|
+
/** The manifest-declared `routePath` (always starts with `/`). */
|
|
84
|
+
readonly routePath: string;
|
|
85
|
+
/** The URL's language. */
|
|
86
|
+
readonly locale: AvailableLanguage;
|
|
87
|
+
/**
|
|
88
|
+
* Every OTHER locale's URL for the same page, suitable for emitting
|
|
89
|
+
* `xhtml:link rel="alternate" hreflang` entries inside the sitemap.
|
|
90
|
+
* Includes an `x-default` synthesized from the default-locale URL
|
|
91
|
+
* (Google's i18n discoverability convention; matches the head
|
|
92
|
+
* renderer's hreflang output).
|
|
93
|
+
*/
|
|
94
|
+
readonly alternates: readonly {
|
|
95
|
+
readonly hreflang: string;
|
|
96
|
+
readonly url: string;
|
|
97
|
+
}[];
|
|
98
|
+
}
|
|
99
|
+
export declare function enumerateMarketingPageSitemapUrls(siteContext: WebsiteSiteContext): readonly MarketingPageSitemapEntry[];
|
|
100
|
+
/** @wildo_source:part:end saas.website.astro.sitemap-coverage */
|
|
101
|
+
//# sourceMappingURL=sitemap-coverage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap-coverage.d.ts","sourceRoot":"","sources":["../../../../src/astro/sitemap-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAK9E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AACH,MAAM,WAAW,yBAAyB;IACxC,iDAAiD;IACjD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0BAA0B;IAC1B,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACrF;AAED,wBAAgB,iCAAiC,CAC/C,WAAW,EAAE,kBAAkB,GAC9B,SAAS,yBAAyB,EAAE,CAiFtC;AACD,iEAAiE"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils.js';
|
|
2
|
+
export function enumerateMarketingPageSitemapUrls(siteContext) {
|
|
3
|
+
const { rootConfig, websiteConfig } = siteContext;
|
|
4
|
+
if (!websiteConfig.publicOrigin) {
|
|
5
|
+
throw new Error('enumerateMarketingPageSitemapUrls: WildoMarketingWebsiteConfig.publicOrigin is required to compose absolute sitemap URLs. Author it in your service\'s `wildo.website.config.ts`.');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Trim trailing slash defensively (the schema-level refinement
|
|
9
|
+
* already rejects them, but the helper is consumed in synthetic
|
|
10
|
+
* test contexts that bypass the parser).
|
|
11
|
+
*/
|
|
12
|
+
const origin = websiteConfig.publicOrigin.replace(/\/+$/, '');
|
|
13
|
+
const defaultLocale = websiteConfig.defaultLocale;
|
|
14
|
+
const locales = websiteConfig.locales.map((entry) => entry.locale);
|
|
15
|
+
const entries = [];
|
|
16
|
+
for (const manifest of rootConfig.pageManifests) {
|
|
17
|
+
const routePath = manifest.routePath;
|
|
18
|
+
const pageRef = manifest.ref;
|
|
19
|
+
/**
|
|
20
|
+
* Pre-compute every locale's URL for this page once — each entry
|
|
21
|
+
* needs the full set as its `alternates` (with itself excluded
|
|
22
|
+
* later if/when the consumer wants the strict
|
|
23
|
+
* "alternates-don't-include-self" shape; today we keep the full
|
|
24
|
+
* set + add `x-default` for parity with the head renderer's
|
|
25
|
+
* hreflang output).
|
|
26
|
+
*/
|
|
27
|
+
const urlsByLocale = new Map();
|
|
28
|
+
for (const locale of locales) {
|
|
29
|
+
const localizedPath = buildLocalizedRoutePath({ locale, defaultLocale, routePath });
|
|
30
|
+
urlsByLocale.set(locale, `${origin}${localizedPath}`);
|
|
31
|
+
}
|
|
32
|
+
const defaultLocaleUrl = urlsByLocale.get(defaultLocale);
|
|
33
|
+
if (defaultLocaleUrl === undefined) {
|
|
34
|
+
throw new Error(`enumerateMarketingPageSitemapUrls: defaultLocale "${String(defaultLocale)}" is missing from WildoMarketingWebsiteConfig.locales (page "${pageRef}"). This is a configuration invariant — locales[] must include the default locale.`);
|
|
35
|
+
}
|
|
36
|
+
for (const locale of locales) {
|
|
37
|
+
const url = urlsByLocale.get(locale);
|
|
38
|
+
if (url === undefined) {
|
|
39
|
+
throw new Error(`enumerateMarketingPageSitemapUrls: missing computed URL for locale "${String(locale)}" on page "${pageRef}" (internal invariant violation).`);
|
|
40
|
+
}
|
|
41
|
+
const alternates = [];
|
|
42
|
+
for (const altLocale of locales) {
|
|
43
|
+
const altUrl = urlsByLocale.get(altLocale);
|
|
44
|
+
if (altUrl !== undefined) {
|
|
45
|
+
alternates.push({ hreflang: String(altLocale), url: altUrl });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `x-default` — Google's hreflang convention for "the URL to
|
|
50
|
+
* serve when no locale signal is available". The marketing-site
|
|
51
|
+
* convention (default locale at the un-prefixed path) makes the
|
|
52
|
+
* default-locale URL the right `x-default` target.
|
|
53
|
+
*/
|
|
54
|
+
alternates.push({ hreflang: 'x-default', url: defaultLocaleUrl });
|
|
55
|
+
entries.push({
|
|
56
|
+
url,
|
|
57
|
+
pageRef,
|
|
58
|
+
routePath,
|
|
59
|
+
locale,
|
|
60
|
+
alternates,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return entries;
|
|
65
|
+
}
|
|
66
|
+
/** @wildo_source:part:end saas.website.astro.sitemap-coverage */
|
|
67
|
+
//# sourceMappingURL=sitemap-coverage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap-coverage.js","sourceRoot":"","sources":["../../../../src/astro/sitemap-coverage.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AAmGjF,MAAM,UAAU,iCAAiC,CAC/C,WAA+B;IAE/B,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,WAAW,CAAC;IAElD,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,mLAAmL,CACpL,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,MAAM,MAAM,GAAG,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC9D,MAAM,aAAa,GAAG,aAAa,CAAC,aAAa,CAAC;IAClD,MAAM,OAAO,GAAiC,aAAa,CAAC,OAAO,CAAC,GAAG,CACrE,CAAC,KAAoC,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CACvD,CAAC;IAEF,MAAM,OAAO,GAAgC,EAAE,CAAC;IAEhD,KAAK,MAAM,QAAQ,IAAI,UAAU,CAAC,aAA+C,EAAE,CAAC;QAClF,MAAM,SAAS,GAAG,QAAQ,CAAC,SAA8B,CAAC;QAC1D,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAwB,CAAC;QAElD;;;;;;;WAOG;QACH,MAAM,YAAY,GAAG,IAAI,GAAG,EAA6B,CAAC;QAC1D,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,aAAa,GAAG,uBAAuB,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC,CAAC;YACpF,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,MAAM,GAAG,aAAa,EAAE,CAAC,CAAC;QACxD,CAAC;QAED,MAAM,gBAAgB,GAAG,YAAY,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACzD,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CACb,qDAAqD,MAAM,CAAC,aAAa,CAAC,gEAAgE,OAAO,oFAAoF,CACtO,CAAC;QACJ,CAAC;QAED,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACrC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;gBACtB,MAAM,IAAI,KAAK,CACb,uEAAuE,MAAM,CAAC,MAAM,CAAC,cAAc,OAAO,mCAAmC,CAC9I,CAAC;YACJ,CAAC;YAED,MAAM,UAAU,GAAwC,EAAE,CAAC;YAC3D,KAAK,MAAM,SAAS,IAAI,OAAO,EAAE,CAAC;gBAChC,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC3C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;oBACzB,UAAU,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;gBAChE,CAAC;YACH,CAAC;YACD;;;;;eAKG;YACH,UAAU,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,gBAAgB,EAAE,CAAC,CAAC;YAElE,OAAO,CAAC,IAAI,CAAC;gBACX,GAAG;gBACH,OAAO;gBACP,SAAS;gBACT,MAAM;gBACN,UAAU;aACX,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AACD,iEAAiE","sourcesContent":["import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils';\nimport type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';\n\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.sitemap-coverage facet:layer:astro facet:family:website\n *\n * `enumerateMarketingPageSitemapUrls(siteContext)` — pure helper that\n * returns every (page-manifest × locale) absolute URL the marketing\n * site's `sitemap.xml` MUST advertise, paired with its hreflang\n * alternates.\n *\n * # Why this helper exists (vs leaning entirely on `@astrojs/sitemap`)\n *\n * `@astrojs/sitemap` (the official Astro plugin) auto-discovers every\n * page Astro emits and emits a `sitemap-index.xml` + sub-sitemaps at\n * build time. That covers the dominant case (every page in\n * `src/pages/**` ships in the sitemap). What it does NOT cover by\n * itself:\n *\n * 1. **Coverage enforcement at unit-test time.** The plugin runs\n * during `astro build`; vitest cannot exercise it without\n * booting the full Astro pipeline. This helper is the testable\n * surface — a regression that drops a manifest-registered route\n * from the expected URL set fails fast in vitest, well before\n * the `astro build` step would surface it (and even then only\n * via end-to-end inspection of the emitted XML).\n * 2. **Programmatic page registration.** Future framework-owned\n * features that synthesize routes outside Astro's\n * file-based router (e.g. data-driven landing variants) need an\n * explicit URL list to pass into `@astrojs/sitemap`'s\n * `customPages` config. This helper is the canonical place to\n * compute that list — consumers spread the result into the\n * plugin config without re-deriving the locale × route logic.\n * 3. **Cross-check.** The same site context that drives the head\n * renderer drives this enumeration; a divergence between\n * \"what the head renderer thinks the canonical URL set is\" and\n * \"what the sitemap publishes\" is a silent drift surface this\n * helper makes auditable.\n *\n * # Output shape\n *\n * Each entry carries:\n * - `url` — the absolute canonical URL (no trailing slash, matches\n * `<link rel=\"canonical\">` output of `renderWebsitePageHeadHtml`).\n * - `pageRef` — the page-manifest `ref` (debug + grouping aid).\n * - `routePath` — the manifest-declared `routePath` (debug + grouping aid).\n * - `locale` — the URL's language.\n * - `alternates` — every other locale's URL for the SAME page,\n * emitted as `xhtml:link rel=\"alternate\" hreflang` entries inside\n * the sitemap (single-sitemap-with-hreflang shape, per Google's\n * recommended sitemap format).\n *\n * # Routing convention (matches `buildLocalizedRoutePath`)\n *\n * Default locale mounts at the un-prefixed path (`/`, `/pricing`);\n * other locales sit under `/<locale>/…`. The helper delegates to\n * `buildLocalizedRoutePath` so a future change to the routing\n * convention lands in one file rather than two.\n *\n * # What this helper does NOT enumerate\n *\n * - **Blog post slugs.** Blog posts share ONE page-manifest ref\n * (`BLOG_POST_PAGE_REF`) but materialize as N URLs — one per\n * `(slug, locale)` MDX file. The MDX file system is the\n * source-of-truth for blog URLs, NOT the page manifest. Astro's\n * file-based router auto-discovers each blog `[slug].astro` route\n * and `@astrojs/sitemap` auto-publishes them. A future helper\n * (`enumerateBlogPostSitemapUrls(...)`) can address the same\n * coverage-enforcement gap for the blog surface specifically.\n * - **Static `.astro` pages without a page manifest.** If a consumer\n * authors a page directly in `src/pages/` without registering a\n * manifest in `WebsiteRootConfig.pageManifests`, this helper will\n * not list it (and `renderWebsitePageHeadHtml` cannot render its\n * `<head>` either). The framework convention is \"every public\n * page has a manifest\" — pages without manifests are a label-pack\n * discipline regression and surface separately via\n * `assertLabelPacksValid`.\n */\nexport interface MarketingPageSitemapEntry {\n /** Absolute canonical URL, no trailing slash. */\n readonly url: string;\n /** The page-manifest `ref` (branded string in the source schema). */\n readonly pageRef: string;\n /** The manifest-declared `routePath` (always starts with `/`). */\n readonly routePath: string;\n /** The URL's language. */\n readonly locale: AvailableLanguage;\n /**\n * Every OTHER locale's URL for the same page, suitable for emitting\n * `xhtml:link rel=\"alternate\" hreflang` entries inside the sitemap.\n * Includes an `x-default` synthesized from the default-locale URL\n * (Google's i18n discoverability convention; matches the head\n * renderer's hreflang output).\n */\n readonly alternates: readonly { readonly hreflang: string; readonly url: string }[];\n}\n\nexport function enumerateMarketingPageSitemapUrls(\n siteContext: WebsiteSiteContext,\n): readonly MarketingPageSitemapEntry[] {\n const { rootConfig, websiteConfig } = siteContext;\n\n if (!websiteConfig.publicOrigin) {\n throw new Error(\n 'enumerateMarketingPageSitemapUrls: WildoMarketingWebsiteConfig.publicOrigin is required to compose absolute sitemap URLs. Author it in your service\\'s `wildo.website.config.ts`.',\n );\n }\n\n /**\n * Trim trailing slash defensively (the schema-level refinement\n * already rejects them, but the helper is consumed in synthetic\n * test contexts that bypass the parser).\n */\n const origin = websiteConfig.publicOrigin.replace(/\\/+$/, '');\n const defaultLocale = websiteConfig.defaultLocale;\n const locales: readonly AvailableLanguage[] = websiteConfig.locales.map(\n (entry: { locale: AvailableLanguage }) => entry.locale,\n );\n\n const entries: MarketingPageSitemapEntry[] = [];\n\n for (const manifest of rootConfig.pageManifests as readonly WebsitePageManifest[]) {\n const routePath = manifest.routePath as unknown as string;\n const pageRef = manifest.ref as unknown as string;\n\n /**\n * Pre-compute every locale's URL for this page once — each entry\n * needs the full set as its `alternates` (with itself excluded\n * later if/when the consumer wants the strict\n * \"alternates-don't-include-self\" shape; today we keep the full\n * set + add `x-default` for parity with the head renderer's\n * hreflang output).\n */\n const urlsByLocale = new Map<AvailableLanguage, string>();\n for (const locale of locales) {\n const localizedPath = buildLocalizedRoutePath({ locale, defaultLocale, routePath });\n urlsByLocale.set(locale, `${origin}${localizedPath}`);\n }\n\n const defaultLocaleUrl = urlsByLocale.get(defaultLocale);\n if (defaultLocaleUrl === undefined) {\n throw new Error(\n `enumerateMarketingPageSitemapUrls: defaultLocale \"${String(defaultLocale)}\" is missing from WildoMarketingWebsiteConfig.locales (page \"${pageRef}\"). This is a configuration invariant — locales[] must include the default locale.`,\n );\n }\n\n for (const locale of locales) {\n const url = urlsByLocale.get(locale);\n if (url === undefined) {\n throw new Error(\n `enumerateMarketingPageSitemapUrls: missing computed URL for locale \"${String(locale)}\" on page \"${pageRef}\" (internal invariant violation).`,\n );\n }\n\n const alternates: { hreflang: string; url: string }[] = [];\n for (const altLocale of locales) {\n const altUrl = urlsByLocale.get(altLocale);\n if (altUrl !== undefined) {\n alternates.push({ hreflang: String(altLocale), url: altUrl });\n }\n }\n /**\n * `x-default` — Google's hreflang convention for \"the URL to\n * serve when no locale signal is available\". The marketing-site\n * convention (default locale at the un-prefixed path) makes the\n * default-locale URL the right `x-default` target.\n */\n alternates.push({ hreflang: 'x-default', url: defaultLocaleUrl });\n\n entries.push({\n url,\n pageRef,\n routePath,\n locale,\n alternates,\n });\n }\n }\n\n return entries;\n}\n/** @wildo_source:part:end saas.website.astro.sitemap-coverage */\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { WebsiteStructuredDataPayload } from '../schemas/structured-data/website-structured-data.shared.schemas';
|
|
2
|
+
import type { WebsiteSectionDefinition } from '../schemas/sections/website-section-definition.shared.schemas';
|
|
3
|
+
export interface RenderPageStructuredDataOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Section definitions whose `getStructuredData()` (when present)
|
|
6
|
+
* contributes payloads to the page. Sections without
|
|
7
|
+
* `getStructuredData` are skipped silently.
|
|
8
|
+
*/
|
|
9
|
+
sections?: readonly Pick<WebsiteSectionDefinition, 'sectionRef' | 'getStructuredData'>[];
|
|
10
|
+
/**
|
|
11
|
+
* Optional page-level payloads contributed by the page-shell
|
|
12
|
+
* itself (typical: `BreadcrumbList`, `Organization`, `WebSite`).
|
|
13
|
+
* Composed AFTER section payloads in the emission order so a
|
|
14
|
+
* section's `@id`-keyed payload wins over a page-level one with
|
|
15
|
+
* the same `@id` (last-wins; see preamble).
|
|
16
|
+
*/
|
|
17
|
+
pagePayloads?: readonly WebsiteStructuredDataPayload[];
|
|
18
|
+
}
|
|
19
|
+
export declare function renderPageStructuredData(opts: RenderPageStructuredDataOptions): string;
|
|
20
|
+
/** @wildo_source:part:end saas.website.astro.structured-data-renderer */
|
|
21
|
+
//# sourceMappingURL=structured-data.renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured-data.renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/structured-data.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAE7B,MAAM,mEAAmE,CAAC;AAC3E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+DAA+D,CAAC;AAgD9G,MAAM,WAAW,+BAA+B;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,IAAI,CAAC,wBAAwB,EAAE,YAAY,GAAG,mBAAmB,CAAC,EAAE,CAAC;IACzF;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,SAAS,4BAA4B,EAAE,CAAC;CACxD;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,+BAA+B,GAAG,MAAM,CAiDtF;AAoDD,yEAAyE"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wildo_source:part:start saas.website.astro.structured-data-renderer facet:layer:astro facet:family:website
|
|
3
|
+
*
|
|
4
|
+
* `renderPageStructuredData()` — collects every section's
|
|
5
|
+
* `getStructuredData()` return + any explicit page-level payloads,
|
|
6
|
+
* normalizes them, dedupes by `@id`, and emits one
|
|
7
|
+
* `<script type="application/ld+json">` block per surviving payload.
|
|
8
|
+
*
|
|
9
|
+
* **Why one `<script>` per payload (NOT a single `@graph` array)**:
|
|
10
|
+
*
|
|
11
|
+
* - Google's structured-data testing tool, Bing webmaster tools, and
|
|
12
|
+
* the Schema.org reference parser all accept multiple sibling
|
|
13
|
+
* `<script type="application/ld+json">` blocks. Emitting them as
|
|
14
|
+
* separate blocks lets each payload be diff-friendly in the
|
|
15
|
+
* page source AND lets per-payload removal happen on a future
|
|
16
|
+
* AEO opt-out without churning the whole graph.
|
|
17
|
+
* - `@graph` is technically valid but harder to author — every
|
|
18
|
+
* payload would need to share `@context` with the wrapper, and
|
|
19
|
+
* per-section authors would have to coordinate to merge their
|
|
20
|
+
* payloads into one shared graph. The "one script per payload"
|
|
21
|
+
* pattern keeps section authoring fully local.
|
|
22
|
+
*
|
|
23
|
+
* **Why dedupe by `@id` (last-wins)**: a page may have a section
|
|
24
|
+
* emit a `Product` payload AND another section emit a `Review`
|
|
25
|
+
* referencing the same `Product` via `@id`. Without dedupe, the
|
|
26
|
+
* `Product` would emit twice (once standalone, once referenced).
|
|
27
|
+
* Last-wins because the section's own emission is closer to the
|
|
28
|
+
* truth than a reference; the dedupe is best-effort and harmless
|
|
29
|
+
* if `@id` is absent (the payload always emits).
|
|
30
|
+
*
|
|
31
|
+
* **`@context` injection**: when a payload omits `@context`, the
|
|
32
|
+
* renderer injects `'https://schema.org'`. Consumers wanting a
|
|
33
|
+
* custom vocabulary supply `@context` themselves and the renderer
|
|
34
|
+
* passes it through unchanged.
|
|
35
|
+
*
|
|
36
|
+
* **HTML-safe serialization**: JSON-LD payloads are serialized via
|
|
37
|
+
* `JSON.stringify` with the closing `</` escaped to `<\/` (and the
|
|
38
|
+
* `<!` opener escaped to `<\!`) to prevent the payload from
|
|
39
|
+
* accidentally closing the surrounding `<script>` tag if the
|
|
40
|
+
* payload contains the string `</script>` (e.g. a blog post about
|
|
41
|
+
* structured data). This is the canonical safe-serialization
|
|
42
|
+
* pattern (see https://html.spec.whatwg.org/#restrictions-for-contents-of-script-elements).
|
|
43
|
+
*/
|
|
44
|
+
const SCHEMA_ORG_DEFAULT_CONTEXT = 'https://schema.org';
|
|
45
|
+
export function renderPageStructuredData(opts) {
|
|
46
|
+
const collected = [];
|
|
47
|
+
if (opts.sections) {
|
|
48
|
+
for (const section of opts.sections) {
|
|
49
|
+
if (!section.getStructuredData)
|
|
50
|
+
continue;
|
|
51
|
+
const value = section.getStructuredData();
|
|
52
|
+
collectInto(collected, value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (opts.pagePayloads) {
|
|
56
|
+
for (const payload of opts.pagePayloads) {
|
|
57
|
+
collected.push(payload);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (collected.length === 0)
|
|
61
|
+
return '';
|
|
62
|
+
/**
|
|
63
|
+
* Dedupe by `@id` (last-wins). Payloads without `@id` always
|
|
64
|
+
* survive. We walk LAST-to-FIRST so the LAST occurrence of an
|
|
65
|
+
* `@id` is the kept one (later writes overwrite earlier ones
|
|
66
|
+
* naturally with this loop direction + reverse at the end).
|
|
67
|
+
*/
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
const deduped = [];
|
|
70
|
+
for (let i = collected.length - 1; i >= 0; i -= 1) {
|
|
71
|
+
const payload = collected[i];
|
|
72
|
+
if (payload === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
const id = payload['@id'];
|
|
75
|
+
if (typeof id === 'string') {
|
|
76
|
+
if (seen.has(id))
|
|
77
|
+
continue;
|
|
78
|
+
seen.add(id);
|
|
79
|
+
}
|
|
80
|
+
deduped.push(payload);
|
|
81
|
+
}
|
|
82
|
+
deduped.reverse();
|
|
83
|
+
const lines = [];
|
|
84
|
+
for (const payload of deduped) {
|
|
85
|
+
const withContext = payload['@context'] === undefined
|
|
86
|
+
? { '@context': SCHEMA_ORG_DEFAULT_CONTEXT, ...payload }
|
|
87
|
+
: { ...payload };
|
|
88
|
+
const json = serializeForScriptTag(withContext);
|
|
89
|
+
lines.push(`<script type="application/ld+json">${json}</script>`);
|
|
90
|
+
}
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Normalize a `getStructuredData()` return value into a flat array
|
|
95
|
+
* the renderer iterates. Handles the four legal shapes
|
|
96
|
+
* (`null` / `undefined` / single payload / readonly array).
|
|
97
|
+
*
|
|
98
|
+
* Mutates the `into` array in place — the function is private to
|
|
99
|
+
* this module and the in-place mutation avoids one allocation per
|
|
100
|
+
* section (sections compose linearly, the cost adds up on long
|
|
101
|
+
* pages).
|
|
102
|
+
*/
|
|
103
|
+
function collectInto(into, value) {
|
|
104
|
+
if (value === null || value === undefined)
|
|
105
|
+
return;
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
for (const item of value) {
|
|
108
|
+
if (item !== null && item !== undefined)
|
|
109
|
+
into.push(item);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Single-payload shape — narrow the readonly-array branch above
|
|
115
|
+
* by elimination. `Array.isArray` returns `false` for readonly
|
|
116
|
+
* arrays in some TS configs but TRUE at runtime; this branch
|
|
117
|
+
* runs only for non-array `WebsiteStructuredDataPayload`.
|
|
118
|
+
*/
|
|
119
|
+
into.push(value);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Serialize a payload for emission inside a `<script>` tag.
|
|
123
|
+
*
|
|
124
|
+
* `JSON.stringify` alone is NOT safe inside `<script>` tags:
|
|
125
|
+
*
|
|
126
|
+
* - A payload field containing the literal string `</script>`
|
|
127
|
+
* (rare but possible on a blog post about HTML / structured
|
|
128
|
+
* data) would close the script tag prematurely and inject the
|
|
129
|
+
* remainder as page HTML.
|
|
130
|
+
* - A payload field containing `<!--` similarly opens an HTML
|
|
131
|
+
* comment that breaks the surrounding parsing context.
|
|
132
|
+
*
|
|
133
|
+
* Both are mitigated by escaping `<` to `\u003c` inside the JSON
|
|
134
|
+
* payload — the JSON parser treats `\u003c` as identical to `<`,
|
|
135
|
+
* so the consumed payload is unchanged, but the HTML parser sees
|
|
136
|
+
* no `<` characters and never gets confused.
|
|
137
|
+
*/
|
|
138
|
+
function serializeForScriptTag(payload) {
|
|
139
|
+
return JSON.stringify(payload).replace(/</g, '\\u003c');
|
|
140
|
+
}
|
|
141
|
+
/** @wildo_source:part:end saas.website.astro.structured-data-renderer */
|
|
142
|
+
//# sourceMappingURL=structured-data.renderer.js.map
|