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,39 @@
1
+ // Whitepapers list page. Enumerates Whitepaper slugs via the content adapter
2
+ // and renders a directory listing. No dedicated list MDX type exists for Whitepaper.
3
+ import { getMdxSlugs as defaultGetMdxSlugs } from "@working-theory/page-renderer";
4
+ import { getTranslations } from "next-intl/server";
5
+ import type { ReactElement } from "react";
6
+
7
+ interface Props {
8
+ params: Promise<{ locale: string }>;
9
+ getMdxSlugs?: typeof defaultGetMdxSlugs;
10
+ }
11
+
12
+ export default async function Page({
13
+ params,
14
+ getMdxSlugs = defaultGetMdxSlugs,
15
+ }: Props): Promise<ReactElement> {
16
+ const { locale } = await params;
17
+ const t = await getTranslations("whitepapers");
18
+ const slugs = await getMdxSlugs("Whitepaper", locale);
19
+
20
+ return (
21
+ <main>
22
+ <h1>{t("heading")}</h1>
23
+ <p data-testid="whitepapers-description">
24
+ {t("description")}
25
+ </p>
26
+ {slugs.length === 0 ? (
27
+ <p data-testid="section-empty-state">{t("emptyState")}</p>
28
+ ) : (
29
+ <ul data-testid="whitepapers-list">
30
+ {slugs.map((slug) => (
31
+ <li key={slug}>
32
+ <a href={`/${locale}/whitepapers/${slug}`}>{slug}</a>
33
+ </li>
34
+ ))}
35
+ </ul>
36
+ )}
37
+ </main>
38
+ );
39
+ }
@@ -0,0 +1,152 @@
1
+ import React from "react";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import "@testing-library/jest-dom";
4
+
5
+ import RootError from "../error";
6
+ import NotFound from "../not-found";
7
+ import ContactError from "../[locale]/contact/error";
8
+ import PurchaseError from "../[locale]/dev/purchase/error";
9
+
10
+ function buildError(message = "Test error"): Error & { digest?: string } {
11
+ const err = new Error(message) as Error & { digest?: string };
12
+ err.digest = "test-digest-1";
13
+ return err;
14
+ }
15
+
16
+ describe("RootError", () => {
17
+ beforeEach(() => {
18
+ jest.spyOn(console, "error").mockImplementation(() => {});
19
+ });
20
+
21
+ afterEach(() => {
22
+ (console.error as jest.Mock).mockRestore();
23
+ });
24
+
25
+ it("renders a fallback heading when an error is provided", () => {
26
+ const err = buildError("Something bad");
27
+ const reset = jest.fn();
28
+ render(<RootError error={err} reset={reset} />);
29
+ expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
30
+ });
31
+
32
+ it("renders a Try again button", () => {
33
+ const err = buildError();
34
+ const reset = jest.fn();
35
+ render(<RootError error={err} reset={reset} />);
36
+ expect(
37
+ screen.getByRole("button", { name: /try again/i }),
38
+ ).toBeInTheDocument();
39
+ });
40
+
41
+ it("calls reset when Try again is clicked", () => {
42
+ const err = buildError();
43
+ const reset = jest.fn();
44
+ render(<RootError error={err} reset={reset} />);
45
+ fireEvent.click(screen.getByRole("button", { name: /try again/i }));
46
+ expect(reset).toHaveBeenCalledTimes(1);
47
+ });
48
+
49
+ it("calls console.error with the error on mount", () => {
50
+ const err = buildError("logged error");
51
+ const reset = jest.fn();
52
+ render(<RootError error={err} reset={reset} />);
53
+ expect(console.error).toHaveBeenCalledWith(err);
54
+ });
55
+ });
56
+
57
+ describe("NotFound", () => {
58
+ it("renders a 404 heading", () => {
59
+ render(<NotFound />);
60
+ expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
61
+ });
62
+
63
+ it("renders a link back to the home page", () => {
64
+ render(<NotFound />);
65
+ const link = screen.getByRole("link");
66
+ expect(link).toHaveAttribute("href", "/");
67
+ });
68
+ });
69
+
70
+ describe("ContactError", () => {
71
+ beforeEach(() => {
72
+ jest.spyOn(console, "error").mockImplementation(() => {});
73
+ });
74
+
75
+ afterEach(() => {
76
+ (console.error as jest.Mock).mockRestore();
77
+ });
78
+
79
+ it("renders a heading specific to the lead-capture flow (message/contact)", () => {
80
+ const err = buildError("Contact error");
81
+ const reset = jest.fn();
82
+ render(<ContactError error={err} reset={reset} />);
83
+ const heading = screen.getByRole("heading", { level: 1 });
84
+ expect(heading.textContent?.toLowerCase()).toMatch(/message|contact/);
85
+ });
86
+
87
+ it("does NOT render generic 'something went wrong' as the heading", () => {
88
+ const err = buildError();
89
+ const reset = jest.fn();
90
+ render(<ContactError error={err} reset={reset} />);
91
+ const heading = screen.getByRole("heading", { level: 1 });
92
+ expect(heading.textContent?.toLowerCase()).not.toBe("something went wrong");
93
+ });
94
+
95
+ it("renders a Try again button that calls reset", () => {
96
+ const err = buildError();
97
+ const reset = jest.fn();
98
+ render(<ContactError error={err} reset={reset} />);
99
+ fireEvent.click(screen.getByRole("button", { name: /try again/i }));
100
+ expect(reset).toHaveBeenCalledTimes(1);
101
+ });
102
+
103
+ it("calls console.error with the error on mount", () => {
104
+ const err = buildError("contact logged");
105
+ const reset = jest.fn();
106
+ render(<ContactError error={err} reset={reset} />);
107
+ expect(console.error).toHaveBeenCalledWith(err);
108
+ });
109
+ });
110
+
111
+ describe("PurchaseError", () => {
112
+ beforeEach(() => {
113
+ jest.spyOn(console, "error").mockImplementation(() => {});
114
+ });
115
+
116
+ afterEach(() => {
117
+ (console.error as jest.Mock).mockRestore();
118
+ });
119
+
120
+ it("renders a heading specific to the checkout flow (checkout/purchase)", () => {
121
+ const err = buildError("Purchase error");
122
+ const reset = jest.fn();
123
+ render(<PurchaseError error={err} reset={reset} />);
124
+ const heading = screen.getByRole("heading", { level: 1 });
125
+ expect(heading.textContent?.toLowerCase()).toMatch(/checkout|purchase/);
126
+ });
127
+
128
+ it("does NOT claim payment was or was not processed in the heading", () => {
129
+ const err = buildError();
130
+ const reset = jest.fn();
131
+ render(<PurchaseError error={err} reset={reset} />);
132
+ const heading = screen.getByRole("heading", { level: 1 });
133
+ expect(heading.textContent?.toLowerCase()).not.toMatch(
134
+ /payment (was|not|failed|charged)/,
135
+ );
136
+ });
137
+
138
+ it("renders a Try again button that calls reset", () => {
139
+ const err = buildError();
140
+ const reset = jest.fn();
141
+ render(<PurchaseError error={err} reset={reset} />);
142
+ fireEvent.click(screen.getByRole("button", { name: /try again/i }));
143
+ expect(reset).toHaveBeenCalledTimes(1);
144
+ });
145
+
146
+ it("calls console.error with the error on mount", () => {
147
+ const err = buildError("purchase logged");
148
+ const reset = jest.fn();
149
+ render(<PurchaseError error={err} reset={reset} />);
150
+ expect(console.error).toHaveBeenCalledWith(err);
151
+ });
152
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Better Auth catch-all route handler.
3
+ *
4
+ * Mounts Better Auth at /api/auth/* per its convention. This is the only
5
+ * api-shaped code permitted in apps/web — all other HTTP logic lives in
6
+ * apps/api (architecture.yaml).
7
+ *
8
+ * Replaces the deleted NextAuth catch-all at app/api/auth/[...nextauth]/.
9
+ * Better Auth's toNextJsHandler returns GET + POST Web Fetch handlers that
10
+ * Next.js App Router calls directly.
11
+ */
12
+ import { toNextJsHandler } from "better-auth/next-js";
13
+
14
+ import { auth } from "../../../../auth";
15
+
16
+ export const { GET, POST } = toNextJsHandler(auth.handler);
@@ -0,0 +1,75 @@
1
+ /* eslint-disable no-restricted-syntax -- i18n-allow: error boundary renders outside the [locale] intl provider context; translations unavailable at this segment (ADR 0019) */
2
+ "use client";
3
+ // Shared base for all per-segment error boundaries in apps/web.
4
+ // Each boundary supplies flow-specific heading and body copy; this component
5
+ // owns the token-sourced styling and the reset button.
6
+ import { clientError } from "@working-theory/page-renderer/edge";
7
+ import { tokens } from "@working-theory/theme";
8
+ import type { ReactElement } from "react";
9
+ import { useEffect } from "react";
10
+
11
+ export interface ErrorFallbackProps {
12
+ error: Error & { digest?: string };
13
+ reset: () => void;
14
+ heading: string;
15
+ body: string;
16
+ }
17
+
18
+ const styles = {
19
+ main: {
20
+ background: tokens.colors.light.bg,
21
+ color: tokens.colors.light.fg,
22
+ minHeight: "100vh",
23
+ display: "flex",
24
+ flexDirection: "column" as const,
25
+ alignItems: "center",
26
+ justifyContent: "center",
27
+ padding: tokens.radius.lg,
28
+ gap: tokens.radius.md,
29
+ },
30
+ heading: {
31
+ fontSize: "1.5rem",
32
+ lineHeight: tokens.lineHeight.md,
33
+ color: tokens.colors.light.errorFg,
34
+ background: tokens.colors.light.errorBg,
35
+ padding: `${tokens.radius.sm} ${tokens.radius.md}`,
36
+ borderRadius: tokens.radius.sm,
37
+ margin: 0,
38
+ },
39
+ body: {
40
+ fontSize: "1rem",
41
+ lineHeight: tokens.lineHeight.md,
42
+ margin: 0,
43
+ },
44
+ button: {
45
+ fontSize: "1rem",
46
+ lineHeight: tokens.lineHeight.md,
47
+ background: tokens.colors.light.brand,
48
+ color: tokens.colors.light.bg,
49
+ border: "none",
50
+ borderRadius: tokens.radius.sm,
51
+ padding: `${tokens.radius.sm} ${tokens.radius.md}`,
52
+ cursor: "pointer",
53
+ },
54
+ } as const;
55
+
56
+ export function ErrorFallback({
57
+ error,
58
+ reset,
59
+ heading,
60
+ body,
61
+ }: ErrorFallbackProps): ReactElement {
62
+ useEffect(() => {
63
+ clientError(error);
64
+ }, [error]);
65
+
66
+ return (
67
+ <main style={styles.main}>
68
+ <h1 style={styles.heading}>{heading}</h1>
69
+ <p style={styles.body}>{body}</p>
70
+ <button type="button" onClick={reset} style={styles.button}>
71
+ Try again
72
+ </button>
73
+ </main>
74
+ );
75
+ }
@@ -0,0 +1,23 @@
1
+ "use client";
2
+ // Next.js App Router root error boundary. Must be a Client Component.
3
+ // Renders when an unhandled error is thrown inside any route segment that
4
+ // does not have its own error.tsx.
5
+ import type { ReactElement } from "react";
6
+
7
+ import { ErrorFallback } from "./error-fallback";
8
+
9
+ interface ErrorProps {
10
+ error: Error & { digest?: string };
11
+ reset: () => void;
12
+ }
13
+
14
+ export default function Error({ error, reset }: ErrorProps): ReactElement {
15
+ return (
16
+ <ErrorFallback
17
+ error={error}
18
+ reset={reset}
19
+ heading="Something went wrong"
20
+ body="An unexpected error occurred. Please try again."
21
+ />
22
+ );
23
+ }
@@ -0,0 +1,18 @@
1
+ // Root shell layout — i18n S2 (#949)
2
+ //
3
+ // Kept as a thin passthrough. The full HTML tree (<html lang>, <head>, <body>)
4
+ // is now owned by app/[locale]/layout.tsx which has access to the locale param.
5
+ // This root layout exists because Next.js requires a root layout at app/layout.tsx
6
+ // when [locale] is a catch-segment; it renders children directly.
7
+ //
8
+ // Do NOT add <html> or <body> here — that would conflict with the locale layout.
9
+
10
+ import type { ReactElement, ReactNode } from "react";
11
+
12
+ export default function RootLayout({
13
+ children,
14
+ }: {
15
+ children: ReactNode;
16
+ }): ReactElement {
17
+ return <>{children}</>;
18
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * GET /llms.txt
3
+ *
4
+ * Returns a plain-text index of AILK's content surface for LLM consumption,
5
+ * following the llms.txt convention (https://llmstxt.org).
6
+ *
7
+ * Aggregates two surfaces:
8
+ * 1. Section-catalog (MDX seeds in content/) via @working-theory/content-adapters
9
+ * 2. Fumadocs blog corpus via blogSource.getPages()
10
+ *
11
+ * Format per entry:
12
+ * - Title: <title>
13
+ * URL: <url>
14
+ * Description: <description>
15
+ *
16
+ * DI note: `createLlmsHandler` accepts optional `getContentAdapter` and
17
+ * `getBlogPages` deps for testing — tests inject stubs to avoid mocking
18
+ * internal modules. The exported `GET` uses the real implementations.
19
+ */
20
+
21
+ import { SCHEMA_ORG_TYPES } from "@working-theory/content-adapters";
22
+ import type { ContentAdapter, SchemaOrgType } from "@working-theory/content-adapters";
23
+ import { getContentAdapter as defaultGetContentAdapter } from "@working-theory/page-renderer";
24
+
25
+ import { blogSource as defaultBlogSource } from "../../lib/source";
26
+ import { resolveBaseUrl, slugToPath } from "../../src/lib/routes";
27
+
28
+ export interface LlmsHandlerDeps {
29
+ /**
30
+ * Content adapter factory. Defaults to the real implementation.
31
+ * Override in tests to inject a stub adapter without mocking the internal module.
32
+ */
33
+ getContentAdapter?: () => ContentAdapter;
34
+ /**
35
+ * Blog pages getter. Defaults to the real blogSource.getPages().
36
+ * Override in tests to inject stub pages without mocking lib/source.
37
+ */
38
+ getBlogPages?: () => Array<{ url: string; data: { title: string; description?: string } }>;
39
+ }
40
+
41
+ /**
42
+ * Build an llms.txt GET handler parameterised by deps.
43
+ * Tests inject stubs to avoid mocking internal modules.
44
+ */
45
+ export function createLlmsHandler(deps: LlmsHandlerDeps = {}) {
46
+ const resolveAdapter = deps.getContentAdapter ?? defaultGetContentAdapter;
47
+ const resolveBlogPages = deps.getBlogPages ?? (() => defaultBlogSource.getPages() as Array<{ url: string; data: { title: string; description?: string } }>);
48
+
49
+ return async function llmsGET(): Promise<Response> {
50
+ const adapter = resolveAdapter();
51
+ const baseUrl = resolveBaseUrl();
52
+ const lines: string[] = ["# AI Launch Kit — content index", ""];
53
+
54
+ // llms.txt is default-locale-only per Phase 3 roadmap §3 (AEO MVP is
55
+ // default-locale; per-locale AEO grading is deferred). Pass the default
56
+ // locale explicitly so the adapter returns the default-locale list (not
57
+ // legacy fallback) once content is fully migrated to locale-prefixed dirs.
58
+ const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? "en";
59
+
60
+ const types = SCHEMA_ORG_TYPES as readonly SchemaOrgType[];
61
+ const pageLists = await Promise.all(
62
+ types.map((type) => adapter.listPages(type, defaultLocale)),
63
+ );
64
+
65
+ for (const pages of pageLists) {
66
+ for (const page of pages) {
67
+ const path = slugToPath(page.type, page.slug);
68
+ const url = `${baseUrl}${path}`;
69
+ lines.push(`- Title: ${page.title}`);
70
+ lines.push(` URL: ${url}`);
71
+ if (page.description) {
72
+ lines.push(` Description: ${page.description}`);
73
+ }
74
+ lines.push("");
75
+ }
76
+ }
77
+
78
+ // Fumadocs blog corpus — blog index entry first, then per-post entries.
79
+ // blogSource.getPages() returns pages ordered by Fumadocs; the index page
80
+ // (/blog) sorts first naturally because its slug array is empty.
81
+ lines.push("");
82
+ for (const page of resolveBlogPages()) {
83
+ lines.push(`- Title: ${page.data.title}`);
84
+ lines.push(` URL: ${baseUrl}${page.url}`);
85
+ if (page.data.description) {
86
+ lines.push(` Description: ${page.data.description}`);
87
+ }
88
+ lines.push("");
89
+ }
90
+
91
+ return new Response(lines.join("\n"), {
92
+ status: 200,
93
+ headers: {
94
+ "content-type": "text/plain; charset=utf-8",
95
+ "cache-control": "public, max-age=3600",
96
+ },
97
+ });
98
+ };
99
+ }
100
+
101
+ export const GET = createLlmsHandler();
@@ -0,0 +1,48 @@
1
+ /* eslint-disable no-restricted-syntax -- i18n-allow: not-found boundary renders outside the [locale] intl provider; translations unavailable at this segment (ADR 0019) */
2
+ // Next.js App Router not-found boundary. Renders for any route that calls
3
+ // notFound() or when no route matches. Server Component by default.
4
+ import { tokens } from "@working-theory/theme";
5
+ import type { ReactElement } from "react";
6
+
7
+ const styles = {
8
+ main: {
9
+ background: tokens.colors.light.bg,
10
+ color: tokens.colors.light.fg,
11
+ minHeight: "100vh",
12
+ display: "flex",
13
+ flexDirection: "column" as const,
14
+ alignItems: "center",
15
+ justifyContent: "center",
16
+ padding: tokens.radius.lg,
17
+ gap: tokens.radius.md,
18
+ },
19
+ heading: {
20
+ fontSize: "1.5rem",
21
+ lineHeight: tokens.lineHeight.md,
22
+ margin: 0,
23
+ },
24
+ body: {
25
+ fontSize: "1rem",
26
+ lineHeight: tokens.lineHeight.md,
27
+ margin: 0,
28
+ },
29
+ link: {
30
+ color: tokens.colors.light.brand,
31
+ fontSize: "1rem",
32
+ lineHeight: tokens.lineHeight.md,
33
+ },
34
+ } as const;
35
+
36
+ export default function NotFound(): ReactElement {
37
+ return (
38
+ <main style={styles.main}>
39
+ <h1 style={styles.heading}>We couldn&apos;t find that page</h1>
40
+ <p style={styles.body}>
41
+ The page you are looking for does not exist or may have moved.
42
+ </p>
43
+ <a href="/" style={styles.link}>
44
+ Return home
45
+ </a>
46
+ </main>
47
+ );
48
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+
5
+ /**
6
+ * Tests for the OG image route handler.
7
+ *
8
+ * Contract (AC from spec):
9
+ * - Returns 200 + image/png MIME for valid params
10
+ * - Sanitization strips control characters (\x00-\x1F\x7F) from title
11
+ * - Sanitization caps title at 200 chars and schemaType at 50 chars
12
+ * - Post-sanitization empty title triggers fallback (302 redirect to static PNG)
13
+ * - On ImageResponse throw, falls back via 302 redirect to /og-default.png
14
+ *
15
+ * Fallback is a 302 redirect to /og-default.png (edge-compatible; no fs access).
16
+ * Per spec § 3: "redirecting is simpler but exposes the fallback URL; both acceptable."
17
+ *
18
+ * Note: next/og's ImageResponse runs in an edge runtime. In tests we mock it
19
+ * to return a minimal Response so we can verify the handler contract without
20
+ * spinning up an actual V8 isolate.
21
+ */
22
+
23
+ // --- Mocks set up BEFORE imports ---
24
+
25
+ // Mock next/og ImageResponse
26
+ const mockImageResponseBody = Buffer.from("PNG_MOCK");
27
+ let shouldImageResponseThrow = false;
28
+
29
+ jest.mock("next/og", () => ({
30
+ ImageResponse: jest.fn().mockImplementation(() => {
31
+ if (shouldImageResponseThrow) {
32
+ throw new Error("ImageResponse failed");
33
+ }
34
+ return new Response(mockImageResponseBody, {
35
+ status: 200,
36
+ headers: { "Content-Type": "image/png" },
37
+ });
38
+ }),
39
+ }));
40
+
41
+ import { GET } from "../route";
42
+
43
+ function makeRequest(params: Record<string, string>): Request {
44
+ const searchParams = new URLSearchParams(params).toString();
45
+ const url = `http://localhost/og${searchParams ? "?" + searchParams : ""}`;
46
+ return new Request(url, { method: "GET" });
47
+ }
48
+
49
+ describe("OG route GET handler", () => {
50
+ beforeEach(() => {
51
+ shouldImageResponseThrow = false;
52
+ jest.spyOn(console, "error").mockImplementation(() => {});
53
+ });
54
+
55
+ afterEach(() => {
56
+ (console.error as jest.Mock).mockRestore();
57
+ });
58
+
59
+ describe("happy path — valid params", () => {
60
+ it("returns 200 for valid title and schemaType", async () => {
61
+ const req = makeRequest({
62
+ title: "AI Launch Kit",
63
+ schemaType: "WebSite",
64
+ });
65
+ const res = await GET(req);
66
+ expect(res.status).toBe(200);
67
+ });
68
+
69
+ it("returns image/png content-type for valid params", async () => {
70
+ const req = makeRequest({
71
+ title: "AI Launch Kit",
72
+ schemaType: "WebSite",
73
+ });
74
+ const res = await GET(req);
75
+ expect(res.headers.get("content-type")).toContain("image/png");
76
+ });
77
+
78
+ it("works with only title (no schemaType)", async () => {
79
+ const req = makeRequest({ title: "Hello World" });
80
+ const res = await GET(req);
81
+ expect(res.status).toBe(200);
82
+ });
83
+
84
+ it("works with no params (uses defaults)", async () => {
85
+ const req = makeRequest({});
86
+ const res = await GET(req);
87
+ expect(res.status).toBe(200);
88
+ });
89
+ });
90
+
91
+ describe("sanitization — control characters stripped", () => {
92
+ it("strips null bytes from title", async () => {
93
+ // Control chars are stripped — should not throw; handler proceeds with sanitized value
94
+ const req = makeRequest({
95
+ title: "Hello\x00World",
96
+ schemaType: "WebSite",
97
+ });
98
+ const res = await GET(req);
99
+ expect(res.status).toBe(200);
100
+ });
101
+
102
+ it("strips other control characters from title", async () => {
103
+ const req = makeRequest({
104
+ title: "Bad\x01\x1FTitle",
105
+ schemaType: "WebSite",
106
+ });
107
+ const res = await GET(req);
108
+ expect(res.status).toBe(200);
109
+ });
110
+
111
+ it("returns fallback when title is empty after sanitization (all control chars)", async () => {
112
+ // A title consisting only of control characters sanitizes to "" → 302 fallback redirect
113
+ const req = makeRequest({
114
+ title: "\x00\x01\x1F\x7F",
115
+ schemaType: "WebSite",
116
+ });
117
+ const res = await GET(req);
118
+ // Must not be 500 — handler always returns a non-error response
119
+ expect(res.status).not.toBe(500);
120
+ // Fallback is a 302 redirect to the static PNG
121
+ expect(res.status).toBe(302);
122
+ expect(res.headers.get("location")).toContain("og-default.png");
123
+ });
124
+
125
+ it("caps title at 200 characters", async () => {
126
+ const longTitle = "A".repeat(300);
127
+ const req = makeRequest({ title: longTitle, schemaType: "WebSite" });
128
+ const res = await GET(req);
129
+ expect(res.status).toBe(200);
130
+ });
131
+
132
+ it("caps schemaType at 50 characters", async () => {
133
+ const longType = "B".repeat(100);
134
+ const req = makeRequest({ title: "Test", schemaType: longType });
135
+ const res = await GET(req);
136
+ expect(res.status).toBe(200);
137
+ });
138
+ });
139
+
140
+ describe("fallback — ImageResponse throws", () => {
141
+ beforeEach(() => {
142
+ shouldImageResponseThrow = true;
143
+ });
144
+
145
+ it("does not return 500 when ImageResponse throws", async () => {
146
+ const req = makeRequest({ title: "Test", schemaType: "WebSite" });
147
+ const res = await GET(req);
148
+ expect(res.status).not.toBe(500);
149
+ });
150
+
151
+ it("returns 302 redirect to static PNG when ImageResponse throws", async () => {
152
+ const req = makeRequest({ title: "Test", schemaType: "WebSite" });
153
+ const res = await GET(req);
154
+ expect(res.status).toBe(302);
155
+ expect(res.headers.get("location")).toContain("og-default.png");
156
+ });
157
+
158
+ it("calls console.error when ImageResponse throws", async () => {
159
+ const req = makeRequest({ title: "Test", schemaType: "WebSite" });
160
+ await GET(req);
161
+ expect(console.error).toHaveBeenCalled();
162
+ });
163
+ });
164
+ });