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