create-ailk 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. package/README.md +87 -14
  2. package/component-catalog.json +735 -95
  3. package/component-catalog.md +13 -7
  4. package/dist/add-module-core.d.ts +22 -0
  5. package/dist/add-module-core.js +50 -0
  6. package/dist/add-module.d.ts +89 -0
  7. package/dist/add-module.js +334 -0
  8. package/dist/category-profile.d.ts +126 -0
  9. package/dist/category-profile.js +262 -0
  10. package/dist/cli.js +89 -29
  11. package/dist/index.d.ts +4 -2
  12. package/dist/index.js +6 -1
  13. package/dist/lib/extract-module-bundle.d.ts +56 -1
  14. package/dist/lib/extract-module-bundle.js +170 -14
  15. package/dist/lib/license-key-files.d.ts +15 -0
  16. package/dist/lib/license-key-files.js +102 -0
  17. package/dist/lib/module-license-gate.d.ts +79 -0
  18. package/dist/lib/module-license-gate.js +160 -12
  19. package/dist/lib/module-tier.d.ts +45 -16
  20. package/dist/lib/module-tier.js +76 -0
  21. package/dist/lib/pro-callout.d.ts +16 -10
  22. package/dist/lib/pro-callout.js +34 -3
  23. package/dist/license-set.d.ts +34 -0
  24. package/dist/license-set.js +194 -0
  25. package/dist/module-architecture.d.ts +20 -0
  26. package/dist/module-architecture.js +77 -19
  27. package/dist/modules/commerce/0.6.1.tar.gz +0 -0
  28. package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
  29. package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
  30. package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
  31. package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
  32. package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
  33. package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
  34. package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
  35. package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
  36. package/dist/modules/workspace/0.6.1.tar.gz +0 -0
  37. package/dist/parse-args.d.ts +85 -10
  38. package/dist/parse-args.js +227 -15
  39. package/dist/programmatic.d.ts +81 -5
  40. package/dist/programmatic.js +291 -32
  41. package/dist/site-chrome.d.ts +18 -7
  42. package/dist/site-chrome.js +32 -14
  43. package/dist/site-roster.d.ts +127 -0
  44. package/dist/site-roster.js +499 -0
  45. package/dist/starter-grid.d.ts +216 -0
  46. package/dist/starter-grid.js +449 -0
  47. package/dist/surfaces.d.ts +20 -7
  48. package/dist/surfaces.js +204 -24
  49. package/dist/sync-routes.d.ts +9 -0
  50. package/dist/sync-routes.js +2 -2
  51. package/dist/template-manifest.d.ts +94 -19
  52. package/dist/template-manifest.js +170 -289
  53. package/dist/template-names.d.ts +14 -12
  54. package/dist/template-names.js +13 -17
  55. package/package.json +6 -4
  56. package/site-templates/_base/content/en/BookCall/book-call.mdx +52 -0
  57. package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
  58. package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
  59. package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
  60. package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
  61. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +65 -0
  62. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +67 -0
  63. package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
  64. package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
  65. package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
  66. package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
  67. package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
  68. package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
  69. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +17 -3
  70. package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
  71. package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
  72. package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
  73. package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
  74. package/site-templates/designs/minimalism/DESIGN.md +162 -0
  75. package/site-templates/designs/standard/DESIGN.md +167 -0
  76. package/site-templates/marketing-site/DESIGN.md +59 -40
  77. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +376 -57
  78. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +230 -90
  79. package/site-templates/marketing-site/ailk-template.json +1 -0
  80. package/site-templates/marketing-site-free/DESIGN.md +59 -40
  81. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +323 -63
  82. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +323 -113
  83. package/site-templates/marketing-site-free/ailk-template.json +3 -2
  84. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
  85. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
  86. package/site-templates/minimalism-site/DESIGN.md +54 -40
  87. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
  88. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +173 -51
  89. package/site-templates/minimalism-site/ailk-template.json +3 -2
  90. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +52 -0
  91. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
  92. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  93. package/site-templates/saas-product/DESIGN.md +59 -40
  94. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
  95. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +369 -0
  96. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -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/shapes/local-business-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  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 +362 -0
  109. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -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/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
  117. package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
  118. package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
  119. package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
  120. package/site-templates/shapes/organization-service/provenance.json +4 -0
  121. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
  122. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
  123. package/site-templates/shapes/person-service/ailk-template.json +10 -0
  124. package/site-templates/shapes/person-service/content/_site.mdx +85 -0
  125. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
  126. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
  127. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  128. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
  129. package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  130. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
  131. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
  132. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
  133. package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
  134. package/site-templates/shapes/person-service/provenance.json +4 -0
  135. package/templates/.claude/settings.json +15 -0
  136. package/templates/.claude/skills/README.md +10 -9
  137. package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
  138. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
  139. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
  140. package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
  141. package/templates/.claude/skills/add-page/references/page-types.md +115 -0
  142. package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
  143. package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
  144. package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
  145. package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
  146. package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
  147. package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
  148. package/templates/.claude/skills/fill-page/SKILL.md +180 -0
  149. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
  150. package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
  151. package/templates/.claude/skills/setup-database/SKILL.md +156 -0
  152. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
  153. package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +73 -41
  154. package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +21 -30
  155. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +204 -0
  156. package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
  157. package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
  158. package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
  159. package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
  160. package/templates/.github/workflows/ci.yml +5 -0
  161. package/templates/CLAUDE.md +8 -2
  162. package/templates/DESIGN.md +56 -44
  163. package/templates/README.md +116 -46
  164. package/templates/apps/api/.env.example +2 -1
  165. package/templates/apps/api/CLAUDE.md +3 -2
  166. package/templates/apps/api/package.json +1 -1
  167. package/templates/apps/api/src/config/__tests__/index.test.ts +146 -10
  168. package/templates/apps/api/src/config/index.ts +46 -14
  169. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
  170. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
  171. package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
  172. package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
  173. package/templates/apps/api/src/lib/license-key-store.ts +45 -0
  174. package/templates/apps/api/src/lib/site.ts +5 -3
  175. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
  176. package/templates/apps/api/src/middleware/auth.ts +9 -0
  177. package/templates/apps/api/src/openapi/spec.ts +63 -12
  178. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
  179. package/templates/apps/api/src/routes/content/get-page.ts +86 -2
  180. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
  181. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
  182. package/templates/apps/api/src/routes/leads/index.ts +46 -1
  183. package/templates/apps/api/src/server.ts +13 -0
  184. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
  185. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
  186. package/templates/apps/api/src/services/lead-routing.ts +140 -30
  187. package/templates/apps/api/src/services/leads-inbox.ts +48 -11
  188. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
  189. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
  190. package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
  191. package/templates/apps/mcp/package.json +2 -1
  192. package/templates/apps/mcp/src/index.ts +7 -0
  193. package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
  194. package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
  195. package/templates/apps/mcp/src/tools/create_project.ts +21 -6
  196. package/templates/apps/web/__tests__/setup.ts +18 -0
  197. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
  198. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
  199. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
  200. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
  201. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
  202. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
  203. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
  204. package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
  205. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
  206. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  207. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  208. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
  209. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +67 -29
  210. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +94 -11
  211. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
  212. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
  213. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +228 -86
  214. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
  215. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
  216. package/templates/apps/web/app/[locale]/contact/page.tsx +115 -7
  217. package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
  218. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  219. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
  220. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
  221. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
  222. package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
  223. package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
  224. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  225. package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
  226. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  227. package/templates/apps/web/app/[locale]/layout.tsx +146 -52
  228. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
  229. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
  230. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
  231. package/templates/apps/web/app/[locale]/newsletter/page.tsx +142 -0
  232. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  233. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
  234. package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
  235. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  236. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
  237. package/templates/apps/web/app/[locale]/{cancellation → purchase-policy}/page.tsx +15 -10
  238. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
  239. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  240. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
  241. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  242. package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
  243. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  244. package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
  245. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
  246. package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +63 -0
  247. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
  248. package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
  249. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  250. package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
  251. package/templates/apps/web/app/error-fallback.tsx +17 -45
  252. package/templates/apps/web/app/not-found.tsx +14 -38
  253. package/templates/apps/web/app/og/route.tsx +6 -0
  254. package/templates/apps/web/eslint.config.mjs +3 -0
  255. package/templates/apps/web/instrumentation.ts +14 -0
  256. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
  257. package/templates/apps/web/lib/license-key-store.ts +52 -0
  258. package/templates/apps/web/lib/nav-config.ts +82 -18
  259. package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
  260. package/templates/apps/web/lib/site-brand.tsx +1 -1
  261. package/templates/apps/web/lib/site-chrome.ts +3 -3
  262. package/templates/apps/web/lib/site-identity.ts +148 -0
  263. package/templates/apps/web/lib/site-theme.ts +11 -0
  264. package/templates/apps/web/messages/en.json +78 -37
  265. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  266. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  267. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  268. package/templates/apps/web/public/favicon-16x16.png +0 -0
  269. package/templates/apps/web/public/favicon-32x32.png +0 -0
  270. package/templates/apps/web/public/favicon.ico +0 -0
  271. package/templates/apps/web/public/favicon.svg +3 -3
  272. package/templates/apps/web/public/logomark.svg +6 -3
  273. package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
  274. package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
  275. package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
  276. package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
  277. package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
  278. package/templates/apps/web/public/logos/salessmyth.svg +4 -0
  279. package/templates/apps/web/public/site.webmanifest +4 -4
  280. package/templates/apps/web/public/wordmark-light.svg +4 -0
  281. package/templates/apps/web/public/wordmark.svg +3 -3
  282. package/templates/apps/web/security-headers.config.ts +5 -1
  283. package/templates/content/_site.mdx +25 -0
  284. package/templates/content/en/AboutPage/about.mdx +150 -144
  285. package/templates/content/en/BlogList/blog.mdx +126 -0
  286. package/templates/content/en/ContactPage/contact.mdx +21 -92
  287. package/templates/content/en/FAQPage/faq.mdx +86 -57
  288. package/templates/content/en/HomePage/home.mdx +268 -217
  289. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +181 -0
  290. package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
  291. package/templates/database/CHANGELOG.md +14 -0
  292. package/templates/database/package.json +1 -1
  293. package/templates/package.json +3 -1
  294. package/templates/tools/lint-architecture.ts +315 -0
  295. package/templates/tools/tsconfig.json +14 -0
  296. package/versions.json +33 -33
  297. package/site-templates/simple-service/DESIGN.md +0 -148
  298. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +0 -240
  299. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -278
  300. package/site-templates/simple-service/ailk-template.json +0 -10
  301. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -67
  302. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
  303. package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
  304. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
  305. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
  306. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
  307. package/site-templates/simple-service/provenance.json +0 -4
  308. package/templates/.claude/skills/add-schema/SKILL.md +0 -277
  309. package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
  310. package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
  311. package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
  312. package/templates/.claude/skills/provision-config/references/runbook-template.md +0 -248
  313. package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
  314. package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
  315. package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
  316. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -150
  317. package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
  318. package/templates/apps/web/app/[locale]/shipping/page.tsx +0 -44
  319. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
  320. package/templates/content/en/LandingPage/blog.mdx +0 -83
  321. package/templates/content/en/LandingPage/features.mdx +0 -103
  322. package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
  323. package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
  324. package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
@@ -9,6 +9,12 @@
9
9
  * as fallback). Cross-domain URLs are rejected synchronously before any I/O.
10
10
  * No `fetch()` is called anywhere in this file.
11
11
  *
12
+ * Sitewide identity (#6597): the content root's `_site.mdx` (or its parent's)
13
+ * is loaded, and the page's JSON-LD is stored as the flat node array
14
+ * `composeGraph` produces — the page's primary first, then the identity nodes
15
+ * the web render path emits — with the audit's identity mode following the
16
+ * site's kind. The same composition the `ailk-audit` CLI uses.
17
+ *
12
18
  * Input: { slug, type? } | { url } (exactly one branch; not both, not neither)
13
19
  * Output: AuditInput ready for runAudit
14
20
  * Session/scope: none — read-only filesystem access via content adapter.
@@ -18,15 +24,25 @@ import { join } from "node:path";
18
24
 
19
25
  import {
20
26
  READINESS_SCAN_TYPES,
27
+ chassisSectionIds,
21
28
  type AuditInput,
22
29
  type ReadinessPageFacts,
23
30
  } from "@working-theory/aeo";
31
+ import {
32
+ identityModeForSite,
33
+ loadSitewideIdentity,
34
+ } from "@working-theory/aeo/cli/build-input";
24
35
  import {
25
36
  type ContentAdapter,
26
37
  type ContentPage,
27
38
  type SchemaOrgType,
28
39
  } from "@working-theory/content-adapters";
29
- import { buildJsonLd, pageTypeRegistry } from "@working-theory/schema";
40
+ import {
41
+ buildJsonLd,
42
+ composeGraph,
43
+ graphNodes,
44
+ pageTypeRegistry,
45
+ } from "@working-theory/schema";
30
46
 
31
47
  import { getMcpContentAdapter } from "./content-adapter.js";
32
48
  import { resolveLocale } from "./resolve-locale.js";
@@ -134,14 +150,14 @@ function extractTypeSlugFromUrl(raw: string): {
134
150
  /**
135
151
  * The site-level view the readiness rules grade (#5949 FR9/D5).
136
152
  *
137
- * Five adapter calls, bounded and type-closed: the collection-style pair for
138
- * `Product` / `Service`, the landing-style pair for the three policy types.
139
- * Independent of site size and of which page was audited (NFR4). The five
140
- * are independent of each other, so they run concurrently (#6012).
153
+ * Three adapter calls, bounded and type-closed: the collection-style pair for
154
+ * `Product` / `Service`, the landing-style read for `PurchasePolicyPage`
155
+ * (#6646). Independent of site size and of which page was audited (NFR4). The
156
+ * three are independent of each other, so they run concurrently (#6012).
141
157
  *
142
158
  * Fail-open-to-pass, stated: under an unresolved adapter every list is `[]`,
143
159
  * `sitePages` is empty, and all three rules pass — a site with no known priced
144
- * page owes no policy page, which is the correct verdict for "the audit could
160
+ * page owes no purchase policy, which is the correct verdict for "the audit could
145
161
  * not see the site". A type the adapter legitimately has no pages of resolves
146
162
  * this way too, silently — it is not an error.
147
163
  *
@@ -185,6 +201,8 @@ async function scanSitePages(
185
201
  const facts: ReadinessPageFacts[] = [];
186
202
  for (const { type, pages } of perType) {
187
203
  for (const page of pages) {
204
+ // The purchase policy's section anchors (#6646) — which policies it carries.
205
+ const sectionIds = chassisSectionIds(page.sections);
188
206
  facts.push({
189
207
  slug: page.slug,
190
208
  type,
@@ -192,6 +210,7 @@ async function scanSitePages(
192
210
  ...(page.stripe?.taxCode !== undefined && {
193
211
  taxCode: page.stripe.taxCode,
194
212
  }),
213
+ ...(sectionIds !== undefined && { chassisSectionIds: sectionIds }),
195
214
  });
196
215
  }
197
216
  }
@@ -251,15 +270,21 @@ export async function buildSinglePageAuditInput(
251
270
  );
252
271
  }
253
272
 
273
+ const site = loadSitewideIdentity(root, locale);
274
+
254
275
  const pageKey = `${type}/${slug}`;
255
276
  const jsonLdByPage: Record<string, unknown> = {};
256
277
  try {
257
278
  // Cast to ContentPageInput — structurally compatible but exactOptionalPropertyTypes
258
279
  // differs between @working-theory/content-adapters ContentPage and @working-theory/schema ContentPageInput.
259
280
  // This is the same pattern used in packages/aeo/src/cli/build-input.ts.
260
- jsonLdByPage[pageKey] = buildJsonLd(
261
- page as Parameters<typeof buildJsonLd>[0],
262
- );
281
+ const primary = buildJsonLd(page as Parameters<typeof buildJsonLd>[0]);
282
+ jsonLdByPage[pageKey] =
283
+ site === undefined
284
+ ? primary
285
+ : graphNodes(
286
+ composeGraph({ primary, site, isHomePage: type === "HomePage" }),
287
+ );
263
288
  } catch {
264
289
  // Omit key on build failure — downstream rule reports "no JSON-LD".
265
290
  // This mirrors packages/aeo/src/cli/build-input.ts behavior.
@@ -270,7 +295,8 @@ export async function buildSinglePageAuditInput(
270
295
  return {
271
296
  pages: [{ slug, type, locale }],
272
297
  locale,
273
- identityMode: DEFAULT_IDENTITY_MODE,
298
+ identityMode:
299
+ site !== undefined ? identityModeForSite(site) : DEFAULT_IDENTITY_MODE,
274
300
  registry: pageTypeRegistry,
275
301
  jsonLdByPage,
276
302
  readiness: {
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Registers the durable home of a renewed licence key (#6677 DD-6).
3
+ *
4
+ * `@working-theory/license` renews the deployment's key before it lapses and
5
+ * keeps the renewed key in a `LicenseKeyStore`. Its default store is a file,
6
+ * which a redeploy wipes. With a database configured, this registers the
7
+ * store backed by ONE deployment-wide `SiteSetting` row,
8
+ * `(DEFAULT_WORKSPACE_ID, "license.renewed_key")`, shared with apps/api and
9
+ * apps/web — so the key apps/api renewed is the key this server resolves.
10
+ *
11
+ * `index.ts` awaits this BEFORE `initMcpCommerceClaim()`, which memoises the
12
+ * claim at boot. Both imports are dynamic and reached only when
13
+ * `DATABASE_URL` is set, so an MDX-only deployment never constructs a Prisma
14
+ * client (the `prisma.ts` header's rule).
15
+ */
16
+
17
+ /**
18
+ * Register the SiteSetting-backed store when `DATABASE_URL` is set. An
19
+ * explicitly set `AILK_LICENSE_STORAGE_PATH` is the operator choosing the
20
+ * file store, and is honoured. Never throws: a failure logs and leaves the
21
+ * file store in place.
22
+ */
23
+ export async function registerLicenseKeyStore(): Promise<void> {
24
+ if (!process.env["DATABASE_URL"] || process.env["AILK_LICENSE_STORAGE_PATH"]) return;
25
+ try {
26
+ const [{ getMcpPrismaClient }, { DEFAULT_WORKSPACE_ID }, license] =
27
+ await Promise.all([
28
+ import("./prisma.js"),
29
+ import("@working-theory/database/tenancy"),
30
+ import("@working-theory/license"),
31
+ ]);
32
+ const prisma = getMcpPrismaClient();
33
+ license.setLicenseKeyStore(
34
+ license.createSiteSettingLicenseKeyStore(
35
+ prisma.siteSetting,
36
+ DEFAULT_WORKSPACE_ID,
37
+ ),
38
+ );
39
+ } catch (err) {
40
+ console.error(
41
+ "[mcp] could not register the SiteSetting licence-key store — the file store stays in place:",
42
+ err instanceof Error ? err.name : "non-Error thrown",
43
+ );
44
+ }
45
+ }
@@ -16,6 +16,12 @@
16
16
  * correctly in production and in integration tests.
17
17
  *
18
18
  * Input: { targetDir, projectName, preset? }
19
+ * The tool builds the Free organization-product starter (`--shape
20
+ * organization-product`, #6754 D9); `preset` stays reserved. Always builds
21
+ * the Free module set (`--modules marketing`, #6759 D6, SEC-7): a licence
22
+ * key sitting in the MCP server's own environment never widens what an
23
+ * anonymous caller receives — the explicit keep list takes precedence over
24
+ * any key-derived level probe before the child process reads its env.
19
25
  * targetDir — absolute or relative path to scaffold into; must not exist
20
26
  * or must be empty (enforced by create-ailk ValidationError)
21
27
  * projectName — project name used in README and package.json
@@ -94,12 +100,21 @@ export async function ailkCreateProject(
94
100
  const targetAbs = resolve(input.targetDir);
95
101
 
96
102
  // Spawn the CLI synchronously — no network, no async I/O needed.
97
- // Hard-coded node invocation; only targetDir flows through user input.
98
- const result = spawnSync("node", [bin, targetAbs], {
99
- stdio: ["ignore", "pipe", "pipe"],
100
- shell: false,
101
- encoding: "utf8",
102
- });
103
+ // Hard-coded node invocation; only targetDir flows through user input. The
104
+ // CLI refuses a run naming no shape and no template, so the tool names one.
105
+ // `--modules marketing` is pinned explicitly (#6759 D6, SEC-7): an explicit
106
+ // keep list takes the keep branch and resolves before the child ever reads
107
+ // AILK_LICENSE_KEY, so a licence key in the server's own environment can
108
+ // never turn this anonymous call into the keyed level probe.
109
+ const result = spawnSync(
110
+ "node",
111
+ [bin, targetAbs, "--shape", "organization-product", "--modules", "marketing"],
112
+ {
113
+ stdio: ["ignore", "pipe", "pipe"],
114
+ shell: false,
115
+ encoding: "utf8",
116
+ },
117
+ );
103
118
 
104
119
  if (result.status !== 0) {
105
120
  // Extract the error message from stderr (CLI writes "[create-ailk] <msg>").
@@ -17,6 +17,24 @@ if (typeof globalThis.TextEncoder === "undefined") {
17
17
  (globalThis as unknown as { TextDecoder: unknown }).TextDecoder = TextDecoder;
18
18
  }
19
19
 
20
+ // SOFT_LOCK_OK: #6510 (D1) — jsdom does not implement ResizeObserver.
21
+ // BannerConsentIsland (the layout's always-mounted consent Banner,
22
+ // placement="bottom") now constructs one when reserveSpace is set — the
23
+ // SAME stub @working-theory/ui's own package test setup already provides
24
+ // (packages/ui/src/__tests__/setup.ts), needed here too because this
25
+ // workspace's jest environment is separate from that package's.
26
+ if (typeof globalThis.ResizeObserver === "undefined") {
27
+ /* eslint-disable no-restricted-syntax -- jsdom stub cannot fully implement ResizeObserver; a double-cast is unavoidable in a global setup file */
28
+ class ResizeObserverStub {
29
+ observe(): void {}
30
+ unobserve(): void {}
31
+ disconnect(): void {}
32
+ }
33
+ globalThis.ResizeObserver =
34
+ ResizeObserverStub as unknown as typeof ResizeObserver;
35
+ /* eslint-enable no-restricted-syntax */
36
+ }
37
+
20
38
  // jsdom does not implement window.matchMedia. Provide a minimal stub so any
21
39
  // component that calls matchMedia (e.g. ThemeToggle via Nav) can run
22
40
  // in tests without crashing. Guard with typeof so server-environment tests
@@ -26,7 +26,11 @@
26
26
  * its rendering on anything the response says about the address.
27
27
  */
28
28
 
29
- import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
29
+ import {
30
+ AuthCard,
31
+ type AuthCardLabels,
32
+ type AuthSubmitData,
33
+ } from "@working-theory/ui/blocks";
30
34
  import { createAuthClient } from "better-auth/react";
31
35
  import type { ReactElement } from "react";
32
36
  import { useState } from "react";
@@ -74,6 +78,10 @@ export function ForgotPasswordCard({
74
78
  setSent(true);
75
79
  };
76
80
 
81
+ const handleSubmit = (data: AuthSubmitData): void => {
82
+ void submit(data.email);
83
+ };
84
+
77
85
  return (
78
86
  <div className="flex flex-col gap-4" data-testid="forgot-password-page">
79
87
  <AuthCard
@@ -81,7 +89,7 @@ export function ForgotPasswordCard({
81
89
  signInHref={signInHref}
82
90
  labels={labels}
83
91
  {...(sent ? { sentTo } : {})}
84
- onSubmit={(data) => void submit(data.email)}
92
+ onSubmit={handleSubmit}
85
93
  errors={errors}
86
94
  />
87
95
  </div>
@@ -59,9 +59,9 @@ export default async function AuthLayout({
59
59
  * container — it rendered 48 x 350 here before the clamp. The shell
60
60
  * owns the row's geometry; the site owns its mark's size.
61
61
  */
62
- <span className="inline-flex h-7 items-center [&>img]:h-full [&>img]:w-auto [&>img]:object-contain">
62
+ <div className="inline-flex h-7 items-center [&>img]:h-full [&>img]:w-auto [&>img]:object-contain">
63
63
  {siteLogo()}
64
- </span>
64
+ </div>
65
65
  }
66
66
  brandHref={localeHref(locale, "/")}
67
67
  footerLinks={footerLinks}
@@ -17,7 +17,11 @@
17
17
  * submits nothing and fails obscurely is worse than the honest message.
18
18
  */
19
19
 
20
- import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
20
+ import {
21
+ AuthCard,
22
+ type AuthCardLabels,
23
+ type AuthSubmitData,
24
+ } from "@working-theory/ui/blocks";
21
25
  import { createAuthClient } from "better-auth/react";
22
26
  import type { ReactElement } from "react";
23
27
  import { useState } from "react";
@@ -93,15 +97,17 @@ export function ResetPasswordCard({
93
97
  navigate(signInHref);
94
98
  };
95
99
 
100
+ const handleSubmit = (data: AuthSubmitData): void => {
101
+ void submit(data.password ?? "", data.confirmPassword ?? "");
102
+ };
103
+
96
104
  return (
97
105
  <div className="flex flex-col gap-4" data-testid="reset-password-page">
98
106
  <AuthCard
99
107
  intent="reset"
100
108
  signInHref={signInHref}
101
109
  labels={labels}
102
- onSubmit={(data) =>
103
- void submit(data.password ?? "", data.confirmPassword ?? "")
104
- }
110
+ onSubmit={handleSubmit}
105
111
  errors={errors}
106
112
  />
107
113
  </div>
@@ -38,7 +38,11 @@
38
38
  * a dead end — AuthCard's own default (`"link"`) assumes reset is wired.
39
39
  */
40
40
 
41
- import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
41
+ import {
42
+ AuthCard,
43
+ type AuthCardLabels,
44
+ type AuthSubmitData,
45
+ } from "@working-theory/ui/blocks";
42
46
  import { Alert, AlertDescription } from "@working-theory/ui/primitives";
43
47
  import type { ReactElement } from "react";
44
48
 
@@ -84,6 +88,21 @@ export function SignInCard({
84
88
 
85
89
  const notConfigured = method === "sso" && providers.length === 0;
86
90
 
91
+ const providerButtons = providers.map((provider) => ({
92
+ provider,
93
+ onClick: () => void launchProvider(provider),
94
+ }));
95
+
96
+ const handleSubmit = (data: AuthSubmitData): void => {
97
+ void submitPassword(
98
+ authClient.signIn.email({
99
+ email: data.email,
100
+ password: data.password ?? "",
101
+ callbackURL: next,
102
+ }),
103
+ );
104
+ };
105
+
87
106
  return (
88
107
  /*
89
108
  * No page framing of its own (#5963): AuthShell owns the brand row, the
@@ -104,19 +123,8 @@ export function SignInCard({
104
123
  {...(forgotHref !== undefined ? { forgotHref } : {})}
105
124
  {...(forgot !== undefined ? { forgot } : {})}
106
125
  {...(notConfigured ? { subhead: notConfiguredSubhead } : {})}
107
- providers={providers.map((provider) => ({
108
- provider,
109
- onClick: () => void launchProvider(provider),
110
- }))}
111
- onSubmit={(data) =>
112
- void submitPassword(
113
- authClient.signIn.email({
114
- email: data.email,
115
- password: data.password ?? "",
116
- callbackURL: next,
117
- }),
118
- )
119
- }
126
+ providers={providerButtons}
127
+ onSubmit={handleSubmit}
120
128
  errors={errors}
121
129
  labels={labels}
122
130
  />
@@ -21,7 +21,11 @@
21
21
  * rendered as an `Alert` above the card for `method="sso"`.
22
22
  */
23
23
 
24
- import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
24
+ import {
25
+ AuthCard,
26
+ type AuthCardLabels,
27
+ type AuthSubmitData,
28
+ } from "@working-theory/ui/blocks";
25
29
  import { Alert, AlertDescription } from "@working-theory/ui/primitives";
26
30
  import type { ReactElement } from "react";
27
31
 
@@ -64,6 +68,25 @@ export function SignUpCard({
64
68
 
65
69
  const notConfigured = method === "sso" && providers.length === 0;
66
70
 
71
+ const providerButtons = providers.map((provider) => ({
72
+ provider,
73
+ onClick: () => void launchProvider(provider),
74
+ }));
75
+
76
+ const handleSubmit = (data: AuthSubmitData): void => {
77
+ // Better Auth requires a `name` on email sign-up; this card collects
78
+ // none, so the address doubles as the display name until the person
79
+ // sets one.
80
+ void submitPassword(
81
+ authClient.signUp.email({
82
+ email: data.email,
83
+ password: data.password ?? "",
84
+ name: data.email,
85
+ callbackURL: next,
86
+ }),
87
+ );
88
+ };
89
+
67
90
  return (
68
91
  <div className="flex flex-col gap-4" data-testid="sign-up-page">
69
92
  {method === "sso" && providerError ? (
@@ -76,23 +99,8 @@ export function SignUpCard({
76
99
  method={method}
77
100
  signInHref={signInHref}
78
101
  {...(notConfigured ? { subhead: notConfiguredSubhead } : {})}
79
- providers={providers.map((provider) => ({
80
- provider,
81
- onClick: () => void launchProvider(provider),
82
- }))}
83
- onSubmit={(data) =>
84
- // Better Auth requires a `name` on email sign-up; this card collects
85
- // none, so the address doubles as the display name until the person
86
- // sets one.
87
- void submitPassword(
88
- authClient.signUp.email({
89
- email: data.email,
90
- password: data.password ?? "",
91
- name: data.email,
92
- callbackURL: next,
93
- }),
94
- )
95
- }
102
+ providers={providerButtons}
103
+ onSubmit={handleSubmit}
96
104
  errors={errors}
97
105
  labels={labels}
98
106
  />
@@ -153,7 +153,11 @@ export default async function AccountPage({ params }: Props): Promise<ReactEleme
153
153
  locale={locale}
154
154
  navItems={navItems}
155
155
  activePath={localeHref(locale, "/account")}
156
- logo={<span className="text-sm font-semibold text-brand">{siteName}</span>}
156
+ logo={
157
+ <Text as="span" variant="body-sm" className="font-semibold">
158
+ {siteName}
159
+ </Text>
160
+ }
157
161
  user={{ name: displayName }}
158
162
  />
159
163
  );
@@ -212,8 +216,12 @@ export default async function AccountPage({ params }: Props): Promise<ReactEleme
212
216
  {
213
217
  label: t("profile.email"),
214
218
  value: (
215
- <span className="inline-flex flex-wrap items-center gap-2">
216
- <span data-testid="account-profile-email">{email}</span>
219
+ <div className="inline-flex flex-wrap items-center gap-2">
220
+ <div data-testid="account-profile-email">
221
+ <Text as="span" variant="body-sm">
222
+ {email}
223
+ </Text>
224
+ </div>
217
225
  <Badge
218
226
  variant={emailVerified ? "success" : "warning"}
219
227
  size="sm"
@@ -222,7 +230,7 @@ export default async function AccountPage({ params }: Props): Promise<ReactEleme
222
230
  >
223
231
  {emailVerified ? t("profile.verified") : t("profile.unverified")}
224
232
  </Badge>
225
- </span>
233
+ </div>
226
234
  ),
227
235
  },
228
236
  ]}
@@ -7,8 +7,11 @@
7
7
  * emitted JSON-LD (the escape hatch, proven with a `HowTo` fixture).
8
8
  * - Unknown slug: getMdxSource returns null → renderPage's own detail-route
9
9
  * branch (no translationsNamespace passed) calls notFound().
10
- * - Resolution: the route asks the adapter for exactly `LandingPage` — the
11
- * one content type it serves.
10
+ * - Resolution: the route asks the adapter for `LandingPage` first and, only
11
+ * when that is null, `BlogList` (the Free /blog teaser — the one list type
12
+ * that cannot own a route directory, since the docs-blog module owns
13
+ * `blog/`). A BlogList source renders through renderPage with its own type,
14
+ * so the JSON-LD is the BlogList builder's `Blog`.
12
15
  */
13
16
  import React from "react";
14
17
  import { render } from "@testing-library/react";
@@ -61,7 +64,7 @@ function makeSource(slug: string, schema?: Record<string, unknown>) {
61
64
  };
62
65
  }
63
66
 
64
- describe("[slug] — the generic LandingPage content route", () => {
67
+ describe("[slug] — the generic content route (LandingPage, then BlogList)", () => {
65
68
  beforeEach(() => {
66
69
  mockNotFound.mockClear();
67
70
  });
@@ -97,7 +100,35 @@ describe("[slug] — the generic LandingPage content route", () => {
97
100
  expect(script?.textContent ?? "").toContain('"HowTo"');
98
101
  });
99
102
 
100
- it("calls notFound() for a slug with no LandingPage source", async () => {
103
+ it("falls back to BlogList when no LandingPage source exists (the Free /blog teaser)", async () => {
104
+ const blog = {
105
+ frontmatter: {
106
+ type: "BlogList" as const,
107
+ slug: "blog",
108
+ title: "Blog",
109
+ description: "This template does not include a blog.",
110
+ sections: [{ id: "hero", type: "hero" as const, data: { headline: "Blog" } }],
111
+ },
112
+ body: "",
113
+ };
114
+ const getMdxSource = jest
115
+ .fn()
116
+ .mockImplementation(async (type: string) => (type === "BlogList" ? blog : null));
117
+
118
+ const element = await SlugPage({
119
+ params: Promise.resolve({ locale: "en", slug: "blog" }),
120
+ getMdxSource,
121
+ });
122
+ const { container } = render(element);
123
+
124
+ expect(getMdxSource).toHaveBeenNthCalledWith(1, "LandingPage", "blog", "en");
125
+ expect(getMdxSource).toHaveBeenNthCalledWith(2, "BlogList", "blog", "en");
126
+ const script = container.querySelector("script[type='application/ld+json']");
127
+ expect(script?.textContent ?? "").toContain('"@type":"Blog"');
128
+ expect(mockNotFound).not.toHaveBeenCalled();
129
+ });
130
+
131
+ it("calls notFound() for a slug with neither a LandingPage nor a BlogList source", async () => {
101
132
  const getMdxSource = jest.fn().mockResolvedValue(null);
102
133
 
103
134
  await expect(
@@ -1,5 +1,5 @@
1
1
  // The one generic content route (#5469 D2). Serves the Free marketing-site
2
- // template's /how-it-works, /features and /blog teaser — and any other
2
+ // template's /blog teaser — and any other
3
3
  // `content/<locale>/LandingPage/<slug>.mdx` a site adds — through exactly the
4
4
  // render path the static routes use (`renderPage`, sections mode, JSON-LD
5
5
  // from frontmatter — a page's `schema:` override rides the escape hatch,
@@ -12,11 +12,17 @@
12
12
  // renders here; with the docs-blog module its `blog/[[...slug]]` directory
13
13
  // wins and the teaser file is inert (#5469 D3).
14
14
  //
15
- // One content type, not two: `WebPage` is a schema.org @type the registry
16
- // emits, not a `SchemaOrgType` content key the adapter accepts, so the three
17
- // pages are `LandingPage` files (the coined slug-keyed landing surface whose
18
- // builder emits a `WebPage` envelope) — the lesson recorded with #5469
19
- // (site-template content composition) has the proving lines.
15
+ // Two content types, in a fixed order: `LandingPage` first (the coined
16
+ // slug-keyed landing surface whose builder emits a `WebPage` envelope), then
17
+ // `BlogList` — the one list type that cannot have a route directory of its
18
+ // own, because the docs-blog module owns `blog/` (`blog/[[...slug]]`) and
19
+ // Next.js refuses a `blog/page.tsx` beside an optional catch-all. So the
20
+ // Free `/blog` teaser is a `BlogList` page (schema `Blog` + an empty
21
+ // `ItemList` — the correct page type, not a LandingPage wearing a `schema:`
22
+ // override the structured-data gate rejects) served here, and still yields
23
+ // to the module's directory by the same precedence. Every other list type
24
+ // has its own route directory (`/features` → ProductsList, `/use-cases` →
25
+ // UseCasesList) and never reaches this route.
20
26
  //
21
27
  // Unknown slug → `notFound()` (renderPage's own detail-route branch: no
22
28
  // `translationsNamespace` is passed, so a null source 404s rather than
@@ -28,7 +34,11 @@
28
34
  // segment and fail any slug outside the build-time list with
29
35
  // DYNAMIC_SERVER_USAGE (a 500, not the 404 an unknown slug must get).
30
36
  import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
31
- import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
37
+ import {
38
+ getMdxSource as loadMdxSource,
39
+ renderPage,
40
+ renderPageMetadata,
41
+ } from "@working-theory/page-renderer";
32
42
  import type { Metadata } from "next";
33
43
  import type { ReactElement } from "react";
34
44
 
@@ -43,11 +53,30 @@ interface Props {
43
53
  getMdxSource?: typeof defaultGetMdxSource;
44
54
  }
45
55
 
56
+ /**
57
+ * Which of the two content types carries this slug. `LandingPage` wins when
58
+ * it exists; `BlogList` is probed only when it does not, so a LandingPage
59
+ * slug never pays a second adapter read. Neither → `LandingPage`, whose null
60
+ * source is what makes renderPage 404.
61
+ */
62
+ async function resolveType(
63
+ slug: string,
64
+ locale: string,
65
+ getMdxSource: typeof defaultGetMdxSource,
66
+ ): Promise<"LandingPage" | "BlogList"> {
67
+ if ((await getMdxSource("LandingPage", slug, locale)) !== null) return "LandingPage";
68
+ if ((await getMdxSource("BlogList", slug, locale)) !== null) return "BlogList";
69
+ return "LandingPage";
70
+ }
71
+
46
72
  export async function generateMetadata({
47
73
  params,
48
- }: Pick<Props, "params">): Promise<Metadata> {
74
+ getMdxSource,
75
+ }: Pick<Props, "params" | "getMdxSource">): Promise<Metadata> {
49
76
  const { locale, slug } = await params;
50
- return renderPageMetadata({ type: "LandingPage", slug, locale });
77
+ const load = getMdxSource ?? loadMdxSource;
78
+ const type = await resolveType(slug, locale, load);
79
+ return renderPageMetadata({ type, slug, locale });
51
80
  }
52
81
 
53
82
  export default async function Page({
@@ -55,6 +84,17 @@ export default async function Page({
55
84
  getMdxSource,
56
85
  }: Props): Promise<ReactElement> {
57
86
  const { locale, slug } = await params;
87
+ const load = getMdxSource ?? loadMdxSource;
88
+ const type = await resolveType(slug, locale, load);
89
+ if (type === "BlogList") {
90
+ return renderPage({
91
+ type: "BlogList",
92
+ slug,
93
+ locale,
94
+ populatedDescriptionTestId: `${slug}-description`,
95
+ ...(getMdxSource !== undefined && { getMdxSource }),
96
+ });
97
+ }
58
98
  return renderPage({
59
99
  type: "LandingPage",
60
100
  slug,
@@ -0,0 +1,50 @@
1
+ // Book-a-call page (#6658 D5) — a BookCall at /book-a-call, reading the
2
+ // fixed slug `book-call` (the monorepo's own content already uses it). Its
3
+ // CTA points at /contact, or at /meet where a template carries scheduling. A
4
+ // module-less surface a template enables by naming it in its roster; with no
5
+ // content the route 404s. Mirrors use-cases/page.tsx: a sections-composed page
6
+ // renders its sections, a prose-only page its MDX body.
7
+ import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
8
+ import {
9
+ getMdxSource as loadMdxSource,
10
+ renderPage,
11
+ renderPageMetadata,
12
+ } from "@working-theory/page-renderer";
13
+ import type { Metadata } from "next";
14
+ import type { ReactElement } from "react";
15
+
16
+ interface Props {
17
+ params: Promise<{ locale: string }>;
18
+ getMdxSource?: typeof defaultGetMdxSource;
19
+ }
20
+
21
+ export async function generateMetadata({
22
+ params,
23
+ }: Pick<Props, "params">): Promise<Metadata> {
24
+ const { locale } = await params;
25
+ return renderPageMetadata({
26
+ type: "BookCall",
27
+ slug: "book-call",
28
+ locale,
29
+ });
30
+ }
31
+
32
+ export default async function Page({
33
+ params,
34
+ getMdxSource,
35
+ }: Props): Promise<ReactElement> {
36
+ const { locale } = await params;
37
+ const source = await (getMdxSource ?? loadMdxSource)("BookCall", "book-call", 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({
44
+ type: "BookCall",
45
+ slug: "book-call",
46
+ locale,
47
+ ...(composed ? {} : { bodyMode: "mdx-body" as const }),
48
+ ...(getMdxSource !== undefined && { getMdxSource }),
49
+ });
50
+ }