create-ailk 0.7.0 → 0.9.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 +112 -10
- package/component-catalog.json +798 -49
- package/component-catalog.md +9 -6
- package/dist/add-module.d.ts +20 -0
- package/dist/add-module.js +43 -7
- package/dist/add-route.d.ts +56 -0
- package/dist/add-route.js +286 -0
- package/dist/apply-design-md.d.ts +7 -0
- package/dist/apply-design-md.js +50 -0
- package/dist/apply-design.d.ts +64 -0
- package/dist/apply-design.js +273 -0
- package/dist/claude-md.d.ts +111 -0
- package/dist/claude-md.js +334 -0
- package/dist/cli.js +114 -28
- package/dist/derive-architecture.js +41 -36
- package/dist/design-compile.d.ts +71 -0
- package/dist/design-compile.js +98 -0
- package/dist/design-package.d.ts +83 -0
- package/dist/design-package.js +468 -0
- package/dist/design-presets.d.ts +86 -0
- package/dist/design-presets.js +642 -0
- package/dist/design-scaffold.d.ts +76 -0
- package/dist/design-scaffold.js +220 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/legacy-nav-renderers.d.ts +45 -0
- package/dist/legacy-nav-renderers.js +545 -0
- package/dist/lib/auth-secret-file.d.ts +19 -0
- package/dist/lib/auth-secret-file.js +112 -0
- package/dist/lib/design-checks.d.ts +78 -0
- package/dist/lib/design-checks.js +174 -0
- package/dist/lib/extract-module-bundle.js +5 -1
- package/dist/lib/license-key-files.d.ts +9 -0
- package/dist/lib/license-key-files.js +2 -2
- package/dist/lib/module-license-gate.d.ts +89 -0
- package/dist/lib/module-license-gate.js +171 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.js +10 -2
- package/dist/lib/render-verdict.d.ts +52 -0
- package/dist/lib/render-verdict.js +77 -0
- package/dist/lib/scaffold-render-sweep.d.ts +105 -0
- package/dist/lib/scaffold-render-sweep.js +236 -0
- package/dist/module-architecture.d.ts +19 -16
- package/dist/module-architecture.js +64 -21
- package/dist/parse-args.d.ts +88 -10
- package/dist/parse-args.js +272 -21
- package/dist/programmatic.d.ts +66 -12
- package/dist/programmatic.js +361 -34
- package/dist/site-chrome.d.ts +19 -9
- package/dist/site-chrome.js +23 -13
- package/dist/site-manifest.d.ts +25 -0
- package/dist/site-manifest.js +254 -0
- package/dist/site-roster.d.ts +41 -0
- package/dist/site-roster.js +149 -11
- package/dist/starter-grid.d.ts +232 -0
- package/dist/starter-grid.js +545 -0
- package/dist/surfaces.d.ts +41 -9
- package/dist/surfaces.js +268 -92
- package/dist/template-manifest.d.ts +65 -29
- package/dist/template-manifest.js +161 -382
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +5 -4
- 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 +0 -1
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
- 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/PrivacyPage/privacy.mdx +15 -0
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +70 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +70 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +70 -0
- package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
- package/site-templates/designs/minimalism/DESIGN.md +98 -6
- package/site-templates/designs/minimalism/sample/README.md +48 -0
- package/site-templates/designs/standard/DESIGN.md +103 -6
- package/site-templates/marketing-site/DESIGN.md +103 -6
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +144 -47
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +82 -41
- package/site-templates/marketing-site-free/DESIGN.md +103 -6
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +150 -48
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +114 -67
- package/site-templates/marketing-site-free/ailk-template.json +1 -1
- 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/saas-product/DESIGN.md +103 -6
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +210 -81
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +356 -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/{minimalism-site → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
- 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 +471 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +354 -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/{minimalism-site → shapes/organization-service}/content/en/ServicesList/services.mdx +3 -4
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +478 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +353 -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 +171 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +10 -11
- 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/rules/architecture.md +7 -0
- package/templates/.claude/skills/README.md +5 -3
- package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
- package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
- package/templates/.claude/skills/add-page/SKILL.md +241 -238
- package/templates/.claude/skills/add-page/references/page-types.md +85 -72
- package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
- package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
- package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
- package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
- package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
- package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
- package/templates/.claude/skills/check-site/SKILL.md +23 -0
- package/templates/.claude/skills/fill-page/SKILL.md +26 -7
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
- 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/setup-deploy/SKILL.md +64 -32
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +44 -36
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
- package/templates/.claude/skills/update-page/SKILL.md +117 -0
- package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
- package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
- package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
- package/templates/.github/workflows/ci.yml +4 -0
- package/templates/.prettierignore +1 -0
- package/templates/CLAUDE.md +146 -103
- package/templates/DESIGN.md +103 -6
- package/templates/README.md +63 -43
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/CLAUDE.md +3 -2
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
- package/templates/apps/api/src/__tests__/server.test.ts +116 -0
- package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
- package/templates/apps/api/src/config/index.ts +35 -16
- 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/stripe.ts +9 -0
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
- package/templates/apps/api/src/middleware/auth.ts +54 -0
- package/templates/apps/api/src/openapi/spec.ts +63 -12
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
- 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/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
- package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
- package/templates/apps/api/src/server.ts +35 -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/__tests__/revalidation.test.ts +90 -15
- package/templates/apps/api/src/services/checkout-buyer.ts +72 -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/api/src/services/revalidation.ts +60 -15
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- package/templates/apps/mcp/src/tools/purchase.ts +4 -2
- package/templates/apps/web/.env.example +13 -0
- package/templates/apps/web/CLAUDE.md +22 -28
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
- package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
- 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 +227 -84
- 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 +105 -15
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- 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]/{use-cases → for}/[slug]/page.tsx +21 -5
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
- package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +65 -17
- 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 +106 -14
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/{book-a-call → portfolio}/page.tsx +12 -13
- package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
- package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
- package/templates/apps/web/app/api/revalidate/route.ts +78 -36
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
- package/templates/apps/web/lib/locale-guard.ts +18 -0
- package/templates/apps/web/lib/meet-placeholder.ts +5 -0
- package/templates/apps/web/lib/nav-config.ts +50 -37
- package/templates/apps/web/lib/site-brand.tsx +13 -9
- package/templates/apps/web/lib/site-identity.ts +14 -0
- package/templates/apps/web/lib/site-logo.tsx +36 -0
- package/templates/apps/web/lib/site-theme.ts +46 -26
- package/templates/apps/web/messages/en.json +45 -5
- package/templates/apps/web/middleware.ts +28 -4
- package/templates/content/_site.mdx +4 -2
- package/templates/content/en/BlogList/blog.mdx +37 -16
- package/templates/content/en/BookCall/book-a-call.mdx +80 -0
- package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
- package/templates/content/en/BookCall/onsite-service.mdx +56 -0
- package/templates/content/en/BookCall/start-project.mdx +56 -0
- package/templates/content/en/ContactPage/contact.mdx +43 -34
- package/templates/content/en/HomePage/home.mdx +10 -22
- package/templates/content/en/LandingPage/meet.mdx +59 -0
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +2 -1
- package/templates/tools/check-claude-md.ts +128 -0
- package/versions.json +33 -33
- package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
- package/site-templates/minimalism-site/DESIGN.md +0 -162
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -470
- package/site-templates/minimalism-site/ailk-template.json +0 -23
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
- package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
- package/site-templates/minimalism-site/provenance.json +0 -4
- package/site-templates/simple-service/DESIGN.md +0 -162
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
- package/site-templates/simple-service/ailk-template.json +0 -11
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
- 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-page/templates/route.tsx.tmpl +0 -28
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
- package/templates/content/en/SoftwareProduct/product.mdx +0 -175
- /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/first-engagement.mdx +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/second-engagement.mdx +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/third-engagement.mdx +0 -0
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
// Contact page. Renders sections via renderPage (sections mode)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Contact page (#6730 D8). Renders sections via renderPage (sections mode)
|
|
2
|
+
// and places ONE contact surface, first match wins: into the page's first
|
|
3
|
+
// `contact-form` section; else into a leading split hero's right column;
|
|
4
|
+
// else after the page.
|
|
5
|
+
//
|
|
6
|
+
// That surface is the shared `LeadCaptureForm` block, bound to
|
|
7
|
+
// `captureLeadAction` — unless the API says this deployment accepts no
|
|
8
|
+
// contact message (production with neither a database nor an email key), in
|
|
9
|
+
// which case it is `ContactFallback`: the site's contact email, never a form
|
|
10
|
+
// that would report a message as sent when it reached nobody. An unreachable
|
|
11
|
+
// API, an older API without the status route, or any other failed read is
|
|
12
|
+
// treated as accepting: the form renders, and its submit reports honestly.
|
|
13
|
+
//
|
|
14
|
+
// The chat-style intake (ConversationalIntakeWrapper, #4534) remains
|
|
15
|
+
// available to a site that prefers it. JSON-LD and translations handled by
|
|
16
|
+
// renderPage.
|
|
17
|
+
import { leads } from "@working-theory/api-client";
|
|
6
18
|
import type {
|
|
7
19
|
getMdxSource as defaultGetMdxSource,
|
|
8
20
|
getPageSections as defaultGetPageSections,
|
|
@@ -13,10 +25,21 @@ import {
|
|
|
13
25
|
renderPage,
|
|
14
26
|
renderPageMetadata,
|
|
15
27
|
} from "@working-theory/page-renderer";
|
|
28
|
+
import {
|
|
29
|
+
ContactFallback,
|
|
30
|
+
type ContactFormField,
|
|
31
|
+
LeadCaptureForm,
|
|
32
|
+
localeHref,
|
|
33
|
+
} from "@working-theory/ui";
|
|
16
34
|
import type { Metadata } from "next";
|
|
35
|
+
import { getTranslations } from "next-intl/server";
|
|
17
36
|
import type { ReactElement } from "react";
|
|
18
37
|
|
|
19
|
-
import {
|
|
38
|
+
import { contactFormText } from "./contact-form-text";
|
|
39
|
+
import { captureLeadAction } from "./contact.actions";
|
|
40
|
+
import { readSiteContactEmail } from "./site-contact-email";
|
|
41
|
+
|
|
42
|
+
export const revalidate = 60;
|
|
20
43
|
|
|
21
44
|
interface Props {
|
|
22
45
|
params: Promise<{ locale: string }>;
|
|
@@ -24,6 +47,41 @@ interface Props {
|
|
|
24
47
|
getPageSections?: typeof defaultGetPageSections;
|
|
25
48
|
}
|
|
26
49
|
|
|
50
|
+
type CaptureStatus = Extract<
|
|
51
|
+
Awaited<ReturnType<typeof leads.captureStatus>>,
|
|
52
|
+
{ ok: true }
|
|
53
|
+
>["body"]["data"];
|
|
54
|
+
|
|
55
|
+
type Section = Awaited<ReturnType<typeof defaultGetPageSections>>[number];
|
|
56
|
+
type ContactFormSection = Extract<Section, { type: "contact-form" }>;
|
|
57
|
+
|
|
58
|
+
const FIELD_TYPES = ["text", "email", "tel", "textarea", "select"] as const;
|
|
59
|
+
|
|
60
|
+
/** A `contact-form` section's declared fields, as the form block renders them. */
|
|
61
|
+
function sectionFields(section: ContactFormSection): ContactFormField[] {
|
|
62
|
+
return section.data.fields.map((f) => ({
|
|
63
|
+
name: f.name,
|
|
64
|
+
label: f.label,
|
|
65
|
+
type: (FIELD_TYPES as readonly string[]).includes(f.type)
|
|
66
|
+
? (f.type as (typeof FIELD_TYPES)[number])
|
|
67
|
+
: "text",
|
|
68
|
+
...(f.required !== undefined ? { required: f.required } : {}),
|
|
69
|
+
...(f.placeholder !== undefined ? { placeholder: f.placeholder } : {}),
|
|
70
|
+
...(f.options !== undefined ? { options: f.options } : {}),
|
|
71
|
+
...(f.colSpan !== undefined ? { colSpan: f.colSpan as 1 | 2 } : {}),
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The API's capture status, or `null` for any failed read (see header). */
|
|
76
|
+
async function readCaptureStatus(): Promise<CaptureStatus | null> {
|
|
77
|
+
try {
|
|
78
|
+
const res = await leads.captureStatus();
|
|
79
|
+
return res.ok && res.body.ok === true ? res.body.data : null;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
27
85
|
export async function generateMetadata({
|
|
28
86
|
params,
|
|
29
87
|
}: Pick<Props, "params">): Promise<Metadata> {
|
|
@@ -37,17 +95,49 @@ export default async function Page({
|
|
|
37
95
|
getPageSections,
|
|
38
96
|
}: Props): Promise<ReactElement> {
|
|
39
97
|
const { locale } = await params;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
98
|
+
const [status, t] = await Promise.all([
|
|
99
|
+
readCaptureStatus(),
|
|
100
|
+
getTranslations({ locale, namespace: "contact" }),
|
|
101
|
+
]);
|
|
102
|
+
const accepting = status?.contact ?? true;
|
|
103
|
+
const contactEmail = readSiteContactEmail();
|
|
104
|
+
const text = contactFormText(t);
|
|
105
|
+
|
|
45
106
|
const source = await (getMdxSource ?? loadMdxSource)("ContactPage", "contact", locale);
|
|
46
107
|
const sections = source ? await (getPageSections ?? loadPageSections)(source.frontmatter) : [];
|
|
108
|
+
const formSection = sections.find(
|
|
109
|
+
(s): s is ContactFormSection => s.type === "contact-form",
|
|
110
|
+
);
|
|
111
|
+
// The section's own published address stands in when _site.mdx has none.
|
|
112
|
+
const fallbackEmail = contactEmail ?? formSection?.data.contactInfo?.email;
|
|
47
113
|
const lead = sections[0];
|
|
48
114
|
const slotted =
|
|
49
|
-
|
|
50
|
-
|
|
115
|
+
formSection?.id ??
|
|
116
|
+
(lead?.type === "hero" && lead.data.variant === "split" ? lead.id : undefined);
|
|
117
|
+
|
|
118
|
+
const surface = accepting ? (
|
|
119
|
+
<LeadCaptureForm
|
|
120
|
+
action={captureLeadAction}
|
|
121
|
+
text={text}
|
|
122
|
+
termsHref={localeHref(locale, "/terms")}
|
|
123
|
+
fallbackEmail={fallbackEmail}
|
|
124
|
+
{...(formSection && {
|
|
125
|
+
fields: sectionFields(formSection),
|
|
126
|
+
submitLabel: formSection.data.submitLabel,
|
|
127
|
+
successMessage: formSection.data.successMessage,
|
|
128
|
+
})}
|
|
129
|
+
/>
|
|
130
|
+
) : (
|
|
131
|
+
<ContactFallback
|
|
132
|
+
email={fallbackEmail}
|
|
133
|
+
text={{
|
|
134
|
+
heading: text.unavailableHeading,
|
|
135
|
+
body: text.unavailableBody,
|
|
136
|
+
noEmail: text.unavailableNoEmail,
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
|
|
51
141
|
const rendered = await renderPage({
|
|
52
142
|
type: "ContactPage",
|
|
53
143
|
slug: "contact",
|
|
@@ -55,12 +145,12 @@ export default async function Page({
|
|
|
55
145
|
translationsNamespace: "contact",
|
|
56
146
|
...(getMdxSource !== undefined && { getMdxSource }),
|
|
57
147
|
...(getPageSections !== undefined && { getPageSections }),
|
|
58
|
-
...(slotted !== undefined && { sectionSlots: { [slotted]:
|
|
148
|
+
...(slotted !== undefined && { sectionSlots: { [slotted]: surface } }),
|
|
59
149
|
});
|
|
60
150
|
return (
|
|
61
151
|
<>
|
|
62
152
|
{rendered}
|
|
63
|
-
{slotted === undefined ?
|
|
153
|
+
{slotted === undefined ? surface : null}
|
|
64
154
|
</>
|
|
65
155
|
);
|
|
66
156
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The site's published contact email (#6730 D10) — `_site.mdx`'s top-level
|
|
3
|
+
* `contactEmail`, else a `LocalBusiness` identity's `email`, else `undefined`.
|
|
4
|
+
*
|
|
5
|
+
* `/contact` shows it in place of the form when the site has nowhere to
|
|
6
|
+
* deliver a message, and hands it to the form's `unavailable` state.
|
|
7
|
+
*
|
|
8
|
+
* A local frontmatter read rather than `@working-theory/site-config/server`'s
|
|
9
|
+
* `loadSiteConfig`: `architecture.yaml` forbids `apps/web` from importing that
|
|
10
|
+
* subpath (it pulls `node:fs` into a tree that also carries edge routes,
|
|
11
|
+
* #1567) — the same reason `lib/site-theme.ts` and `lib/site-brand.tsx` read
|
|
12
|
+
* locally. The value is still validated against the canonical schema from
|
|
13
|
+
* the client-safe `/schema` entry. Any failure (no file, no frontmatter, an
|
|
14
|
+
* invalid address) is `undefined`: the fallback then says the site has
|
|
15
|
+
* published no contact details, which is true.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
import { SiteConfigSchema } from "@working-theory/site-config/schema";
|
|
22
|
+
import { parse as parseYaml } from "yaml";
|
|
23
|
+
|
|
24
|
+
import { resolveContentRoot } from "../../../lib/site-brand";
|
|
25
|
+
|
|
26
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
27
|
+
|
|
28
|
+
const EmailSchema = SiteConfigSchema.shape.contactEmail.unwrap();
|
|
29
|
+
|
|
30
|
+
function validEmail(value: unknown): string | undefined {
|
|
31
|
+
const parsed = EmailSchema.safeParse(value);
|
|
32
|
+
return parsed.success ? parsed.data : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readSiteContactEmail(): string | undefined {
|
|
36
|
+
try {
|
|
37
|
+
const raw = readFileSync(join(resolveContentRoot(), "_site.mdx"), "utf8");
|
|
38
|
+
const match = FRONTMATTER_RE.exec(raw);
|
|
39
|
+
if (!match) return undefined;
|
|
40
|
+
const fm: unknown = parseYaml(match[1] ?? "");
|
|
41
|
+
if (!fm || typeof fm !== "object") return undefined;
|
|
42
|
+
|
|
43
|
+
const { contactEmail, siteIdentity } = fm as {
|
|
44
|
+
contactEmail?: unknown;
|
|
45
|
+
siteIdentity?: { kind?: unknown; email?: unknown };
|
|
46
|
+
};
|
|
47
|
+
return (
|
|
48
|
+
validEmail(contactEmail) ??
|
|
49
|
+
(siteIdentity?.kind === "LocalBusiness" ? validEmail(siteIdentity.email) : undefined)
|
|
50
|
+
);
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Feature detail page (#6721) — one feature at /features/<slug>, listed by
|
|
2
|
+
// the /features page. Reads the Feature MDX for the given slug, renders via
|
|
3
|
+
// @working-theory/page-renderer, and emits a JSON-LD WebPage whose mainEntity
|
|
4
|
+
// is the product's SoftwareApplication, named for the feature. Unknown slugs
|
|
5
|
+
// 404. Composed `sections[]` render as sections; a prose page renders its
|
|
6
|
+
// MDX body.
|
|
7
|
+
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
8
|
+
import {
|
|
9
|
+
getMdxSource as loadMdxSource,
|
|
10
|
+
getMdxSlugs,
|
|
11
|
+
renderPage,
|
|
12
|
+
renderPageMetadata,
|
|
13
|
+
} from "@working-theory/page-renderer";
|
|
14
|
+
import type { Metadata } from "next";
|
|
15
|
+
import type { ReactElement } from "react";
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
params: Promise<{ locale: string; slug: string }>;
|
|
19
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function generateMetadata({
|
|
23
|
+
params,
|
|
24
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
25
|
+
const { locale, slug } = await params;
|
|
26
|
+
return renderPageMetadata({ type: "Feature", slug, locale });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function generateStaticParams({
|
|
30
|
+
params,
|
|
31
|
+
}: {
|
|
32
|
+
params: { locale: string };
|
|
33
|
+
}): Promise<{ slug: string }[]> {
|
|
34
|
+
const slugs = await getMdxSlugs("Feature", params.locale);
|
|
35
|
+
return slugs.map((slug) => ({ slug }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default async function Page({
|
|
39
|
+
params,
|
|
40
|
+
getMdxSource,
|
|
41
|
+
}: Props): Promise<ReactElement> {
|
|
42
|
+
const { locale, slug } = await params;
|
|
43
|
+
// A page that composes `sections[]` (the starter seeds, #6721) renders
|
|
44
|
+
// them; a prose page renders its MDX body — the same rule the /features hub
|
|
45
|
+
// applies. JSON-LD is built from the frontmatter either way.
|
|
46
|
+
const source = await (getMdxSource ?? loadMdxSource)("Feature", slug, locale);
|
|
47
|
+
const composed =
|
|
48
|
+
source !== null &&
|
|
49
|
+
Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
|
|
50
|
+
((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) >
|
|
51
|
+
0;
|
|
52
|
+
return renderPage({
|
|
53
|
+
type: "Feature",
|
|
54
|
+
slug,
|
|
55
|
+
locale,
|
|
56
|
+
...(composed
|
|
57
|
+
? {}
|
|
58
|
+
: {
|
|
59
|
+
bodyMode: "mdx-body" as const,
|
|
60
|
+
populatedDescriptionTestId: "feature-description",
|
|
61
|
+
}),
|
|
62
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Features list page. Reads FeaturesList MDX via the content adapter and emits
|
|
2
|
+
// a JSON-LD CollectionPage via the dispatcher. List-only — no detail route.
|
|
3
|
+
import type {
|
|
4
|
+
getMdxSource as defaultGetMdxSource,
|
|
5
|
+
} from "@working-theory/page-renderer";
|
|
6
|
+
import {
|
|
7
|
+
getMdxSource as loadMdxSource,
|
|
8
|
+
renderPage,
|
|
9
|
+
renderPageMetadata,
|
|
10
|
+
} from "@working-theory/page-renderer";
|
|
11
|
+
import type { Metadata } from "next";
|
|
12
|
+
import type { ReactElement } from "react";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
params: Promise<{ locale: string }>;
|
|
16
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function generateMetadata({
|
|
20
|
+
params,
|
|
21
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
22
|
+
const { locale } = await params;
|
|
23
|
+
return renderPageMetadata({
|
|
24
|
+
type: "FeaturesList",
|
|
25
|
+
slug: "features",
|
|
26
|
+
locale,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default async function Page({
|
|
31
|
+
params,
|
|
32
|
+
getMdxSource,
|
|
33
|
+
}: Props): Promise<ReactElement> {
|
|
34
|
+
const { locale } = await params;
|
|
35
|
+
// A FeaturesList page that composes `sections[]` (the Free template's
|
|
36
|
+
// /features, operator ruling 2026-09-23) renders them; a prose-only page
|
|
37
|
+
// keeps the MDX-body render. The CollectionPage JSON-LD is built from the
|
|
38
|
+
// sections either way (buildFeaturesList).
|
|
39
|
+
const source = await (getMdxSource ?? loadMdxSource)("FeaturesList", "features", locale);
|
|
40
|
+
const composed =
|
|
41
|
+
source !== null &&
|
|
42
|
+
Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
|
|
43
|
+
((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) > 0;
|
|
44
|
+
return renderPage({
|
|
45
|
+
type: "FeaturesList",
|
|
46
|
+
slug: "features",
|
|
47
|
+
locale,
|
|
48
|
+
translationsNamespace: "features",
|
|
49
|
+
...(composed ? {} : { bodyMode: "mdx-body" as const }),
|
|
50
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
// UseCase detail page (#6658 D4
|
|
2
|
-
// the /
|
|
1
|
+
// UseCase detail page (#6658 D4; route renamed to /for by #6878 D2) — one
|
|
2
|
+
// customer page at /for/<slug>, listed by the /for hub. Reads the UseCase MDX
|
|
3
|
+
// for the given slug, renders via
|
|
3
4
|
// @working-theory/page-renderer, and emits a JSON-LD WebPage whose mainEntity
|
|
4
|
-
// is an Article. Unknown slugs 404.
|
|
5
|
+
// is an Article. Unknown slugs 404. Composed `sections[]` render as sections
|
|
6
|
+
// (#6721); a prose page renders its MDX body.
|
|
5
7
|
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
6
8
|
import {
|
|
9
|
+
getMdxSource as loadMdxSource,
|
|
7
10
|
getMdxSlugs,
|
|
8
11
|
renderPage,
|
|
9
12
|
renderPageMetadata,
|
|
@@ -37,12 +40,25 @@ export default async function Page({
|
|
|
37
40
|
getMdxSource,
|
|
38
41
|
}: Props): Promise<ReactElement> {
|
|
39
42
|
const { locale, slug } = await params;
|
|
43
|
+
// A page that composes `sections[]` (the starter seeds, #6721) renders
|
|
44
|
+
// them; a prose page renders its MDX body — the same rule the /for hub
|
|
45
|
+
// applies. JSON-LD is built from the frontmatter either way.
|
|
46
|
+
const source = await (getMdxSource ?? loadMdxSource)("UseCase", slug, locale);
|
|
47
|
+
const composed =
|
|
48
|
+
source !== null &&
|
|
49
|
+
Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
|
|
50
|
+
((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) >
|
|
51
|
+
0;
|
|
40
52
|
return renderPage({
|
|
41
53
|
type: "UseCase",
|
|
42
54
|
slug,
|
|
43
55
|
locale,
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
...(composed
|
|
57
|
+
? {}
|
|
58
|
+
: {
|
|
59
|
+
bodyMode: "mdx-body" as const,
|
|
60
|
+
populatedDescriptionTestId: "use-case-description",
|
|
61
|
+
}),
|
|
46
62
|
...(getMdxSource !== undefined && { getMdxSource }),
|
|
47
63
|
});
|
|
48
64
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for for/page.tsx → @working-theory/page-renderer migration
|
|
3
|
+
* (route renamed from use-cases/ by #6878 D2).
|
|
4
|
+
*
|
|
5
|
+
* Two-commit strategy:
|
|
6
|
+
* Commit 1: this file added; snapshots captured against pre-migration route (RED-passing).
|
|
7
|
+
* Commit 2: route migrated; same snapshots verify diff=0 (GREEN).
|
|
8
|
+
*
|
|
9
|
+
* Two states tested:
|
|
10
|
+
* - empty: getMdxSource returns null → empty-state branch
|
|
11
|
+
* - populated: getMdxSource returns a minimal MdxPageSource → full render
|
|
12
|
+
*
|
|
13
|
+
* Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
|
|
14
|
+
*/
|
|
15
|
+
import React from "react";
|
|
16
|
+
import { render } from "@testing-library/react";
|
|
17
|
+
|
|
18
|
+
// ── @working-theory/ui mock ─────────────────────────────────────────────────────────────
|
|
19
|
+
jest.mock("@working-theory/ui", () => ({
|
|
20
|
+
// #4446/#4447: renderPage's own fallback/empty-state title now composes
|
|
21
|
+
// Heading (mobile type-budget classes, #4417) instead of a bare <h1>.
|
|
22
|
+
Heading: ({
|
|
23
|
+
children,
|
|
24
|
+
level,
|
|
25
|
+
}: {
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
level: number;
|
|
28
|
+
}): React.ReactElement => React.createElement(`h${level}`, null, children),
|
|
29
|
+
SectionRenderer: ({ sections }: { sections: unknown[] }) =>
|
|
30
|
+
React.createElement("div", {
|
|
31
|
+
"data-testid": "section-renderer",
|
|
32
|
+
"data-section-count": String(sections.length),
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
import ForPage from "../page";
|
|
37
|
+
|
|
38
|
+
const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
|
|
39
|
+
|
|
40
|
+
describe("ForPage — snapshot regression", () => {
|
|
41
|
+
it("empty state renders deterministic HTML", async () => {
|
|
42
|
+
const element = await ForPage({
|
|
43
|
+
params: Promise.resolve({ locale: "en" }),
|
|
44
|
+
getMdxSource: getMdxSourceEmpty,
|
|
45
|
+
});
|
|
46
|
+
const { container } = render(element);
|
|
47
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
48
|
+
`"<main id="main"><h1>Customers</h1><p data-testid="section-empty-state">Customer pages coming soon.</p></main>"`,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("populated state renders deterministic HTML", async () => {
|
|
53
|
+
const source = {
|
|
54
|
+
frontmatter: {
|
|
55
|
+
type: "UseCasesList" as const,
|
|
56
|
+
slug: "for",
|
|
57
|
+
title: "Use Cases",
|
|
58
|
+
description: "How teams use our platform.",
|
|
59
|
+
},
|
|
60
|
+
body: "",
|
|
61
|
+
};
|
|
62
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
|
|
63
|
+
|
|
64
|
+
const element = await ForPage({
|
|
65
|
+
params: Promise.resolve({ locale: "en" }),
|
|
66
|
+
getMdxSource: getMdxSourcePopulated,
|
|
67
|
+
});
|
|
68
|
+
const { container } = render(element);
|
|
69
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
70
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Use Cases","description":"How teams use our platform.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</script><main id="main"><h1>Use Cases</h1><p data-testid="for-description">How teams use our platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source=""></div></article></div></div></main>"`,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Customers (/for) hub page (#6878 D2 — renamed from /use-cases). Reads
|
|
2
|
+
// UseCasesList MDX via the content adapter and emits a JSON-LD CollectionPage
|
|
3
|
+
// via the dispatcher. List-only — no detail route.
|
|
3
4
|
import type {
|
|
4
5
|
getMdxSource as defaultGetMdxSource,
|
|
5
6
|
} from "@working-theory/page-renderer";
|
|
@@ -22,7 +23,7 @@ export async function generateMetadata({
|
|
|
22
23
|
const { locale } = await params;
|
|
23
24
|
return renderPageMetadata({
|
|
24
25
|
type: "UseCasesList",
|
|
25
|
-
slug: "
|
|
26
|
+
slug: "for",
|
|
26
27
|
locale,
|
|
27
28
|
});
|
|
28
29
|
}
|
|
@@ -33,19 +34,19 @@ export default async function Page({
|
|
|
33
34
|
}: Props): Promise<ReactElement> {
|
|
34
35
|
const { locale } = await params;
|
|
35
36
|
// A UseCasesList page that composes `sections[]` (the Free template's
|
|
36
|
-
// /
|
|
37
|
+
// /for hub, operator ruling 2026-09-23) renders them; a prose-only page
|
|
37
38
|
// keeps the MDX-body render. The CollectionPage JSON-LD is built from the
|
|
38
39
|
// sections either way (buildUseCasesList).
|
|
39
|
-
const source = await (getMdxSource ?? loadMdxSource)("UseCasesList", "
|
|
40
|
+
const source = await (getMdxSource ?? loadMdxSource)("UseCasesList", "for", locale);
|
|
40
41
|
const composed =
|
|
41
42
|
source !== null &&
|
|
42
43
|
Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
|
|
43
44
|
((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) > 0;
|
|
44
45
|
return renderPage({
|
|
45
46
|
type: "UseCasesList",
|
|
46
|
-
slug: "
|
|
47
|
+
slug: "for",
|
|
47
48
|
locale,
|
|
48
|
-
translationsNamespace: "
|
|
49
|
+
translationsNamespace: "for",
|
|
49
50
|
...(composed ? {} : { bodyMode: "mdx-body" as const }),
|
|
50
51
|
...(getMdxSource !== undefined && { getMdxSource }),
|
|
51
52
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// `GlossaryList` MDX with slug `glossary` and emits a JSON-LD CollectionPage
|
|
3
3
|
// whose mainEntity is a DefinedTermSet built from its `glossary-index`
|
|
4
4
|
// sections. A module-less surface a template enables by naming it in its
|
|
5
|
-
// roster; with no content the route 404s. Mirrors
|
|
5
|
+
// roster; with no content the route 404s. Mirrors for/page.tsx: a
|
|
6
6
|
// sections-composed page renders its sections, a prose-only page its MDX body.
|
|
7
7
|
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
8
8
|
import {
|