create-ailk 0.21.0 → 0.23.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 (342) hide show
  1. package/component-catalog.json +186 -8
  2. package/component-catalog.md +9 -7
  3. package/dist/add-module.js +20 -14
  4. package/dist/add-route.d.ts +55 -1
  5. package/dist/add-route.js +294 -12
  6. package/dist/cli.js +27 -10
  7. package/dist/derive-architecture.d.ts +12 -3
  8. package/dist/derive-architecture.js +20 -8
  9. package/dist/design-package.js +7 -2
  10. package/dist/lib/design-checks.js +4 -2
  11. package/dist/lib/env-catalog.js +5 -5
  12. package/dist/lib/npm-auth.d.ts +9 -0
  13. package/dist/lib/npm-auth.js +48 -0
  14. package/dist/lib/scaffold-render-sweep.d.ts +3 -2
  15. package/dist/lib/scaffold-render-sweep.js +5 -8
  16. package/dist/module-architecture.js +99 -9
  17. package/dist/paid-sections.d.ts +21 -2
  18. package/dist/paid-sections.js +69 -7
  19. package/dist/programmatic.js +19 -5
  20. package/dist/seed-flows.d.ts +4 -1
  21. package/dist/seed-flows.js +50 -4
  22. package/dist/site-chrome.d.ts +8 -0
  23. package/dist/site-chrome.js +17 -1
  24. package/dist/spoke-checkout.d.ts +65 -41
  25. package/dist/spoke-checkout.js +208 -430
  26. package/dist/starter-grid.d.ts +0 -3
  27. package/dist/starter-grid.js +24 -14
  28. package/dist/surfaces.js +52 -26
  29. package/dist/sync-routes.d.ts +38 -1
  30. package/dist/sync-routes.js +187 -13
  31. package/dist/template-record.d.ts +4 -3
  32. package/dist/template-record.js +8 -4
  33. package/dist/topology.js +25 -1
  34. package/dist/update.js +15 -0
  35. package/package.json +3 -3
  36. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
  37. package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
  38. package/site-templates/designs/minimalism/DESIGN.md +1 -1
  39. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
  40. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
  41. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
  42. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
  43. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
  44. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
  45. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
  46. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  47. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
  48. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
  49. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  50. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
  51. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
  52. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
  53. package/templates/.env.example +9 -7
  54. package/templates/.github/workflows/ci.yml +13 -0
  55. package/templates/README.md +1 -0
  56. package/templates/_gitignore +4 -0
  57. package/templates/apps/api/.env.example +27 -0
  58. package/templates/apps/api/CLAUDE.md +87 -17
  59. package/templates/apps/api/package.json +1 -0
  60. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
  61. package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
  62. package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
  63. package/templates/apps/api/src/config/index.ts +109 -0
  64. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  65. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +19 -0
  66. package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
  67. package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
  68. package/templates/apps/api/src/lib/consent-store.ts +28 -0
  69. package/templates/apps/api/src/lib/error-serializer.ts +74 -7
  70. package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
  71. package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
  72. package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
  73. package/templates/apps/api/src/lib/stripe.ts +74 -4
  74. package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
  75. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +14 -3
  76. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
  77. package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
  78. package/templates/apps/api/src/middleware/auth.ts +13 -6
  79. package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
  80. package/templates/apps/api/src/openapi/spec.ts +33 -3
  81. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
  82. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
  83. package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -0
  84. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
  85. package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1300 -0
  86. package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
  87. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
  88. package/templates/apps/api/src/routes/checkout/session.ts +57 -25
  89. package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
  90. package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +386 -0
  91. package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
  92. package/templates/apps/api/src/routes/consent/account.ts +268 -0
  93. package/templates/apps/api/src/routes/consent/index.ts +15 -7
  94. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
  95. package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
  96. package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
  97. package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
  98. package/templates/apps/api/src/routes/content/get-page.ts +4 -0
  99. package/templates/apps/api/src/routes/content/index.ts +16 -2
  100. package/templates/apps/api/src/routes/content/offerings.ts +184 -0
  101. package/templates/apps/api/src/routes/content/pricing.ts +176 -0
  102. package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
  103. package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +515 -0
  104. package/templates/apps/api/src/routes/leads/index.ts +164 -7
  105. package/templates/apps/api/src/routes/webhooks/README.md +66 -0
  106. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -0
  107. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
  108. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
  109. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +5 -5
  110. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
  111. package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
  112. package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
  113. package/templates/apps/api/src/routes/webhooks/stripe.ts +245 -36
  114. package/templates/apps/api/src/server.ts +163 -46
  115. package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
  116. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -2
  117. package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
  118. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +1383 -133
  119. package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
  120. package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
  121. package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
  122. package/templates/apps/api/src/services/checkout-buyer.ts +100 -4
  123. package/templates/apps/api/src/services/consent.ts +95 -0
  124. package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
  125. package/templates/apps/api/src/services/entitlements.ts +157 -1
  126. package/templates/apps/api/src/services/lead-routing.ts +431 -47
  127. package/templates/apps/api/src/services/magnet-completion.ts +242 -0
  128. package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -0
  129. package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
  130. package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +868 -0
  131. package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
  132. package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +396 -0
  133. package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
  134. package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
  135. package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
  136. package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
  137. package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
  138. package/templates/apps/api/src/services/offerings/connection.ts +1003 -0
  139. package/templates/apps/api/src/services/offerings/index.ts +614 -0
  140. package/templates/apps/api/src/services/offerings/source.ts +140 -0
  141. package/templates/apps/api/src/services/offerings/store.ts +150 -0
  142. package/templates/apps/api/src/services/person.ts +164 -17
  143. package/templates/apps/api/src/services/plan-offerings.ts +125 -0
  144. package/templates/apps/api/src/services/public-site-resolver.ts +27 -15
  145. package/templates/apps/mcp/CLAUDE.md +22 -14
  146. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
  147. package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
  148. package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
  149. package/templates/apps/mcp/__tests__/http-transport.test.ts +4 -4
  150. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
  151. package/templates/apps/mcp/__tests__/server.test.ts +13 -11
  152. package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
  153. package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
  154. package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
  155. package/templates/apps/mcp/src/server.ts +5 -0
  156. package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
  157. package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
  158. package/templates/apps/mcp/src/tools/purchase.ts +30 -2
  159. package/templates/apps/mcp/src/workspace-server.ts +15 -0
  160. package/templates/apps/web/.env.example +24 -31
  161. package/templates/apps/web/CLAUDE.md +393 -9
  162. package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
  163. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
  164. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
  165. package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
  166. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
  167. package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
  168. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
  169. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
  170. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
  171. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
  172. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
  173. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
  174. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
  175. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
  176. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
  177. package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
  178. package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
  179. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
  180. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
  181. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
  182. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
  183. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
  184. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
  185. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
  186. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
  187. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
  188. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
  189. package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
  190. package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
  191. package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
  192. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
  193. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
  194. package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
  195. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
  196. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +295 -0
  197. package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +120 -0
  198. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +23 -0
  199. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +319 -0
  200. package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
  201. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
  202. package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +141 -0
  203. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +424 -0
  204. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +72 -11
  205. package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
  206. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  207. package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
  208. package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
  209. package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
  210. package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
  211. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
  212. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +17 -1
  213. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
  214. package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +160 -13
  215. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +466 -36
  216. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +7 -0
  217. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +172 -57
  218. package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
  219. package/templates/apps/web/app/[locale]/contact/page.tsx +99 -60
  220. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
  221. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
  222. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +341 -4
  223. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
  224. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
  225. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
  226. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +257 -0
  227. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -0
  228. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
  229. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
  230. package/templates/apps/web/app/[locale]/flows/[slug]/account-invite.actions.ts +35 -0
  231. package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -0
  232. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
  233. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
  234. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +1 -1
  235. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  236. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
  237. package/templates/apps/web/app/[locale]/layout.tsx +55 -4
  238. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
  239. package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
  240. package/templates/apps/web/app/[locale]/page.tsx +13 -1
  241. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  242. package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
  243. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
  244. package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
  245. package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
  246. package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
  247. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
  248. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +2 -2
  249. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
  250. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  251. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
  252. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  253. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
  254. package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
  255. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
  256. package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
  257. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
  258. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
  259. package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
  260. package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
  261. package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
  262. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  263. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
  264. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
  265. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
  266. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
  267. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
  268. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
  269. package/templates/apps/web/auth.ts +28 -2
  270. package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
  271. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
  272. package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
  273. package/templates/apps/web/lib/__tests__/deck-page.test.ts +7 -1
  274. package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
  275. package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -0
  276. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
  277. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
  278. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +101 -25
  279. package/templates/apps/web/lib/account-invite.ts +37 -0
  280. package/templates/apps/web/lib/auth-routes.ts +36 -2
  281. package/templates/apps/web/lib/checkout-config.ts +16 -0
  282. package/templates/apps/web/lib/consent-policies.ts +22 -0
  283. package/templates/apps/web/lib/deck-page.ts +7 -0
  284. package/templates/apps/web/lib/deliverable-grant.ts +18 -0
  285. package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
  286. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
  287. package/templates/apps/web/lib/in-page-magnet.ts +416 -0
  288. package/templates/apps/web/lib/magnet-routes.ts +42 -0
  289. package/templates/apps/web/lib/modules.generated.ts +9 -0
  290. package/templates/apps/web/lib/newsletter.actions.ts +202 -0
  291. package/templates/apps/web/lib/offerings-pricing.ts +326 -0
  292. package/templates/apps/web/lib/posting-title.ts +17 -0
  293. package/templates/apps/web/lib/prisma-for-auth.ts +8 -0
  294. package/templates/apps/web/lib/sign-up-agreements.ts +449 -0
  295. package/templates/apps/web/lib/site-chrome.ts +2 -0
  296. package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
  297. package/templates/apps/web/messages/en.json +71 -20
  298. package/templates/apps/web/middleware.ts +12 -2
  299. package/templates/apps/web/package.json +1 -0
  300. package/templates/apps/web/security-headers.config.ts +17 -0
  301. package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
  302. package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
  303. package/templates/apps/web/src/lib/lead-write.ts +90 -0
  304. package/templates/apps/web/src/lib/rate-limit.ts +79 -0
  305. package/templates/content/en/BlogList/blog.mdx +4 -71
  306. package/templates/content/en/BookCall/book-a-call.mdx +1 -41
  307. package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
  308. package/templates/content/en/BookCall/onsite-service.mdx +1 -17
  309. package/templates/content/en/BookCall/start-project.mdx +1 -17
  310. package/templates/content/en/ContactPage/contact.mdx +6 -63
  311. package/templates/content/en/HomePage/home.mdx +3 -21
  312. package/templates/database/CHANGELOG.md +69 -0
  313. package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
  314. package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
  315. package/templates/database/__tests__/flow-completion.db.test.ts +136 -0
  316. package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
  317. package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
  318. package/templates/database/__tests__/offerings-migration.test.ts +197 -0
  319. package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
  320. package/templates/database/__tests__/offerings.db.test.ts +303 -0
  321. package/templates/database/__tests__/seed.test.ts +64 -1
  322. package/templates/database/auth/schema.prisma +4 -0
  323. package/templates/database/config/schema.prisma +91 -0
  324. package/templates/database/inbox/schema.prisma +17 -2
  325. package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
  326. package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -0
  327. package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
  328. package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
  329. package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
  330. package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
  331. package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
  332. package/templates/database/package.json +1 -1
  333. package/templates/database/seed.ts +62 -3
  334. package/versions.json +35 -35
  335. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
  336. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
  337. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
  338. package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
  339. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
  340. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
  341. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
  342. package/templates/apps/web/lib/meeting-slugs.ts +0 -63
@@ -0,0 +1,352 @@
1
+ // oss: false
2
+ /**
3
+ * magnet-page.tsx — the standard magnet page pair (#8840 D8).
4
+ *
5
+ * Every multi-step magnet is two pages, the canon's #5053 split moved to the
6
+ * magnet's own path: an indexable LANDING (`/<magnet>`, inside the site's
7
+ * chrome — a centred hero of the flow record's `landing` copy, a CTA to
8
+ * `/<magnet>/start`, and the `Assessment` page type's single-layer
9
+ * `WebApplication` JSON-LD) and a `noindex, follow` WALK (`/<magnet>/start`,
10
+ * chrome-free by `lib/magnet-routes.ts`, the back arrow alone above step 1).
11
+ * This module is the only code that renders either half: `/flows/<slug>`,
12
+ * the careers apply pages and every pair `create-ailk add-route magnet`
13
+ * writes are callers of the factories below.
14
+ *
15
+ * The read: one `flows.get(slug)` shared per server instance for 15 seconds
16
+ * per slug (the /contact idiom, #8731), so a page's metadata, its body and a
17
+ * burst of views make one hub read. A slug the schema refuses is `absent`
18
+ * with no read and no cache entry, each insert first drops the stale
19
+ * entries, and the map holds at most 256 slugs, so random URLs cannot grow
20
+ * it (SEC-8840-G). A refused
21
+ * connection or a 5xx is `unavailable`: both pages then show the site's
22
+ * contact email instead of a 500, are `noindex`, and retry nothing. A site
23
+ * with no hub to ask — the read answers 503 `flows_not_configured`, or a
24
+ * failed read with no API origin configured — is `not-configured` (#9219);
25
+ * a pair renders it as `unavailable`, and `/reports/<slug>` uses it to 404
26
+ * an unknown slug where a hub outage shows the contact email. A walk
27
+ * that exists apart from its flow (`/careers/apply`) passes a `title`: its
28
+ * fallback then carries that page heading, and an absent flow renders the
29
+ * same fallback rather than a 404 (#9219).
30
+ *
31
+ * A flow whose served config carries a `deliverable` (#8848 D7) has one
32
+ * route, `/reports/<slug>`: both halves redirect there (307) before
33
+ * rendering anything, so `/flows/<slug>` and every `add-route magnet` pair
34
+ * hand it over and an ungated deliverable never gets a form here. The
35
+ * deliverable's own walk calls `renderMagnetStart` with `asDeliverable`.
36
+ */
37
+ import { flows } from "@working-theory/api-client";
38
+ import { JsonLd } from "@working-theory/jsonld";
39
+ import { flowRouteMetadata } from "@working-theory/metadata";
40
+ import { buildJsonLd } from "@working-theory/schema";
41
+ import { ContactFallback, Container, Heading, localeHref } from "@working-theory/ui";
42
+ import { NavBackLink } from "@working-theory/ui/chrome";
43
+ import { SectionRenderer } from "@working-theory/ui/sections";
44
+ import type { FlowConfig, PageSection } from "@working-theory/validation";
45
+ import { flowSlugSchema, resolveLocalizedString } from "@working-theory/validation";
46
+ import type { Metadata } from "next";
47
+ import { notFound, redirect } from "next/navigation";
48
+ import { getTranslations } from "next-intl/server";
49
+ import type { ReactElement } from "react";
50
+
51
+ import { readSiteContactEmail } from "../../contact/site-contact-email";
52
+
53
+ import { FlowStepperClient } from "./FlowStepperClient";
54
+
55
+ /** The marker every standard magnet page carries in its header comment (D13). */
56
+ export const MAGNET_PAGE_MARKER = "Standard magnet page (#8840)";
57
+
58
+ /** How long one slug's read is shared across requests (/contact's #8731 value). */
59
+ const MAGNET_READ_TTL_MS = 15_000;
60
+
61
+ /** At most this many slugs are held at once; past it a read is not shared. */
62
+ const MAGNET_READ_MAX_ENTRIES = 256;
63
+
64
+ export type MagnetRead =
65
+ | { kind: "ok"; config: FlowConfig }
66
+ | { kind: "absent" }
67
+ | { kind: "unavailable" }
68
+ | { kind: "not-configured" };
69
+
70
+ const magnetReads = new Map<string, { at: number; read: Promise<MagnetRead> }>();
71
+
72
+ /**
73
+ * Whether an API origin is configured: the three names `getApiBase()` in
74
+ * packages/api-client/src/http.ts reads, in step with it (as `/pricing`
75
+ * checks them). Without one that function answers `localhost:3001`.
76
+ */
77
+ function apiOriginConfigured(): boolean {
78
+ return Boolean(
79
+ process.env.AILK_API_URL ||
80
+ process.env.NEXT_PUBLIC_AILK_API_URL ||
81
+ process.env.NEXT_PUBLIC_API_URL,
82
+ );
83
+ }
84
+
85
+ /** The flows route's own answer when its hub has no database (apps/api `flows_not_configured`). */
86
+ function isFlowsNotConfigured(body: unknown): boolean {
87
+ const error = (body as { error?: { code?: unknown } } | null)?.error;
88
+ return error?.code === "flows_not_configured";
89
+ }
90
+
91
+ /** A read that failed: `not-configured` when there is no hub to ask, else `unavailable`. */
92
+ function failedRead(): MagnetRead {
93
+ return apiOriginConfigured() ? { kind: "unavailable" } : { kind: "not-configured" };
94
+ }
95
+
96
+ async function fetchMagnet(slug: string): Promise<MagnetRead> {
97
+ try {
98
+ const res = await flows.get(slug);
99
+ if (res.ok) return { kind: "ok", config: res.body };
100
+ if (res.status === 503 && isFlowsNotConfigured(res.body)) return { kind: "not-configured" };
101
+ return res.status >= 500 ? failedRead() : { kind: "absent" };
102
+ } catch {
103
+ return failedRead();
104
+ }
105
+ }
106
+
107
+ /** The served flow config for `slug`, shared per instance for 15 seconds. */
108
+ export function readMagnet(slug: string): Promise<MagnetRead> {
109
+ if (!flowSlugSchema.safeParse(slug).success) {
110
+ return Promise.resolve({ kind: "absent" });
111
+ }
112
+ const now = Date.now();
113
+ const hit = magnetReads.get(slug);
114
+ if (hit && now - hit.at < MAGNET_READ_TTL_MS) return hit.read;
115
+ for (const [key, entry] of magnetReads) {
116
+ if (now - entry.at >= MAGNET_READ_TTL_MS) magnetReads.delete(key);
117
+ }
118
+ const read = fetchMagnet(slug);
119
+ if (magnetReads.size < MAGNET_READ_MAX_ENTRIES) magnetReads.set(slug, { at: now, read });
120
+ return read;
121
+ }
122
+
123
+ /** The landing's title and line: the record's `landing` copy, else today's. */
124
+ async function landingCopy(
125
+ config: FlowConfig,
126
+ locale: string,
127
+ ): Promise<{ title: string; description: string; meta?: string; cta: string }> {
128
+ const t = await getTranslations({ locale, namespace: "flow" });
129
+ const pick = (value: Record<string, string> | undefined): string | undefined =>
130
+ value === undefined ? undefined : resolveLocalizedString(value, locale, config.defaultLocale);
131
+ const { landing } = config;
132
+ const meta = pick(landing?.meta);
133
+ return {
134
+ title: pick(landing?.title) ?? pick(config.steps[0]!.copy.title)!,
135
+ description: pick(landing?.description) ?? t("landingIntro"),
136
+ ...(meta !== undefined ? { meta } : {}),
137
+ cta: pick(landing?.cta) ?? t("getStarted"),
138
+ };
139
+ }
140
+
141
+ /**
142
+ * The site's contact email in place of a magnet the hub cannot serve (D8).
143
+ * With a `title`, the page's own `<h1>` sits above it (#9219).
144
+ */
145
+ export async function magnetUnavailable(locale: string, title?: string): Promise<ReactElement> {
146
+ const t = await getTranslations({ locale, namespace: "flow" });
147
+ return (
148
+ <Container className="py-section">
149
+ <div className="mx-auto max-w-container-sm">
150
+ {title !== undefined ? (
151
+ <Heading level={1} className="mb-block-tight">
152
+ {title}
153
+ </Heading>
154
+ ) : null}
155
+ <ContactFallback
156
+ email={readSiteContactEmail()}
157
+ text={{
158
+ heading: t("unavailableHeading"),
159
+ body: t("unavailableBody"),
160
+ noEmail: t("unavailableNoEmail"),
161
+ }}
162
+ />
163
+ </div>
164
+ </Container>
165
+ );
166
+ }
167
+
168
+ export interface MagnetLandingInput {
169
+ slug: string;
170
+ /** The landing's own path, e.g. `/diagnostic`; the CTA goes to `<path>/start`. */
171
+ path: string;
172
+ locale: string;
173
+ }
174
+
175
+ /** The indexable landing of a magnet pair (D8). */
176
+ export async function renderMagnetLanding({
177
+ slug,
178
+ path,
179
+ locale,
180
+ }: MagnetLandingInput): Promise<ReactElement> {
181
+ const read = await readMagnet(slug);
182
+ if (read.kind === "absent") notFound();
183
+ if (read.kind !== "ok") return magnetUnavailable(locale);
184
+ // #8848 D7 — a deliverable's one route.
185
+ if (read.config.deliverable !== undefined) redirect(localeHref(locale, `/reports/${slug}`));
186
+
187
+ const copy = await landingCopy(read.config, locale);
188
+ const sections: PageSection[] = [
189
+ {
190
+ id: "flow-landing-hero",
191
+ type: "hero",
192
+ data: {
193
+ variant: "centered",
194
+ headline: copy.title,
195
+ subheadline: copy.description,
196
+ ...(copy.meta !== undefined ? { body: copy.meta } : {}),
197
+ primaryCta: { label: copy.cta, href: localeHref(locale, `${path}/start`) },
198
+ },
199
+ },
200
+ ];
201
+
202
+ return (
203
+ <>
204
+ <JsonLd
205
+ schema={buildJsonLd({
206
+ type: "Assessment",
207
+ slug: read.config.slug,
208
+ title: copy.title,
209
+ description: copy.description,
210
+ locale,
211
+ })}
212
+ />
213
+ <main>
214
+ <SectionRenderer sections={sections} locale={locale} />
215
+ </main>
216
+ </>
217
+ );
218
+ }
219
+
220
+ /** The landing's metadata: indexable under the site's own rule; a fallback never is. */
221
+ export async function renderMagnetLandingMetadata({
222
+ slug,
223
+ locale,
224
+ }: Pick<MagnetLandingInput, "slug" | "locale">): Promise<Metadata> {
225
+ const read = await readMagnet(slug);
226
+ if (read.kind === "absent") return {};
227
+ if (read.kind !== "ok") return flowRouteMetadata();
228
+ const copy = await landingCopy(read.config, locale);
229
+ return { title: copy.title, description: copy.description };
230
+ }
231
+
232
+ export interface MagnetStartInput {
233
+ slug: string;
234
+ locale: string;
235
+ /** Where the back arrow goes, unlocalized (the landing, or the posting). */
236
+ backHref: string;
237
+ /** Page-supplied `hidden` field values (a posting's slug as `job`). */
238
+ hiddenValues?: Readonly<Record<string, string>>;
239
+ /** Replaces step 1's title at every locale it holds (a posting's title). */
240
+ firstStepTitle?: string;
241
+ /** #8848 D7 — the deliverable's own walk (`/reports/<slug>/start`): no redirect. */
242
+ asDeliverable?: boolean;
243
+ /**
244
+ * #9219 — a walk that exists apart from its flow (`/careers/apply`): the
245
+ * `<h1>` of its fallback, which an absent flow renders too, never a 404.
246
+ */
247
+ title?: string;
248
+ }
249
+
250
+ function withFirstStepTitle(config: FlowConfig, title: string): FlowConfig {
251
+ const [first, ...rest] = config.steps;
252
+ if (!first) return config;
253
+ const titled = Object.fromEntries(Object.keys(first.copy.title).map((key) => [key, title]));
254
+ return { ...config, steps: [{ ...first, copy: { ...first.copy, title: titled } }, ...rest] };
255
+ }
256
+
257
+ /** The chrome-free, noindex walk of a magnet pair (D8). */
258
+ export async function renderMagnetStart({
259
+ slug,
260
+ locale,
261
+ backHref,
262
+ hiddenValues,
263
+ firstStepTitle,
264
+ asDeliverable = false,
265
+ title,
266
+ }: MagnetStartInput): Promise<ReactElement> {
267
+ const read = await readMagnet(slug);
268
+ if (read.kind === "absent" && title === undefined) notFound();
269
+ if (read.kind === "ok" && read.config.deliverable !== undefined && !asDeliverable) {
270
+ redirect(localeHref(locale, `/reports/${slug}/start`));
271
+ }
272
+ const t = await getTranslations({ locale, namespace: "flow" });
273
+ const back = <NavBackLink href={localeHref(locale, backHref)} label={t("back")} />;
274
+ if (read.kind !== "ok") {
275
+ return (
276
+ <>
277
+ <div className="px-control-x pt-control-y">{back}</div>
278
+ {await magnetUnavailable(locale, title)}
279
+ </>
280
+ );
281
+ }
282
+
283
+ const config =
284
+ firstStepTitle !== undefined ? withFirstStepTitle(read.config, firstStepTitle) : read.config;
285
+ return (
286
+ <FlowStepperClient
287
+ config={config}
288
+ locale={locale}
289
+ slug={read.config.slug}
290
+ header={back}
291
+ {...(hiddenValues !== undefined ? { hiddenValues } : {})}
292
+ />
293
+ );
294
+ }
295
+
296
+ /** The walk's metadata: always `noindex, follow` (`flowRouteMetadata`). */
297
+ export async function renderMagnetStartMetadata({
298
+ slug,
299
+ locale,
300
+ firstStepTitle,
301
+ title,
302
+ }: Pick<MagnetStartInput, "slug" | "locale" | "firstStepTitle" | "title">): Promise<Metadata> {
303
+ const read = await readMagnet(slug);
304
+ if (read.kind !== "ok") return flowRouteMetadata(title !== undefined ? { title } : {});
305
+ const firstStep = read.config.steps[0];
306
+ if (!firstStep) return flowRouteMetadata();
307
+ return flowRouteMetadata({
308
+ title:
309
+ firstStepTitle ??
310
+ resolveLocalizedString(firstStep.copy.title, locale, read.config.defaultLocale),
311
+ });
312
+ }
313
+
314
+ // ─── Factories for a pair at a path (D8, D12) ──────────────────────────────
315
+
316
+ /** A magnet pair: the flow it walks, and the landing's own path. */
317
+ export interface MagnetRef {
318
+ slug: string;
319
+ path: string;
320
+ }
321
+
322
+ interface MagnetPageProps {
323
+ params: Promise<{ locale: string }>;
324
+ }
325
+
326
+ export function magnetLanding(ref: MagnetRef) {
327
+ return async function MagnetLandingPage({ params }: MagnetPageProps): Promise<ReactElement> {
328
+ const { locale } = await params;
329
+ return renderMagnetLanding({ slug: ref.slug, path: ref.path, locale });
330
+ };
331
+ }
332
+
333
+ export function magnetLandingMetadata(ref: MagnetRef) {
334
+ return async function generateMetadata({ params }: MagnetPageProps): Promise<Metadata> {
335
+ const { locale } = await params;
336
+ return renderMagnetLandingMetadata({ slug: ref.slug, locale });
337
+ };
338
+ }
339
+
340
+ export function magnetStart(ref: MagnetRef) {
341
+ return async function MagnetStartPage({ params }: MagnetPageProps): Promise<ReactElement> {
342
+ const { locale } = await params;
343
+ return renderMagnetStart({ slug: ref.slug, locale, backHref: ref.path });
344
+ };
345
+ }
346
+
347
+ export function magnetStartMetadata(ref: MagnetRef) {
348
+ return async function generateMetadata({ params }: MagnetPageProps): Promise<Metadata> {
349
+ const { locale } = await params;
350
+ return renderMagnetStartMetadata({ slug: ref.slug, locale });
351
+ };
352
+ }
@@ -1,6 +1,8 @@
1
1
  // oss: false
2
2
  /**
3
- * /[locale]/flows/[slug] — the tool's LANDING route (#5053 D3/D5).
3
+ * /[locale]/flows/[slug] — the tool's LANDING route (#5053 D3/D5). Standard
4
+ * magnet page (#8840): rendered by `magnet-page.tsx`'s `renderMagnetLanding`
5
+ * with the pair's path `/flows/<slug>`.
4
6
  *
5
7
  * An interactive tool is TWO routes. This is the indexed half: the document
6
8
  * that describes the tool, carries its structured data, and links to the
@@ -18,92 +20,29 @@
18
20
  * resting on #5019, which names `assessment` and `calculator` among the
19
21
  * builders that keep that shape byte for byte).
20
22
  *
21
- * Copy: a served `FlowConfig` carries no tool-level name or description, so
22
- * the landing derives its name from the flow's own first-step title — the
23
- * same string this route already used as its page title before the split —
24
- * and takes its supporting line and CTA label from the site's `flow`
25
- * message namespace. No consuming site's wording ships here.
26
- *
27
- * Same `flows.get` fetch and same 404-on-`!res.ok` as the flow route: a flow
28
- * that is absent or unconfigured has no landing either. Dynamic rendering
29
- * for the same reason — the config is DB-backed live data.
23
+ * Copy (#8840 D8): the flow record's `landing` block — its title, line, meta
24
+ * line and button — else the flow's own first-step title and the site's
25
+ * `flow` messages. No consuming site's wording ships here. A flow that is
26
+ * absent has no landing either; a hub that cannot be reached shows the site's
27
+ * contact email. Dynamic rendering — the config is DB-backed live data.
30
28
  */
31
- import { flows } from "@working-theory/api-client";
32
- import { JsonLd } from "@working-theory/jsonld";
33
- import { buildJsonLd } from "@working-theory/schema";
34
- import { SectionRenderer } from "@working-theory/ui/sections";
35
- import type { FlowConfig, PageSection } from "@working-theory/validation";
36
- import { resolveLocalizedString } from "@working-theory/validation";
37
29
  import type { Metadata } from "next";
38
- import { notFound } from "next/navigation";
39
- import { getTranslations } from "next-intl/server";
40
30
  import type { ReactElement } from "react";
41
31
 
32
+ import { renderMagnetLanding, renderMagnetLandingMetadata } from "./magnet-page";
33
+
42
34
  export const dynamic = "force-dynamic";
43
35
 
44
36
  interface Props {
45
37
  params: Promise<{ locale: string; slug: string }>;
46
38
  }
47
39
 
48
- /** The tool's name — the flow's own first-step title in the active locale. */
49
- function toolName(config: FlowConfig, locale: string): string {
50
- return resolveLocalizedString(
51
- config.steps[0]!.copy.title,
52
- locale,
53
- config.defaultLocale,
54
- );
55
- }
56
-
57
40
  export async function generateMetadata({ params }: Props): Promise<Metadata> {
58
41
  const { locale, slug } = await params;
59
- const res = await flows.get(slug);
60
- if (!res.ok) return {};
61
-
62
- const t = await getTranslations({ locale, namespace: "flow" });
63
- return {
64
- title: toolName(res.body, locale),
65
- description: t("landingIntro"),
66
- };
42
+ return renderMagnetLandingMetadata({ slug, locale });
67
43
  }
68
44
 
69
45
  export default async function Page({ params }: Props): Promise<ReactElement> {
70
46
  const { locale, slug } = await params;
71
- const res = await flows.get(slug);
72
- if (!res.ok) {
73
- notFound();
74
- }
75
-
76
- const t = await getTranslations({ locale, namespace: "flow" });
77
- const name = toolName(res.body, locale);
78
- const intro = t("landingIntro");
79
- const startHref = `/flows/${res.body.slug}/start`;
80
-
81
- const sections: PageSection[] = [
82
- {
83
- id: "flow-landing-hero",
84
- type: "hero",
85
- data: {
86
- headline: name,
87
- subheadline: intro,
88
- primaryCta: { label: t("getStarted"), href: startHref },
89
- },
90
- },
91
- ];
92
-
93
- return (
94
- <>
95
- <JsonLd
96
- schema={buildJsonLd({
97
- type: "Assessment",
98
- slug: res.body.slug,
99
- title: name,
100
- description: intro,
101
- locale,
102
- })}
103
- />
104
- <main>
105
- <SectionRenderer sections={sections} locale={locale} />
106
- </main>
107
- </>
108
- );
47
+ return renderMagnetLanding({ slug, path: `/flows/${slug}`, locale });
109
48
  }
@@ -1,23 +1,19 @@
1
1
  // oss: false
2
2
  /**
3
3
  * /[locale]/flows/[slug]/start — the flow half of the two-route tool
4
- * (#5053 D3/D5). Moved here from `../page.tsx`, which is now the indexed
5
- * landing; the fetch, the 404-on-`!res.ok` and the dynamic rendering are
6
- * unchanged from #3957 D8.
4
+ * (#5053 D3/D5). Standard magnet page (#8840): rendered by
5
+ * `magnet-page.tsx`'s `renderMagnetStart`, chrome-free with the back arrow to
6
+ * `/flows/<slug>` (`lib/magnet-routes.ts`).
7
7
  *
8
- * `flowRouteMetadata` (`@working-theory/metadata`) supplies `noindex,
9
- * follow` — the whole point of D4 is that this route does NOT write the
10
- * robots value, it gets it from what it mounts. `follow`, not `nofollow`:
11
- * the links out of a flow still carry.
8
+ * `flowRouteMetadata` (`@working-theory/metadata`, mounted by the renderer)
9
+ * supplies `noindex, follow` — the whole point of D4 is that this route does
10
+ * NOT write the robots value, it gets it from what it mounts. `follow`, not
11
+ * `nofollow`: the links out of a flow still carry.
12
12
  */
13
- import { flows } from "@working-theory/api-client";
14
- import { flowRouteMetadata } from "@working-theory/metadata";
15
- import { resolveLocalizedString } from "@working-theory/validation";
16
13
  import type { Metadata } from "next";
17
- import { notFound } from "next/navigation";
18
14
  import type { ReactElement } from "react";
19
15
 
20
- import { FlowStepperClient } from "../FlowStepperClient";
16
+ import { renderMagnetStart, renderMagnetStartMetadata } from "../magnet-page";
21
17
 
22
18
  export const dynamic = "force-dynamic";
23
19
 
@@ -27,27 +23,10 @@ interface Props {
27
23
 
28
24
  export async function generateMetadata({ params }: Props): Promise<Metadata> {
29
25
  const { locale, slug } = await params;
30
- const res = await flows.get(slug);
31
- if (!res.ok) return flowRouteMetadata();
32
-
33
- const firstStep = res.body.steps[0];
34
- if (!firstStep) return flowRouteMetadata();
35
-
36
- return flowRouteMetadata({
37
- title: resolveLocalizedString(
38
- firstStep.copy.title,
39
- locale,
40
- res.body.defaultLocale,
41
- ),
42
- });
26
+ return renderMagnetStartMetadata({ slug, locale });
43
27
  }
44
28
 
45
29
  export default async function Page({ params }: Props): Promise<ReactElement> {
46
30
  const { locale, slug } = await params;
47
- const res = await flows.get(slug);
48
- if (!res.ok) {
49
- notFound();
50
- }
51
-
52
- return <FlowStepperClient config={res.body} locale={locale} slug={slug} />;
31
+ return renderMagnetStart({ slug, locale, backHref: `/flows/${slug}` });
53
32
  }
@@ -80,7 +80,7 @@ describe("ForPage — snapshot regression", () => {
80
80
  });
81
81
  const { container } = render(element);
82
82
  expect(container.innerHTML).toMatchInlineSnapshot(
83
- `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Use Cases","description":"How teams use our platform.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</script><main id="main"><div><h1>Use Cases</h1><p data-testid="for-description" class="mt-block-tight text-lead">How teams use our platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source=""></div></article></div></div></div></main>"`,
83
+ `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Use Cases","description":"How teams use our platform.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</script><main id="main"><div><h1>Use Cases</h1><p data-testid="for-description" class="mt-block-tight text-lead">How teams use our platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source=""></div></article></div></div></div></main>"`,
84
84
  );
85
85
  });
86
86
  });
@@ -58,7 +58,7 @@ describe("how-to/[slug] — snapshot regression", () => {
58
58
  });
59
59
  const { container } = render(element);
60
60
  expect(container.innerHTML).toMatchInlineSnapshot(`
61
- "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Deploy AILK to Vercel","description":"Step-by-step guide to deploying AI Launch Kit on Vercel.","mainEntity":{"@context":"https://schema.org","@type":"HowTo","name":"Deploy AILK to Vercel","description":"Step-by-step guide to deploying AI Launch Kit on Vercel."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"How-To Guides","item":"http://localhost:3000/en/how-to"},{"@type":"ListItem","position":3,"name":"Deploy AILK to Vercel","item":"http://localhost:3000/en/how-to/deploy-ailk-vercel"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Deploy AILK to Vercel</h1><p data-testid="how-to-description" class="mt-block-tight text-lead">Step-by-step guide to deploying AI Launch Kit on Vercel.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&amp;>*:first-child]:mt-0 [&amp;_h2]:mt-block-loose [&amp;_h2]:scroll-mt-24 [&amp;_h2]:text-heading-3 [&amp;_h2]:font-display [&amp;_h2]:text-(--color-fg) [&amp;_h3]:mt-block-loose [&amp;_h3]:scroll-mt-24 [&amp;_h3]:text-heading-4 [&amp;_h3]:font-display [&amp;_h3]:text-(--color-fg) [&amp;_h4]:mt-block-tight [&amp;_h4]:scroll-mt-24 [&amp;_h4]:text-heading-4 [&amp;_h4]:font-display [&amp;_h4]:text-(--color-fg) [&amp;_h5]:mt-block-tight [&amp;_h5]:scroll-mt-24 [&amp;_h5]:text-heading-4 [&amp;_h5]:font-display [&amp;_h5]:text-(--color-fg) [&amp;_h6]:mt-block-tight [&amp;_h6]:scroll-mt-24 [&amp;_h6]:text-heading-4 [&amp;_h6]:font-display [&amp;_h6]:text-(--color-fg) [&amp;_p]:mt-paragraph [&amp;_strong]:font-semibold [&amp;_strong]:text-(--color-fg) [&amp;_em]:italic [&amp;_ul]:mt-paragraph [&amp;_ul]:list-disc [&amp;_ul]:ps-6 [&amp;_ol]:mt-paragraph [&amp;_ol]:list-decimal [&amp;_ol]:ps-6 [&amp;_li]:mt-1 [&amp;_li]:ps-1 [&amp;_li]:marker:text-(--color-text-muted) [&amp;_li>ul]:mt-1 [&amp;_li>ol]:mt-1 [&amp;_blockquote]:mt-paragraph [&amp;_blockquote]:border-s-4 [&amp;_blockquote]:border-(--color-border-strong) [&amp;_blockquote]:ps-4 [&amp;_blockquote]:italic [&amp;_blockquote]:text-(--color-text-muted) [&amp;_hr]:my-block-loose [&amp;_hr]:border-(--color-border) [&amp;_a]:text-(--color-accent) [&amp;_a]:underline [&amp;_a]:underline-offset-4 [&amp;_:not(pre)>code]:rounded [&amp;_:not(pre)>code]:bg-(--color-surface-secondary) [&amp;_:not(pre)>code]:px-1.5 [&amp;_:not(pre)>code]:py-0.5 [&amp;_:not(pre)>code]:text-code [&amp;_:not(pre)>code]:font-mono [&amp;_:not(pre)>code]:text-(--color-fg) [&amp;_pre]:mt-paragraph [&amp;_pre]:overflow-auto [&amp;_pre]:rounded-md [&amp;_pre]:border-hairline [&amp;_pre]:border-(--color-border) [&amp;_pre]:bg-(--color-bg) [&amp;_pre]:p-4 [&amp;_pre]:font-mono [&amp;_pre]:text-body-sm [&amp;_img]:mt-paragraph [&amp;_img]:h-auto [&amp;_img]:max-w-full [&amp;_img]:rounded-lg [&amp;_table]:mt-paragraph [&amp;_table]:w-full [&amp;_table]:border-collapse [&amp;_table]:text-body-sm [&amp;_th]:border-b [&amp;_th]:border-(--color-border) [&amp;_th]:bg-(--color-surface-secondary) [&amp;_th]:px-3 [&amp;_th]:py-2 [&amp;_th]:text-start [&amp;_th]:font-semibold [&amp;_th]:text-(--color-fg) [&amp;_td]:border-b [&amp;_td]:border-(--color-border) [&amp;_td]:px-3 [&amp;_td]:py-2 [&amp;_kbd]:rounded [&amp;_kbd]:border-hairline [&amp;_kbd]:border-(--color-border) [&amp;_kbd]:bg-(--color-surface-secondary) [&amp;_kbd]:px-1.5 [&amp;_kbd]:py-0.5 [&amp;_kbd]:text-caption [&amp;_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">## Steps
61
+ "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Deploy AILK to Vercel","description":"Step-by-step guide to deploying AI Launch Kit on Vercel.","mainEntity":{"@context":"https://schema.org","@type":"HowTo","name":"Deploy AILK to Vercel","description":"Step-by-step guide to deploying AI Launch Kit on Vercel."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"How-To Guides","item":"http://localhost:3000/en/how-to"},{"@type":"ListItem","position":3,"name":"Deploy AILK to Vercel","item":"http://localhost:3000/en/how-to/deploy-ailk-vercel"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Deploy AILK to Vercel</h1><p data-testid="how-to-description" class="mt-block-tight text-lead">Step-by-step guide to deploying AI Launch Kit on Vercel.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&amp;>*:first-child]:mt-0 [&amp;_h2]:mt-block-loose [&amp;_h2]:scroll-mt-24 [&amp;_h2]:text-heading-3 [&amp;_h2]:font-display [&amp;_h2]:text-(--color-fg) [&amp;_h3]:mt-block-loose [&amp;_h3]:scroll-mt-24 [&amp;_h3]:text-heading-4 [&amp;_h3]:font-display [&amp;_h3]:text-(--color-fg) [&amp;_h4]:mt-block-tight [&amp;_h4]:scroll-mt-24 [&amp;_h4]:text-heading-4 [&amp;_h4]:font-display [&amp;_h4]:text-(--color-fg) [&amp;_h5]:mt-block-tight [&amp;_h5]:scroll-mt-24 [&amp;_h5]:text-heading-4 [&amp;_h5]:font-display [&amp;_h5]:text-(--color-fg) [&amp;_h6]:mt-block-tight [&amp;_h6]:scroll-mt-24 [&amp;_h6]:text-heading-4 [&amp;_h6]:font-display [&amp;_h6]:text-(--color-fg) [&amp;_p]:mt-paragraph [&amp;_strong]:font-semibold [&amp;_strong]:text-(--color-fg) [&amp;_em]:italic [&amp;_ul]:mt-paragraph [&amp;_ul]:list-disc [&amp;_ul]:ps-6 [&amp;_ol]:mt-paragraph [&amp;_ol]:list-decimal [&amp;_ol]:ps-6 [&amp;_li]:mt-1 [&amp;_li]:ps-1 [&amp;_li]:marker:text-(--color-text-muted) [&amp;_li>ul]:mt-1 [&amp;_li>ol]:mt-1 [&amp;_blockquote]:mt-paragraph [&amp;_blockquote]:border-s-4 [&amp;_blockquote]:border-(--color-border-strong) [&amp;_blockquote]:ps-4 [&amp;_blockquote]:italic [&amp;_blockquote]:text-(--color-text-muted) [&amp;_hr]:my-block-loose [&amp;_hr]:border-(--color-border) [&amp;_a]:text-(--color-accent) [&amp;_a]:underline [&amp;_a]:underline-offset-4 [&amp;_:not(pre)>code]:rounded [&amp;_:not(pre)>code]:bg-(--color-surface-secondary) [&amp;_:not(pre)>code]:px-1.5 [&amp;_:not(pre)>code]:py-0.5 [&amp;_:not(pre)>code]:text-code [&amp;_:not(pre)>code]:font-mono [&amp;_:not(pre)>code]:text-(--color-fg) [&amp;_pre]:mt-paragraph [&amp;_pre]:overflow-auto [&amp;_pre]:rounded-md [&amp;_pre]:border-hairline [&amp;_pre]:border-(--color-border) [&amp;_pre]:bg-(--color-bg) [&amp;_pre]:p-4 [&amp;_pre]:font-mono [&amp;_pre]:text-body-sm [&amp;_img]:mt-paragraph [&amp;_img]:h-auto [&amp;_img]:max-w-full [&amp;_img]:rounded-lg [&amp;_table]:mt-paragraph [&amp;_table]:w-full [&amp;_table]:border-collapse [&amp;_table]:text-body-sm [&amp;_th]:border-b [&amp;_th]:border-(--color-border) [&amp;_th]:bg-(--color-surface-secondary) [&amp;_th]:px-3 [&amp;_th]:py-2 [&amp;_th]:text-start [&amp;_th]:font-semibold [&amp;_th]:text-(--color-fg) [&amp;_td]:border-b [&amp;_td]:border-(--color-border) [&amp;_td]:px-3 [&amp;_td]:py-2 [&amp;_kbd]:rounded [&amp;_kbd]:border-hairline [&amp;_kbd]:border-(--color-border) [&amp;_kbd]:bg-(--color-surface-secondary) [&amp;_kbd]:px-1.5 [&amp;_kbd]:py-0.5 [&amp;_kbd]:text-caption [&amp;_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">## Steps
62
62
 
63
63
  Body content.</div></div></article></div></div></div></main>"
64
64
  `);