create-ailk 0.1.1 → 0.3.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 (284) hide show
  1. package/component-catalog.md +210 -52
  2. package/dist/cli.js +0 -0
  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 +12 -3
  6. package/dist/lib/extract-module-bundle.js +236 -24
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/module-license-gate.d.ts +2 -0
  10. package/dist/lib/module-license-gate.js +1 -0
  11. package/dist/lib/paid-paths.d.ts +16 -0
  12. package/dist/lib/paid-paths.js +90 -0
  13. package/dist/module-architecture.d.ts +139 -23
  14. package/dist/module-architecture.js +668 -25
  15. package/dist/parse-args.d.ts +2 -2
  16. package/dist/parse-args.js +3 -1
  17. package/dist/programmatic.d.ts +2 -1
  18. package/dist/programmatic.js +15 -4
  19. package/dist/surfaces.d.ts +12 -36
  20. package/dist/surfaces.js +35 -5
  21. package/dist/sync-routes.js +31 -1
  22. package/package.json +14 -15
  23. package/templates/.claude/agents/web.md +2 -3
  24. package/templates/.claude/rules/architecture.md +5 -5
  25. package/templates/.claude/skills/README.md +2 -1
  26. package/templates/.env.example +21 -9
  27. package/templates/CONVENTIONS.md +6 -7
  28. package/templates/apps/api/.env.example +9 -0
  29. package/templates/apps/api/CLAUDE.md +35 -4
  30. package/templates/apps/api/package.json +0 -1
  31. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  32. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  33. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  34. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  35. package/templates/apps/api/src/config/index.ts +19 -0
  36. package/templates/apps/api/src/config/modules.ts +12 -14
  37. package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
  38. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  39. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  40. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  41. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  42. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  43. package/templates/apps/api/src/openapi/index.ts +9 -3
  44. package/templates/apps/api/src/openapi/spec.ts +505 -65
  45. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  46. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  49. package/templates/apps/api/src/routes/content/index.ts +38 -11
  50. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
  59. package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
  60. package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
  61. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  63. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  64. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  65. package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
  66. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  67. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  68. package/templates/apps/api/src/server.ts +471 -133
  69. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  70. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  71. package/templates/apps/api/src/services/consent.ts +236 -0
  72. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  73. package/templates/apps/api/src/services/listing-config.ts +58 -0
  74. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  75. package/templates/apps/api/src/services/project-listings.ts +602 -29
  76. package/templates/apps/api/src/vercel-handler.ts +60 -26
  77. package/templates/apps/mcp/.env.example +8 -0
  78. package/templates/apps/mcp/CLAUDE.md +2 -2
  79. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  80. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  81. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  82. package/templates/apps/mcp/src/config/modules.ts +14 -13
  83. package/templates/apps/mcp/src/server.ts +8 -4
  84. package/templates/apps/mcp/src/tools/index.ts +7 -22
  85. package/templates/apps/web/.env.example +15 -0
  86. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  87. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  88. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  90. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  91. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  92. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  93. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  94. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  95. package/templates/apps/web/app/[locale]/layout.tsx +13 -1
  96. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  97. package/templates/apps/web/jest.config.cjs +20 -13
  98. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  99. package/templates/apps/web/lib/site-brand.tsx +4 -1
  100. package/templates/apps/web/lib/site-theme.ts +74 -0
  101. package/templates/apps/web/next.config.mjs +1 -3
  102. package/templates/apps/web/package.json +1 -6
  103. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  104. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  105. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  106. package/templates/apps/web/public/favicon-16x16.png +0 -0
  107. package/templates/apps/web/public/favicon-32x32.png +0 -0
  108. package/templates/apps/web/public/favicon.ico +0 -0
  109. package/templates/apps/web/public/favicon.svg +6 -3
  110. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  111. package/templates/apps/web/public/logomark.svg +4 -0
  112. package/templates/apps/web/public/site.webmanifest +2 -2
  113. package/templates/apps/web/public/wordmark.svg +4 -0
  114. package/templates/content/_site.mdx +12 -0
  115. package/templates/database/CHANGELOG.md +94 -0
  116. package/templates/database/inbox/schema.prisma +165 -0
  117. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  118. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  119. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  120. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  121. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  122. package/templates/database/package.json +1 -1
  123. package/templates/database/scripts/db-generate-locked.sh +0 -0
  124. package/templates/package.json +1 -1
  125. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  126. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  127. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  128. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  129. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  130. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  131. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  132. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  133. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  134. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  135. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  136. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  137. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  138. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  139. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  140. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  141. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  142. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  143. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  144. package/templates/apps/api/src/lib/stripe.ts +0 -52
  145. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  146. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  147. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  148. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  149. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  150. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  151. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  152. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  153. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  154. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  155. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  156. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  157. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  158. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  159. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  160. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  161. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  162. package/templates/apps/api/src/routes/flows/README.md +0 -147
  163. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  164. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  165. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  166. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  167. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  168. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  169. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  170. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  171. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  172. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  173. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  174. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  175. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  176. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  177. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  178. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  179. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  180. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  181. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  182. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  183. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  184. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  185. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  186. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  187. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  188. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  189. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  190. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  191. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  192. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  193. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  194. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  195. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  196. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  197. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  198. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  199. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  200. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  201. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  202. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  203. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  204. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  205. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  206. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  207. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  208. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  209. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  210. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  211. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  212. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  213. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  214. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  215. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  216. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  217. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  218. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  219. package/templates/apps/api/src/services/flow-engine.ts +0 -1278
  220. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  221. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  222. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  223. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  224. package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
  225. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  226. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  227. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  228. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  229. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  230. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  231. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  232. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  233. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  234. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  235. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  236. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  237. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  238. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  239. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  240. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  241. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  242. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  243. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  246. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  247. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  248. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  249. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  250. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  251. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  258. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  259. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  260. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  261. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  262. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  263. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  264. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  265. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  266. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  267. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  268. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  269. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  270. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  271. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  272. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  273. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  274. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  275. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  276. package/templates/apps/web/content/en/blog/README.txt +0 -25
  277. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  278. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  279. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  280. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  281. package/templates/apps/web/lib/source.ts +0 -34
  282. package/templates/apps/web/public/logomark-dark.svg +0 -4
  283. package/templates/apps/web/public/logomark-light.svg +0 -4
  284. 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
- }