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,130 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ContactForm — client component that wires the contact form to
5
+ * captureLeadAction via useFormState.
6
+ *
7
+ * Validation errors render inline next to each field.
8
+ * Success shows a confirmation message.
9
+ * Server errors show a generic fallback message.
10
+ *
11
+ * Honeypot + rate-limit enforcement lives in apps/api; no duplication here.
12
+ */
13
+
14
+ import { useTranslations } from "next-intl";
15
+ import { useFormState, useFormStatus } from "react-dom";
16
+
17
+ import { captureLeadAction } from "./contact.actions";
18
+
19
+ function SubmitButton() {
20
+ const t = useTranslations("contact");
21
+ const { pending } = useFormStatus();
22
+ return (
23
+ <button type="submit" disabled={pending} data-testid="contact-submit">
24
+ {pending ? t("sending") : t("sendMessage")}
25
+ </button>
26
+ );
27
+ }
28
+
29
+ export function ContactForm() {
30
+ const t = useTranslations("contact");
31
+ const [state, formAction] = useFormState(captureLeadAction, null);
32
+
33
+ if (state?.kind === "success") {
34
+ return (
35
+ <p data-testid="contact-success">
36
+ {t("successMessage")}
37
+ </p>
38
+ );
39
+ }
40
+
41
+ const fieldErrors = state?.kind === "error" ? (state.fieldErrors ?? {}) : {};
42
+ const formError =
43
+ state?.kind === "error" && !state.fieldErrors ? state.message : undefined;
44
+
45
+ return (
46
+ <form action={formAction} noValidate>
47
+ {formError && (
48
+ <p role="alert" data-testid="contact-form-error">
49
+ {formError}
50
+ </p>
51
+ )}
52
+
53
+ {/* Honeypot — hidden from users; bots fill it; API rejects non-empty */}
54
+ <input
55
+ type="text"
56
+ name="website"
57
+ tabIndex={-1}
58
+ aria-hidden="true"
59
+ style={{ display: "none" }}
60
+ />
61
+
62
+ <div>
63
+ <label htmlFor="contact-name">{t("nameLabel")}</label>
64
+ <input
65
+ id="contact-name"
66
+ name="name"
67
+ type="text"
68
+ autoComplete="name"
69
+ required
70
+ aria-describedby={fieldErrors.name ? "contact-name-error" : undefined}
71
+ />
72
+ {fieldErrors.name && (
73
+ <span
74
+ id="contact-name-error"
75
+ role="alert"
76
+ data-testid="contact-name-error"
77
+ >
78
+ {fieldErrors.name}
79
+ </span>
80
+ )}
81
+ </div>
82
+
83
+ <div>
84
+ <label htmlFor="contact-email">{t("emailLabel")}</label>
85
+ <input
86
+ id="contact-email"
87
+ name="email"
88
+ type="email"
89
+ autoComplete="email"
90
+ required
91
+ aria-describedby={
92
+ fieldErrors.email ? "contact-email-error" : undefined
93
+ }
94
+ />
95
+ {fieldErrors.email && (
96
+ <span
97
+ id="contact-email-error"
98
+ role="alert"
99
+ data-testid="contact-email-error"
100
+ >
101
+ {fieldErrors.email}
102
+ </span>
103
+ )}
104
+ </div>
105
+
106
+ <div>
107
+ <label htmlFor="contact-message">{t("messageLabel")}</label>
108
+ <textarea
109
+ id="contact-message"
110
+ name="message"
111
+ rows={5}
112
+ aria-describedby={
113
+ fieldErrors.message ? "contact-message-error" : undefined
114
+ }
115
+ />
116
+ {fieldErrors.message && (
117
+ <span
118
+ id="contact-message-error"
119
+ role="alert"
120
+ data-testid="contact-message-error"
121
+ >
122
+ {fieldErrors.message}
123
+ </span>
124
+ )}
125
+ </div>
126
+
127
+ <SubmitButton />
128
+ </form>
129
+ );
130
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Tests for captureLeadAction Server Action.
3
+ *
4
+ * Acceptance criteria verified here:
5
+ * - captureLeadAction(prevState, validFormData) calls api-client leads.submit
6
+ * and returns success state
7
+ * - captureLeadAction(prevState, invalidFormData) returns Zod validation errors
8
+ * without calling api-client
9
+ * - captureLeadAction returns generic error on API failure
10
+ * - Parity: captureLeadAction is paired with ailkCaptureLead per ADR 0013
11
+ */
12
+
13
+ import { captureLeadAction } from "../contact.actions.js";
14
+
15
+ // Mock @working-theory/api-client
16
+ jest.mock("@working-theory/api-client", () => ({
17
+ leads: {
18
+ submit: jest.fn(),
19
+ },
20
+ }));
21
+
22
+ import { leads } from "@working-theory/api-client";
23
+ const mockSubmit = leads.submit as jest.Mock;
24
+
25
+ afterEach(() => {
26
+ jest.clearAllMocks();
27
+ });
28
+
29
+ function makeFormData(fields: Record<string, string>): FormData {
30
+ const fd = new FormData();
31
+ for (const [key, value] of Object.entries(fields)) {
32
+ fd.append(key, value);
33
+ }
34
+ return fd;
35
+ }
36
+
37
+ const VALID_FIELDS = {
38
+ name: "Jane Smith",
39
+ email: "jane@example.com",
40
+ message: "I am interested in your services.",
41
+ };
42
+
43
+ const INITIAL_STATE = null;
44
+
45
+ describe("captureLeadAction — valid input", () => {
46
+ it("returns success state when api-client responds ok", async () => {
47
+ mockSubmit.mockResolvedValueOnce({
48
+ ok: true,
49
+ body: { id: "lead_abc123", persisted: "db" },
50
+ });
51
+
52
+ const result = await captureLeadAction(
53
+ INITIAL_STATE,
54
+ makeFormData(VALID_FIELDS),
55
+ );
56
+
57
+ expect(result).toMatchObject({ kind: "success" });
58
+ });
59
+
60
+ it("calls leads.submit with mapped payload from formData", async () => {
61
+ mockSubmit.mockResolvedValueOnce({
62
+ ok: true,
63
+ body: { id: "lead_abc123", persisted: "db" },
64
+ });
65
+
66
+ await captureLeadAction(INITIAL_STATE, makeFormData(VALID_FIELDS));
67
+
68
+ expect(mockSubmit).toHaveBeenCalledTimes(1);
69
+ const body = mockSubmit.mock.calls[0][0] as {
70
+ email: string;
71
+ payload: { name: string; message?: string };
72
+ };
73
+ expect(body.email).toBe("jane@example.com");
74
+ expect(body.payload.name).toBe("Jane Smith");
75
+ expect(body.payload.message).toBe("I am interested in your services.");
76
+ });
77
+ });
78
+
79
+ describe("captureLeadAction — invalid input (Zod errors)", () => {
80
+ it("returns field errors when email is invalid without calling api-client", async () => {
81
+ const result = await captureLeadAction(
82
+ INITIAL_STATE,
83
+ makeFormData({ name: "Jane", email: "not-an-email" }),
84
+ );
85
+
86
+ expect(result).toMatchObject({
87
+ kind: "error",
88
+ fieldErrors: expect.any(Object),
89
+ });
90
+ expect(mockSubmit).not.toHaveBeenCalled();
91
+ });
92
+
93
+ it("returns field errors when name is missing", async () => {
94
+ const result = await captureLeadAction(
95
+ INITIAL_STATE,
96
+ makeFormData({ name: "", email: "jane@example.com" }),
97
+ );
98
+
99
+ expect(result).toMatchObject({
100
+ kind: "error",
101
+ fieldErrors: expect.any(Object),
102
+ });
103
+ expect(mockSubmit).not.toHaveBeenCalled();
104
+ });
105
+
106
+ it("returns field errors when email is missing", async () => {
107
+ const result = await captureLeadAction(
108
+ INITIAL_STATE,
109
+ makeFormData({ name: "Jane", email: "" }),
110
+ );
111
+
112
+ expect(result).toMatchObject({
113
+ kind: "error",
114
+ fieldErrors: expect.any(Object),
115
+ });
116
+ expect(mockSubmit).not.toHaveBeenCalled();
117
+ });
118
+ });
119
+
120
+ describe("captureLeadAction — API error", () => {
121
+ it("returns generic error when api-client responds not ok", async () => {
122
+ mockSubmit.mockResolvedValueOnce({
123
+ ok: false,
124
+ status: 500,
125
+ body: { error: "internal_error" },
126
+ });
127
+
128
+ const result = await captureLeadAction(
129
+ INITIAL_STATE,
130
+ makeFormData(VALID_FIELDS),
131
+ );
132
+
133
+ expect(result).toMatchObject({
134
+ kind: "error",
135
+ message: expect.any(String),
136
+ });
137
+ });
138
+
139
+ it("returns generic error when api-client throws", async () => {
140
+ mockSubmit.mockRejectedValueOnce(new Error("Network error"));
141
+
142
+ const result = await captureLeadAction(
143
+ INITIAL_STATE,
144
+ makeFormData(VALID_FIELDS),
145
+ );
146
+
147
+ expect(result).toMatchObject({
148
+ kind: "error",
149
+ message: expect.any(String),
150
+ });
151
+ });
152
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * HTML snapshot regression tests for contact/page.tsx → @working-theory/page-renderer migration.
3
+ *
4
+ * Contact uses sections mode (not bodyMode: "mdx-body") with a ContactForm wrapper:
5
+ * <>{renderPage output}<ContactForm /></>
6
+ *
7
+ * Two-commit strategy:
8
+ * Commit 1: this file added; snapshots captured against pre-migration route (RED-passing).
9
+ * Commit 2: route migrated; same snapshots verify diff=0 (GREEN).
10
+ *
11
+ * Two states tested:
12
+ * - empty: getMdxSource returns null → renderPage empty-state + ContactForm present
13
+ * - populated: getMdxSource returns populated source → renderPage sections + ContactForm present
14
+ *
15
+ * Note on ContactForm in tests: ContactForm is a "use client" component that uses
16
+ * useFormState/useFormStatus hooks. jsdom does not support these hooks. The component
17
+ * renders as an opaque stub via the next-intl and react-dom mocks below. The snapshot
18
+ * asserts the ContactForm component is present in the output (non-null).
19
+ *
20
+ * Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
21
+ */
22
+ import React from "react";
23
+ import { render } from "@testing-library/react";
24
+
25
+ // ── @working-theory/ui mock ─────────────────────────────────────────────────────────────
26
+ jest.mock("@working-theory/ui", () => ({
27
+ SectionRenderer: ({ sections }: { sections: unknown[] }) =>
28
+ React.createElement("div", {
29
+ "data-testid": "section-renderer",
30
+ "data-section-count": String(sections.length),
31
+ }),
32
+ }));
33
+
34
+ // ── ContactForm mock ──────────────────────────────────────────────────────────
35
+ // ContactForm is "use client" and uses useFormState/useFormStatus. Mock it as a
36
+ // deterministic stub so the snapshot is stable.
37
+ // eslint-disable-next-line no-restricted-syntax -- test-seam mock required; ContactForm uses useFormState/useFormStatus which crash in jsdom; internal-module mock is the narrowest possible scope; tracked in #507
38
+ jest.mock("../ContactForm", () => ({
39
+ ContactForm: () =>
40
+ React.createElement("div", { "data-testid": "contact-form-stub" }),
41
+ }));
42
+
43
+ import ContactPage from "../page";
44
+
45
+ const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
46
+ const getPageSectionsEmpty = jest.fn().mockResolvedValue([]);
47
+
48
+ describe("ContactPage — snapshot regression", () => {
49
+ it("empty state renders deterministic HTML (renderPage empty-state + ContactForm present)", async () => {
50
+ const element = await ContactPage({
51
+ params: Promise.resolve({ locale: "en" }),
52
+ getMdxSource: getMdxSourceEmpty,
53
+ getPageSections: getPageSectionsEmpty,
54
+ });
55
+ const { container } = render(element);
56
+ // ContactForm must be present even in empty-state
57
+ expect(
58
+ container.querySelector('[data-testid="contact-form-stub"]'),
59
+ ).not.toBeNull();
60
+ expect(container.innerHTML).toMatchInlineSnapshot(
61
+ `"<main><h1>Contact</h1><p data-testid="section-empty-state">Contact page content coming soon.</p></main><div data-testid="contact-form-stub"></div>"`,
62
+ );
63
+ });
64
+
65
+ it("populated state renders deterministic HTML (renderPage sections + ContactForm present)", async () => {
66
+ const source = {
67
+ frontmatter: {
68
+ type: "ContactPage" as const,
69
+ slug: "contact",
70
+ title: "Contact Us",
71
+ description: "Get in touch.",
72
+ },
73
+ body: "",
74
+ };
75
+ const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
76
+ const getPageSectionsPopulated = jest.fn().mockResolvedValue([]);
77
+
78
+ const element = await ContactPage({
79
+ params: Promise.resolve({ locale: "en" }),
80
+ getMdxSource: getMdxSourcePopulated,
81
+ getPageSections: getPageSectionsPopulated,
82
+ });
83
+ const { container } = render(element);
84
+ // ContactForm must be present in populated state too
85
+ expect(
86
+ container.querySelector('[data-testid="contact-form-stub"]'),
87
+ ).not.toBeNull();
88
+ expect(container.innerHTML).toMatchInlineSnapshot(
89
+ `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"ContactPage","inLanguage":"en","name":"Contact Us","description":"Get in touch.","mainEntity":{"@type":"ContactPoint","contactType":"customer support"}}</script><main><h1>Contact Us</h1><p data-testid="contact-description">Get in touch.</p><div data-testid="section-renderer" data-section-count="0"></div></main><div data-testid="contact-form-stub"></div>"`,
90
+ );
91
+ });
92
+ });
@@ -0,0 +1,79 @@
1
+ "use server";
2
+
3
+ /**
4
+ * captureLeadAction — Server Action paired with ailkCaptureLead MCP tool.
5
+ *
6
+ * Validates contact-form input with submitLeadInputSchema (shared schema per
7
+ * ADR 0013), calls POST /api/leads/submit via @working-theory/api-client, and returns
8
+ * a discriminated ActionResult suitable for useFormState.
9
+ *
10
+ * Canonical filename: contact.actions.ts (*.actions.ts per ADR 0013 / U0 spec
11
+ * parity-test regex `ACTION_FILE_RE = /\.actions\.ts$/`).
12
+ *
13
+ * Honeypot + rate-limit enforcement lives in apps/api; no duplication here.
14
+ */
15
+
16
+ import { leads } from "@working-theory/api-client";
17
+ import { submitLeadInputSchema } from "@working-theory/validation";
18
+
19
+ export type ActionResult =
20
+ | { kind: "success" }
21
+ | {
22
+ kind: "error";
23
+ message?: string;
24
+ fieldErrors?: Record<string, string>;
25
+ }
26
+ | null;
27
+
28
+ export async function captureLeadAction(
29
+ _prevState: ActionResult,
30
+ formData: FormData,
31
+ ): Promise<ActionResult> {
32
+ const raw = {
33
+ name: formData.get("name"),
34
+ email: formData.get("email"),
35
+ message: formData.get("message") ?? undefined,
36
+ phone: formData.get("phone") ?? undefined,
37
+ };
38
+
39
+ const parsed = submitLeadInputSchema.safeParse(raw);
40
+ if (!parsed.success) {
41
+ const fieldErrors: Record<string, string> = {};
42
+ for (const issue of parsed.error.issues) {
43
+ const field = String(issue.path[0] ?? "form");
44
+ if (!fieldErrors[field]) {
45
+ fieldErrors[field] = issue.message;
46
+ }
47
+ }
48
+ return { kind: "error", fieldErrors };
49
+ }
50
+
51
+ const { name, email, message, phone } = parsed.data;
52
+ const siteId = process.env.AILK_SITE_ID ?? "default";
53
+ const body = {
54
+ siteId,
55
+ source: "web",
56
+ email,
57
+ payload: {
58
+ name,
59
+ ...(message !== undefined && { message }),
60
+ ...(phone !== undefined && { phone }),
61
+ },
62
+ };
63
+
64
+ try {
65
+ const res = await leads.submit(body);
66
+ if (!res.ok) {
67
+ return {
68
+ kind: "error",
69
+ message: "Something went wrong, please try again.",
70
+ };
71
+ }
72
+ return { kind: "success" };
73
+ } catch {
74
+ return {
75
+ kind: "error",
76
+ message: "Something went wrong, please try again.",
77
+ };
78
+ }
79
+ }
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ // Per-segment error boundary for the /contact (lead-capture) route.
3
+ import type { ReactElement } from "react";
4
+
5
+ import { ErrorFallback } from "../../error-fallback";
6
+
7
+ interface ErrorProps {
8
+ error: Error & { digest?: string };
9
+ reset: () => void;
10
+ }
11
+
12
+ export default function ContactError({
13
+ error,
14
+ reset,
15
+ }: ErrorProps): ReactElement {
16
+ return (
17
+ <ErrorFallback
18
+ error={error}
19
+ reset={reset}
20
+ heading="Your message didn't go through"
21
+ body="We weren't able to send your message. Please try again — your input has been preserved."
22
+ />
23
+ );
24
+ }
@@ -0,0 +1,38 @@
1
+ // Contact page. Renders sections via renderPage (sections mode) then appends
2
+ // ContactForm as a wrapper. JSON-LD and translations handled by renderPage.
3
+ import type {
4
+ getMdxSource as defaultGetMdxSource,
5
+ getPageSections as defaultGetPageSections,
6
+ } from "@working-theory/page-renderer";
7
+ import { renderPage } from "@working-theory/page-renderer";
8
+ import type { ReactElement } from "react";
9
+
10
+ import { ContactForm } from "./ContactForm";
11
+
12
+ interface Props {
13
+ params: Promise<{ locale: string }>;
14
+ getMdxSource?: typeof defaultGetMdxSource;
15
+ getPageSections?: typeof defaultGetPageSections;
16
+ }
17
+
18
+ export default async function Page({
19
+ params,
20
+ getMdxSource,
21
+ getPageSections,
22
+ }: Props): Promise<ReactElement> {
23
+ const { locale } = await params;
24
+ const rendered = await renderPage({
25
+ type: "ContactPage",
26
+ slug: "contact",
27
+ locale,
28
+ translationsNamespace: "contact",
29
+ ...(getMdxSource !== undefined && { getMdxSource }),
30
+ ...(getPageSections !== undefined && { getPageSections }),
31
+ });
32
+ return (
33
+ <>
34
+ {rendered}
35
+ <ContactForm />
36
+ </>
37
+ );
38
+ }
@@ -0,0 +1,34 @@
1
+ // Course detail page. Reads the Course MDX for the given slug, renders via
2
+ // @working-theory/page-renderer, and emits a JSON-LD <script>. Unknown slugs trigger 404.
3
+ import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
4
+ import { getMdxSlugs, renderPage } from "@working-theory/page-renderer";
5
+ import type { ReactElement } from "react";
6
+
7
+ interface Props {
8
+ params: Promise<{ locale: string; slug: string }>;
9
+ getMdxSource?: typeof defaultGetMdxSource;
10
+ }
11
+
12
+ export async function generateStaticParams({
13
+ params,
14
+ }: {
15
+ params: { locale: string };
16
+ }): Promise<{ slug: string }[]> {
17
+ const slugs = await getMdxSlugs("Course", params.locale);
18
+ return slugs.map((slug) => ({ slug }));
19
+ }
20
+
21
+ export default async function Page({
22
+ params,
23
+ getMdxSource,
24
+ }: Props): Promise<ReactElement> {
25
+ const { locale, slug } = await params;
26
+ return renderPage({
27
+ type: "Course",
28
+ slug,
29
+ locale,
30
+ bodyMode: "mdx-body",
31
+ populatedDescriptionTestId: "course-description",
32
+ ...(getMdxSource !== undefined && { getMdxSource }),
33
+ });
34
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * HTML snapshot regression tests for courses routes — @working-theory/page-renderer migration.
3
+ *
4
+ * Node version: captured against Node 22.x (same as CI).
5
+ * MDX seed content: DI-injected stub — no fs reads, deterministic.
6
+ *
7
+ * Two-commit strategy:
8
+ * Commit 1: this file added; snapshots captured against pre-migration routes.
9
+ * Commit 2: routes migrated; same snapshots verify diff=0.
10
+ *
11
+ * Routes covered:
12
+ * - courses/page (empty: no CoursesList MDX)
13
+ * - courses/page (populated: CoursesList MDX present)
14
+ * - courses/[slug] (populated: populated state)
15
+ * - courses/[slug] (notFound: unknown slug triggers notFound())
16
+ *
17
+ * Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
18
+ * A non-zero diff indicates a behavior regression in the migration. Escalate.
19
+ */
20
+ import React from "react";
21
+ import { render } from "@testing-library/react";
22
+
23
+ jest.mock("next-mdx-remote/rsc", () => ({
24
+ __esModule: true,
25
+ MDXRemote: ({ source }: { source: string }): React.ReactElement =>
26
+ React.createElement("div", { "data-testid": "mdx-rendered" }, source),
27
+ }));
28
+
29
+ jest.mock("next/navigation", () => ({
30
+ __esModule: true,
31
+ notFound: jest.fn(() => {
32
+ throw new Error("NEXT_NOT_FOUND");
33
+ }),
34
+ }));
35
+
36
+ import CoursesPage from "../page";
37
+ import CourseSlugPage from "../[slug]/page";
38
+ import { notFound } from "next/navigation";
39
+
40
+ const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
41
+
42
+ describe("courses/page — snapshot regression", () => {
43
+ it("empty state renders deterministic HTML", async () => {
44
+ const getMdxSource = jest.fn().mockResolvedValue(null);
45
+
46
+ const element = await CoursesPage({
47
+ params: Promise.resolve({ locale: "en" }),
48
+ getMdxSource,
49
+ });
50
+ const { container } = render(element);
51
+ expect(container.innerHTML).toMatchInlineSnapshot(
52
+ `"<main><h1>Courses</h1><p data-testid="section-empty-state">Courses listing coming soon.</p></main>"`,
53
+ );
54
+ });
55
+
56
+ it("populated state renders deterministic HTML", async () => {
57
+ const getMdxSource = jest.fn().mockResolvedValue({
58
+ frontmatter: {
59
+ type: "CoursesList" as const,
60
+ slug: "courses",
61
+ title: "Courses",
62
+ description: "Structured learning paths for teams adopting AILK.",
63
+ },
64
+ body: "## Courses\n\nBody content.",
65
+ });
66
+
67
+ const element = await CoursesPage({
68
+ params: Promise.resolve({ locale: "en" }),
69
+ getMdxSource,
70
+ });
71
+ const { container } = render(element);
72
+ expect(container.innerHTML).toMatchInlineSnapshot(`
73
+ "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Courses","description":"Structured learning paths for teams adopting AILK.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Course"}]}}</script><main><h1>Courses</h1><p data-testid="courses-description">Structured learning paths for teams adopting AILK.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">## Courses
74
+
75
+ Body content.</div></article></div></div></main>"
76
+ `);
77
+ });
78
+ });
79
+
80
+ describe("courses/[slug] — snapshot regression", () => {
81
+ beforeEach(() => {
82
+ mockNotFound.mockClear();
83
+ });
84
+
85
+ it("populated state renders deterministic HTML", async () => {
86
+ const getMdxSource = jest.fn().mockResolvedValue({
87
+ frontmatter: {
88
+ type: "Course" as const,
89
+ slug: "aeo-fundamentals",
90
+ title: "AEO Fundamentals",
91
+ description: "A practical course covering Layer 1 AEO rules.",
92
+ },
93
+ body: "## AEO Fundamentals\n\nBody content.",
94
+ });
95
+
96
+ const element = await CourseSlugPage({
97
+ params: Promise.resolve({ locale: "en", slug: "aeo-fundamentals" }),
98
+ getMdxSource,
99
+ });
100
+ const { container } = render(element);
101
+ expect(container.innerHTML).toMatchInlineSnapshot(`
102
+ "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AEO Fundamentals","description":"A practical course covering Layer 1 AEO rules.","mainEntity":{"@context":"https://schema.org","@type":"Course","name":"AEO Fundamentals","description":"A practical course covering Layer 1 AEO rules."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Courses","item":"http://localhost:3000/en/courses"},{"@type":"ListItem","position":3,"name":"AEO Fundamentals","item":"http://localhost:3000/en/courses/aeo-fundamentals"}]}]}</script><main><h1>AEO Fundamentals</h1><p data-testid="course-description">A practical course covering Layer 1 AEO rules.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">## AEO Fundamentals
103
+
104
+ Body content.</div></article></div></div></main>"
105
+ `);
106
+ });
107
+
108
+ it("unknown slug triggers notFound()", async () => {
109
+ const getMdxSource = jest.fn().mockResolvedValue(null);
110
+
111
+ await expect(
112
+ CourseSlugPage({
113
+ params: Promise.resolve({ locale: "en", slug: "unknown-slug" }),
114
+ getMdxSource,
115
+ }),
116
+ ).rejects.toThrow("NEXT_NOT_FOUND");
117
+ expect(mockNotFound).toHaveBeenCalled();
118
+ });
119
+ });