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.
Files changed (310) hide show
  1. package/README.md +112 -10
  2. package/component-catalog.json +798 -49
  3. package/component-catalog.md +9 -6
  4. package/dist/add-module.d.ts +20 -0
  5. package/dist/add-module.js +43 -7
  6. package/dist/add-route.d.ts +56 -0
  7. package/dist/add-route.js +286 -0
  8. package/dist/apply-design-md.d.ts +7 -0
  9. package/dist/apply-design-md.js +50 -0
  10. package/dist/apply-design.d.ts +64 -0
  11. package/dist/apply-design.js +273 -0
  12. package/dist/claude-md.d.ts +111 -0
  13. package/dist/claude-md.js +334 -0
  14. package/dist/cli.js +114 -28
  15. package/dist/derive-architecture.js +41 -36
  16. package/dist/design-compile.d.ts +71 -0
  17. package/dist/design-compile.js +98 -0
  18. package/dist/design-package.d.ts +83 -0
  19. package/dist/design-package.js +468 -0
  20. package/dist/design-presets.d.ts +86 -0
  21. package/dist/design-presets.js +642 -0
  22. package/dist/design-scaffold.d.ts +76 -0
  23. package/dist/design-scaffold.js +220 -0
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +4 -1
  26. package/dist/legacy-nav-renderers.d.ts +45 -0
  27. package/dist/legacy-nav-renderers.js +545 -0
  28. package/dist/lib/auth-secret-file.d.ts +19 -0
  29. package/dist/lib/auth-secret-file.js +112 -0
  30. package/dist/lib/design-checks.d.ts +78 -0
  31. package/dist/lib/design-checks.js +174 -0
  32. package/dist/lib/extract-module-bundle.js +5 -1
  33. package/dist/lib/license-key-files.d.ts +9 -0
  34. package/dist/lib/license-key-files.js +2 -2
  35. package/dist/lib/module-license-gate.d.ts +89 -0
  36. package/dist/lib/module-license-gate.js +171 -12
  37. package/dist/lib/module-tier.d.ts +45 -16
  38. package/dist/lib/module-tier.js +76 -0
  39. package/dist/lib/pro-callout.js +10 -2
  40. package/dist/lib/render-verdict.d.ts +52 -0
  41. package/dist/lib/render-verdict.js +77 -0
  42. package/dist/lib/scaffold-render-sweep.d.ts +105 -0
  43. package/dist/lib/scaffold-render-sweep.js +236 -0
  44. package/dist/module-architecture.d.ts +19 -16
  45. package/dist/module-architecture.js +64 -21
  46. package/dist/parse-args.d.ts +88 -10
  47. package/dist/parse-args.js +272 -21
  48. package/dist/programmatic.d.ts +66 -12
  49. package/dist/programmatic.js +361 -34
  50. package/dist/site-chrome.d.ts +19 -9
  51. package/dist/site-chrome.js +23 -13
  52. package/dist/site-manifest.d.ts +25 -0
  53. package/dist/site-manifest.js +254 -0
  54. package/dist/site-roster.d.ts +41 -0
  55. package/dist/site-roster.js +149 -11
  56. package/dist/starter-grid.d.ts +232 -0
  57. package/dist/starter-grid.js +545 -0
  58. package/dist/surfaces.d.ts +41 -9
  59. package/dist/surfaces.js +268 -92
  60. package/dist/template-manifest.d.ts +65 -29
  61. package/dist/template-manifest.js +161 -382
  62. package/dist/template-names.d.ts +14 -12
  63. package/dist/template-names.js +13 -17
  64. package/package.json +5 -4
  65. package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
  66. package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
  67. package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
  68. package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
  69. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
  70. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
  71. package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
  72. package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
  73. package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
  74. package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
  75. package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
  76. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
  77. package/site-templates/_base/content/en/UseCase/persona-1.mdx +70 -0
  78. package/site-templates/_base/content/en/UseCase/persona-2.mdx +70 -0
  79. package/site-templates/_base/content/en/UseCase/persona-3.mdx +70 -0
  80. package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
  81. package/site-templates/designs/minimalism/DESIGN.md +98 -6
  82. package/site-templates/designs/minimalism/sample/README.md +48 -0
  83. package/site-templates/designs/standard/DESIGN.md +103 -6
  84. package/site-templates/marketing-site/DESIGN.md +103 -6
  85. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +144 -47
  86. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +82 -41
  87. package/site-templates/marketing-site-free/DESIGN.md +103 -6
  88. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +150 -48
  89. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +114 -67
  90. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  91. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
  92. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
  93. package/site-templates/saas-product/DESIGN.md +103 -6
  94. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
  95. package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +210 -81
  96. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +356 -0
  97. package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
  98. package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
  99. package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
  100. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  101. package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
  102. package/site-templates/{minimalism-site → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
  103. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
  104. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
  105. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
  106. package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
  107. package/site-templates/shapes/local-business-service/provenance.json +4 -0
  108. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +471 -0
  109. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +354 -0
  110. package/site-templates/shapes/organization-service/ailk-template.json +10 -0
  111. package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
  112. package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
  113. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  114. package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
  115. package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  116. package/site-templates/{minimalism-site → shapes/organization-service}/content/en/ServicesList/services.mdx +3 -4
  117. package/site-templates/shapes/organization-service/provenance.json +4 -0
  118. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +478 -0
  119. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +353 -0
  120. package/site-templates/shapes/person-service/ailk-template.json +10 -0
  121. package/site-templates/shapes/person-service/content/_site.mdx +85 -0
  122. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
  123. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
  124. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  125. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +171 -0
  126. package/site-templates/{simple-service → shapes/person-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +10 -11
  127. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
  128. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
  129. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
  130. package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
  131. package/site-templates/shapes/person-service/provenance.json +4 -0
  132. package/templates/.claude/rules/architecture.md +7 -0
  133. package/templates/.claude/skills/README.md +5 -3
  134. package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
  135. package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
  136. package/templates/.claude/skills/add-page/SKILL.md +241 -238
  137. package/templates/.claude/skills/add-page/references/page-types.md +85 -72
  138. package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
  139. package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
  140. package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
  141. package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
  142. package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
  143. package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
  144. package/templates/.claude/skills/check-site/SKILL.md +23 -0
  145. package/templates/.claude/skills/fill-page/SKILL.md +26 -7
  146. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
  147. package/templates/.claude/skills/setup-database/SKILL.md +156 -0
  148. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
  149. package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
  150. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +44 -36
  151. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
  152. package/templates/.claude/skills/update-page/SKILL.md +117 -0
  153. package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
  154. package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
  155. package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
  156. package/templates/.github/workflows/ci.yml +4 -0
  157. package/templates/.prettierignore +1 -0
  158. package/templates/CLAUDE.md +146 -103
  159. package/templates/DESIGN.md +103 -6
  160. package/templates/README.md +63 -43
  161. package/templates/apps/api/.env.example +2 -1
  162. package/templates/apps/api/CLAUDE.md +3 -2
  163. package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
  164. package/templates/apps/api/src/__tests__/server.test.ts +116 -0
  165. package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
  166. package/templates/apps/api/src/config/index.ts +35 -16
  167. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
  168. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
  169. package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
  170. package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
  171. package/templates/apps/api/src/lib/stripe.ts +9 -0
  172. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
  173. package/templates/apps/api/src/middleware/auth.ts +54 -0
  174. package/templates/apps/api/src/openapi/spec.ts +63 -12
  175. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
  176. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
  177. package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
  178. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
  179. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
  180. package/templates/apps/api/src/routes/leads/index.ts +46 -1
  181. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
  182. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
  183. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
  184. package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
  185. package/templates/apps/api/src/server.ts +35 -0
  186. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
  187. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
  188. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
  189. package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
  190. package/templates/apps/api/src/services/lead-routing.ts +140 -30
  191. package/templates/apps/api/src/services/leads-inbox.ts +48 -11
  192. package/templates/apps/api/src/services/revalidation.ts +60 -15
  193. package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
  194. package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
  195. package/templates/apps/mcp/src/tools/create_project.ts +21 -6
  196. package/templates/apps/mcp/src/tools/purchase.ts +4 -2
  197. package/templates/apps/web/.env.example +13 -0
  198. package/templates/apps/web/CLAUDE.md +22 -28
  199. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
  200. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
  201. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
  202. package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
  203. package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
  204. package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
  205. package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
  206. package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
  207. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
  208. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
  209. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
  210. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
  211. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
  212. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
  213. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
  214. package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
  215. package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
  216. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
  217. package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
  218. package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +21 -5
  219. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
  220. package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
  221. package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
  222. package/templates/apps/web/app/[locale]/layout.tsx +65 -17
  223. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
  224. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
  225. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
  226. package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
  227. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
  228. package/templates/apps/web/app/[locale]/{book-a-call → portfolio}/page.tsx +12 -13
  229. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
  230. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
  231. package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
  232. package/templates/apps/web/app/api/revalidate/route.ts +78 -36
  233. package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
  234. package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
  235. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
  236. package/templates/apps/web/lib/locale-guard.ts +18 -0
  237. package/templates/apps/web/lib/meet-placeholder.ts +5 -0
  238. package/templates/apps/web/lib/nav-config.ts +50 -37
  239. package/templates/apps/web/lib/site-brand.tsx +13 -9
  240. package/templates/apps/web/lib/site-identity.ts +14 -0
  241. package/templates/apps/web/lib/site-logo.tsx +36 -0
  242. package/templates/apps/web/lib/site-theme.ts +46 -26
  243. package/templates/apps/web/messages/en.json +45 -5
  244. package/templates/apps/web/middleware.ts +28 -4
  245. package/templates/content/_site.mdx +4 -2
  246. package/templates/content/en/BlogList/blog.mdx +37 -16
  247. package/templates/content/en/BookCall/book-a-call.mdx +80 -0
  248. package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
  249. package/templates/content/en/BookCall/onsite-service.mdx +56 -0
  250. package/templates/content/en/BookCall/start-project.mdx +56 -0
  251. package/templates/content/en/ContactPage/contact.mdx +43 -34
  252. package/templates/content/en/HomePage/home.mdx +10 -22
  253. package/templates/content/en/LandingPage/meet.mdx +59 -0
  254. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  255. package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
  256. package/templates/database/CHANGELOG.md +14 -0
  257. package/templates/database/package.json +1 -1
  258. package/templates/package.json +2 -1
  259. package/templates/tools/check-claude-md.ts +128 -0
  260. package/versions.json +33 -33
  261. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -53
  262. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
  263. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
  264. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
  265. package/site-templates/minimalism-site/DESIGN.md +0 -162
  266. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -470
  267. package/site-templates/minimalism-site/ailk-template.json +0 -23
  268. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
  269. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -53
  270. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
  271. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
  272. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
  273. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
  274. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
  275. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
  276. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
  277. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
  278. package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
  279. package/site-templates/minimalism-site/provenance.json +0 -4
  280. package/site-templates/simple-service/DESIGN.md +0 -162
  281. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
  282. package/site-templates/simple-service/ailk-template.json +0 -11
  283. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
  284. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
  285. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
  286. package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
  287. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
  288. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
  289. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
  290. package/site-templates/simple-service/provenance.json +0 -4
  291. package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
  292. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
  293. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
  294. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
  295. package/templates/content/en/SoftwareProduct/product.mdx +0 -175
  296. /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
  297. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
  298. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
  299. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
  300. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
  301. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
  302. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
  303. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
  304. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
  305. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
  306. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
  307. /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
  308. /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/first-engagement.mdx +0 -0
  309. /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/second-engagement.mdx +0 -0
  310. /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/third-engagement.mdx +0 -0
@@ -27,7 +27,7 @@ import { Telemetry, VerificationMeta } from "@working-theory/analytics";
27
27
  import { MarketingShell } from "@working-theory/templates/shells";
28
28
  import { themeToCssVarsBothModes } from "@working-theory/theme";
29
29
  import { Link, isRtl, localeHref } from "@working-theory/ui";
30
- import { LocaleSwitcherNav } from "@working-theory/ui/blocks";
30
+ import { BRAND_PATHS, LocaleSwitcherNav } from "@working-theory/ui/blocks";
31
31
  import {
32
32
  Banner,
33
33
  Footer,
@@ -50,10 +50,12 @@ import { auth } from "../../auth";
50
50
  import { GoogleAnalytics } from "../../components/google-analytics";
51
51
  import { SignOutTrigger } from "../../components/sign-out-trigger";
52
52
  import { isAuthPath } from "../../lib/auth-routes";
53
+ import { assertEnabledLocale } from "../../lib/locale-guard";
53
54
  import { footerColumns, navItems } from "../../lib/nav-config";
54
55
  import { getOgImageUrl } from "../../lib/og";
55
- import { SITE_NAME, siteLogo } from "../../lib/site-brand";
56
+ import { SITE_NAME } from "../../lib/site-brand";
56
57
  import { siteChrome } from "../../lib/site-chrome";
58
+ import { siteLogo } from "../../lib/site-logo";
57
59
  // Registers the `_site.mdx` identity with page-renderer at module load, so
58
60
  // every page rendered under this layout carries the sitewide JSON-LD (#6597).
59
61
  import "../../lib/site-identity";
@@ -211,6 +213,9 @@ export default async function LocaleLayout({
211
213
  params,
212
214
  }: Props): Promise<ReactElement> {
213
215
  const { locale } = await params;
216
+ // #6107 — a segment that is not an enabled locale is a 404, before any
217
+ // message load or session read.
218
+ assertEnabledLocale(locale);
214
219
  const messages = await getMessages();
215
220
  const themeMode = await resolveThemeMode();
216
221
  const consentDecided = await hasConsentChoice();
@@ -227,6 +232,7 @@ export default async function LocaleLayout({
227
232
  // The Free marketing-site chrome (#5469 D5): the Product flyout's three
228
233
  // entries + descriptions, the top-level items, and the Get started CTA.
229
234
  product: tNav("product"),
235
+ services: tNav("services"),
230
236
  useCases: tNav("useCases"),
231
237
  useCasesDesc: tNav("useCasesDesc"),
232
238
  footerSitePagesHeading: tNav("footerSitePagesHeading"),
@@ -238,6 +244,20 @@ export default async function LocaleLayout({
238
244
  feature4: tNav("feature4"),
239
245
  feature5: tNav("feature5"),
240
246
  allFeatures: tNav("allFeatures"),
247
+ // #6878 D6 — one flyout rule for every shape: the offering column
248
+ // (Features or Services) beside the Customers column (the /for
249
+ // persona pages), plus the Customers footer column.
250
+ flyoutFeaturesHeading: tNav("flyoutFeaturesHeading"),
251
+ flyoutServicesHeading: tNav("flyoutServicesHeading"),
252
+ flyoutCustomersHeading: tNav("flyoutCustomersHeading"),
253
+ footerCustomersHeading: tNav("footerCustomersHeading"),
254
+ allCustomers: tNav("allCustomers"),
255
+ customer1: tNav("customer1"),
256
+ customer2: tNav("customer2"),
257
+ customer3: tNav("customer3"),
258
+ service1: tNav("service1"),
259
+ service2: tNav("service2"),
260
+ service3: tNav("service3"),
241
261
  howItWorks: tNav("howItWorks"),
242
262
  howItWorksDesc: tNav("howItWorksDesc"),
243
263
  features: tNav("features"),
@@ -245,6 +265,7 @@ export default async function LocaleLayout({
245
265
  faq: tNav("faq"),
246
266
  faqDesc: tNav("faqDesc"),
247
267
  pricing: tNav("pricing"),
268
+ portfolio: tNav("portfolio"),
248
269
  about: tNav("about"),
249
270
  // The footer's column groups (#6468): their two headings + Contact.
250
271
  contact: tNav("contact"),
@@ -284,10 +305,13 @@ export default async function LocaleLayout({
284
305
  // enforcement, the callbacks are the hook for @working-theory/consent.
285
306
  const tConsent = await getTranslations({ locale, namespace: "consent" });
286
307
 
287
- // Right-side action cluster (#4984). Signed out → the Free template's pair
288
- // (#5469 D5): a primary "Get started" → /contact beside the outline-strong
289
- // "Sign in" link (the operator's auth-pair idiom, #4370, MINUS Sign up —
290
- // this site offers no sign-up flow). Signed in → the user's name (a
308
+ // Right-side action cluster (#4984). Signed out → the auth pair: an
309
+ // outline-strong "Sign in" → /sign-in beside a primary "Sign up" → /sign-up
310
+ // (the operator's auth-pair idiom, #4370). Every shape ships the (auth)
311
+ // route group, so both routes exist in every scaffold; whether the header
312
+ // shows them is the design's nav setting (#6797 — an earlier revision of
313
+ // this comment said the Free site had no sign-up flow; it renders both and
314
+ // the service shapes now roster both). Signed in → the user's name (a
291
315
  // non-navigating label) + an outline-strong "Sign out"; both authored as
292
316
  // `kind: "flow"` so Nav hands their triggers back through
293
317
  // `renderFlowTrigger` (see NAV_CLIENT_ACTION_FLOW above).
@@ -319,7 +343,7 @@ export default async function LocaleLayout({
319
343
  const hasFlowAction = navActions.some((action) => action.kind === "flow");
320
344
 
321
345
  // Shared brand mark injected into both Nav and Footer chrome slots. The
322
- // mark comes from site-brand.tsx (the mark the /sign-in card also renders);
346
+ // mark comes from site-logo.tsx (the mark the /sign-in card also renders);
323
347
  // both add the shipped wordmark beside it — the Nav composes mark + wordmark
324
348
  // on desktop and mark-only below `md` (#5469 D5), the columns Footer shows
325
349
  // the full lockup at every width (#6507).
@@ -364,11 +388,10 @@ export default async function LocaleLayout({
364
388
  // `(marketing)` group — a ~35-directory move for a five-page delta (spec D1).
365
389
  const isAuthRoute = isAuthPath(pathname, enabledLocales);
366
390
 
367
- // Placeholder social handles + copyright (#5469 D5): `{{COMPANY_HANDLE}}` /
368
- // `{{COMPANY_NAME}}` are create-ailk markers substituted at scaffold time
391
+ // Placeholder social handles (#5469 D5): `{{COMPANY_HANDLE}}` is a
392
+ // create-ailk marker substituted at scaffold time
369
393
  // (packages/create-ailk/src/programmatic.ts), so a customer's site never
370
- // carries the kit vendor's own identity here. The year is computed, never
371
- // typed, so the placeholder does not go stale.
394
+ // carries the kit vendor's own identity here.
372
395
  const socialLinks = [
373
396
  { platform: "facebook", href: "https://facebook.com/{{COMPANY_HANDLE}}" },
374
397
  { platform: "instagram", href: "https://instagram.com/{{COMPANY_HANDLE}}" },
@@ -378,6 +401,9 @@ export default async function LocaleLayout({
378
401
  href: "https://linkedin.com/company/{{COMPANY_HANDLE}}",
379
402
  },
380
403
  { platform: "youtube", href: "https://youtube.com/@{{COMPANY_HANDLE}}" },
404
+ // #6739 — the kit's own repo, operator-directed: the Free template links
405
+ // back to where it came from.
406
+ { platform: "github", href: "https://github.com/working-theory-labs/ai-launch-kit" },
381
407
  ] as const;
382
408
  // Footer column groups (#6468), generated beside `navItems` as label keys;
383
409
  // `Footer` takes translated strings, so resolve them through the same map.
@@ -389,8 +415,9 @@ export default async function LocaleLayout({
389
415
  items: col.items.map((item) => ({ ...item, label: labelFor(item.labelKey) })),
390
416
  }));
391
417
 
392
- // NO_HAND_IMPLEMENT_OK: operator-directed live Free-template pass (2026-09-23).
393
- const copyrightText = `© ${new Date().getFullYear()} SalesSmyth LLC. All rights reserved.`;
418
+ // The site's own name (#6942) — `SITE_NAME` resolves from `_site.mdx`'s
419
+ // `siteName:`, so a customer's footer never carries the kit vendor's name.
420
+ const copyrightText = `© ${new Date().getFullYear()} ${SITE_NAME}. All rights reserved.`;
394
421
 
395
422
  // Footer legal links (#5612 D3). The centered variant folds in FAQ — the
396
423
  // roster's third always-scaffolded core route (`/faq`, alongside `/privacy`
@@ -400,6 +427,10 @@ export default async function LocaleLayout({
400
427
  const legalLinks = [
401
428
  { label: "Privacy Policy", href: "/privacy" },
402
429
  { label: "Terms of Service", href: "/terms" },
430
+ // #6739 — a link, not the reopen button: the privacy page's
431
+ // #cookie-settings anchor explains the choice, and arriving there
432
+ // reopens the consent banner (BannerConsentIsland).
433
+ { label: tConsent("settingsLabel"), href: "/privacy#cookie-settings" },
403
434
  ...(siteChrome.footer === "centered"
404
435
  ? [{ label: navLabels.items.faq, href: "/faq" }]
405
436
  : []),
@@ -515,11 +546,29 @@ export default async function LocaleLayout({
515
546
  variant="contrast"
516
547
  placement="above-nav"
517
548
  // NO_HAND_IMPLEMENT_OK: operator-directed live Free-template pass.
518
- message="Get started with this template."
549
+ message={
550
+ <>
551
+ <a
552
+ href="https://github.com/working-theory-labs/ai-launch-kit"
553
+ className="inline-flex items-center gap-1 font-bold text-current underline underline-offset-2"
554
+ >
555
+ Get started for free
556
+ <svg
557
+ viewBox="0 0 24 24"
558
+ aria-hidden="true"
559
+ className="size-4 fill-current"
560
+ >
561
+ <path d={BRAND_PATHS.github} />
562
+ </svg>
563
+ </a>
564
+ {"\u00A0 "}
565
+ or
566
+ </>
567
+ }
519
568
  messageWeight="regular"
520
569
  action={{
521
- label: "Download for free",
522
- href: "https://ailaunchkit.ai/docs/get-started",
570
+ label: "upgrade to Pro",
571
+ href: "https://ailaunchkit.ai/pricing",
523
572
  kind: "link",
524
573
  }}
525
574
  routes={{ include: [homePath] }}
@@ -592,7 +641,6 @@ export default async function LocaleLayout({
592
641
  bottomBarAlign="content"
593
642
  copyrightText={copyrightText}
594
643
  legalLinks={legalLinks}
595
- consentSettingsLabel={tConsent("settingsLabel")}
596
644
  languageSwitcher={
597
645
  <LocaleSwitcherNav
598
646
  current={locale}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * subscribeNewsletterAction (#6730 D9) — the Server Action twin of the
3
+ * `ailkSubscribeNewsletter` MCP tool.
4
+ */
5
+
6
+ import { subscribeNewsletterAction } from "../newsletter.actions.js";
7
+
8
+ jest.mock("@working-theory/api-client", () => ({
9
+ leads: { subscribe: jest.fn() },
10
+ }));
11
+
12
+ import { leads } from "@working-theory/api-client";
13
+ const mockSubscribe = leads.subscribe as jest.Mock;
14
+
15
+ function formWith(email: string): FormData {
16
+ const fd = new FormData();
17
+ fd.append("email", email);
18
+ fd.append("consent", "true");
19
+ return fd;
20
+ }
21
+
22
+ afterEach(() => jest.clearAllMocks());
23
+
24
+ describe("subscribeNewsletterAction", () => {
25
+ it("calls leads.subscribe with source newsletter and the address", async () => {
26
+ mockSubscribe.mockResolvedValueOnce({ ok: true, body: { id: "subscr_1", persisted: "db" } });
27
+
28
+ const result = await subscribeNewsletterAction(null, formWith("ada@example.com"));
29
+
30
+ expect(result).toEqual({ kind: "success" });
31
+ expect(mockSubscribe).toHaveBeenCalledWith(
32
+ expect.objectContaining({ source: "newsletter", email: "ada@example.com" }),
33
+ );
34
+ });
35
+
36
+ it("an invalid address → a field error, no API call", async () => {
37
+ const result = await subscribeNewsletterAction(null, formWith("not-an-email"));
38
+
39
+ expect(result).toMatchObject({ kind: "error", fieldErrors: { email: expect.any(String) } });
40
+ expect(mockSubscribe).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it("503 lead_capture_not_configured → unavailable", async () => {
44
+ mockSubscribe.mockResolvedValueOnce({
45
+ ok: false,
46
+ status: 503,
47
+ body: { error: "lead_capture_not_configured" },
48
+ });
49
+
50
+ const result = await subscribeNewsletterAction(null, formWith("ada@example.com"));
51
+
52
+ expect(result).toEqual({ kind: "unavailable" });
53
+ });
54
+
55
+ it("any other failure → the generic error, never success", async () => {
56
+ mockSubscribe.mockResolvedValueOnce({ ok: false, status: 503, body: { error: "persist_failed" } });
57
+ expect(await subscribeNewsletterAction(null, formWith("a@b.co"))).toMatchObject({
58
+ kind: "error",
59
+ });
60
+
61
+ mockSubscribe.mockRejectedValueOnce(new Error("ECONNREFUSED"));
62
+ jest.spyOn(console, "error").mockImplementationOnce(() => undefined);
63
+ expect(await subscribeNewsletterAction(null, formWith("a@b.co"))).toMatchObject({
64
+ kind: "error",
65
+ });
66
+ });
67
+
68
+ it("a filled honeypot → success, and nothing is sent", async () => {
69
+ const fd = formWith("ada@example.com");
70
+ fd.append("ailk_hp", "spam");
71
+
72
+ expect(await subscribeNewsletterAction(null, fd)).toEqual({ kind: "success" });
73
+ expect(mockSubscribe).not.toHaveBeenCalled();
74
+ });
75
+ });
@@ -0,0 +1,163 @@
1
+ /**
2
+ * newsletter/page.tsx — a signup only when there is somewhere to keep it
3
+ * (#6730 D9).
4
+ *
5
+ * Boundaries mocked: `@working-theory/ui` (the section tree is a stub that
6
+ * renders one element per section, with a `newsletter` section rendering the
7
+ * form its content declares), `@working-theory/api-client`
8
+ * (`leads.captureStatus`), `next-intl/server`, and the Server Action module.
9
+ */
10
+ import React from "react";
11
+ import { render, screen } from "@testing-library/react";
12
+
13
+ jest.mock("@working-theory/ui", () => ({
14
+ Heading: ({ children, level }: { children: React.ReactNode; level: number }) =>
15
+ React.createElement(`h${level}`, null, children),
16
+ SectionRenderer: ({
17
+ sections,
18
+ }: {
19
+ sections: Array<{
20
+ id: string;
21
+ type: string;
22
+ data: { action?: string; primaryCta?: { href: string } };
23
+ }>;
24
+ }) =>
25
+ React.createElement(
26
+ "div",
27
+ { "data-testid": "section-renderer" },
28
+ sections.map((s) =>
29
+ s.type === "newsletter"
30
+ ? React.createElement(
31
+ "form",
32
+ { key: s.id, "data-section-id": s.id, action: s.data.action },
33
+ "signup",
34
+ )
35
+ : React.createElement(
36
+ "div",
37
+ { key: s.id, "data-section-id": s.id },
38
+ s.data.primaryCta
39
+ ? React.createElement("a", { href: s.data.primaryCta.href }, "cta")
40
+ : null,
41
+ ),
42
+ ),
43
+ ),
44
+ localeHref: (locale: string, path: string) => `/${locale}${path}`,
45
+ LeadCaptureForm: (props: { fields?: Array<{ name: string; type: string }> }) =>
46
+ React.createElement("div", {
47
+ "data-testid": "lead-capture-form",
48
+ "data-fields": (props.fields ?? []).map((f) => `${f.name}:${f.type}`).join(","),
49
+ }),
50
+ }));
51
+
52
+ jest.mock("@working-theory/api-client", () => ({
53
+ leads: { captureStatus: jest.fn() },
54
+ }));
55
+
56
+ jest.mock("next-intl/server", () => ({
57
+ getTranslations: async () => (key: string) => key,
58
+ }));
59
+
60
+ // eslint-disable-next-line no-restricted-syntax -- test-seam mock: the Server Action module is a "use server" boundary; the page only passes it through
61
+ jest.mock("../newsletter.actions", () => ({
62
+ subscribeNewsletterAction: jest.fn(),
63
+ }));
64
+
65
+ import { leads } from "@working-theory/api-client";
66
+
67
+ import NewsletterPage from "../page";
68
+
69
+ const mockCaptureStatus = leads.captureStatus as jest.Mock;
70
+ const status = (newsletter: boolean) => ({
71
+ ok: true,
72
+ status: 200,
73
+ body: { ok: true, data: { contact: true, newsletter } },
74
+ });
75
+
76
+ const source = {
77
+ frontmatter: {
78
+ type: "NewsletterSignup" as const,
79
+ slug: "newsletter",
80
+ title: "Newsletter",
81
+ description: "What subscribers get.",
82
+ },
83
+ body: "",
84
+ };
85
+
86
+ /** `_base`'s content: a hero whose CTA points at `#signup`, the signup section, an FAQ. */
87
+ const BASE_SECTIONS = [
88
+ { id: "hero", type: "hero", data: { headline: "News", primaryCta: { label: "Subscribe", href: "#signup" } } },
89
+ { id: "signup", type: "newsletter", data: { action: "/v1/leads/subscribe", source: "newsletter" } },
90
+ { id: "faq", type: "faq", data: { items: [] } },
91
+ ];
92
+
93
+ /** #6738's Free content: no signup section. */
94
+ const FREE_SECTIONS = [
95
+ { id: "hero", type: "hero", data: { headline: "News" } },
96
+ { id: "faq", type: "faq", data: { items: [] } },
97
+ ];
98
+
99
+ async function renderWith(sections: unknown[]) {
100
+ const element = await NewsletterPage({
101
+ params: Promise.resolve({ locale: "en" }),
102
+ getMdxSource: jest.fn().mockResolvedValue(source),
103
+ getPageSections: jest.fn().mockResolvedValue(sections),
104
+ });
105
+ return render(element);
106
+ }
107
+
108
+ beforeEach(() => jest.clearAllMocks());
109
+
110
+ describe("NewsletterPage — #6730 D9", () => {
111
+ it("newsletter:true + no newsletter section → one appended form with a single email field", async () => {
112
+ mockCaptureStatus.mockResolvedValue(status(true));
113
+
114
+ await renderWith(FREE_SECTIONS);
115
+
116
+ expect(screen.getAllByTestId("lead-capture-form")).toHaveLength(1);
117
+ expect(screen.getByTestId("lead-capture-form")).toHaveAttribute(
118
+ "data-fields",
119
+ "email:email",
120
+ );
121
+ });
122
+
123
+ it("newsletter:false + no newsletter section → no form at all", async () => {
124
+ mockCaptureStatus.mockResolvedValue(status(false));
125
+
126
+ const { container } = await renderWith(FREE_SECTIONS);
127
+
128
+ expect(screen.queryByTestId("lead-capture-form")).toBeNull();
129
+ expect(container.querySelector("form")).toBeNull();
130
+ });
131
+
132
+ it("newsletter:true + _base's newsletter section → that section renders, no second form", async () => {
133
+ mockCaptureStatus.mockResolvedValue(status(true));
134
+
135
+ const { container } = await renderWith(BASE_SECTIONS);
136
+
137
+ expect(container.querySelector("form[action='/v1/leads/subscribe']")).not.toBeNull();
138
+ expect(screen.queryByTestId("lead-capture-form")).toBeNull();
139
+ expect(container.querySelector("a[href='#signup']")).not.toBeNull();
140
+ });
141
+
142
+ it.each([
143
+ ["newsletter:false", () => mockCaptureStatus.mockResolvedValue(status(false))],
144
+ ["the status read rejecting", () => mockCaptureStatus.mockRejectedValue(new Error("ECONNREFUSED"))],
145
+ ["a 404 (older API)", () => mockCaptureStatus.mockResolvedValue({ ok: false, status: 404, body: {} })],
146
+ ])(
147
+ "%s + _base's newsletter section → the section is absent, and so is the hero CTA to it",
148
+ async (_label, arrange) => {
149
+ arrange();
150
+
151
+ const { container } = await renderWith(BASE_SECTIONS);
152
+
153
+ expect(container.querySelector("[data-section-id='signup']")).toBeNull();
154
+ expect(container.querySelector("form")).toBeNull();
155
+ expect(container.innerHTML).not.toContain("/v1/leads/subscribe");
156
+ expect(container.querySelector("a[href='#signup']")).toBeNull();
157
+ expect(screen.queryByTestId("lead-capture-form")).toBeNull();
158
+ // The rest of the page still renders.
159
+ expect(container.querySelector("[data-section-id='hero']")).not.toBeNull();
160
+ expect(container.querySelector("[data-section-id='faq']")).not.toBeNull();
161
+ },
162
+ );
163
+ });
@@ -0,0 +1,66 @@
1
+ "use server";
2
+
3
+ /**
4
+ * subscribeNewsletterAction — Server Action paired with the
5
+ * `ailkSubscribeNewsletter` MCP tool (#6730 D9).
6
+ *
7
+ * `/newsletter` renders the shared `LeadCaptureForm` bound to this action
8
+ * when the API accepts a subscription (a database is set up) and the page's
9
+ * content declares no signup section of its own. Validates the address with
10
+ * the tool's own input schema, calls `POST /v1/leads/subscribe` through
11
+ * `@working-theory/api-client`, and maps the outcome the same way
12
+ * `captureLeadAction` does: the API's 503 `lead_capture_not_configured` →
13
+ * `unavailable`; any other non-2xx or thrown error → the generic `error`;
14
+ * only a 2xx is `success`.
15
+ */
16
+
17
+ import { leads } from "@working-theory/api-client";
18
+ import { LEAD_CAPTURE_HONEYPOT, type LeadCaptureFormState } from "@working-theory/ui";
19
+ import { subscribeNewsletterInputSchema } from "@working-theory/validation";
20
+
21
+ const GENERIC_ERROR = "Something went wrong, please try again.";
22
+
23
+ export async function subscribeNewsletterAction(
24
+ _prevState: LeadCaptureFormState,
25
+ formData: FormData,
26
+ ): Promise<LeadCaptureFormState> {
27
+ // Honeypot: a bot that fills the hidden field gets a person's answer, and
28
+ // nothing is sent.
29
+ const honeypot = formData.get(LEAD_CAPTURE_HONEYPOT);
30
+ if (typeof honeypot === "string" && honeypot !== "") return { kind: "success" };
31
+
32
+ const parsed = subscribeNewsletterInputSchema.safeParse({
33
+ email: formData.get("email"),
34
+ });
35
+ if (!parsed.success) {
36
+ return {
37
+ kind: "error",
38
+ fieldErrors: { email: parsed.error.issues[0]?.message ?? "Invalid email" },
39
+ };
40
+ }
41
+
42
+ const siteId = process.env.AILK_SITE_ID ?? "default";
43
+ try {
44
+ const res = await leads.subscribe({
45
+ siteId,
46
+ source: "newsletter",
47
+ email: parsed.data.email,
48
+ });
49
+ if (!res.ok) {
50
+ const error =
51
+ typeof res.body === "object" && res.body !== null
52
+ ? (res.body as { error?: unknown }).error
53
+ : undefined;
54
+ if (res.status === 503 && error === "lead_capture_not_configured") {
55
+ return { kind: "unavailable" };
56
+ }
57
+ return { kind: "error", message: GENERIC_ERROR };
58
+ }
59
+ return { kind: "success" };
60
+ } catch (err) {
61
+ // The api-client never attaches the request payload to a thrown error,
62
+ // so logging `err` carries no subscriber address.
63
+ console.error("[subscribeNewsletterAction]", err);
64
+ return { kind: "error", message: GENERIC_ERROR };
65
+ }
66
+ }
@@ -1,21 +1,78 @@
1
- // Newsletter page (#6658 D5) — a NewsletterSignup at /newsletter. Its
2
- // `newsletter` section posts to lead capture (`/v1/leads/subscribe`), which
3
- // persists a Subscriber and sends no verification email. A module-less
4
- // surface a template enables by naming it in its roster; with no content the
5
- // route 404s. Mirrors use-cases/page.tsx: a sections-composed page renders its
6
- // sections, a prose-only page its MDX body.
7
- import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
1
+ // Newsletter page (#6658 D5) — a NewsletterSignup at /newsletter. A
2
+ // module-less surface a template enables by naming it in its roster; with no
3
+ // content the route 404s. Mirrors for/page.tsx: a sections-composed
4
+ // page renders its sections, a prose-only page its MDX body.
5
+ //
6
+ // A signup only when there is somewhere to keep it (#6730 D9). The API says
7
+ // whether it accepts a subscription (`GET /v1/leads/capture` — a database is
8
+ // set up):
9
+ //
10
+ // - accepting: a content `newsletter` section renders as authored (its own
11
+ // form posts to `/v1/leads/subscribe`); a page with no such section gets
12
+ // the shared `LeadCaptureForm` (one email field) appended, bound to
13
+ // `subscribeNewsletterAction`.
14
+ // - otherwise — including an unreachable API — every `newsletter` section
15
+ // is dropped before render, and a leading hero's CTA that pointed at a
16
+ // dropped section's anchor goes with it, so no signup form of any kind
17
+ // renders and no button points at a section that is gone. The API
18
+ // refuses a subscribe in that state anyway, so a cached page can never
19
+ // record a false success.
20
+ import { leads } from "@working-theory/api-client";
21
+ import type {
22
+ getMdxSource as defaultGetMdxSource,
23
+ getPageSections as defaultGetPageSections,
24
+ } from "@working-theory/page-renderer";
8
25
  import {
9
26
  getMdxSource as loadMdxSource,
27
+ getPageSections as loadPageSections,
10
28
  renderPage,
11
29
  renderPageMetadata,
12
30
  } from "@working-theory/page-renderer";
31
+ import { LeadCaptureForm, localeHref } from "@working-theory/ui";
13
32
  import type { Metadata } from "next";
33
+ import { getTranslations } from "next-intl/server";
14
34
  import type { ReactElement } from "react";
15
35
 
36
+ import { subscribeNewsletterAction } from "./newsletter.actions";
37
+
38
+ export const revalidate = 60;
39
+
16
40
  interface Props {
17
41
  params: Promise<{ locale: string }>;
18
42
  getMdxSource?: typeof defaultGetMdxSource;
43
+ getPageSections?: typeof defaultGetPageSections;
44
+ }
45
+
46
+ type Section = Awaited<ReturnType<typeof defaultGetPageSections>>[number];
47
+
48
+ /** Whether the API accepts a subscription; `false` for any failed read. */
49
+ async function acceptsSubscription(): Promise<boolean> {
50
+ try {
51
+ const res = await leads.captureStatus();
52
+ return res.ok && res.body.ok === true && res.body.data.newsletter;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Drop every `newsletter` section, and any leading-hero CTA whose `href` is
60
+ * the `#<id>` anchor of a section that was dropped.
61
+ */
62
+ function withoutSignup(sections: Section[]): Section[] {
63
+ const dropped = new Set(
64
+ sections.filter((s) => s.type === "newsletter").map((s) => `#${s.id}`),
65
+ );
66
+ if (dropped.size === 0) return sections;
67
+ return sections
68
+ .filter((s) => s.type !== "newsletter")
69
+ .map((s, i) => {
70
+ if (i !== 0 || s.type !== "hero") return s;
71
+ const data = { ...s.data };
72
+ if (data.primaryCta && dropped.has(data.primaryCta.href)) delete data.primaryCta;
73
+ if (data.secondaryCta && dropped.has(data.secondaryCta.href)) delete data.secondaryCta;
74
+ return { ...s, data };
75
+ });
19
76
  }
20
77
 
21
78
  export async function generateMetadata({
@@ -32,19 +89,54 @@ export async function generateMetadata({
32
89
  export default async function Page({
33
90
  params,
34
91
  getMdxSource,
92
+ getPageSections,
35
93
  }: Props): Promise<ReactElement> {
36
94
  const { locale } = await params;
37
- const source = await (getMdxSource ?? loadMdxSource)("NewsletterSignup", "newsletter", locale);
38
- const composed =
39
- source !== null &&
40
- Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
41
- ((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) >
42
- 0;
43
- return renderPage({
95
+ const load = getMdxSource ?? loadMdxSource;
96
+ const loadSections = getPageSections ?? loadPageSections;
97
+ const [source, accepting] = await Promise.all([
98
+ load("NewsletterSignup", "newsletter", locale),
99
+ acceptsSubscription(),
100
+ ]);
101
+ const declared = source ? await loadSections(source.frontmatter) : [];
102
+ const composed = declared.length > 0;
103
+ const hasSignupSection = declared.some((s) => s.type === "newsletter");
104
+
105
+ const rendered = await renderPage({
44
106
  type: "NewsletterSignup",
45
107
  slug: "newsletter",
46
108
  locale,
47
109
  ...(composed ? {} : { bodyMode: "mdx-body" as const }),
48
110
  ...(getMdxSource !== undefined && { getMdxSource }),
111
+ getPageSections: accepting
112
+ ? loadSections
113
+ : async (frontmatter: unknown) => withoutSignup(await loadSections(frontmatter)),
49
114
  });
115
+
116
+ if (!accepting || hasSignupSection || source === null) return rendered;
117
+
118
+ const [t, tc] = await Promise.all([
119
+ getTranslations({ locale, namespace: "newsletter" }),
120
+ getTranslations({ locale, namespace: "contact" }),
121
+ ]);
122
+ return (
123
+ <>
124
+ {rendered}
125
+ <LeadCaptureForm
126
+ action={subscribeNewsletterAction}
127
+ fields={[{ name: "email", label: t("emailLabel"), type: "email", required: true }]}
128
+ termsHref={localeHref(locale, "/terms")}
129
+ text={{
130
+ submit: t("submit"),
131
+ sending: t("sending"),
132
+ success: t("success"),
133
+ consentStatement: t("consentStatement"),
134
+ genericError: tc("genericError"),
135
+ unavailableHeading: tc("unavailableHeading"),
136
+ unavailableBody: tc("unavailableBody", { email: "{email}" }),
137
+ unavailableNoEmail: tc("unavailableNoEmail"),
138
+ }}
139
+ />
140
+ </>
141
+ );
50
142
  }