create-ailk 0.2.0 → 0.4.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 (292) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/cli.js +19 -9
  3. package/dist/lib/apply-module-patches.d.ts +47 -0
  4. package/dist/lib/apply-module-patches.js +370 -0
  5. package/dist/lib/extract-module-bundle.d.ts +51 -3
  6. package/dist/lib/extract-module-bundle.js +327 -28
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/fetch-template.d.ts +20 -0
  10. package/dist/lib/fetch-template.js +129 -0
  11. package/dist/lib/module-license-gate.d.ts +10 -0
  12. package/dist/lib/module-license-gate.js +13 -4
  13. package/dist/lib/paid-paths.d.ts +16 -0
  14. package/dist/lib/paid-paths.js +90 -0
  15. package/dist/module-architecture.d.ts +197 -23
  16. package/dist/module-architecture.js +820 -25
  17. package/dist/parse-args.d.ts +9 -2
  18. package/dist/parse-args.js +46 -1
  19. package/dist/programmatic.d.ts +28 -2
  20. package/dist/programmatic.js +115 -8
  21. package/dist/surfaces.d.ts +12 -36
  22. package/dist/surfaces.js +77 -5
  23. package/dist/sync-routes.js +31 -1
  24. package/dist/template-manifest.d.ts +92 -0
  25. package/dist/template-manifest.js +168 -0
  26. package/dist/template-names.d.ts +41 -0
  27. package/dist/template-names.js +44 -0
  28. package/package.json +3 -2
  29. package/site-templates/marketing-site-free/ailk-template.json +8 -0
  30. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +103 -0
  31. package/site-templates/marketing-site-free/content/en/PrivacyPage/privacy.mdx +79 -0
  32. package/site-templates/marketing-site-free/content/en/TermsPage/terms.mdx +80 -0
  33. package/site-templates/marketing-site-free/provenance.json +4 -0
  34. package/templates/.claude/agents/web.md +2 -3
  35. package/templates/.claude/rules/architecture.md +5 -5
  36. package/templates/.claude/skills/README.md +2 -1
  37. package/templates/.env.example +21 -9
  38. package/templates/CONVENTIONS.md +6 -7
  39. package/templates/apps/api/.env.example +9 -0
  40. package/templates/apps/api/CLAUDE.md +33 -5
  41. package/templates/apps/api/package.json +0 -1
  42. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  43. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  44. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  45. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +166 -0
  46. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  47. package/templates/apps/api/src/config/index.ts +19 -0
  48. package/templates/apps/api/src/config/modules.ts +12 -14
  49. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  50. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  51. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  52. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  53. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  54. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  55. package/templates/apps/api/src/openapi/index.ts +9 -3
  56. package/templates/apps/api/src/openapi/spec.ts +505 -65
  57. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  58. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  59. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  60. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  61. package/templates/apps/api/src/routes/content/index.ts +38 -11
  62. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  63. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  64. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  65. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  66. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  67. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  68. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  69. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  70. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  71. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  72. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  73. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  74. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  75. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  76. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  77. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  78. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  79. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  80. package/templates/apps/api/src/server.ts +504 -137
  81. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  82. package/templates/apps/api/src/services/listing-config.ts +58 -0
  83. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  84. package/templates/apps/api/src/services/project-listings.ts +229 -4
  85. package/templates/apps/api/src/vercel-handler.ts +60 -26
  86. package/templates/apps/mcp/.env.example +8 -0
  87. package/templates/apps/mcp/CLAUDE.md +2 -2
  88. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  89. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  90. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  91. package/templates/apps/mcp/src/config/modules.ts +14 -13
  92. package/templates/apps/mcp/src/server.ts +8 -4
  93. package/templates/apps/mcp/src/tools/index.ts +7 -22
  94. package/templates/apps/web/.env.example +15 -0
  95. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  96. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  97. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  98. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  99. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  100. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  101. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  102. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  103. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  104. package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
  105. package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
  106. package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
  107. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  108. package/templates/apps/web/jest.config.cjs +19 -13
  109. package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
  110. package/templates/apps/web/lib/server-api.ts +28 -12
  111. package/templates/apps/web/messages/en.json +58 -0
  112. package/templates/apps/web/next.config.mjs +1 -3
  113. package/templates/apps/web/package.json +0 -6
  114. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  115. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  116. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  117. package/templates/apps/web/public/favicon-16x16.png +0 -0
  118. package/templates/apps/web/public/favicon-32x32.png +0 -0
  119. package/templates/apps/web/public/favicon.ico +0 -0
  120. package/templates/apps/web/public/favicon.svg +6 -3
  121. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  122. package/templates/apps/web/public/logomark.svg +4 -0
  123. package/templates/apps/web/public/site.webmanifest +2 -2
  124. package/templates/apps/web/public/wordmark.svg +4 -0
  125. package/templates/database/CHANGELOG.md +47 -0
  126. package/templates/database/inbox/schema.prisma +67 -0
  127. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  128. package/templates/database/package.json +1 -1
  129. package/templates/package.json +1 -1
  130. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  131. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  132. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  133. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  134. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  135. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  136. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  137. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  138. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  139. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  140. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  141. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  142. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  143. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  144. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  145. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  146. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  147. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  148. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  149. package/templates/apps/api/src/lib/stripe.ts +0 -52
  150. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  151. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  152. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  153. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  154. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  155. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  156. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  157. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  158. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  159. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  160. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  161. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  162. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  163. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  164. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  165. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  166. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  167. package/templates/apps/api/src/routes/flows/README.md +0 -147
  168. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  169. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  170. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  171. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  172. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  173. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  174. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  175. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  176. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  177. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  178. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  179. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  180. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  181. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  182. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  183. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  184. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  185. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  186. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  187. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  188. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  189. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  190. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  191. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  192. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  193. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  194. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  195. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  196. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  197. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  198. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  199. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  200. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  201. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  202. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  203. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  204. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  205. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  206. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  207. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  208. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  209. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  210. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  211. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  212. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  213. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  214. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  215. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  216. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  217. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  218. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  219. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  220. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  221. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  222. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  223. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  224. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  225. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  226. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  227. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  228. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  229. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  230. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  231. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  232. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  233. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  234. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  235. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  236. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  237. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  238. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  239. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  240. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  241. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  242. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  243. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  244. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  245. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  246. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  247. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  248. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  249. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  250. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  251. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  258. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  259. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  260. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  261. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  262. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  263. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  264. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  265. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  266. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  267. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  268. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  269. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  270. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  271. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  272. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  273. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  274. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  275. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  276. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  277. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  278. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  279. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  280. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  281. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  282. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  283. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  284. package/templates/apps/web/content/en/blog/README.txt +0 -25
  285. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  286. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  287. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  288. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  289. package/templates/apps/web/lib/source.ts +0 -34
  290. package/templates/apps/web/public/logomark-dark.svg +0 -4
  291. package/templates/apps/web/public/logomark-light.svg +0 -4
  292. package/templates/apps/web/source.config.ts +0 -78
@@ -1,96 +0,0 @@
1
- /**
2
- * #5048 — the reference mount.
3
- *
4
- * Two things are worth pinning here and nothing else is: that an emailed
5
- * resume link's query parameters reach the section as a resume, and that a
6
- * freshly-minted draft's id and token are written back into the URL rather
7
- * than into a cookie or browser storage. Everything else about the surface is
8
- * tested where it lives, in `packages/ui`.
9
- */
10
- import { render, screen } from "@testing-library/react";
11
-
12
- const replace = jest.fn();
13
- let searchParams = new URLSearchParams();
14
-
15
- jest.mock("next/navigation", () => ({
16
- useRouter: () => ({ replace }),
17
- useSearchParams: () => searchParams,
18
- }));
19
-
20
- // The section is the subject of its own tests; here it is a probe that records
21
- // what the mount handed it.
22
- const received: Record<string, unknown>[] = [];
23
- jest.mock("@working-theory/ui", () => ({
24
- ListingApplicationFlow: (props: Record<string, unknown>) => {
25
- received.push(props);
26
- return <div data-testid="listing-application-flow" />;
27
- },
28
- }));
29
-
30
- jest.mock("@working-theory/api-client", () => ({
31
- projectListings: { start: jest.fn(), patchDraft: jest.fn(), submit: jest.fn() },
32
- }));
33
-
34
- // The import sits below the mocks deliberately: `jest.mock` calls are hoisted,
35
- // but the module under test must not be evaluated before they are registered.
36
- import { ListingApplicationClient } from "../ListingApplicationClient";
37
-
38
- const config = { slug: "apply", defaultLocale: "en", siteKey: "pk_test", steps: [] };
39
- const messages = {} as never;
40
-
41
- function mount() {
42
- return render(
43
- <ListingApplicationClient
44
- config={config as never}
45
- locale="en"
46
- messages={messages}
47
- />,
48
- );
49
- }
50
-
51
- beforeEach(() => {
52
- received.length = 0;
53
- replace.mockClear();
54
- searchParams = new URLSearchParams();
55
- });
56
-
57
- describe("ListingApplicationClient", () => {
58
- it("passes no resume when the URL carries no link parameters", () => {
59
- mount();
60
- expect(screen.getByTestId("listing-application-flow")).toBeInTheDocument();
61
- expect(received[0]).not.toHaveProperty("resume");
62
- });
63
-
64
- it("rehydrates a draft from an emailed resume link's query parameters", () => {
65
- searchParams = new URLSearchParams("draft=listing_7&token=tok_from_the_email");
66
- mount();
67
- expect(received[0]!["resume"]).toEqual({
68
- id: "listing_7",
69
- resumeToken: "tok_from_the_email",
70
- });
71
- });
72
-
73
- it("needs BOTH parameters — a half link is not a resume", () => {
74
- searchParams = new URLSearchParams("draft=listing_7");
75
- mount();
76
- expect(received[0]).not.toHaveProperty("resume");
77
- });
78
-
79
- it("writes a newly minted draft back into the URL, not into a cookie", () => {
80
- mount();
81
- const onDraftStarted = received[0]!["onDraftStarted"] as (s: {
82
- id: string;
83
- resumeToken: string;
84
- }) => void;
85
- onDraftStarted({ id: "listing_9", resumeToken: "tok_new" });
86
-
87
- expect(replace).toHaveBeenCalledWith(
88
- "?draft=listing_9&token=tok_new",
89
- { scroll: false },
90
- );
91
- // The token is caller-held by design (#5048 (a)) — an httpOnly cookie
92
- // cannot travel in an emailed link and a non-browser caller cannot present
93
- // one at all. Nothing here may quietly set one.
94
- expect(document.cookie).toBe("");
95
- });
96
- });
@@ -1,117 +0,0 @@
1
- // oss: false
2
- /**
3
- * Tests for the scheduling module's booking route (#4633).
4
- *
5
- * Two things matter here and nothing else does. The page must hand the booker
6
- * the event identifier and the module's own confirmation path, so a successful
7
- * booking lands on this site rather than on the provider's; and it must never
8
- * put a credential into the section data, because section data is exactly what
9
- * crosses into the client island.
10
- */
11
- import React from "react";
12
- import { render } from "@testing-library/react";
13
-
14
- jest.mock("next/navigation", () => ({
15
- __esModule: true,
16
- notFound: jest.fn(() => {
17
- throw new Error("NEXT_NOT_FOUND");
18
- }),
19
- }));
20
-
21
- jest.mock("next-intl/server", () => ({
22
- __esModule: true,
23
- // Echo the key back so an assertion names the message key, not the copy —
24
- // the copy is the consuming site's to change.
25
- getTranslations: jest.fn().mockResolvedValue((key: string) => key),
26
- }));
27
-
28
- // Capture what the page hands the catalog rather than rendering the real
29
- // booker: the island's own behaviour is covered in packages/ui, and mounting it
30
- // here would put a network stub between this test and the thing it asserts.
31
- const capturedSections: unknown[] = [];
32
- jest.mock("@working-theory/ui/sections", () => ({
33
- __esModule: true,
34
- SectionRenderer: ({ sections }: { sections: unknown[] }) => {
35
- capturedSections.splice(0, capturedSections.length, ...sections);
36
- return React.createElement("div", {
37
- "data-testid": "section-renderer",
38
- "data-section-count": String(sections.length),
39
- });
40
- },
41
- }));
42
-
43
- import { notFound } from "next/navigation";
44
- import SchedulePage from "../page";
45
-
46
- const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
47
-
48
- const CAL_LINK = "acme/intro-call";
49
-
50
- describe("SchedulePage — the module's booking route", () => {
51
- const originalEnv = process.env["SCHEDULING_CAL_LINK"];
52
-
53
- beforeEach(() => {
54
- mockNotFound.mockClear();
55
- capturedSections.length = 0;
56
- process.env["SCHEDULING_CAL_LINK"] = CAL_LINK;
57
- });
58
-
59
- afterAll(() => {
60
- if (originalEnv === undefined) delete process.env["SCHEDULING_CAL_LINK"];
61
- else process.env["SCHEDULING_CAL_LINK"] = originalEnv;
62
- });
63
-
64
- it("renders one booker section from the catalog", async () => {
65
- const element = await SchedulePage({
66
- params: Promise.resolve({ locale: "en" }),
67
- });
68
- const { container } = render(element);
69
-
70
- expect(
71
- container.querySelector('[data-testid="section-renderer"]'),
72
- ).not.toBeNull();
73
- expect(capturedSections).toHaveLength(1);
74
- expect((capturedSections[0] as { type: string }).type).toBe("booker");
75
- });
76
-
77
- it("sends a successful booking to the page this module owns", async () => {
78
- render(await SchedulePage({ params: Promise.resolve({ locale: "en" }) }));
79
- expect(capturedSections).toHaveLength(1);
80
-
81
- const data = (capturedSections[0] as { data: Record<string, unknown> }).data;
82
- expect(data["calLink"]).toBe(CAL_LINK);
83
- expect(data["bookedRedirect"]).toBe("/schedule/confirmed");
84
- });
85
-
86
- it("puts no credential into the section data the client island receives", async () => {
87
- render(await SchedulePage({ params: Promise.resolve({ locale: "en" }) }));
88
- // Fixture self-guard: an empty capture would make every assertion below
89
- // pass vacuously.
90
- expect(capturedSections).toHaveLength(1);
91
-
92
- // Whatever the page decides to pass, the SHAPE is fixed: the event
93
- // identifier, page framing, and the redirect. Anything else appearing here
94
- // is a value crossing into the browser that nobody decided to send.
95
- const data = (capturedSections[0] as { data: Record<string, unknown> }).data;
96
- expect(Object.keys(data).sort()).toEqual([
97
- "bookedRedirect",
98
- "calLink",
99
- "eyebrow",
100
- "headline",
101
- ]);
102
- // Belt and braces: no serialized value may contain a credential-shaped
103
- // string, however the page came by it.
104
- const serialized = JSON.stringify(data);
105
- expect(serialized).not.toMatch(/api[_-]?key/i);
106
- expect(serialized).not.toMatch(/\bcal_(live|test)_/);
107
- });
108
-
109
- it("404s when no event is configured", async () => {
110
- delete process.env["SCHEDULING_CAL_LINK"];
111
-
112
- await expect(
113
- SchedulePage({ params: Promise.resolve({ locale: "en" }) }),
114
- ).rejects.toThrow("NEXT_NOT_FOUND");
115
- expect(mockNotFound).toHaveBeenCalled();
116
- });
117
- });
@@ -1,96 +0,0 @@
1
- // oss: false
2
- /**
3
- * Tests for the scheduling module's confirmation route (#4633).
4
- *
5
- * The page's whole claim is that it is finished but owns no design: it renders
6
- * real content from the site's own message catalog, through the site's own
7
- * catalog components, and contributes no styling of its own. Both halves are
8
- * asserted — that it renders a full page rather than a placeholder, and that
9
- * every string in it came from the `schedule` namespace rather than from this
10
- * file.
11
- */
12
- import React from "react";
13
- import { render } from "@testing-library/react";
14
-
15
- const requestedKeys: string[] = [];
16
- jest.mock("next-intl/server", () => ({
17
- __esModule: true,
18
- getTranslations: jest.fn().mockResolvedValue((key: string) => {
19
- requestedKeys.push(key);
20
- return `copy:${key}`;
21
- }),
22
- }));
23
-
24
- const capturedSections: unknown[] = [];
25
- jest.mock("@working-theory/ui/sections", () => ({
26
- __esModule: true,
27
- SectionRenderer: ({ sections }: { sections: unknown[] }) => {
28
- capturedSections.splice(0, capturedSections.length, ...sections);
29
- return React.createElement("div", {
30
- "data-testid": "section-renderer",
31
- "data-section-count": String(sections.length),
32
- });
33
- },
34
- }));
35
-
36
- import ConfirmedPage, { generateMetadata } from "../page";
37
-
38
- describe("ConfirmedPage — the module-owned confirmation page", () => {
39
- beforeEach(() => {
40
- capturedSections.length = 0;
41
- requestedKeys.length = 0;
42
- });
43
-
44
- it("renders a finished page, not a placeholder", async () => {
45
- const element = await ConfirmedPage({
46
- params: Promise.resolve({ locale: "en" }),
47
- });
48
- const { container } = render(element);
49
-
50
- expect(
51
- container.querySelector('[data-testid="section-renderer"]'),
52
- ).not.toBeNull();
53
- // Confirmation, what happens next, and a way onward — three sections, all
54
- // populated. A stub would be one section or none.
55
- expect(capturedSections.map((s) => (s as { type: string }).type)).toEqual([
56
- "hero",
57
- "content-block",
58
- "cta",
59
- ]);
60
- });
61
-
62
- it("takes every string from the site's own message catalog", async () => {
63
- render(await ConfirmedPage({ params: Promise.resolve({ locale: "en" }) }));
64
- expect(capturedSections).toHaveLength(3);
65
-
66
- // Every rendered value resolved through getTranslations, so changing the
67
- // copy is an edit to messages/<locale>.json — not a fork of this route.
68
- const serialized = JSON.stringify(capturedSections);
69
- for (const value of serialized.matchAll(/"(?:headline|subheadline|body|description|label)":"([^"]*)"/g)) {
70
- expect(value[1]).toMatch(/^copy:confirmed\./);
71
- }
72
- expect(requestedKeys.length).toBeGreaterThan(0);
73
- expect(requestedKeys.every((k) => k.startsWith("confirmed."))).toBe(true);
74
- });
75
-
76
- it("declares no styling of its own — the site's design system decides", async () => {
77
- render(await ConfirmedPage({ params: Promise.resolve({ locale: "en" }) }));
78
- // Fixture self-guard: an empty capture would make the assertion vacuous.
79
- expect(capturedSections).toHaveLength(3);
80
-
81
- // No className, no color, no spacing anywhere in what the page contributes.
82
- // This is what lets a site with its own design system adopt the page
83
- // without fighting it.
84
- const serialized = JSON.stringify(capturedSections);
85
- expect(serialized).not.toMatch(/className|style|#[0-9a-fA-F]{3,8}\b/);
86
- });
87
-
88
- it("is not indexable", async () => {
89
- // Reachable only by having just booked; indexing it would surface a
90
- // confirmation page to people who never made a booking.
91
- const metadata = await generateMetadata({
92
- params: Promise.resolve({ locale: "en" }),
93
- });
94
- expect(metadata.robots).toEqual({ index: false, follow: true });
95
- });
96
- });
@@ -1,92 +0,0 @@
1
- // oss: false
2
- /**
3
- * /[locale]/schedule/confirmed — the scheduling module's confirmation page
4
- * (#4633). The destination `BookerIsland` navigates to the moment the booking
5
- * POST resolves (its `bookedRedirect` prop), so the visitor never leaves this
6
- * site to find out the booking worked.
7
- *
8
- * The module OWNS this page. It is not a marketing page that happens to follow
9
- * a booker: it ships with the module, it is gated with the module, and a
10
- * scaffold without the module has no such route. That is why it lives under
11
- * `schedule/` rather than at `/thanks/<slug>` — `/thanks` is a core marketing
12
- * surface every scaffold carries, and a page in it would outlive the module
13
- * that gave it a reason to exist.
14
- *
15
- * FINISHED, NOT A STUB — and it owns no design of its own. Every visual
16
- * decision resolves through the consuming site's theme tokens and its
17
- * `SectionRenderer` catalog; this file contributes a section list and nothing
18
- * else. The copy is read from the site's own `schedule` message namespace,
19
- * so customizing the page is an edit to `messages/<locale>.json` — the same
20
- * surface the site already edits for every other page — not a fork of this
21
- * route. A site whose design system differs has nothing here to fight.
22
- *
23
- * Carries no booking detail because it receives none: the redirect is a
24
- * client-side `router.push` with no state, and re-reading the booking would
25
- * mean a second provider call to tell the visitor something the confirmation
26
- * email already carries. It confirms, and it says what happens next.
27
- *
28
- * `noindex`: a booking confirmation is reachable only by having just booked.
29
- */
30
- import { SectionRenderer } from "@working-theory/ui/sections";
31
- import type { PageSection } from "@working-theory/validation";
32
- import type { Metadata } from "next";
33
- import { getTranslations } from "next-intl/server";
34
- import type { ReactElement } from "react";
35
-
36
- interface Props {
37
- params: Promise<{ locale: string }>;
38
- }
39
-
40
- export async function generateMetadata({ params }: Props): Promise<Metadata> {
41
- const { locale } = await params;
42
- const t = await getTranslations({ locale, namespace: "schedule" });
43
- return {
44
- title: t("confirmed.title"),
45
- description: t("confirmed.description"),
46
- robots: { index: false, follow: true },
47
- };
48
- }
49
-
50
- export default async function Page({ params }: Props): Promise<ReactElement> {
51
- const { locale } = await params;
52
- const t = await getTranslations({ locale, namespace: "schedule" });
53
-
54
- const sections: PageSection[] = [
55
- {
56
- id: "schedule-confirmed-hero",
57
- type: "hero",
58
- data: {
59
- headline: t("confirmed.headline"),
60
- subheadline: t("confirmed.subheadline"),
61
- },
62
- },
63
- {
64
- id: "schedule-confirmed-prepare",
65
- type: "content-block",
66
- data: {
67
- headline: t("confirmed.prepareHeadline"),
68
- body: t("confirmed.prepareBody"),
69
- },
70
- },
71
- {
72
- id: "schedule-confirmed-cta",
73
- type: "cta",
74
- data: {
75
- headline: t("confirmed.ctaHeadline"),
76
- description: t("confirmed.ctaDescription"),
77
- primaryCta: { label: t("confirmed.ctaPrimaryLabel"), href: "/" },
78
- secondaryCta: {
79
- label: t("confirmed.ctaSecondaryLabel"),
80
- href: "/contact",
81
- variant: "ghost",
82
- },
83
- },
84
- },
85
- ];
86
-
87
- return (
88
- <main>
89
- <SectionRenderer sections={sections} locale={locale} />
90
- </main>
91
- );
92
- }
@@ -1,82 +0,0 @@
1
- // oss: false
2
- /**
3
- * /[locale]/schedule — the scheduling module's booking page (#4633).
4
- *
5
- * Exists only because the `scheduling` module is installed: `create-ailk
6
- * --modules marketing,scheduling` copies this directory, and a scaffold
7
- * without the module never receives it (packages/create-ailk/src/surfaces.ts,
8
- * the `schedule` gateable surface).
9
- *
10
- * API-backed rather than MDX-backed, the same shape as the `flows` surface:
11
- * everything the page shows about the event — its title, its duration, its
12
- * meeting type, its open slots — is DERIVED by the `booker` section from
13
- * apps/api's `/v1/schedule/availability`, so there is no content file that
14
- * could disagree with the calendar. The one thing the scaffolder cannot know
15
- * is WHICH event to book, so that arrives as configuration.
16
- *
17
- * CREDENTIALS. `SCHEDULING_CAL_LINK` is read here, on the server, and it is
18
- * not the API key — the key lives in apps/api's scheduling service and is
19
- * never read by this app at all. `calLink` is a public identifier (it is the
20
- * provider's own public booking-page path) and reaching the browser is its
21
- * job: the booker sends it back to `/v1/schedule/*` to name the event. It is
22
- * deliberately NOT a `NEXT_PUBLIC_` variable so that the whole scheduling
23
- * config surface stays server-read, which is the rule architecture.yaml's
24
- * scheduling block enforces.
25
- *
26
- * Unconfigured is a 404, not an error page — the flows precedent: to a
27
- * visitor, a booking page for an event the operator never named is
28
- * indistinguishable from a page that does not exist.
29
- */
30
- import { SectionRenderer } from "@working-theory/ui/sections";
31
- import { registerSchedulingSections } from "@working-theory/ui/slices/scheduling";
32
- import type { PageSection } from "@working-theory/validation";
33
- import type { Metadata } from "next";
34
- import { notFound } from "next/navigation";
35
- import { getTranslations } from "next-intl/server";
36
- import type { ReactElement } from "react";
37
-
38
- // `SectionRenderer`'s oss dispatch maps `booker` to a placeholder, because the
39
- // real component strips with this slice; this call contributes the real one and
40
- // must stay an explicit statement, not a bare side-effect import (#5214 D4).
41
- registerSchedulingSections();
42
-
43
- /** Site-relative path of the confirmation page this module also owns. */
44
- const BOOKED_REDIRECT = "/schedule/confirmed";
45
-
46
- interface Props {
47
- params: Promise<{ locale: string }>;
48
- }
49
-
50
- export async function generateMetadata({ params }: Props): Promise<Metadata> {
51
- const { locale } = await params;
52
- const t = await getTranslations({ locale, namespace: "schedule" });
53
- return { title: t("title"), description: t("description") };
54
- }
55
-
56
- export default async function Page({ params }: Props): Promise<ReactElement> {
57
- const { locale } = await params;
58
- const calLink = process.env["SCHEDULING_CAL_LINK"];
59
- if (!calLink) {
60
- notFound();
61
- }
62
-
63
- const t = await getTranslations({ locale, namespace: "schedule" });
64
- const sections: PageSection[] = [
65
- {
66
- id: "schedule-booker",
67
- type: "booker",
68
- data: {
69
- calLink,
70
- eyebrow: t("eyebrow"),
71
- headline: t("headline"),
72
- bookedRedirect: BOOKED_REDIRECT,
73
- },
74
- },
75
- ];
76
-
77
- return (
78
- <main>
79
- <SectionRenderer sections={sections} locale={locale} />
80
- </main>
81
- );
82
- }
@@ -1,25 +0,0 @@
1
- Blog posts live here — one .mdx file per post.
2
-
3
- The /blog route is OFF by default. Turn it on by scaffolding the blog surface:
4
-
5
- npx create-ailk --sync-architecture marketing --surfaces blog .
6
-
7
- then add the route with the `scaffold-page` skill, or re-scaffold with
8
- `--surfaces blog`.
9
-
10
- Frontmatter contract (enforced at build time by source.config.ts):
11
-
12
- ---
13
- title: string # required
14
- description: string # required
15
- date: 2026-01-31 # optional, ISO date — datePublished
16
- updated: 2026-02-14 # optional, ISO date — dateModified
17
- image: /og/my-post.png # optional
18
- author: # optional
19
- name: Jane Doe
20
- avatar: /avatars/jane.png
21
- tags: [launch, notes] # optional
22
- ---
23
-
24
- This file is a placeholder so the directory survives packaging. Delete it once
25
- you have posts — it is not read by anything (the loader globs *.mdx only).
@@ -1,28 +0,0 @@
1
- ---
2
- title: Documentation
3
- description: Documentation for {{COMPANY_NAME}}. Replace this page with your own getting-started guide.
4
- ---
5
-
6
- # Documentation
7
-
8
- This is the docs landing page. Replace it with whatever a new reader of your
9
- documentation needs first.
10
-
11
- ## Adding a page
12
-
13
- Every `.mdx` file in this directory becomes a page under `/docs`. The filename
14
- is the URL segment; `index.mdx` is the section root. Two frontmatter fields are
15
- required:
16
-
17
- ```yaml
18
- ---
19
- title: string
20
- description: string
21
- ---
22
- ```
23
-
24
- ## Turning docs off
25
-
26
- The `/docs` route is part of the `docs` surface. If your site does not need
27
- documentation, re-scaffold without it and the route will not exist — an absent
28
- route is better than one serving an empty page.
@@ -1,31 +0,0 @@
1
- /**
2
- * Jest stub for @/.source/server — the fumadocs-mdx codegen output.
3
- *
4
- * The real .source/server.ts uses top-level await and ESM-only imports from
5
- * fumadocs-mdx/runtime/server, which ts-jest cannot load in CJS mode.
6
- * This stub provides the minimal shape needed by lib/source.ts so that
7
- * the loader() call in that file does not throw at import time.
8
- *
9
- * Tests that exercise the blog/docs/llms routes inject their own source
10
- * stubs via props, so the loader's output is never used in test execution.
11
- *
12
- * Referenced by: apps/web/jest.config.cjs moduleNameMapper '^@/\\.source/server$'
13
- * Added: issue #1077 — fumadocs-core ESM + .source/server CJS compatibility
14
- * Updated: issue #1459 — added docs collection stub alongside blog
15
- *
16
- * @note The real .source/ directory is gitignored (codegen output).
17
- * This committed stub lives in lib/__mocks__/ so it is tracked by git.
18
- */
19
- "use strict";
20
-
21
- exports.blog = {
22
- toFumadocsSource: function () {
23
- return { files: [] };
24
- },
25
- };
26
-
27
- exports.docs = {
28
- toFumadocsSource: function () {
29
- return { files: [] };
30
- },
31
- };
@@ -1,95 +0,0 @@
1
- /**
2
- * Adapter: Fumadocs blog source → `BlogListDataSchema` (issue #2713).
3
- *
4
- * The `@working-theory/ui` `BlogList` section is presentational (ADR-0027): it renders,
5
- * the app maps source → data. This adapter turns `blogSource.getPages()` into
6
- * the section's `data` prop, applying the field mapping from the issue:
7
- *
8
- * title ← frontmatter.title
9
- * slug ← page.url (with the `/blog/` baseUrl prefix stripped, since
10
- * the section re-prepends it: `href={`/blog/${slug}`}`)
11
- * excerpt ← frontmatter.description
12
- * publishedAt ← frontmatter.date
13
- * coverImage ← frontmatter.image
14
- * author ← frontmatter.author
15
- * categories ← frontmatter.tags
16
- *
17
- * Locale-aware: the locale is threaded so callers can pass a locale-prefixed
18
- * baseUrl for pagination when the route paginates (>1 page).
19
- */
20
- import type { BlogListData } from "@working-theory/validation";
21
-
22
- /** Frontmatter shape consumed here — must match `source.config.ts` blog schema. */
23
- interface BlogFrontmatter {
24
- title: string;
25
- description: string;
26
- date?: string;
27
- updated?: string;
28
- image?: string;
29
- author?: { name: string; avatar?: string };
30
- tags?: string[];
31
- }
32
-
33
- /** The subset of a Fumadocs page entry this adapter reads. */
34
- interface BlogPageEntry {
35
- url: string;
36
- data: unknown;
37
- }
38
-
39
- /** Optional pagination inputs; pagination is emitted only when totalPages > 1. */
40
- interface BlogListPagination {
41
- page: number;
42
- totalPages: number;
43
- baseUrl: string;
44
- }
45
-
46
- type BlogPostPreview = BlogListData["posts"][number];
47
-
48
- /**
49
- * Strip the `/blog/` baseUrl prefix the section re-prepends when linking
50
- * (`href={`/blog/${slug}`}`). The bare baseUrl (`/blog`, the index page that
51
- * `getPages()` also returns) maps to an empty slug so the link stays `/blog/`
52
- * rather than doubling the prefix.
53
- */
54
- function toSlug(url: string): string {
55
- if (url === "/blog") return "";
56
- return url.replace(/^\/blog\//, "");
57
- }
58
-
59
- function toPreview(entry: BlogPageEntry): BlogPostPreview {
60
- const fm = entry.data as BlogFrontmatter;
61
- return {
62
- title: fm.title,
63
- slug: toSlug(entry.url),
64
- excerpt: fm.description,
65
- publishedAt: fm.date ?? "",
66
- ...(fm.image ? { coverImage: { src: fm.image, alt: fm.title } } : {}),
67
- ...(fm.author
68
- ? {
69
- author: {
70
- name: fm.author.name,
71
- ...(fm.author.avatar
72
- ? { avatar: { src: fm.author.avatar, alt: fm.author.name } }
73
- : {}),
74
- },
75
- }
76
- : {}),
77
- ...(fm.tags && fm.tags.length > 0 ? { categories: fm.tags } : {}),
78
- };
79
- }
80
-
81
- /**
82
- * Map Fumadocs blog pages to `BlogList` section data.
83
- *
84
- * @param pages blog source entries (`blogSource.getPages()`).
85
- * @param pagination optional pagination; emitted only when `totalPages > 1`.
86
- */
87
- export function toBlogListData(
88
- pages: BlogPageEntry[],
89
- pagination?: BlogListPagination,
90
- ): BlogListData {
91
- return {
92
- posts: pages.map(toPreview),
93
- ...(pagination && pagination.totalPages > 1 ? { pagination } : {}),
94
- };
95
- }