create-ailk 0.6.1 → 0.8.0
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/README.md +87 -14
- package/component-catalog.json +735 -95
- package/component-catalog.md +13 -7
- package/dist/add-module-core.d.ts +22 -0
- package/dist/add-module-core.js +50 -0
- package/dist/add-module.d.ts +89 -0
- package/dist/add-module.js +334 -0
- package/dist/category-profile.d.ts +126 -0
- package/dist/category-profile.js +262 -0
- package/dist/cli.js +89 -29
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -1
- package/dist/lib/extract-module-bundle.d.ts +56 -1
- package/dist/lib/extract-module-bundle.js +170 -14
- package/dist/lib/license-key-files.d.ts +15 -0
- package/dist/lib/license-key-files.js +102 -0
- package/dist/lib/module-license-gate.d.ts +79 -0
- package/dist/lib/module-license-gate.js +160 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.d.ts +16 -10
- package/dist/lib/pro-callout.js +34 -3
- package/dist/license-set.d.ts +34 -0
- package/dist/license-set.js +194 -0
- package/dist/module-architecture.d.ts +20 -0
- package/dist/module-architecture.js +77 -19
- package/dist/modules/commerce/0.6.1.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
- package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
- package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
- package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
- package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
- package/dist/modules/workspace/0.6.1.tar.gz +0 -0
- package/dist/parse-args.d.ts +85 -10
- package/dist/parse-args.js +227 -15
- package/dist/programmatic.d.ts +81 -5
- package/dist/programmatic.js +291 -32
- package/dist/site-chrome.d.ts +18 -7
- package/dist/site-chrome.js +32 -14
- package/dist/site-roster.d.ts +127 -0
- package/dist/site-roster.js +499 -0
- package/dist/starter-grid.d.ts +216 -0
- package/dist/starter-grid.js +449 -0
- package/dist/surfaces.d.ts +20 -7
- package/dist/surfaces.js +204 -24
- package/dist/sync-routes.d.ts +9 -0
- package/dist/sync-routes.js +2 -2
- package/dist/template-manifest.d.ts +94 -19
- package/dist/template-manifest.js +170 -289
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +6 -4
- package/site-templates/_base/content/en/BookCall/book-call.mdx +52 -0
- package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
- package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +65 -0
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +67 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
- package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +17 -3
- package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
- package/site-templates/designs/minimalism/DESIGN.md +162 -0
- package/site-templates/designs/standard/DESIGN.md +167 -0
- package/site-templates/marketing-site/DESIGN.md +59 -40
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +376 -57
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +230 -90
- package/site-templates/marketing-site/ailk-template.json +1 -0
- package/site-templates/marketing-site-free/DESIGN.md +59 -40
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +323 -63
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +323 -113
- package/site-templates/marketing-site-free/ailk-template.json +3 -2
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
- package/site-templates/minimalism-site/DESIGN.md +54 -40
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +173 -51
- package/site-templates/minimalism-site/ailk-template.json +3 -2
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/saas-product/DESIGN.md +59 -40
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +369 -0
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
- package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
- package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/local-business-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
- package/site-templates/shapes/local-business-service/provenance.json +4 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
- package/site-templates/shapes/organization-service/ailk-template.json +10 -0
- package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
- package/site-templates/shapes/person-service/ailk-template.json +10 -0
- package/site-templates/shapes/person-service/content/_site.mdx +85 -0
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
- package/site-templates/shapes/person-service/provenance.json +4 -0
- package/templates/.claude/settings.json +15 -0
- package/templates/.claude/skills/README.md +10 -9
- package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
- package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
- package/templates/.claude/skills/add-page/references/page-types.md +115 -0
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
- package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
- package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
- package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
- package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
- package/templates/.claude/skills/fill-page/SKILL.md +180 -0
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
- package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
- package/templates/.claude/skills/setup-database/SKILL.md +156 -0
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
- package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +73 -41
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +21 -30
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +204 -0
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
- package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
- package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
- package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/CLAUDE.md +8 -2
- package/templates/DESIGN.md +56 -44
- package/templates/README.md +116 -46
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/CLAUDE.md +3 -2
- package/templates/apps/api/package.json +1 -1
- package/templates/apps/api/src/config/__tests__/index.test.ts +146 -10
- package/templates/apps/api/src/config/index.ts +46 -14
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
- package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
- package/templates/apps/api/src/lib/license-key-store.ts +45 -0
- package/templates/apps/api/src/lib/site.ts +5 -3
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
- package/templates/apps/api/src/middleware/auth.ts +9 -0
- package/templates/apps/api/src/openapi/spec.ts +63 -12
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
- package/templates/apps/api/src/routes/content/get-page.ts +86 -2
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
- package/templates/apps/api/src/routes/leads/index.ts +46 -1
- package/templates/apps/api/src/server.ts +13 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
- package/templates/apps/api/src/services/lead-routing.ts +140 -30
- package/templates/apps/api/src/services/leads-inbox.ts +48 -11
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/package.json +2 -1
- package/templates/apps/mcp/src/index.ts +7 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
- package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- package/templates/apps/web/__tests__/setup.ts +18 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
- package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +67 -29
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +94 -11
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +228 -86
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
- package/templates/apps/web/app/[locale]/contact/page.tsx +115 -7
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/layout.tsx +146 -52
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +142 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/{cancellation → purchase-policy}/page.tsx +15 -10
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +63 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
- package/templates/apps/web/app/error-fallback.tsx +17 -45
- package/templates/apps/web/app/not-found.tsx +14 -38
- package/templates/apps/web/app/og/route.tsx +6 -0
- package/templates/apps/web/eslint.config.mjs +3 -0
- package/templates/apps/web/instrumentation.ts +14 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
- package/templates/apps/web/lib/license-key-store.ts +52 -0
- package/templates/apps/web/lib/nav-config.ts +82 -18
- package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
- package/templates/apps/web/lib/site-brand.tsx +1 -1
- package/templates/apps/web/lib/site-chrome.ts +3 -3
- package/templates/apps/web/lib/site-identity.ts +148 -0
- package/templates/apps/web/lib/site-theme.ts +11 -0
- package/templates/apps/web/messages/en.json +78 -37
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +3 -3
- package/templates/apps/web/public/logomark.svg +6 -3
- package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
- package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +4 -4
- package/templates/apps/web/public/wordmark-light.svg +4 -0
- package/templates/apps/web/public/wordmark.svg +3 -3
- package/templates/apps/web/security-headers.config.ts +5 -1
- package/templates/content/_site.mdx +25 -0
- package/templates/content/en/AboutPage/about.mdx +150 -144
- package/templates/content/en/BlogList/blog.mdx +126 -0
- package/templates/content/en/ContactPage/contact.mdx +21 -92
- package/templates/content/en/FAQPage/faq.mdx +86 -57
- package/templates/content/en/HomePage/home.mdx +268 -217
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +181 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +3 -1
- package/templates/tools/lint-architecture.ts +315 -0
- package/templates/tools/tsconfig.json +14 -0
- package/versions.json +33 -33
- package/site-templates/simple-service/DESIGN.md +0 -148
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +0 -240
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -278
- package/site-templates/simple-service/ailk-template.json +0 -10
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -67
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
- package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
- package/site-templates/simple-service/provenance.json +0 -4
- package/templates/.claude/skills/add-schema/SKILL.md +0 -277
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
- package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
- package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
- package/templates/.claude/skills/provision-config/references/runbook-template.md +0 -248
- package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -150
- package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
- package/templates/apps/web/app/[locale]/shipping/page.tsx +0 -44
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
- package/templates/content/en/LandingPage/blog.mdx +0 -83
- package/templates/content/en/LandingPage/features.mdx +0 -103
- package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
|
@@ -747,9 +747,11 @@ export function buildOpenApiSpec(
|
|
|
747
747
|
"form fields — for the given siteId. `source` is a free-form tag the caller " +
|
|
748
748
|
"sets to identify the channel (for example `agent`); it is stored as-is and " +
|
|
749
749
|
"not checked against a fixed vocabulary. A 201 response means the lead " +
|
|
750
|
-
"landed
|
|
751
|
-
"
|
|
752
|
-
"
|
|
750
|
+
"landed — in the database, a local file fallback (outside production " +
|
|
751
|
+
"only), or, on a deployment with no database, the site operator's inbox " +
|
|
752
|
+
"by email (`persisted: \"email\"`); with a database, the response's " +
|
|
753
|
+
"`email` field reports, best-effort, whether the operator was notified. " +
|
|
754
|
+
"A lead is never reported captured when nothing stored or delivered it.",
|
|
753
755
|
requestBody: {
|
|
754
756
|
required: true,
|
|
755
757
|
content: {
|
|
@@ -777,8 +779,8 @@ export function buildOpenApiSpec(
|
|
|
777
779
|
},
|
|
778
780
|
"503": {
|
|
779
781
|
description:
|
|
780
|
-
"Lead capture unavailable — persistence failed,
|
|
781
|
-
"
|
|
782
|
+
"Lead capture unavailable — persistence failed, delivery failed, or " +
|
|
783
|
+
"this deployment has nowhere to put the lead",
|
|
782
784
|
content: {
|
|
783
785
|
"application/json": {
|
|
784
786
|
schema: {
|
|
@@ -787,6 +789,7 @@ export function buildOpenApiSpec(
|
|
|
787
789
|
{
|
|
788
790
|
$ref: "#/components/schemas/LeadCaptureNotConfiguredResponse",
|
|
789
791
|
},
|
|
792
|
+
{ $ref: "#/components/schemas/DeliveryFailedResponse" },
|
|
790
793
|
],
|
|
791
794
|
},
|
|
792
795
|
},
|
|
@@ -795,6 +798,36 @@ export function buildOpenApiSpec(
|
|
|
795
798
|
},
|
|
796
799
|
},
|
|
797
800
|
},
|
|
801
|
+
"/v1/leads/capture": {
|
|
802
|
+
get: {
|
|
803
|
+
operationId: "getLeadCaptureStatus",
|
|
804
|
+
summary: "Whether this site accepts a contact message or a signup",
|
|
805
|
+
description:
|
|
806
|
+
"Two booleans: `contact` — a `POST /v1/leads/submit` would be accepted; " +
|
|
807
|
+
"`newsletter` — a `POST /v1/leads/subscribe` would be. A web page reads " +
|
|
808
|
+
"it to show the contact form or the site's contact email. It never says " +
|
|
809
|
+
"how a lead is delivered. No MCP twin (recorded parity exception): the " +
|
|
810
|
+
"three lead-write tools already answer the same question with a 503.",
|
|
811
|
+
responses: {
|
|
812
|
+
"200": {
|
|
813
|
+
description: "Lead-capture status",
|
|
814
|
+
content: {
|
|
815
|
+
"application/json": {
|
|
816
|
+
schema: {
|
|
817
|
+
type: "object",
|
|
818
|
+
required: ["ok", "data"],
|
|
819
|
+
properties: {
|
|
820
|
+
ok: { type: "boolean", enum: [true] },
|
|
821
|
+
data: { $ref: "#/components/schemas/LeadCaptureStatus" },
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
"429": { $ref: "#/components/responses/RateLimited" },
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
},
|
|
798
831
|
"/v1/leads/subscribe": {
|
|
799
832
|
post: {
|
|
800
833
|
operationId: "subscribeLead",
|
|
@@ -833,8 +866,8 @@ export function buildOpenApiSpec(
|
|
|
833
866
|
},
|
|
834
867
|
"503": {
|
|
835
868
|
description:
|
|
836
|
-
"Lead capture unavailable — persistence failed,
|
|
837
|
-
"
|
|
869
|
+
"Lead capture unavailable — persistence failed, delivery failed, or " +
|
|
870
|
+
"this deployment has nowhere to put the lead",
|
|
838
871
|
content: {
|
|
839
872
|
"application/json": {
|
|
840
873
|
schema: {
|
|
@@ -1033,8 +1066,8 @@ export function buildOpenApiSpec(
|
|
|
1033
1066
|
},
|
|
1034
1067
|
"503": {
|
|
1035
1068
|
description:
|
|
1036
|
-
"Lead capture unavailable — persistence failed,
|
|
1037
|
-
"
|
|
1069
|
+
"Lead capture unavailable — persistence failed, delivery failed, or " +
|
|
1070
|
+
"this deployment has nowhere to put the lead",
|
|
1038
1071
|
content: {
|
|
1039
1072
|
"application/json": {
|
|
1040
1073
|
schema: {
|
|
@@ -1043,6 +1076,7 @@ export function buildOpenApiSpec(
|
|
|
1043
1076
|
{
|
|
1044
1077
|
$ref: "#/components/schemas/LeadCaptureNotConfiguredResponse",
|
|
1045
1078
|
},
|
|
1079
|
+
{ $ref: "#/components/schemas/DeliveryFailedResponse" },
|
|
1046
1080
|
],
|
|
1047
1081
|
},
|
|
1048
1082
|
},
|
|
@@ -1489,10 +1523,11 @@ export function buildOpenApiSpec(
|
|
|
1489
1523
|
type: "string",
|
|
1490
1524
|
description:
|
|
1491
1525
|
"Server-assigned record id — a DB-prefixed entity id when " +
|
|
1492
|
-
'persisted is "db",
|
|
1493
|
-
'persisted is "file"'
|
|
1526
|
+
'persisted is "db", a `fallback-<ts>-<rand>` file id when ' +
|
|
1527
|
+
'persisted is "file", or an `email-<ts>-<rand>` id when persisted ' +
|
|
1528
|
+
'is "email" (no database; the operator email was the delivery)',
|
|
1494
1529
|
},
|
|
1495
|
-
persisted: { type: "string", enum: ["db", "file"] },
|
|
1530
|
+
persisted: { type: "string", enum: ["db", "file", "email"] },
|
|
1496
1531
|
email: { $ref: "#/components/schemas/LeadEmailResult" },
|
|
1497
1532
|
},
|
|
1498
1533
|
},
|
|
@@ -1511,6 +1546,22 @@ export function buildOpenApiSpec(
|
|
|
1511
1546
|
error: { type: "string", enum: ["persist_failed"] },
|
|
1512
1547
|
},
|
|
1513
1548
|
},
|
|
1549
|
+
DeliveryFailedResponse: {
|
|
1550
|
+
type: "object",
|
|
1551
|
+
required: ["error", "message"],
|
|
1552
|
+
properties: {
|
|
1553
|
+
error: { type: "string", enum: ["delivery_failed"] },
|
|
1554
|
+
message: { type: "string" },
|
|
1555
|
+
},
|
|
1556
|
+
},
|
|
1557
|
+
LeadCaptureStatus: {
|
|
1558
|
+
type: "object",
|
|
1559
|
+
required: ["contact", "newsletter"],
|
|
1560
|
+
properties: {
|
|
1561
|
+
contact: { type: "boolean" },
|
|
1562
|
+
newsletter: { type: "boolean" },
|
|
1563
|
+
},
|
|
1564
|
+
},
|
|
1514
1565
|
LeadCaptureNotConfiguredResponse: {
|
|
1515
1566
|
type: "object",
|
|
1516
1567
|
required: ["error", "message"],
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* No internal module mocks — the route exercises the real adapter stack.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
9
12
|
import type { FastifyInstance } from "fastify";
|
|
10
13
|
|
|
11
14
|
import { setupContentFixture } from "../../../__tests__/factories/content-fixture";
|
|
@@ -17,6 +20,30 @@ import { contentRoutes } from "../index";
|
|
|
17
20
|
|
|
18
21
|
const fixture = setupContentFixture();
|
|
19
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A `_site.mdx` in the fixture's content root — an Organization identity at
|
|
25
|
+
* `https://example.com` (#6597). `logo` adds an identity-declared logo.
|
|
26
|
+
*/
|
|
27
|
+
function writeSiteMdx(root: string, opts: { logo?: string } = {}): void {
|
|
28
|
+
writeFileSync(
|
|
29
|
+
join(root, "_site.mdx"),
|
|
30
|
+
[
|
|
31
|
+
"---",
|
|
32
|
+
'version: "1"',
|
|
33
|
+
"siteName: Fixture Site",
|
|
34
|
+
"baseUrl: https://example.com",
|
|
35
|
+
"defaultLocale: en",
|
|
36
|
+
"siteIdentity:",
|
|
37
|
+
" kind: Organization",
|
|
38
|
+
" name: Fixture Co",
|
|
39
|
+
" url: https://example.com",
|
|
40
|
+
...(opts.logo !== undefined ? [` logo: ${opts.logo}`] : []),
|
|
41
|
+
"---",
|
|
42
|
+
"",
|
|
43
|
+
].join("\n"),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
20
47
|
describe("GET /api/content/:type/:slug", () => {
|
|
21
48
|
let app: FastifyInstance;
|
|
22
49
|
|
|
@@ -31,6 +58,7 @@ describe("GET /api/content/:type/:slug", () => {
|
|
|
31
58
|
|
|
32
59
|
it("returns the page with resolved JSON-LD at the document root", async () => {
|
|
33
60
|
// The fixture writes "home" HomePage in beforeEach by default.
|
|
61
|
+
writeSiteMdx(fixture.getContentRoot());
|
|
34
62
|
const res = await app.inject({
|
|
35
63
|
method: "GET",
|
|
36
64
|
url: "/v1/content/HomePage/home",
|
|
@@ -39,8 +67,8 @@ describe("GET /api/content/:type/:slug", () => {
|
|
|
39
67
|
expect(res.statusCode).toBe(200);
|
|
40
68
|
const body = JSON.parse(res.body);
|
|
41
69
|
expect(body["@context"]).toBe("https://schema.org");
|
|
42
|
-
// HomePage
|
|
43
|
-
//
|
|
70
|
+
// A HomePage body keeps its @graph envelope: the WebPage spine plus the
|
|
71
|
+
// sitewide identity from _site.mdx (#6597). Page fields live on the spine.
|
|
44
72
|
const graph = body["@graph"] as Array<Record<string, unknown>>;
|
|
45
73
|
expect(graph.map((n) => n["@type"])).toEqual([
|
|
46
74
|
"WebPage",
|
|
@@ -405,9 +433,8 @@ describe("GET /v1/content/:type/:slug under CONTENT_ADAPTER=database (#5852)", (
|
|
|
405
433
|
// route's capability branch is exercised through the real resolver — the file
|
|
406
434
|
// header's "no internal module mocks" (#507) holds for these cases too.
|
|
407
435
|
|
|
408
|
-
import { mkdtempSync
|
|
436
|
+
import { mkdtempSync } from "node:fs";
|
|
409
437
|
import { tmpdir } from "node:os";
|
|
410
|
-
import { join } from "node:path";
|
|
411
438
|
|
|
412
439
|
import * as contentAdapterModule from "../../../lib/content-adapter.js";
|
|
413
440
|
import { resetContentAdapter } from "../../../lib/content-adapter.js";
|
|
@@ -453,7 +480,12 @@ describe("GET /v1/content/:type/:slug?include=body (#5890)", () => {
|
|
|
453
480
|
});
|
|
454
481
|
|
|
455
482
|
// AC-1, the floor half — the unparameterised read is unchanged to the byte.
|
|
483
|
+
// #6597 rewrote this pin by hand: the HomePage @graph is now composed from
|
|
484
|
+
// the fixture's _site.mdx rather than the page title (AC-9 checks every
|
|
485
|
+
// field of the pre-#6597 body is still present). The #5890 claim it guards
|
|
486
|
+
// — no `body` key without the parameter — is unchanged.
|
|
456
487
|
it("serves a response byte-identical to the pre-change handler when the parameter is absent", async () => {
|
|
488
|
+
writeSiteMdx(fixture.getContentRoot());
|
|
457
489
|
const res = await app.inject({
|
|
458
490
|
method: "GET",
|
|
459
491
|
url: "/v1/content/HomePage/home",
|
|
@@ -461,7 +493,7 @@ describe("GET /v1/content/:type/:slug?include=body (#5890)", () => {
|
|
|
461
493
|
|
|
462
494
|
expect(res.statusCode).toBe(200);
|
|
463
495
|
expect(res.body).toMatchInlineSnapshot(
|
|
464
|
-
`"{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"HomePage home title","description":"HomePage home description"},{"@context":"https://schema.org","@type":"Organization","name":"
|
|
496
|
+
`"{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"HomePage home title","description":"HomePage home description","mainEntity":{"@id":"https://example.com/#organization"},"isPartOf":{"@id":"https://example.com/#website"}},{"@context":"https://schema.org","@type":"Organization","@id":"https://example.com/#organization","name":"Fixture Co","url":"https://example.com"},{"@context":"https://schema.org","@type":"WebSite","@id":"https://example.com/#website","name":"Fixture Site","url":"https://example.com","inLanguage":"en","publisher":{"@id":"https://example.com/#organization"}}],"page":{"type":"HomePage","slug":"home","title":"HomePage home title","description":"HomePage home description"}}"`,
|
|
465
497
|
);
|
|
466
498
|
expect(JSON.parse(res.body)).not.toHaveProperty("body");
|
|
467
499
|
});
|
|
@@ -693,3 +725,93 @@ describe("GET /v1/content/:type/:slug?include=body under CONTENT_ADAPTER=databas
|
|
|
693
725
|
expect(prisma.artifactVersion.findFirst).not.toHaveBeenCalled();
|
|
694
726
|
});
|
|
695
727
|
});
|
|
728
|
+
|
|
729
|
+
// ─── #6597 AC-9 — the /v1 HomePage response keeps its @graph envelope ─────────
|
|
730
|
+
|
|
731
|
+
describe("GET /v1/content/HomePage/home — sitewide identity envelope (#6597)", () => {
|
|
732
|
+
let app: FastifyInstance;
|
|
733
|
+
|
|
734
|
+
beforeEach(async () => {
|
|
735
|
+
app = require("fastify")();
|
|
736
|
+
await app.register(contentRoutes);
|
|
737
|
+
await fixture.writeFixturePage("HomePage", "home", {
|
|
738
|
+
type: "HomePage",
|
|
739
|
+
slug: "home",
|
|
740
|
+
title: "HomePage home title",
|
|
741
|
+
description: "HomePage home description",
|
|
742
|
+
image: "https://example.com/home.png",
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
afterEach(async () => {
|
|
747
|
+
await app.close();
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
async function homeBody(query = ""): Promise<{
|
|
751
|
+
body: Record<string, unknown>;
|
|
752
|
+
graph: Array<Record<string, unknown>>;
|
|
753
|
+
}> {
|
|
754
|
+
const res = await app.inject({ method: "GET", url: `/v1/content/HomePage/home${query}` });
|
|
755
|
+
expect(res.statusCode).toBe(200);
|
|
756
|
+
const body = JSON.parse(res.body) as Record<string, unknown>;
|
|
757
|
+
return { body, graph: body["@graph"] as Array<Record<string, unknown>> };
|
|
758
|
+
}
|
|
759
|
+
const byType = (graph: Array<Record<string, unknown>>, type: string) =>
|
|
760
|
+
graph.find((n) => n["@type"] === type)!;
|
|
761
|
+
|
|
762
|
+
it("composes WebPage + Organization + WebSite from _site.mdx, keeping every pre-#6597 field", async () => {
|
|
763
|
+
writeSiteMdx(fixture.getContentRoot());
|
|
764
|
+
const { body, graph } = await homeBody();
|
|
765
|
+
expect(body["@context"]).toBe("https://schema.org");
|
|
766
|
+
expect(graph.map((n) => n["@type"])).toEqual(["WebPage", "Organization", "WebSite"]);
|
|
767
|
+
|
|
768
|
+
const org = byType(graph, "Organization");
|
|
769
|
+
expect(org["@id"]).toBe("https://example.com/#organization");
|
|
770
|
+
expect(org["name"]).toBe("Fixture Co");
|
|
771
|
+
// The pre-#6597 Organization took `logo` from the page image; kept as the
|
|
772
|
+
// fallback when the identity declares none.
|
|
773
|
+
expect(org["logo"]).toBe("https://example.com/home.png");
|
|
774
|
+
|
|
775
|
+
// Every key of every node of the pre-#6597 pinned body survives.
|
|
776
|
+
const pinned: Record<string, string[]> = {
|
|
777
|
+
WebPage: ["@context", "@type", "inLanguage", "name", "description"],
|
|
778
|
+
Organization: ["@context", "@type", "name"],
|
|
779
|
+
WebSite: ["@context", "@type", "name", "inLanguage"],
|
|
780
|
+
};
|
|
781
|
+
for (const [type, keys] of Object.entries(pinned)) {
|
|
782
|
+
for (const key of keys) expect(byType(graph, type)).toHaveProperty([key]);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const webPage = byType(graph, "WebPage");
|
|
786
|
+
expect(byType(graph, "WebSite")["inLanguage"]).toBe(webPage["inLanguage"]);
|
|
787
|
+
expect(webPage["inLanguage"]).toBe("en");
|
|
788
|
+
expect(graph.filter((n) => n["name"] === "HomePage home title")).toEqual([webPage]);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it("the WebSite and WebPage inLanguage stay one value under ?locale=<second locale>", async () => {
|
|
792
|
+
writeSiteMdx(fixture.getContentRoot());
|
|
793
|
+
const { graph } = await homeBody("?locale=pt-BR");
|
|
794
|
+
expect(byType(graph, "WebSite")["inLanguage"]).toBe(byType(graph, "WebPage")["inLanguage"]);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it("an identity-declared logo wins over the page image", async () => {
|
|
798
|
+
writeSiteMdx(fixture.getContentRoot(), { logo: "https://example.com/logo.png" });
|
|
799
|
+
const { graph } = await homeBody();
|
|
800
|
+
expect(byType(graph, "Organization")["logo"]).toBe("https://example.com/logo.png");
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it("with no _site.mdx the body still has @context and a one-node @graph", async () => {
|
|
804
|
+
const { body, graph } = await homeBody();
|
|
805
|
+
expect(body["@context"]).toBe("https://schema.org");
|
|
806
|
+
expect(graph.map((n) => n["@type"])).toEqual(["WebPage"]);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it("other types keep their bare JSON-LD — no @graph key", async () => {
|
|
810
|
+
writeSiteMdx(fixture.getContentRoot());
|
|
811
|
+
const res = await app.inject({ method: "GET", url: "/v1/content/AboutPage/about" });
|
|
812
|
+
expect(res.statusCode).toBe(200);
|
|
813
|
+
const body = JSON.parse(res.body) as Record<string, unknown>;
|
|
814
|
+
expect(body).not.toHaveProperty("@graph");
|
|
815
|
+
expect(body["@type"]).toBe("AboutPage");
|
|
816
|
+
});
|
|
817
|
+
});
|
|
@@ -23,7 +23,15 @@
|
|
|
23
23
|
* independent `getPageSource` call. Two independent adapter reads can
|
|
24
24
|
* straddle a publish that lands between them (frontmatter from revision N,
|
|
25
25
|
* body from revision N+1); one read cannot.
|
|
26
|
+
*
|
|
27
|
+
* #6597: a `HomePage` body keeps its `@context` + `@graph` envelope, now
|
|
28
|
+
* composed from the deployment's `_site.mdx` identity (identity node, optional
|
|
29
|
+
* secondary, WebSite) rather than the page title. Every other type's body is
|
|
30
|
+
* the page's own JSON-LD, spread at the root, as before.
|
|
26
31
|
*/
|
|
32
|
+
import { readFileSync } from "node:fs";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
|
|
27
35
|
import {
|
|
28
36
|
SCHEMA_ORG_TYPES,
|
|
29
37
|
type ContentAdapter,
|
|
@@ -32,10 +40,23 @@ import {
|
|
|
32
40
|
type MdxPageSource,
|
|
33
41
|
type SchemaOrgType,
|
|
34
42
|
} from "@working-theory/content-adapters";
|
|
35
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
buildJsonLd,
|
|
45
|
+
composeGraph,
|
|
46
|
+
graphNodes,
|
|
47
|
+
type ContentPageInput,
|
|
48
|
+
type JsonLdObject,
|
|
49
|
+
type SitewideIdentityInput,
|
|
50
|
+
} from "@working-theory/schema";
|
|
51
|
+
import {
|
|
52
|
+
SiteConfigSchema,
|
|
53
|
+
type SiteConfig,
|
|
54
|
+
} from "@working-theory/site-config/schema";
|
|
36
55
|
import type { FastifyPluginAsync } from "fastify";
|
|
56
|
+
import { parse as parseYaml } from "yaml";
|
|
37
57
|
import { z } from "zod";
|
|
38
58
|
|
|
59
|
+
import { getContentRoot } from "../../lib/content-adapter.js";
|
|
39
60
|
import { resolveLocale } from "../../lib/locales.js";
|
|
40
61
|
import { resolvePublicContentAdapter } from "../../services/public-site-resolver.js";
|
|
41
62
|
|
|
@@ -64,6 +85,67 @@ function hasPageSource(
|
|
|
64
85
|
);
|
|
65
86
|
}
|
|
66
87
|
|
|
88
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
89
|
+
|
|
90
|
+
/** `_site.mdx` per content root, read once; `null` when it does not load. */
|
|
91
|
+
const siteConfigByRoot = new Map<string, SiteConfig | null>();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The deployment's `_site.mdx`, validated against `SiteConfigSchema`, or
|
|
95
|
+
* `undefined` on any failure (absent, unreadable, invalid). A local read with
|
|
96
|
+
* the client-safe `/schema` entry: `@working-theory/site-config/server` imports
|
|
97
|
+
* `server-only`, which throws outside a React Server Components runtime.
|
|
98
|
+
*/
|
|
99
|
+
function readSiteConfig(contentRoot: string): SiteConfig | undefined {
|
|
100
|
+
if (!siteConfigByRoot.has(contentRoot)) {
|
|
101
|
+
let config: SiteConfig | null = null;
|
|
102
|
+
try {
|
|
103
|
+
const raw = readFileSync(join(contentRoot, "_site.mdx"), "utf8");
|
|
104
|
+
const match = FRONTMATTER_RE.exec(raw);
|
|
105
|
+
if (match) {
|
|
106
|
+
const parsed = SiteConfigSchema.safeParse(parseYaml(match[1] ?? ""));
|
|
107
|
+
if (parsed.success) config = parsed.data;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Absent or unreadable — no site.
|
|
111
|
+
}
|
|
112
|
+
siteConfigByRoot.set(contentRoot, config);
|
|
113
|
+
}
|
|
114
|
+
return siteConfigByRoot.get(contentRoot) ?? undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The `HomePage` body's `@graph` (#6597, spec D11): the spine plus the site's
|
|
119
|
+
* sitewide identity nodes. The WebSite's `inLanguage` is the spine's own, so
|
|
120
|
+
* both carry one value; the Organization falls back to the page's `image` as
|
|
121
|
+
* its `logo` when the identity declares none, as the title-derived node did.
|
|
122
|
+
*/
|
|
123
|
+
function homePageGraph(
|
|
124
|
+
primary: JsonLdObject,
|
|
125
|
+
image: string | undefined,
|
|
126
|
+
): { "@context": "https://schema.org"; "@graph": JsonLdObject[] } {
|
|
127
|
+
const config = readSiteConfig(getContentRoot());
|
|
128
|
+
let site: SitewideIdentityInput | undefined;
|
|
129
|
+
if (config !== undefined) {
|
|
130
|
+
const identity = config.siteIdentity;
|
|
131
|
+
site = {
|
|
132
|
+
origin: config.baseUrl.replace(/\/+$/, ""),
|
|
133
|
+
siteName: config.siteName,
|
|
134
|
+
baseUrl: config.baseUrl,
|
|
135
|
+
inLanguage:
|
|
136
|
+
typeof primary["inLanguage"] === "string" ? primary["inLanguage"] : undefined,
|
|
137
|
+
identity:
|
|
138
|
+
identity.kind !== "Person" && identity.logo === undefined && image !== undefined
|
|
139
|
+
? { ...identity, logo: image }
|
|
140
|
+
: identity,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
"@context": "https://schema.org",
|
|
145
|
+
"@graph": graphNodes(composeGraph({ primary, site, isHomePage: true })),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
67
149
|
export const getPageRoute: FastifyPluginAsync = async (fastify) => {
|
|
68
150
|
fastify.get<{
|
|
69
151
|
Params: { type: string; slug: string };
|
|
@@ -165,7 +247,9 @@ export const getPageRoute: FastifyPluginAsync = async (fastify) => {
|
|
|
165
247
|
// now preserves `sections`; before, it was stripped at validation.
|
|
166
248
|
...(page.sections !== undefined && { sections: page.sections }),
|
|
167
249
|
};
|
|
168
|
-
const
|
|
250
|
+
const primary = buildJsonLd(input);
|
|
251
|
+
const jsonLd =
|
|
252
|
+
page.type === "HomePage" ? homePageGraph(primary, page.image) : primary;
|
|
169
253
|
return reply.send({
|
|
170
254
|
...jsonLd,
|
|
171
255
|
page,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file capture.test.ts
|
|
3
|
+
* `GET /v1/leads/capture` (#6730 D4) — the four D1 environments, and the
|
|
4
|
+
* body's exact shape: two booleans, never the mode or an env name.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FastifyInstance } from "fastify";
|
|
8
|
+
|
|
9
|
+
// The route module also carries the write routes; their dependencies are
|
|
10
|
+
// external boundaries this read never reaches.
|
|
11
|
+
// eslint-disable-next-line no-restricted-syntax -- prisma is a DB boundary; __mocks__/prisma.ts is the repo-standard double
|
|
12
|
+
jest.mock("../../../lib/prisma.js");
|
|
13
|
+
jest.mock("@working-theory/email", () => ({
|
|
14
|
+
sendLeadNotification: jest.fn(),
|
|
15
|
+
sendRequestConfirmEmail: jest.fn(),
|
|
16
|
+
sendRequestPaidLinkEmail: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import { leadCaptureStatusRoute } from "../index";
|
|
20
|
+
|
|
21
|
+
const ENV_KEYS = [
|
|
22
|
+
"DATABASE_URL",
|
|
23
|
+
"NODE_ENV",
|
|
24
|
+
"RESEND_API_KEY",
|
|
25
|
+
"AILK_LEAD_NOTIFICATION_TO",
|
|
26
|
+
"AILK_LEAD_NOTIFICATION_FROM",
|
|
27
|
+
"LEAD_CAPTURE_FILE_DISABLED",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
const EMAIL_ENV = {
|
|
31
|
+
RESEND_API_KEY: "re_test",
|
|
32
|
+
AILK_LEAD_NOTIFICATION_TO: "owner@example.com",
|
|
33
|
+
AILK_LEAD_NOTIFICATION_FROM: "noreply@example.com",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe("GET /v1/leads/capture", () => {
|
|
37
|
+
let app: FastifyInstance;
|
|
38
|
+
let saved: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>>;
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
saved = {};
|
|
42
|
+
for (const k of ENV_KEYS) saved[k] = process.env[k];
|
|
43
|
+
for (const k of ENV_KEYS) delete process.env[k];
|
|
44
|
+
const fastify = require("fastify")();
|
|
45
|
+
await fastify.register(leadCaptureStatusRoute);
|
|
46
|
+
app = fastify;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
await app.close();
|
|
51
|
+
for (const k of ENV_KEYS) {
|
|
52
|
+
if (saved[k] === undefined) delete process.env[k];
|
|
53
|
+
else process.env[k] = saved[k];
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const get = () => app.inject({ method: "GET", url: "/v1/leads/capture" });
|
|
58
|
+
|
|
59
|
+
it.each([
|
|
60
|
+
["inbox", { DATABASE_URL: "postgres://x", NODE_ENV: "production" }, true, true],
|
|
61
|
+
["email", { ...EMAIL_ENV, NODE_ENV: "production" }, true, false],
|
|
62
|
+
["local", { NODE_ENV: "development" }, true, true],
|
|
63
|
+
["none", { NODE_ENV: "production" }, false, false],
|
|
64
|
+
] as const)("%s → contact=%s newsletter=%s", async (_mode, env, contact, newsletter) => {
|
|
65
|
+
Object.assign(process.env, env);
|
|
66
|
+
|
|
67
|
+
const res = await get();
|
|
68
|
+
|
|
69
|
+
expect(res.statusCode).toBe(200);
|
|
70
|
+
expect(res.json()).toEqual({ ok: true, data: { contact, newsletter } });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("the body carries exactly ok + data{contact,newsletter} — no mode, no env names", async () => {
|
|
74
|
+
Object.assign(process.env, EMAIL_ENV, { NODE_ENV: "production" });
|
|
75
|
+
|
|
76
|
+
const res = await get();
|
|
77
|
+
const body = res.json();
|
|
78
|
+
|
|
79
|
+
expect(Object.keys(body).sort()).toEqual(["data", "ok"]);
|
|
80
|
+
expect(Object.keys(body.data).sort()).toEqual(["contact", "newsletter"]);
|
|
81
|
+
expect(res.body).not.toMatch(/email|inbox|RESEND|DATABASE|owner@example\.com/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("is cacheable for a minute", async () => {
|
|
85
|
+
const res = await get();
|
|
86
|
+
expect(res.headers["cache-control"]).toBe("public, max-age=60");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -1236,3 +1236,103 @@ describe("#6335 — the shared prisma mock declares no workspace default", () =>
|
|
|
1236
1236
|
});
|
|
1237
1237
|
});
|
|
1238
1238
|
});
|
|
1239
|
+
|
|
1240
|
+
// ─── #6730 D2/D5 — email delivery and the production refusal, over HTTP ─────
|
|
1241
|
+
|
|
1242
|
+
describe("POST /v1/leads/submit — #6730 delivery modes", () => {
|
|
1243
|
+
let app: FastifyInstance;
|
|
1244
|
+
const ENV_KEYS = [
|
|
1245
|
+
"DATABASE_URL",
|
|
1246
|
+
"NODE_ENV",
|
|
1247
|
+
"RESEND_API_KEY",
|
|
1248
|
+
"AILK_LEAD_NOTIFICATION_TO",
|
|
1249
|
+
"AILK_LEAD_NOTIFICATION_FROM",
|
|
1250
|
+
"LEAD_CAPTURE_FILE",
|
|
1251
|
+
"AILK_INBOX_FALLBACK_DIR",
|
|
1252
|
+
] as const;
|
|
1253
|
+
let saved: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>>;
|
|
1254
|
+
const sendLeadNotification = jest.requireMock("@working-theory/email")
|
|
1255
|
+
.sendLeadNotification as jest.Mock;
|
|
1256
|
+
const writeLeadFallbackSpy = jest.spyOn(
|
|
1257
|
+
jest.requireActual("../../../lib/file-fallback.js"),
|
|
1258
|
+
"writeLeadFallback",
|
|
1259
|
+
);
|
|
1260
|
+
|
|
1261
|
+
const submit = () =>
|
|
1262
|
+
app.inject({
|
|
1263
|
+
method: "POST",
|
|
1264
|
+
url: "/v1/leads/submit",
|
|
1265
|
+
payload: {
|
|
1266
|
+
kind: "submit",
|
|
1267
|
+
source: "contact",
|
|
1268
|
+
email: "visitor@example.com",
|
|
1269
|
+
payload: { message: "hello" },
|
|
1270
|
+
},
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
beforeEach(async () => {
|
|
1274
|
+
jest.clearAllMocks();
|
|
1275
|
+
saved = {};
|
|
1276
|
+
for (const k of ENV_KEYS) saved[k] = process.env[k];
|
|
1277
|
+
for (const k of ENV_KEYS) delete process.env[k];
|
|
1278
|
+
process.env.NODE_ENV = "production";
|
|
1279
|
+
const fastify = require("fastify")();
|
|
1280
|
+
await fastify.register(leadSubmitRoute);
|
|
1281
|
+
app = fastify;
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
afterEach(async () => {
|
|
1285
|
+
await app.close();
|
|
1286
|
+
for (const k of ENV_KEYS) {
|
|
1287
|
+
if (saved[k] === undefined) delete process.env[k];
|
|
1288
|
+
else process.env[k] = saved[k];
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
describe("email delivery (no DATABASE_URL, the three email vars)", () => {
|
|
1293
|
+
beforeEach(() => {
|
|
1294
|
+
process.env.RESEND_API_KEY = "re_test";
|
|
1295
|
+
process.env.AILK_LEAD_NOTIFICATION_TO = "owner@example.com";
|
|
1296
|
+
process.env.AILK_LEAD_NOTIFICATION_FROM = "noreply@example.com";
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
it("an accepted email → 201 persisted:email; no Prisma write", async () => {
|
|
1300
|
+
sendLeadNotification.mockResolvedValueOnce({ ok: true, messageId: "msg-e" });
|
|
1301
|
+
|
|
1302
|
+
const res = await submit();
|
|
1303
|
+
|
|
1304
|
+
expect(res.statusCode).toBe(201);
|
|
1305
|
+
expect(res.json()).toMatchObject({ persisted: "email" });
|
|
1306
|
+
expect(res.json().id).toMatch(/^email-/);
|
|
1307
|
+
expect(prisma.lead.create).not.toHaveBeenCalled();
|
|
1308
|
+
expect(sendLeadNotification).toHaveBeenCalledTimes(1);
|
|
1309
|
+
expect(sendLeadNotification.mock.calls[0][0].recipientEmail).toBe("owner@example.com");
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
it("a refused email → 503 delivery_failed, never 201", async () => {
|
|
1313
|
+
sendLeadNotification.mockResolvedValueOnce({
|
|
1314
|
+
ok: false,
|
|
1315
|
+
error: { kind: "provider_error", message: "rejected" },
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
const res = await submit();
|
|
1319
|
+
|
|
1320
|
+
expect(res.statusCode).toBe(503);
|
|
1321
|
+
expect(res.json()).toEqual({
|
|
1322
|
+
error: "delivery_failed",
|
|
1323
|
+
message: "The message could not be delivered.",
|
|
1324
|
+
});
|
|
1325
|
+
});
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
it("production with neither → 503 lead_capture_not_configured; no Prisma, no file, no email", async () => {
|
|
1329
|
+
const res = await submit();
|
|
1330
|
+
|
|
1331
|
+
expect(res.statusCode).toBe(503);
|
|
1332
|
+
expect(res.json().error).toBe("lead_capture_not_configured");
|
|
1333
|
+
expect(prisma.lead.create).not.toHaveBeenCalled();
|
|
1334
|
+
expect(writeLeadFallbackSpy).not.toHaveBeenCalled();
|
|
1335
|
+
expect(sendLeadNotification).not.toHaveBeenCalled();
|
|
1336
|
+
await expect(fs.access(fileEnv.getCaptureFilePath())).rejects.toThrow();
|
|
1337
|
+
});
|
|
1338
|
+
});
|