create-ailk 0.1.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 (533) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +80 -0
  5. package/dist/copy.d.ts +8 -0
  6. package/dist/copy.js +109 -0
  7. package/dist/index.d.ts +10 -0
  8. package/dist/index.js +10 -0
  9. package/dist/lib/pro-callout.d.ts +12 -0
  10. package/dist/lib/pro-callout.js +20 -0
  11. package/dist/parse-args.d.ts +13 -0
  12. package/dist/parse-args.js +81 -0
  13. package/dist/programmatic.d.ts +49 -0
  14. package/dist/programmatic.js +89 -0
  15. package/dist/substitute.d.ts +31 -0
  16. package/dist/substitute.js +45 -0
  17. package/dist/validate.d.ts +8 -0
  18. package/dist/validate.js +70 -0
  19. package/package.json +47 -0
  20. package/templates/.claude/.gitkeep +0 -0
  21. package/templates/.claude/agents/web.md +80 -0
  22. package/templates/.claude/rules/architecture.md +62 -0
  23. package/templates/.claude/rules/preserve-existing-style.md +51 -0
  24. package/templates/.claude/skills/README.md +43 -0
  25. package/templates/.claude/skills/add-schema/SKILL.md +270 -0
  26. package/templates/.claude/skills/add-schema/references/conflict-handling.md +200 -0
  27. package/templates/.claude/skills/add-schema/references/file-finding.md +123 -0
  28. package/templates/.claude/skills/add-schema/references/schema-types.md +131 -0
  29. package/templates/.claude/skills/audit-site/SKILL.md +213 -0
  30. package/templates/.claude/skills/audit-site/references/common-errors.md +113 -0
  31. package/templates/.claude/skills/audit-site/references/findings-glossary.md +138 -0
  32. package/templates/.claude/skills/audit-site/references/severity-levels.md +114 -0
  33. package/templates/.claude/skills/provision-config/SKILL.md +243 -0
  34. package/templates/.claude/skills/provision-config/references/env-vars-template.md +184 -0
  35. package/templates/.claude/skills/provision-config/references/runbook-template.md +248 -0
  36. package/templates/.claude/skills/provision-config/references/vercel-json-template.md +98 -0
  37. package/templates/.claude/skills/scaffold-auth/SKILL.md +261 -0
  38. package/templates/.claude/skills/scaffold-auth/references/env-vars.md +91 -0
  39. package/templates/.claude/skills/scaffold-auth/references/oauth-app-setup.md +171 -0
  40. package/templates/.claude/skills/scaffold-commerce/SKILL.md +310 -0
  41. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +71 -0
  42. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +83 -0
  43. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +72 -0
  44. package/templates/.claude/skills/scaffold-page/SKILL.md +243 -0
  45. package/templates/.claude/skills/scaffold-page/references/page-types.md +169 -0
  46. package/templates/.claude/skills/scaffold-page/templates/content.mdx.tmpl +28 -0
  47. package/templates/.claude/skills/scaffold-page/templates/route.tsx.tmpl +28 -0
  48. package/templates/.claude/skills/suggest-site-pages/SKILL.md +242 -0
  49. package/templates/.claude/skills/suggest-site-pages/references/categories.md +287 -0
  50. package/templates/.env.example +205 -0
  51. package/templates/.gitkeep +2 -0
  52. package/templates/CLAUDE.md +142 -0
  53. package/templates/CONVENTIONS.md +222 -0
  54. package/templates/README.md +110 -0
  55. package/templates/apps/api/.env.example +40 -0
  56. package/templates/apps/api/CLAUDE.md +63 -0
  57. package/templates/apps/api/api/index.ts +144 -0
  58. package/templates/apps/api/jest.config.cjs +74 -0
  59. package/templates/apps/api/package.json +54 -0
  60. package/templates/apps/api/src/__tests__/factories/content-fixture.ts +138 -0
  61. package/templates/apps/api/src/__tests__/factories/index.ts +70 -0
  62. package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +82 -0
  63. package/templates/apps/api/src/__tests__/index.test.ts +118 -0
  64. package/templates/apps/api/src/__tests__/server.test.ts +207 -0
  65. package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +126 -0
  66. package/templates/apps/api/src/__tests__/setup.ts +24 -0
  67. package/templates/apps/api/src/cli.ts +30 -0
  68. package/templates/apps/api/src/config/__tests__/index.test.ts +97 -0
  69. package/templates/apps/api/src/config/index.ts +56 -0
  70. package/templates/apps/api/src/index.ts +32 -0
  71. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +15 -0
  72. package/templates/apps/api/src/lib/__mocks__/prisma.ts +45 -0
  73. package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +178 -0
  74. package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +100 -0
  75. package/templates/apps/api/src/lib/__tests__/errors.test.ts +94 -0
  76. package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +144 -0
  77. package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +8 -0
  78. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +227 -0
  79. package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +125 -0
  80. package/templates/apps/api/src/lib/__tests__/prisma.test.ts +165 -0
  81. package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +129 -0
  82. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +74 -0
  83. package/templates/apps/api/src/lib/content-adapter.ts +86 -0
  84. package/templates/apps/api/src/lib/error-serializer.ts +77 -0
  85. package/templates/apps/api/src/lib/errors.ts +95 -0
  86. package/templates/apps/api/src/lib/file-fallback.ts +87 -0
  87. package/templates/apps/api/src/lib/lead-capture-store.ts +110 -0
  88. package/templates/apps/api/src/lib/locales.ts +201 -0
  89. package/templates/apps/api/src/lib/outbound-http.ts +122 -0
  90. package/templates/apps/api/src/lib/prisma.ts +21 -0
  91. package/templates/apps/api/src/lib/require-paid-tier.ts +53 -0
  92. package/templates/apps/api/src/lib/stripe.ts +30 -0
  93. package/templates/apps/api/src/lib/webhook-idempotency.ts +92 -0
  94. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +111 -0
  95. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +172 -0
  96. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +119 -0
  97. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +138 -0
  98. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +149 -0
  99. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +139 -0
  100. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +175 -0
  101. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +76 -0
  102. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +153 -0
  103. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +72 -0
  104. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +292 -0
  105. package/templates/apps/api/src/middleware/auth.ts +246 -0
  106. package/templates/apps/api/src/middleware/rate-limit.ts +132 -0
  107. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +129 -0
  108. package/templates/apps/api/src/openapi/index.ts +12 -0
  109. package/templates/apps/api/src/openapi/spec.ts +408 -0
  110. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +193 -0
  111. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +201 -0
  112. package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +294 -0
  113. package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +486 -0
  114. package/templates/apps/api/src/routes/auth/tokens.ts +199 -0
  115. package/templates/apps/api/src/routes/auth/user.ts +244 -0
  116. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +588 -0
  117. package/templates/apps/api/src/routes/checkout/index.ts +13 -0
  118. package/templates/apps/api/src/routes/checkout/sessions.ts +185 -0
  119. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +239 -0
  120. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +201 -0
  121. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +141 -0
  122. package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +122 -0
  123. package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +44 -0
  124. package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +118 -0
  125. package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +51 -0
  126. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +252 -0
  127. package/templates/apps/api/src/routes/content/create.ts +114 -0
  128. package/templates/apps/api/src/routes/content/delete.ts +81 -0
  129. package/templates/apps/api/src/routes/content/get-page.ts +98 -0
  130. package/templates/apps/api/src/routes/content/index.ts +32 -0
  131. package/templates/apps/api/src/routes/content/list-pages.ts +83 -0
  132. package/templates/apps/api/src/routes/content/list-types.ts +19 -0
  133. package/templates/apps/api/src/routes/content/rate-limit.ts +13 -0
  134. package/templates/apps/api/src/routes/content/slug.ts +9 -0
  135. package/templates/apps/api/src/routes/content/update.ts +111 -0
  136. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +415 -0
  137. package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +165 -0
  138. package/templates/apps/api/src/routes/leads/index.ts +130 -0
  139. package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +296 -0
  140. package/templates/apps/api/src/routes/test/auth/session.ts +141 -0
  141. package/templates/apps/api/src/routes/webhooks/README.md +57 -0
  142. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +247 -0
  143. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +198 -0
  144. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +633 -0
  145. package/templates/apps/api/src/routes/webhooks/index.ts +41 -0
  146. package/templates/apps/api/src/routes/webhooks/stripe.ts +253 -0
  147. package/templates/apps/api/src/server.ts +157 -0
  148. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +395 -0
  149. package/templates/apps/api/src/services/enrichment-shim.ts +53 -0
  150. package/templates/apps/api/src/services/lead-routing.ts +338 -0
  151. package/templates/apps/api/tsconfig.json +34 -0
  152. package/templates/apps/api/vercel.json +17 -0
  153. package/templates/apps/mcp/.env.example +52 -0
  154. package/templates/apps/mcp/CLAUDE.md +84 -0
  155. package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +52 -0
  156. package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +46 -0
  157. package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +36 -0
  158. package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +54 -0
  159. package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +110 -0
  160. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +213 -0
  161. package/templates/apps/mcp/__tests__/audit.test.ts +217 -0
  162. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +52 -0
  163. package/templates/apps/mcp/__tests__/create_page.test.ts +201 -0
  164. package/templates/apps/mcp/__tests__/delete_page.test.ts +176 -0
  165. package/templates/apps/mcp/__tests__/get_business_hours.test.ts +82 -0
  166. package/templates/apps/mcp/__tests__/get_faq.test.ts +80 -0
  167. package/templates/apps/mcp/__tests__/get_locations.test.ts +65 -0
  168. package/templates/apps/mcp/__tests__/get_offerings.test.ts +90 -0
  169. package/templates/apps/mcp/__tests__/get_pricing.test.ts +80 -0
  170. package/templates/apps/mcp/__tests__/get_site_info.test.ts +81 -0
  171. package/templates/apps/mcp/__tests__/http-transport.test.ts +323 -0
  172. package/templates/apps/mcp/__tests__/lib/errors.test.ts +75 -0
  173. package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +75 -0
  174. package/templates/apps/mcp/__tests__/list_pages.test.ts +112 -0
  175. package/templates/apps/mcp/__tests__/locale.test.ts +135 -0
  176. package/templates/apps/mcp/__tests__/parity.test.ts +205 -0
  177. package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +34 -0
  178. package/templates/apps/mcp/__tests__/read_content.test.ts +153 -0
  179. package/templates/apps/mcp/__tests__/request_callback.test.ts +163 -0
  180. package/templates/apps/mcp/__tests__/search_content.test.ts +153 -0
  181. package/templates/apps/mcp/__tests__/server.test.ts +115 -0
  182. package/templates/apps/mcp/__tests__/submit_lead.test.ts +165 -0
  183. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +161 -0
  184. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +227 -0
  185. package/templates/apps/mcp/__tests__/tools/scaffold.test.ts +237 -0
  186. package/templates/apps/mcp/__tests__/update_page.test.ts +195 -0
  187. package/templates/apps/mcp/jest.config.cjs +82 -0
  188. package/templates/apps/mcp/package.json +37 -0
  189. package/templates/apps/mcp/src/abuse/daily-cap.ts +47 -0
  190. package/templates/apps/mcp/src/abuse/email-validator.ts +25 -0
  191. package/templates/apps/mcp/src/abuse/honeypot.ts +21 -0
  192. package/templates/apps/mcp/src/abuse/index.ts +111 -0
  193. package/templates/apps/mcp/src/abuse/user-agent-deny.ts +58 -0
  194. package/templates/apps/mcp/src/http-transport.ts +303 -0
  195. package/templates/apps/mcp/src/index.ts +67 -0
  196. package/templates/apps/mcp/src/lib/audit-single-page.ts +194 -0
  197. package/templates/apps/mcp/src/lib/errors.ts +61 -0
  198. package/templates/apps/mcp/src/lib/locales.ts +171 -0
  199. package/templates/apps/mcp/src/lib/require-paid-tier.ts +44 -0
  200. package/templates/apps/mcp/src/lib/resolve-locale.ts +30 -0
  201. package/templates/apps/mcp/src/server.ts +139 -0
  202. package/templates/apps/mcp/src/tools/audit.ts +103 -0
  203. package/templates/apps/mcp/src/tools/capture_lead.ts +52 -0
  204. package/templates/apps/mcp/src/tools/create_page.ts +95 -0
  205. package/templates/apps/mcp/src/tools/delete_page.ts +73 -0
  206. package/templates/apps/mcp/src/tools/get_business_hours.ts +66 -0
  207. package/templates/apps/mcp/src/tools/get_faq.ts +69 -0
  208. package/templates/apps/mcp/src/tools/get_locations.ts +85 -0
  209. package/templates/apps/mcp/src/tools/get_offerings.ts +66 -0
  210. package/templates/apps/mcp/src/tools/get_pricing.ts +74 -0
  211. package/templates/apps/mcp/src/tools/get_site_info.ts +84 -0
  212. package/templates/apps/mcp/src/tools/index.ts +125 -0
  213. package/templates/apps/mcp/src/tools/list_pages.ts +72 -0
  214. package/templates/apps/mcp/src/tools/purchase.ts +89 -0
  215. package/templates/apps/mcp/src/tools/read_content.ts +61 -0
  216. package/templates/apps/mcp/src/tools/request_callback.ts +51 -0
  217. package/templates/apps/mcp/src/tools/scaffold.ts +119 -0
  218. package/templates/apps/mcp/src/tools/search_content.ts +59 -0
  219. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +47 -0
  220. package/templates/apps/mcp/src/tools/update_page.ts +91 -0
  221. package/templates/apps/mcp/tsconfig.json +19 -0
  222. package/templates/apps/web/.env.example +47 -0
  223. package/templates/apps/web/CLAUDE.md +81 -0
  224. package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +94 -0
  225. package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +75 -0
  226. package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +82 -0
  227. package/templates/apps/web/__tests__/auth/env-helpers.test.ts +168 -0
  228. package/templates/apps/web/__tests__/authority-list-routes.test.tsx +208 -0
  229. package/templates/apps/web/__tests__/better-stack.test.ts +66 -0
  230. package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +87 -0
  231. package/templates/apps/web/__tests__/blog-source-populated.test.ts +130 -0
  232. package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +60 -0
  233. package/templates/apps/web/__tests__/google-analytics.test.tsx +104 -0
  234. package/templates/apps/web/__tests__/i18n/middleware.test.ts +119 -0
  235. package/templates/apps/web/__tests__/i18n/routing.test.ts +114 -0
  236. package/templates/apps/web/__tests__/i18n/translations.test.ts +191 -0
  237. package/templates/apps/web/__tests__/json-ld.test.ts +98 -0
  238. package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +580 -0
  239. package/templates/apps/web/__tests__/layout-stealth.test.ts +39 -0
  240. package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +103 -0
  241. package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +114 -0
  242. package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +94 -0
  243. package/templates/apps/web/__tests__/page.test.tsx +230 -0
  244. package/templates/apps/web/__tests__/products-services-phase1.test.tsx +321 -0
  245. package/templates/apps/web/__tests__/projects-routes.test.tsx +392 -0
  246. package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +475 -0
  247. package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +53 -0
  248. package/templates/apps/web/__tests__/routes/blog-route.test.tsx +191 -0
  249. package/templates/apps/web/__tests__/routes/llms-txt.test.ts +123 -0
  250. package/templates/apps/web/__tests__/routes/robots-txt.test.ts +52 -0
  251. package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +78 -0
  252. package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +203 -0
  253. package/templates/apps/web/__tests__/section-routes.test.tsx +693 -0
  254. package/templates/apps/web/__tests__/security-headers.test.ts +201 -0
  255. package/templates/apps/web/__tests__/sentry-config.test.ts +116 -0
  256. package/templates/apps/web/__tests__/setup.ts +82 -0
  257. package/templates/apps/web/__tests__/sitemap-locales.test.ts +415 -0
  258. package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +844 -0
  259. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +81 -0
  260. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +110 -0
  261. package/templates/apps/web/app/.well-known/mcp.json/route.ts +58 -0
  262. package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +120 -0
  263. package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +202 -0
  264. package/templates/apps/web/app/[locale]/about/page.tsx +31 -0
  265. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +173 -0
  266. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +45 -0
  267. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +117 -0
  268. package/templates/apps/web/app/[locale]/careers/page.tsx +25 -0
  269. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +44 -0
  270. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +113 -0
  271. package/templates/apps/web/app/[locale]/case-studies/page.tsx +25 -0
  272. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +63 -0
  273. package/templates/apps/web/app/[locale]/clients/page.tsx +27 -0
  274. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +130 -0
  275. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +152 -0
  276. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +92 -0
  277. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +79 -0
  278. package/templates/apps/web/app/[locale]/contact/error.tsx +24 -0
  279. package/templates/apps/web/app/[locale]/contact/page.tsx +38 -0
  280. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +34 -0
  281. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +119 -0
  282. package/templates/apps/web/app/[locale]/courses/page.tsx +25 -0
  283. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +160 -0
  284. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +25 -0
  285. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +117 -0
  286. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +58 -0
  287. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +118 -0
  288. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +72 -0
  289. package/templates/apps/web/app/[locale]/downloads/page.tsx +25 -0
  290. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +47 -0
  291. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +130 -0
  292. package/templates/apps/web/app/[locale]/events/page.tsx +25 -0
  293. package/templates/apps/web/app/[locale]/faq/page.tsx +36 -0
  294. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +34 -0
  295. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +78 -0
  296. package/templates/apps/web/app/[locale]/how-to/page.tsx +40 -0
  297. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +43 -0
  298. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +129 -0
  299. package/templates/apps/web/app/[locale]/integrations/page.tsx +25 -0
  300. package/templates/apps/web/app/[locale]/launch-service/page.tsx +31 -0
  301. package/templates/apps/web/app/[locale]/layout.tsx +241 -0
  302. package/templates/apps/web/app/[locale]/page.tsx +41 -0
  303. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +44 -0
  304. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +113 -0
  305. package/templates/apps/web/app/[locale]/partners/page.tsx +25 -0
  306. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +43 -0
  307. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +129 -0
  308. package/templates/apps/web/app/[locale]/products/page.tsx +25 -0
  309. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +39 -0
  310. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +138 -0
  311. package/templates/apps/web/app/[locale]/projects/page.tsx +30 -0
  312. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +51 -0
  313. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +117 -0
  314. package/templates/apps/web/app/[locale]/resources/page.tsx +27 -0
  315. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +43 -0
  316. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +129 -0
  317. package/templates/apps/web/app/[locale]/services/page.tsx +25 -0
  318. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +34 -0
  319. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +81 -0
  320. package/templates/apps/web/app/[locale]/software/page.tsx +39 -0
  321. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +34 -0
  322. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +82 -0
  323. package/templates/apps/web/app/[locale]/tech-articles/page.tsx +39 -0
  324. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +63 -0
  325. package/templates/apps/web/app/[locale]/testimonials/page.tsx +27 -0
  326. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +63 -0
  327. package/templates/apps/web/app/[locale]/use-cases/page.tsx +27 -0
  328. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +34 -0
  329. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +82 -0
  330. package/templates/apps/web/app/[locale]/whitepapers/page.tsx +39 -0
  331. package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +152 -0
  332. package/templates/apps/web/app/api/auth/[...all]/route.ts +16 -0
  333. package/templates/apps/web/app/error-fallback.tsx +75 -0
  334. package/templates/apps/web/app/error.tsx +23 -0
  335. package/templates/apps/web/app/layout.tsx +18 -0
  336. package/templates/apps/web/app/llms.txt/route.ts +101 -0
  337. package/templates/apps/web/app/not-found.tsx +48 -0
  338. package/templates/apps/web/app/og/__tests__/route.test.ts +164 -0
  339. package/templates/apps/web/app/og/route.tsx +135 -0
  340. package/templates/apps/web/app/robots.txt/route.ts +36 -0
  341. package/templates/apps/web/app/sitemap.xml/route.ts +146 -0
  342. package/templates/apps/web/auth.ts +141 -0
  343. package/templates/apps/web/components/ThemeToggle.tsx +60 -0
  344. package/templates/apps/web/components/google-analytics.tsx +34 -0
  345. package/templates/apps/web/content/en/blog/index.mdx +15 -0
  346. package/templates/apps/web/content/en/blog/launching-ailk.mdx +20 -0
  347. package/templates/apps/web/content/en/docs/getting-started.mdx +30 -0
  348. package/templates/apps/web/i18n/request.ts +36 -0
  349. package/templates/apps/web/jest.config.cjs +100 -0
  350. package/templates/apps/web/lib/__mocks__/next-mdx-remote-rsc.js +12 -0
  351. package/templates/apps/web/lib/__mocks__/source-server.js +31 -0
  352. package/templates/apps/web/lib/__tests__/og.test.ts +65 -0
  353. package/templates/apps/web/lib/auth-env.ts +66 -0
  354. package/templates/apps/web/lib/blog-list-data.ts +95 -0
  355. package/templates/apps/web/lib/blog-post-data.ts +79 -0
  356. package/templates/apps/web/lib/nav-config.ts +13 -0
  357. package/templates/apps/web/lib/observability/better-stack.ts +27 -0
  358. package/templates/apps/web/lib/og.ts +23 -0
  359. package/templates/apps/web/lib/prisma-for-auth.ts +34 -0
  360. package/templates/apps/web/lib/source.ts +34 -0
  361. package/templates/apps/web/messages/en.json +131 -0
  362. package/templates/apps/web/middleware.ts +80 -0
  363. package/templates/apps/web/next.config.mjs +167 -0
  364. package/templates/apps/web/package.json +62 -0
  365. package/templates/apps/web/public/.well-known/security.txt +17 -0
  366. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  367. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  368. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  369. package/templates/apps/web/public/favicon-16x16.png +0 -0
  370. package/templates/apps/web/public/favicon-32x32.png +0 -0
  371. package/templates/apps/web/public/favicon.ico +0 -0
  372. package/templates/apps/web/public/favicon.svg +4 -0
  373. package/templates/apps/web/public/logo-dark.svg +21 -0
  374. package/templates/apps/web/public/logo-light.svg +21 -0
  375. package/templates/apps/web/public/logomark-dark.svg +4 -0
  376. package/templates/apps/web/public/logomark-light.svg +4 -0
  377. package/templates/apps/web/public/og-default.png +0 -0
  378. package/templates/apps/web/public/site.webmanifest +19 -0
  379. package/templates/apps/web/scripts/assert-content-bundled.ts +55 -0
  380. package/templates/apps/web/security-headers.config.ts +132 -0
  381. package/templates/apps/web/sentry.client.config.ts +27 -0
  382. package/templates/apps/web/sentry.edge.config.ts +15 -0
  383. package/templates/apps/web/sentry.server.config.ts +20 -0
  384. package/templates/apps/web/source.config.ts +78 -0
  385. package/templates/apps/web/src/components/SiteNav.tsx +21 -0
  386. package/templates/apps/web/src/lib/routes.ts +37 -0
  387. package/templates/apps/web/tsconfig.json +35 -0
  388. package/templates/apps/web/types/messages.d.ts +19 -0
  389. package/templates/content/.gitkeep +0 -0
  390. package/templates/content/CLAUDE.md +85 -0
  391. package/templates/content/_site.mdx +38 -0
  392. package/templates/content/ar/HomePage/home.mdx +123 -0
  393. package/templates/content/ar/_site.mdx +35 -0
  394. package/templates/content/de/_site.mdx +29 -0
  395. package/templates/content/en/.gitkeep +0 -0
  396. package/templates/content/en/AboutPage/about.mdx +19 -0
  397. package/templates/content/en/AboutPage/about.schema.json +82 -0
  398. package/templates/content/en/BookCall/book-call.mdx +19 -0
  399. package/templates/content/en/BookCall/book-call.schema.json +121 -0
  400. package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +20 -0
  401. package/templates/content/en/CareersList/careers.mdx +24 -0
  402. package/templates/content/en/CaseStudiesList/case-studies.mdx +22 -0
  403. package/templates/content/en/CaseStudiesList/showcase.mdx +8 -0
  404. package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +32 -0
  405. package/templates/content/en/CaseStudy/schema-builder-migration.mdx +41 -0
  406. package/templates/content/en/ClientsList/clients.mdx +38 -0
  407. package/templates/content/en/ContactPage/contact.mdx +19 -0
  408. package/templates/content/en/ContactPage/contact.schema.json +100 -0
  409. package/templates/content/en/ContactPoint/primary.mdx +14 -0
  410. package/templates/content/en/Course/aeo-fundamentals.mdx +37 -0
  411. package/templates/content/en/Course/schema-builder-workshop.mdx +62 -0
  412. package/templates/content/en/CoursesList/courses.mdx +22 -0
  413. package/templates/content/en/DownloadsList/downloads.mdx +22 -0
  414. package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +39 -0
  415. package/templates/content/en/Event/ailk-launch-day-2026.mdx +35 -0
  416. package/templates/content/en/EventsList/events.mdx +26 -0
  417. package/templates/content/en/FAQPage/faq.mdx +420 -0
  418. package/templates/content/en/FAQPage/faq.schema.json +231 -0
  419. package/templates/content/en/GlossaryList/glossary.mdx +19 -0
  420. package/templates/content/en/GlossaryList/glossary.schema.json +300 -0
  421. package/templates/content/en/HomePage/home.mdx +312 -0
  422. package/templates/content/en/HomePage/home.schema.json +88 -0
  423. package/templates/content/en/HomePage/launch-service.mdx +475 -0
  424. package/templates/content/en/HomePage/launch-service.schema.json +138 -0
  425. package/templates/content/en/HowTo/add-mdx-content.mdx +38 -0
  426. package/templates/content/en/HowTo/configure-aeo-scoring.mdx +35 -0
  427. package/templates/content/en/HowTo/set-up-json-ld.mdx +36 -0
  428. package/templates/content/en/Integration/github-actions.mdx +31 -0
  429. package/templates/content/en/Integration/openai-api.mdx +26 -0
  430. package/templates/content/en/Integration/vercel.mdx +32 -0
  431. package/templates/content/en/IntegrationsList/integrations.mdx +22 -0
  432. package/templates/content/en/JobPosting/content-strategist.mdx +46 -0
  433. package/templates/content/en/JobPosting/developer-advocate.mdx +47 -0
  434. package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +57 -0
  435. package/templates/content/en/LandingPage/aeo-audit.mdx +19 -0
  436. package/templates/content/en/LandingPage/aeo-audit.schema.json +92 -0
  437. package/templates/content/en/LandingPage/agency-waitlist.mdx +340 -0
  438. package/templates/content/en/LandingPage/agency-waitlist.schema.json +152 -0
  439. package/templates/content/en/LandingPage/developers.mdx +19 -0
  440. package/templates/content/en/LandingPage/developers.schema.json +109 -0
  441. package/templates/content/en/LandingPage/for-agencies.mdx +390 -0
  442. package/templates/content/en/LandingPage/for-agencies.schema.json +107 -0
  443. package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +417 -0
  444. package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +105 -0
  445. package/templates/content/en/LandingPage/for-businesses.mdx +19 -0
  446. package/templates/content/en/LandingPage/for-businesses.schema.json +96 -0
  447. package/templates/content/en/LandingPage/platform.mdx +369 -0
  448. package/templates/content/en/LandingPage/platform.schema.json +166 -0
  449. package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +19 -0
  450. package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +102 -0
  451. package/templates/content/en/LandingPage/vs-wordpress.mdx +19 -0
  452. package/templates/content/en/LandingPage/vs-wordpress.schema.json +53 -0
  453. package/templates/content/en/NewsletterSignup/newsletter.mdx +236 -0
  454. package/templates/content/en/NewsletterSignup/newsletter.schema.json +91 -0
  455. package/templates/content/en/Organization/site-owner.mdx +16 -0
  456. package/templates/content/en/Partner/prisma.mdx +31 -0
  457. package/templates/content/en/Partner/supabase.mdx +30 -0
  458. package/templates/content/en/Partner/vercel.mdx +28 -0
  459. package/templates/content/en/PartnersList/partners.mdx +22 -0
  460. package/templates/content/en/Person/founder.mdx +18 -0
  461. package/templates/content/en/Pricing/pricing.mdx +538 -0
  462. package/templates/content/en/Pricing/pricing.schema.json +209 -0
  463. package/templates/content/en/PrivacyPage/privacy.mdx +165 -0
  464. package/templates/content/en/PrivacyPage/privacy.schema.json +64 -0
  465. package/templates/content/en/Product/ailk.mdx +19 -0
  466. package/templates/content/en/Product/ailk.schema.json +94 -0
  467. package/templates/content/en/Product/api-launch-kit.mdx +26 -0
  468. package/templates/content/en/Product/launch-kit.mdx +28 -0
  469. package/templates/content/en/ProductsList/products.mdx +26 -0
  470. package/templates/content/en/Project/ailk-phase-2.mdx +54 -0
  471. package/templates/content/en/ProjectsList/projects.mdx +25 -0
  472. package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +27 -0
  473. package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +28 -0
  474. package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +39 -0
  475. package/templates/content/en/ResourcesHub/resources.mdx +22 -0
  476. package/templates/content/en/Service/consulting.mdx +22 -0
  477. package/templates/content/en/Service/implementation.mdx +30 -0
  478. package/templates/content/en/Service/launch-service.mdx +232 -0
  479. package/templates/content/en/Service/launch.mdx +228 -0
  480. package/templates/content/en/ServicesList/services.mdx +84 -0
  481. package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +44 -0
  482. package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +47 -0
  483. package/templates/content/en/SoftwareProduct/ailk-schema.mdx +49 -0
  484. package/templates/content/en/TeamList/team.mdx +63 -0
  485. package/templates/content/en/TeamMember/founder.mdx +72 -0
  486. package/templates/content/en/TermsPage/terms.mdx +19 -0
  487. package/templates/content/en/TermsPage/terms.schema.json +99 -0
  488. package/templates/content/en/TestimonialsList/testimonials.mdx +44 -0
  489. package/templates/content/en/UseCasesList/use-cases.mdx +30 -0
  490. package/templates/content/en/Whitepaper/ax-first-product-development.mdx +27 -0
  491. package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +27 -0
  492. package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +31 -0
  493. package/templates/content/en-XA/AboutPage/about.mdx +8 -0
  494. package/templates/content/en-XA/FAQPage/faq.mdx +8 -0
  495. package/templates/content/en-XA/HomePage/home.mdx +8 -0
  496. package/templates/content/es/_site.mdx +30 -0
  497. package/templates/content/fr/_site.mdx +30 -0
  498. package/templates/content/ja/_site.mdx +30 -0
  499. package/templates/content/pt-BR/AboutPage/about.mdx +8 -0
  500. package/templates/content/pt-BR/FAQPage/faq.mdx +8 -0
  501. package/templates/content/pt-BR/HomePage/home.mdx +23 -0
  502. package/templates/database/CHANGELOG.md +5 -0
  503. package/templates/database/README.md +234 -0
  504. package/templates/database/__tests__/user-cascade.test.ts +125 -0
  505. package/templates/database/auth/.gitkeep +0 -0
  506. package/templates/database/config/.gitkeep +0 -0
  507. package/templates/database/content/.gitkeep +0 -0
  508. package/templates/database/inbox/.gitkeep +0 -0
  509. package/templates/database/inbox/README.md +3 -0
  510. package/templates/database/inbox/__tests__/schema.test.ts +40 -0
  511. package/templates/database/inbox/schema.prisma +52 -0
  512. package/templates/database/jest.config.cjs +29 -0
  513. package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +328 -0
  514. package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +100 -0
  515. package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +28 -0
  516. package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +25 -0
  517. package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +42 -0
  518. package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +67 -0
  519. package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +21 -0
  520. package/templates/database/migrations/migration_lock.toml +3 -0
  521. package/templates/database/ops/.gitkeep +0 -0
  522. package/templates/database/package.json +45 -0
  523. package/templates/database/prisma.config.ts +20 -0
  524. package/templates/database/schema.prisma +487 -0
  525. package/templates/database/scripts/db-generate-locked.sh +80 -0
  526. package/templates/database/seed-test.ts +85 -0
  527. package/templates/database/tsconfig.build.json +16 -0
  528. package/templates/database/tsconfig.json +9 -0
  529. package/templates/package.json +125 -0
  530. package/templates/pnpm-workspace.yaml +6 -0
  531. package/templates/project.yaml +59 -0
  532. package/templates/tsconfig.base.json +18 -0
  533. package/templates/tsconfig.json +45 -0
@@ -0,0 +1,693 @@
1
+ // Section-catalog route skeleton tests.
2
+ // Each route is an async server component; we await the returned ReactElement.
3
+ // getMdxSource and getPageSections are injected via optional props instead of
4
+ // mocking internal modules — tests are independent of the on-disk /content/
5
+ // tree without coupling to module internals.
6
+ // next-mdx-remote/rsc is mocked (external boundary — RSC compiler not available under jsdom).
7
+ //
8
+ // Coverage per spec:
9
+ // - route-renders (with seed MDX) — confirms page renders and emits JSON-LD
10
+ // - empty-state (without seed MDX) — confirms graceful fallback, no crash
11
+
12
+ import { render, screen } from "@testing-library/react";
13
+
14
+ jest.mock("next-mdx-remote/rsc", () => ({
15
+ __esModule: true,
16
+ MDXRemote: ({ source }: { source: string }): React.ReactElement => (
17
+ <div data-testid="mdx-rendered">{source}</div>
18
+ ),
19
+ }));
20
+
21
+ jest.mock("next/navigation", () => ({
22
+ __esModule: true,
23
+ notFound: jest.fn(() => {
24
+ throw new Error("NEXT_NOT_FOUND");
25
+ }),
26
+ }));
27
+
28
+ // Renders a real <h1>/<p> for a leading hero section — mirroring the real
29
+ // Hero section component (packages/ui) closely enough to exercise the
30
+ // renderPage leading-hero carve-out (#3301: renderPage omits its own
31
+ // top-level <h1>/<p> when sections[0] is a hero, trusting the hero section
32
+ // to supply the page's sole heading + lead paragraph).
33
+ jest.mock("@working-theory/ui", () => ({
34
+ __esModule: true,
35
+ SectionRenderer: ({
36
+ sections,
37
+ }: {
38
+ sections: Array<{
39
+ type: string;
40
+ data?: { headline?: string; subheadline?: string };
41
+ }>;
42
+ }): React.ReactElement => (
43
+ <div data-testid="section-renderer" data-section-count={sections.length}>
44
+ {sections.map((section, i) =>
45
+ section.type === "hero" ? (
46
+ <div key={i} data-testid="section-item">
47
+ <h1>{section.data?.headline}</h1>
48
+ {section.data?.subheadline ? (
49
+ <p>{section.data.subheadline}</p>
50
+ ) : null}
51
+ </div>
52
+ ) : (
53
+ <div key={i} data-testid="section-item" />
54
+ ),
55
+ )}
56
+ </div>
57
+ ),
58
+ }));
59
+
60
+ import * as React from "react";
61
+
62
+ import { notFound } from "next/navigation";
63
+
64
+ import AboutPage from "../app/[locale]/about/page";
65
+ import ContactPage from "../app/[locale]/contact/page";
66
+ import FaqPage from "../app/[locale]/faq/page";
67
+ import ProductsPage from "../app/[locale]/products/page";
68
+ import ProductSlugPage from "../app/[locale]/products/[slug]/page";
69
+ import ServicesPage from "../app/[locale]/services/page";
70
+ import ServiceSlugPage from "../app/[locale]/services/[slug]/page";
71
+ import ProjectsPage from "../app/[locale]/projects/page";
72
+ import ProjectSlugPage from "../app/[locale]/projects/[slug]/page";
73
+
74
+ import type { getMdxSource, getPageSections } from "@working-theory/page-renderer";
75
+
76
+ type GetMdxSource = typeof getMdxSource;
77
+ type GetPageSections = typeof getPageSections;
78
+
79
+ const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
80
+
81
+ async function renderAsync(
82
+ element: Promise<React.ReactElement> | React.ReactElement,
83
+ ): Promise<void> {
84
+ const resolved = await element;
85
+ render(resolved);
86
+ }
87
+
88
+ const CONTACT_HERO = {
89
+ id: "hero-1",
90
+ type: "hero" as const,
91
+ data: { headline: "Contact AI Launch Kit" },
92
+ };
93
+ const CONTACT_CONTENT_BLOCK = {
94
+ id: "cb-1",
95
+ type: "content-block" as const,
96
+ data: { body: "Open an issue on GitHub for bugs and feature requests." },
97
+ };
98
+ const CONTACT_CTA = {
99
+ id: "cta-1",
100
+ type: "cta" as const,
101
+ data: {
102
+ headline: "Have a question?",
103
+ primaryCta: { label: "Open an issue", href: "https://github.com/tailwind-ai/ai-launch-kit/issues" },
104
+ },
105
+ };
106
+
107
+ const FAQ_HERO = {
108
+ id: "hero-1",
109
+ type: "hero" as const,
110
+ data: { headline: "Frequently Asked Questions" },
111
+ };
112
+ const FAQ_FAQ = {
113
+ id: "faq-1",
114
+ type: "faq" as const,
115
+ data: {
116
+ headline: "Common questions",
117
+ items: [
118
+ { question: "What is AEO?", answer: "AEO is Answer Engine Optimization." },
119
+ ],
120
+ variant: "offset" as const,
121
+ presentation: "expanded" as const,
122
+ },
123
+ };
124
+
125
+ const ABOUT_HERO = {
126
+ id: "hero-1",
127
+ type: "hero" as const,
128
+ data: { headline: "About AI Launch Kit" },
129
+ };
130
+ const ABOUT_CONTENT_BLOCK = {
131
+ id: "cb-1",
132
+ type: "content-block" as const,
133
+ data: { body: "The AX-first template extracted from production." },
134
+ };
135
+ const ABOUT_CTA = {
136
+ id: "cta-1",
137
+ type: "cta" as const,
138
+ data: {
139
+ headline: "Try it now",
140
+ primaryCta: { label: "Clone", href: "https://github.com" },
141
+ },
142
+ };
143
+
144
+ describe("apps/web — section-catalog route skeletons", () => {
145
+ beforeEach(() => {
146
+ mockNotFound.mockClear();
147
+ });
148
+
149
+ // ── /about ────────────────────────────────────────────────────────────────
150
+
151
+ describe("/about", () => {
152
+ describe("with AboutPage MDX present and sections", () => {
153
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
154
+ frontmatter: {
155
+ type: "AboutPage",
156
+ slug: "about",
157
+ title: "About AI Launch Kit",
158
+ description: "Why AILK exists.",
159
+ },
160
+ body: "# About\n\nBody copy.",
161
+ });
162
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([
163
+ ABOUT_HERO,
164
+ ABOUT_CONTENT_BLOCK,
165
+ ABOUT_CTA,
166
+ ]);
167
+
168
+ beforeEach(() => {
169
+ stubGetMdxSource.mockClear();
170
+ stubGetPageSections.mockClear();
171
+ });
172
+
173
+ it("renders the h1 from MDX frontmatter", async () => {
174
+ await renderAsync(AboutPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
175
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
176
+ "About AI Launch Kit",
177
+ );
178
+ });
179
+
180
+ it("emits a JSON-LD script tag with @type AboutPage", async () => {
181
+ await renderAsync(AboutPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
182
+ const script = screen.getByTestId("json-ld");
183
+ expect(script.getAttribute("type")).toBe("application/ld+json");
184
+ const parsed = JSON.parse(script.innerHTML) as { "@type": string };
185
+ expect(parsed["@type"]).toBe("AboutPage");
186
+ });
187
+
188
+ it("renders SectionRenderer with sections from frontmatter", async () => {
189
+ await renderAsync(AboutPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
190
+ const renderer = screen.getByTestId("section-renderer");
191
+ expect(renderer.dataset.sectionCount).toBe("3");
192
+ });
193
+
194
+ it("does not render the MDX body directly", async () => {
195
+ await renderAsync(AboutPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
196
+ expect(screen.queryByTestId("mdx-body")).toBeNull();
197
+ });
198
+ });
199
+
200
+ describe("with AboutPage MDX present but no sections (empty state)", () => {
201
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
202
+ frontmatter: {
203
+ type: "AboutPage",
204
+ slug: "about",
205
+ title: "About AI Launch Kit",
206
+ description: "Why AILK exists.",
207
+ },
208
+ body: "# About\n\nBody copy.",
209
+ });
210
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([]);
211
+
212
+ it("renders the page without crashing (200)", async () => {
213
+ await renderAsync(AboutPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
214
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
215
+ "About AI Launch Kit",
216
+ );
217
+ });
218
+ });
219
+
220
+ describe("with no AboutPage MDX (empty state)", () => {
221
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue(null);
222
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([]);
223
+
224
+ it("renders the fallback sentinel without crashing", async () => {
225
+ await renderAsync(AboutPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
226
+ expect(screen.getByTestId("section-empty-state")).toBeInTheDocument();
227
+ });
228
+ });
229
+ });
230
+
231
+ // ── /contact ──────────────────────────────────────────────────────────────
232
+
233
+ describe("/contact", () => {
234
+ describe("with ContactPage MDX present and sections", () => {
235
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
236
+ frontmatter: {
237
+ type: "ContactPage",
238
+ slug: "contact",
239
+ title: "Contact",
240
+ description: "Get in touch.",
241
+ },
242
+ body: "# Contact\n\nBody.",
243
+ });
244
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([
245
+ CONTACT_HERO,
246
+ CONTACT_CONTENT_BLOCK,
247
+ CONTACT_CTA,
248
+ ]);
249
+
250
+ beforeEach(() => {
251
+ stubGetMdxSource.mockClear();
252
+ stubGetPageSections.mockClear();
253
+ });
254
+
255
+ it("renders the h1 from MDX frontmatter", async () => {
256
+ await renderAsync(ContactPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
257
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
258
+ "Contact",
259
+ );
260
+ });
261
+
262
+ it("emits a JSON-LD script tag with @type ContactPage", async () => {
263
+ await renderAsync(ContactPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
264
+ const script = screen.getByTestId("json-ld");
265
+ const parsed = JSON.parse(script.innerHTML) as { "@type": string };
266
+ expect(parsed["@type"]).toBe("ContactPage");
267
+ });
268
+
269
+ it("renders SectionRenderer with sections from frontmatter", async () => {
270
+ await renderAsync(ContactPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
271
+ const renderer = screen.getByTestId("section-renderer");
272
+ expect(renderer.dataset.sectionCount).toBe("3");
273
+ });
274
+
275
+ it("does not render the MDX body directly", async () => {
276
+ await renderAsync(ContactPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
277
+ expect(screen.queryByTestId("mdx-body")).toBeNull();
278
+ });
279
+ });
280
+
281
+ describe("with ContactPage MDX present but no sections (empty state)", () => {
282
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
283
+ frontmatter: {
284
+ type: "ContactPage",
285
+ slug: "contact",
286
+ title: "Contact",
287
+ description: "Get in touch.",
288
+ },
289
+ body: "# Contact\n\nBody.",
290
+ });
291
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([]);
292
+
293
+ it("renders the page without crashing (200)", async () => {
294
+ await renderAsync(ContactPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
295
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
296
+ "Contact",
297
+ );
298
+ });
299
+
300
+ it("renders a SectionRenderer with zero sections", async () => {
301
+ await renderAsync(ContactPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
302
+ const renderer = screen.getByTestId("section-renderer");
303
+ expect(renderer.dataset.sectionCount).toBe("0");
304
+ });
305
+ });
306
+
307
+ describe("with no ContactPage MDX (empty state)", () => {
308
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue(null);
309
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([]);
310
+
311
+ it("renders the fallback sentinel without crashing", async () => {
312
+ await renderAsync(ContactPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
313
+ expect(screen.getByTestId("section-empty-state")).toBeInTheDocument();
314
+ });
315
+ });
316
+ });
317
+
318
+ // ── /faq ──────────────────────────────────────────────────────────────────
319
+
320
+ describe("/faq", () => {
321
+ describe("with FAQPage MDX present and sections", () => {
322
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
323
+ frontmatter: {
324
+ type: "FAQPage",
325
+ slug: "faq",
326
+ title: "Frequently Asked Questions",
327
+ description: "Answers to common questions.",
328
+ schema: {
329
+ mainEntity: [
330
+ {
331
+ "@type": "Question",
332
+ name: "What is AEO?",
333
+ acceptedAnswer: {
334
+ "@type": "Answer",
335
+ text: "AEO is Answer Engine Optimization.",
336
+ },
337
+ },
338
+ ],
339
+ },
340
+ },
341
+ body: "# FAQ\n\nBody.",
342
+ });
343
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([FAQ_HERO, FAQ_FAQ]);
344
+
345
+ beforeEach(() => {
346
+ stubGetMdxSource.mockClear();
347
+ stubGetPageSections.mockClear();
348
+ });
349
+
350
+ it("renders the h1 from MDX frontmatter", async () => {
351
+ await renderAsync(FaqPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
352
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
353
+ "Frequently Asked Questions",
354
+ );
355
+ });
356
+
357
+ it("emits a JSON-LD script tag with @type FAQPage", async () => {
358
+ await renderAsync(FaqPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
359
+ const script = screen.getByTestId("json-ld");
360
+ const parsed = JSON.parse(script.innerHTML) as { "@type": string };
361
+ expect(parsed["@type"]).toBe("FAQPage");
362
+ });
363
+
364
+ it("emits FAQPage JSON-LD with mainEntity populated from schema override", async () => {
365
+ await renderAsync(FaqPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
366
+ const script = screen.getByTestId("json-ld");
367
+ const parsed = JSON.parse(script.innerHTML) as {
368
+ "@type": string;
369
+ mainEntity: Array<{ "@type": string; name: string }>;
370
+ };
371
+ expect(Array.isArray(parsed.mainEntity)).toBe(true);
372
+ const [firstItem] = parsed.mainEntity;
373
+ expect(firstItem?.["@type"]).toBe("Question");
374
+ expect(firstItem?.name).toBe("What is AEO?");
375
+ });
376
+
377
+ it("renders SectionRenderer with sections from frontmatter", async () => {
378
+ await renderAsync(FaqPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
379
+ const renderer = screen.getByTestId("section-renderer");
380
+ expect(renderer.dataset.sectionCount).toBe("2");
381
+ });
382
+
383
+ it("does not render the MDX body directly", async () => {
384
+ await renderAsync(FaqPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
385
+ expect(screen.queryByTestId("mdx-body")).toBeNull();
386
+ });
387
+ });
388
+
389
+ describe("with FAQPage MDX present but no sections (empty state)", () => {
390
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
391
+ frontmatter: {
392
+ type: "FAQPage",
393
+ slug: "faq",
394
+ title: "Frequently Asked Questions",
395
+ description: "Answers to common questions.",
396
+ },
397
+ body: "# FAQ\n\nBody.",
398
+ });
399
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([]);
400
+
401
+ it("renders the page without crashing (200)", async () => {
402
+ await renderAsync(FaqPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
403
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
404
+ "Frequently Asked Questions",
405
+ );
406
+ });
407
+ });
408
+
409
+ describe("with no FAQPage MDX (empty state)", () => {
410
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue(null);
411
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([]);
412
+
413
+ it("renders the fallback sentinel without crashing", async () => {
414
+ await renderAsync(FaqPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
415
+ expect(screen.getByTestId("section-empty-state")).toBeInTheDocument();
416
+ });
417
+
418
+ it("emits a scaffold JSON-LD with @type FAQPage", async () => {
419
+ await renderAsync(FaqPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
420
+ const script = screen.getByTestId("json-ld");
421
+ const parsed = JSON.parse(script.innerHTML) as { "@type": string };
422
+ expect(parsed["@type"]).toBe("FAQPage");
423
+ });
424
+ });
425
+ });
426
+
427
+ // ── /products ─────────────────────────────────────────────────────────────
428
+
429
+ describe("/products", () => {
430
+ describe("with ProductsList MDX present", () => {
431
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
432
+ frontmatter: {
433
+ type: "ProductsList",
434
+ slug: "products",
435
+ title: "Products",
436
+ description: "All products.",
437
+ },
438
+ body: "# Products\n\nBody.",
439
+ });
440
+
441
+ it("renders the products listing page without crashing", async () => {
442
+ await renderAsync(ProductsPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource }));
443
+ expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
444
+ });
445
+
446
+ it("emits a JSON-LD with @type CollectionPage (productsListJsonLd)", async () => {
447
+ await renderAsync(ProductsPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource }));
448
+ const script = screen.getByTestId("json-ld");
449
+ const parsed = JSON.parse(script.innerHTML) as { "@type": string };
450
+ expect(parsed["@type"]).toBe("CollectionPage");
451
+ });
452
+ });
453
+
454
+ describe("with no ProductsList MDX (empty state)", () => {
455
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue(null);
456
+
457
+ it("renders the fallback sentinel without crashing", async () => {
458
+ await renderAsync(ProductsPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource }));
459
+ expect(screen.getByTestId("section-empty-state")).toBeInTheDocument();
460
+ });
461
+ });
462
+ });
463
+
464
+ // ── /products/[slug] ──────────────────────────────────────────────────────
465
+
466
+ describe("/products/[slug]", () => {
467
+ describe("with Product MDX present", () => {
468
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
469
+ frontmatter: {
470
+ type: "Product",
471
+ slug: "launch-kit",
472
+ title: "AI Launch Kit",
473
+ description: "The AX-first OSS template.",
474
+ },
475
+ body: "# AI Launch Kit\n\nBody.",
476
+ });
477
+
478
+ it("renders the h1 from MDX frontmatter", async () => {
479
+ await renderAsync(
480
+ ProductSlugPage({ params: Promise.resolve({ locale: "en", slug: "launch-kit" }), getMdxSource: stubGetMdxSource }),
481
+ );
482
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
483
+ "AI Launch Kit",
484
+ );
485
+ });
486
+
487
+ it("emits a JSON-LD script tag", async () => {
488
+ await renderAsync(
489
+ ProductSlugPage({ params: Promise.resolve({ locale: "en", slug: "launch-kit" }), getMdxSource: stubGetMdxSource }),
490
+ );
491
+ const script = screen.getByTestId("json-ld");
492
+ const outer = JSON.parse(script.innerHTML) as { "@type": string; "@graph": Array<{ "@type": string; mainEntity: { "@type": string } }> };
493
+ expect(outer["@type"]).toBeUndefined();
494
+ const primary = outer["@graph"][0]!;
495
+ expect(primary["@type"]).toBe("WebPage");
496
+ expect(primary.mainEntity["@type"]).toBe("SoftwareApplication");
497
+ });
498
+ });
499
+
500
+ describe("with no Product MDX (unknown slug)", () => {
501
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue(null);
502
+
503
+ it("calls notFound() for an unknown slug", async () => {
504
+ await expect(
505
+ ProductSlugPage({ params: Promise.resolve({ locale: "en", slug: "unknown" }), getMdxSource: stubGetMdxSource }),
506
+ ).rejects.toThrow("NEXT_NOT_FOUND");
507
+ expect(mockNotFound).toHaveBeenCalled();
508
+ });
509
+ });
510
+ });
511
+
512
+ // ── /services ─────────────────────────────────────────────────────────────
513
+
514
+ describe("/services", () => {
515
+ describe("with ServicesList MDX present", () => {
516
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
517
+ frontmatter: {
518
+ type: "ServicesList",
519
+ slug: "services",
520
+ title: "Services",
521
+ description: "All services.",
522
+ },
523
+ body: "# Services\n\nBody.",
524
+ });
525
+
526
+ it("renders the services listing page without crashing", async () => {
527
+ await renderAsync(ServicesPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource }));
528
+ expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
529
+ });
530
+
531
+ it("emits a JSON-LD with @type CollectionPage (servicesListJsonLd)", async () => {
532
+ await renderAsync(ServicesPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource }));
533
+ const script = screen.getByTestId("json-ld");
534
+ const parsed = JSON.parse(script.innerHTML) as { "@type": string };
535
+ expect(parsed["@type"]).toBe("CollectionPage");
536
+ });
537
+ });
538
+
539
+ describe("with no ServicesList MDX (empty state)", () => {
540
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue(null);
541
+
542
+ it("renders the fallback sentinel without crashing", async () => {
543
+ await renderAsync(ServicesPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource }));
544
+ expect(screen.getByTestId("section-empty-state")).toBeInTheDocument();
545
+ });
546
+ });
547
+ });
548
+
549
+ // ── /services/[slug] ──────────────────────────────────────────────────────
550
+
551
+ describe("/services/[slug]", () => {
552
+ describe("with Service MDX present", () => {
553
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
554
+ frontmatter: {
555
+ type: "Service",
556
+ slug: "consulting",
557
+ title: "Implementation consulting",
558
+ description: "Help adapting AI Launch Kit.",
559
+ },
560
+ body: "# Implementation consulting\n\nBody.",
561
+ });
562
+
563
+ it("renders the h1 from MDX frontmatter", async () => {
564
+ await renderAsync(
565
+ ServiceSlugPage({ params: Promise.resolve({ locale: "en", slug: "consulting" }), getMdxSource: stubGetMdxSource }),
566
+ );
567
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
568
+ "Implementation consulting",
569
+ );
570
+ });
571
+
572
+ it("emits a JSON-LD script tag", async () => {
573
+ await renderAsync(
574
+ ServiceSlugPage({ params: Promise.resolve({ locale: "en", slug: "consulting" }), getMdxSource: stubGetMdxSource }),
575
+ );
576
+ const script = screen.getByTestId("json-ld");
577
+ const outer = JSON.parse(script.innerHTML) as { "@type": string; "@graph": Array<{ "@type": string; mainEntity: { "@type": string } }> };
578
+ expect(outer["@type"]).toBeUndefined();
579
+ const primary = outer["@graph"][0]!;
580
+ expect(primary["@type"]).toBe("WebPage");
581
+ expect(primary.mainEntity["@type"]).toBe("Service");
582
+ });
583
+ });
584
+
585
+ describe("with no Service MDX (unknown slug)", () => {
586
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue(null);
587
+
588
+ it("calls notFound() for an unknown slug", async () => {
589
+ await expect(
590
+ ServiceSlugPage({ params: Promise.resolve({ locale: "en", slug: "unknown" }), getMdxSource: stubGetMdxSource }),
591
+ ).rejects.toThrow("NEXT_NOT_FOUND");
592
+ expect(mockNotFound).toHaveBeenCalled();
593
+ });
594
+ });
595
+ });
596
+
597
+ // ── /projects ─────────────────────────────────────────────────────────────
598
+
599
+ describe("/projects", () => {
600
+ // ProjectsList is in SchemaOrgType; page reads from MDX.
601
+ // Smoke test: no MDX mock → falls back to the sentinel without crashing.
602
+ it("renders the fallback sentinel when no MDX is present", async () => {
603
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue(null);
604
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([]);
605
+ await renderAsync(ProjectsPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
606
+ expect(
607
+ screen.getByTestId("section-empty-state"),
608
+ ).toBeInTheDocument();
609
+ });
610
+
611
+ it("renders the h1 from MDX frontmatter when MDX is present", async () => {
612
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
613
+ frontmatter: {
614
+ type: "ProjectsList",
615
+ slug: "projects",
616
+ title: "Projects",
617
+ description: "A selection of build engagements.",
618
+ },
619
+ body: "# Projects",
620
+ });
621
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([]);
622
+ await renderAsync(ProjectsPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
623
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
624
+ "Projects",
625
+ );
626
+ });
627
+
628
+ it("emits a JSON-LD script tag with @type CollectionPage when MDX present", async () => {
629
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
630
+ frontmatter: {
631
+ type: "ProjectsList",
632
+ slug: "projects",
633
+ title: "Projects",
634
+ description: "A selection of build engagements.",
635
+ },
636
+ body: "# Projects",
637
+ });
638
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([]);
639
+ await renderAsync(ProjectsPage({ params: Promise.resolve({ locale: "en" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }));
640
+ const script = screen.getByTestId("json-ld");
641
+ const parsed = JSON.parse(script.innerHTML) as { "@type": string };
642
+ expect(parsed["@type"]).toBe("CollectionPage");
643
+ });
644
+ });
645
+
646
+ // ── /projects/[slug] ──────────────────────────────────────────────────────
647
+
648
+ describe("/projects/[slug]", () => {
649
+ // Project is in SchemaOrgType; page reads from MDX and calls notFound() on miss.
650
+ it("renders the project detail from MDX frontmatter", async () => {
651
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
652
+ frontmatter: {
653
+ type: "Project",
654
+ slug: "ailk-phase-2",
655
+ title: "AILK Phase 2 — Builder Port",
656
+ description: "Expanding the schema.org builder layer.",
657
+ publishedAt: "2026-05-07",
658
+ },
659
+ body: "# AILK Phase 2",
660
+ });
661
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([]);
662
+ await renderAsync(
663
+ ProjectSlugPage({ params: Promise.resolve({ locale: "en", slug: "ailk-phase-2" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }),
664
+ );
665
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
666
+ "AILK Phase 2 — Builder Port",
667
+ );
668
+ });
669
+
670
+ it("emits a JSON-LD script tag with @type CreativeWork", async () => {
671
+ const stubGetMdxSource = jest.fn<ReturnType<GetMdxSource>, Parameters<GetMdxSource>>().mockResolvedValue({
672
+ frontmatter: {
673
+ type: "Project",
674
+ slug: "ailk-phase-2",
675
+ title: "AILK Phase 2 — Builder Port",
676
+ description: "Expanding the schema.org builder layer.",
677
+ publishedAt: "2026-05-07",
678
+ },
679
+ body: "# AILK Phase 2",
680
+ });
681
+ const stubGetPageSections = jest.fn<ReturnType<GetPageSections>, Parameters<GetPageSections>>().mockResolvedValue([]);
682
+ await renderAsync(
683
+ ProjectSlugPage({ params: Promise.resolve({ locale: "en", slug: "ailk-phase-2" }), getMdxSource: stubGetMdxSource, getPageSections: stubGetPageSections }),
684
+ );
685
+ const script = screen.getByTestId("json-ld");
686
+ const outer = JSON.parse(script.innerHTML) as { "@type": string; "@graph": Array<{ "@type": string; mainEntity: { "@type": string } }> };
687
+ expect(outer["@type"]).toBeUndefined();
688
+ const primary = outer["@graph"][0]!;
689
+ expect(primary["@type"]).toBe("WebPage");
690
+ expect(primary.mainEntity["@type"]).toBe("CreativeWork");
691
+ });
692
+ });
693
+ });