create-ailk 0.6.0 → 0.7.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 (248) hide show
  1. package/README.md +44 -5
  2. package/component-catalog.json +146 -21
  3. package/component-catalog.md +9 -6
  4. package/dist/add-module-core.d.ts +22 -0
  5. package/dist/add-module-core.js +50 -0
  6. package/dist/add-module.d.ts +89 -0
  7. package/dist/add-module.js +334 -0
  8. package/dist/category-profile.d.ts +126 -0
  9. package/dist/category-profile.js +262 -0
  10. package/dist/cli.js +124 -13
  11. package/dist/copy.d.ts +14 -0
  12. package/dist/copy.js +38 -1
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +6 -1
  15. package/dist/lib/extract-module-bundle.d.ts +76 -2
  16. package/dist/lib/extract-module-bundle.js +219 -8
  17. package/dist/lib/license-key-files.d.ts +15 -0
  18. package/dist/lib/license-key-files.js +102 -0
  19. package/dist/lib/module-license-gate.d.ts +6 -1
  20. package/dist/lib/module-license-gate.js +6 -1
  21. package/dist/lib/pro-callout.d.ts +16 -10
  22. package/dist/lib/pro-callout.js +27 -4
  23. package/dist/license-set.d.ts +34 -0
  24. package/dist/license-set.js +194 -0
  25. package/dist/module-architecture.d.ts +20 -0
  26. package/dist/module-architecture.js +77 -19
  27. package/dist/modules/commerce/0.6.0.tar.gz +0 -0
  28. package/dist/modules/commerce/0.6.1.tar.gz +0 -0
  29. package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
  30. package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
  31. package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
  32. package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
  33. package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
  34. package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
  35. package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
  36. package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
  37. package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
  38. package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
  39. package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
  40. package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
  41. package/dist/modules/workspace/0.6.0.tar.gz +0 -0
  42. package/dist/modules/workspace/0.6.1.tar.gz +0 -0
  43. package/dist/parse-args.d.ts +7 -0
  44. package/dist/parse-args.js +23 -1
  45. package/dist/programmatic.d.ts +46 -0
  46. package/dist/programmatic.js +144 -19
  47. package/dist/site-chrome.d.ts +18 -7
  48. package/dist/site-chrome.js +32 -14
  49. package/dist/site-roster.d.ts +127 -0
  50. package/dist/site-roster.js +494 -0
  51. package/dist/surfaces.d.ts +20 -7
  52. package/dist/surfaces.js +149 -28
  53. package/dist/sync-routes.d.ts +9 -0
  54. package/dist/sync-routes.js +2 -2
  55. package/dist/template-manifest.d.ts +58 -2
  56. package/dist/template-manifest.js +196 -69
  57. package/package.json +6 -4
  58. package/site-templates/_base/content/en/BookCall/book-call.mdx +53 -0
  59. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +66 -0
  60. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +68 -0
  61. package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
  62. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +2 -3
  63. package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
  64. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +23 -0
  65. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +23 -0
  66. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +23 -0
  67. package/site-templates/designs/minimalism/DESIGN.md +162 -0
  68. package/site-templates/designs/standard/DESIGN.md +162 -0
  69. package/site-templates/marketing-site/DESIGN.md +162 -0
  70. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +312 -57
  71. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +209 -90
  72. package/site-templates/marketing-site/ailk-template.json +1 -0
  73. package/site-templates/marketing-site-free/DESIGN.md +162 -0
  74. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +260 -66
  75. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +294 -114
  76. package/site-templates/marketing-site-free/ailk-template.json +3 -2
  77. package/site-templates/minimalism-site/DESIGN.md +162 -0
  78. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
  79. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +185 -51
  80. package/site-templates/minimalism-site/ailk-template.json +3 -2
  81. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +53 -0
  82. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
  83. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
  84. package/site-templates/saas-product/DESIGN.md +162 -0
  85. package/site-templates/simple-service/DESIGN.md +162 -0
  86. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +135 -26
  87. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +114 -59
  88. package/site-templates/simple-service/ailk-template.json +4 -3
  89. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +53 -0
  90. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +1 -36
  91. package/site-templates/simple-service/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
  92. package/templates/.claude/settings.json +15 -0
  93. package/templates/.claude/skills/README.md +8 -8
  94. package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
  95. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
  96. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
  97. package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
  98. package/templates/.claude/skills/add-page/references/page-types.md +111 -0
  99. package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
  100. package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
  101. package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
  102. package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
  103. package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
  104. package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
  105. package/templates/.claude/skills/fill-page/SKILL.md +180 -0
  106. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
  107. package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
  108. package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +8 -8
  109. package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +2 -2
  110. package/templates/.claude/skills/{provision-config → setup-deploy}/references/runbook-template.md +8 -8
  111. package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
  112. package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
  113. package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
  114. package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
  115. package/templates/.github/workflows/ci.yml +5 -0
  116. package/templates/CLAUDE.md +10 -5
  117. package/templates/DESIGN.md +162 -0
  118. package/templates/README.md +134 -38
  119. package/templates/apps/api/.env.example +2 -1
  120. package/templates/apps/api/package.json +1 -1
  121. package/templates/apps/api/src/config/__tests__/index.test.ts +37 -2
  122. package/templates/apps/api/src/config/index.ts +19 -6
  123. package/templates/apps/api/src/lib/license-key-store.ts +45 -0
  124. package/templates/apps/api/src/lib/site.ts +5 -3
  125. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
  126. package/templates/apps/api/src/middleware/auth.ts +9 -0
  127. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
  128. package/templates/apps/api/src/routes/content/get-page.ts +86 -2
  129. package/templates/apps/api/src/server.ts +6 -0
  130. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
  131. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
  132. package/templates/apps/mcp/package.json +2 -1
  133. package/templates/apps/mcp/src/index.ts +7 -0
  134. package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
  135. package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
  136. package/templates/apps/web/PRODUCT.md +86 -0
  137. package/templates/apps/web/__tests__/setup.ts +18 -0
  138. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
  139. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
  140. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
  141. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
  142. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
  143. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
  144. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
  145. package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
  146. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
  147. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  148. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  149. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
  150. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +94 -33
  151. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +53 -23
  152. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +96 -0
  153. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +84 -2
  154. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -6
  155. package/templates/apps/web/app/[locale]/contact/page.tsx +25 -7
  156. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  157. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
  158. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
  159. package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
  160. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  161. package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
  162. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  163. package/templates/apps/web/app/[locale]/layout.tsx +113 -51
  164. package/templates/apps/web/app/[locale]/newsletter/page.tsx +50 -0
  165. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  166. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  167. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
  168. package/templates/apps/web/app/[locale]/{shipping → purchase-policy}/page.tsx +15 -10
  169. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
  170. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  171. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
  172. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  173. package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
  174. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  175. package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
  176. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
  177. package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +48 -0
  178. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
  179. package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
  180. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  181. package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
  182. package/templates/apps/web/app/error-fallback.tsx +17 -45
  183. package/templates/apps/web/app/not-found.tsx +14 -38
  184. package/templates/apps/web/app/og/route.tsx +6 -0
  185. package/templates/apps/web/eslint.config.mjs +3 -0
  186. package/templates/apps/web/instrumentation.ts +14 -0
  187. package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
  188. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
  189. package/templates/apps/web/lib/license-key-store.ts +52 -0
  190. package/templates/apps/web/lib/nav-config.ts +64 -12
  191. package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
  192. package/templates/apps/web/lib/site-brand.tsx +4 -8
  193. package/templates/apps/web/lib/site-chrome.ts +3 -3
  194. package/templates/apps/web/lib/site-identity.ts +134 -0
  195. package/templates/apps/web/lib/site-theme.ts +11 -0
  196. package/templates/apps/web/messages/en.json +52 -37
  197. package/templates/apps/web/middleware.ts +1 -1
  198. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  199. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  200. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  201. package/templates/apps/web/public/favicon-16x16.png +0 -0
  202. package/templates/apps/web/public/favicon-32x32.png +0 -0
  203. package/templates/apps/web/public/favicon.ico +0 -0
  204. package/templates/apps/web/public/favicon.svg +3 -3
  205. package/templates/apps/web/public/logomark.svg +6 -3
  206. package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
  207. package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
  208. package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
  209. package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
  210. package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
  211. package/templates/apps/web/public/logos/salessmyth.svg +4 -0
  212. package/templates/apps/web/public/site.webmanifest +4 -4
  213. package/templates/apps/web/public/wordmark-light.svg +4 -0
  214. package/templates/apps/web/public/wordmark.svg +3 -3
  215. package/templates/apps/web/security-headers.config.ts +5 -1
  216. package/templates/apps/web/src/lib/commerce-sections.ts +35 -3
  217. package/templates/content/_site.mdx +41 -3
  218. package/templates/content/en/AboutPage/about.mdx +150 -144
  219. package/templates/content/en/BlogList/blog.mdx +105 -0
  220. package/templates/content/en/ContactPage/contact.mdx +44 -85
  221. package/templates/content/en/FAQPage/faq.mdx +86 -57
  222. package/templates/content/en/HomePage/home.mdx +276 -217
  223. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +182 -0
  224. package/templates/content/en/SoftwareProduct/product.mdx +175 -0
  225. package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
  226. package/templates/database/CHANGELOG.md +14 -0
  227. package/templates/database/package.json +3 -2
  228. package/templates/package.json +3 -1
  229. package/templates/tools/lint-architecture.ts +315 -0
  230. package/templates/tools/tsconfig.json +14 -0
  231. package/versions.json +33 -33
  232. package/templates/.claude/skills/add-schema/SKILL.md +0 -277
  233. package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
  234. package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
  235. package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
  236. package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
  237. package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
  238. package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
  239. package/templates/apps/web/app/[locale]/cancellation/page.tsx +0 -44
  240. package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
  241. package/templates/apps/web/public/logo-dark.svg +0 -21
  242. package/templates/apps/web/public/logo-light.svg +0 -21
  243. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
  244. package/templates/content/en/LandingPage/blog.mdx +0 -83
  245. package/templates/content/en/LandingPage/features.mdx +0 -103
  246. package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
  247. package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
  248. package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
@@ -27,8 +27,7 @@ jest.mock("@working-theory/ui", () => ({
27
27
  }: {
28
28
  children: React.ReactNode;
29
29
  level: number;
30
- }): React.ReactElement =>
31
- React.createElement(`h${level}`, null, children),
30
+ }): React.ReactElement => React.createElement(`h${level}`, null, children),
32
31
  SectionRenderer: ({ sections }: { sections: unknown[] }) =>
33
32
  React.createElement("div", {
34
33
  "data-testid": "section-renderer",
@@ -59,7 +58,7 @@ describe("ResourcesPage — snapshot regression", () => {
59
58
  });
60
59
  const { container } = render(element);
61
60
  expect(container.innerHTML).toMatchInlineSnapshot(
62
- `"<main><h1>Resources</h1><p data-testid="section-empty-state">Resources listing coming soon.</p></main>"`,
61
+ `"<main id="main"><h1>Resources</h1><p data-testid="section-empty-state">Resources listing coming soon.</p></main>"`,
63
62
  );
64
63
  });
65
64
 
@@ -81,7 +80,7 @@ describe("ResourcesPage — snapshot regression", () => {
81
80
  });
82
81
  const { container } = render(element);
83
82
  expect(container.innerHTML).toMatchInlineSnapshot(
84
- `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Resources","description":"Guides, templates, and tools.","mainEntity":{"@type":"ItemList"}}</script><main><h1>Resources</h1><p data-testid="resources-description">Guides, templates, and tools.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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></main>"`,
83
+ `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Resources","description":"Guides, templates, and tools.","mainEntity":{"@type":"ItemList"}}</script><main id="main"><h1>Resources</h1><p data-testid="resources-description">Guides, templates, and tools.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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></main>"`,
85
84
  );
86
85
  });
87
86
  });
@@ -111,7 +110,7 @@ describe("ResourceDetailPage — snapshot regression", () => {
111
110
  });
112
111
  const { container } = render(element);
113
112
  expect(container.innerHTML).toMatchInlineSnapshot(
114
- `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Getting Started","description":"A guide for new users.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"Getting Started","description":"A guide for new users."}},{"@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":"Resources","item":"http://localhost:3000/en/resources"},{"@type":"ListItem","position":3,"name":"Getting Started","item":"http://localhost:3000/en/resources/getting-started"}]}]}</script><main><h1>Getting Started</h1><p data-testid="resource-description">A guide for new users.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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></main>"`,
113
+ `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Getting Started","description":"A guide for new users.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"Getting Started","description":"A guide for new users."}},{"@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":"Resources","item":"http://localhost:3000/en/resources"},{"@type":"ListItem","position":3,"name":"Getting Started","item":"http://localhost:3000/en/resources/getting-started"}]}]}</script><main id="main"><h1>Getting Started</h1><p data-testid="resource-description">A guide for new users.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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></main>"`,
115
114
  );
116
115
  });
117
116
 
@@ -73,7 +73,7 @@ describe("ServicesPage — snapshot regression", () => {
73
73
  });
74
74
  const { container } = render(element);
75
75
  expect(container.innerHTML).toMatchInlineSnapshot(
76
- `"<main><h1 class="text-heading-1 font-display font-normal tracking-tighter text-fg hero-type-display">Services</h1><p data-testid="section-empty-state">Services listing coming soon.</p></main>"`,
76
+ `"<main id="main"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Services</h1><p data-testid="section-empty-state">Services listing coming soon.</p></main>"`,
77
77
  );
78
78
  });
79
79
 
@@ -86,7 +86,7 @@ describe("ServicesPage — snapshot regression", () => {
86
86
  });
87
87
  const { container } = render(element);
88
88
  expect(container.innerHTML).toMatchInlineSnapshot(`
89
- "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Services","description":"All services.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Service"}]}}</script><main><h1>Services</h1><p data-testid="services-description">All services.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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="# Services
89
+ "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Services","description":"All services.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Service"}]}}</script><main id="main"><h1>Services</h1><p data-testid="services-description">All services.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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="# Services
90
90
 
91
91
  Body."></div></article></div></div></main>"
92
92
  `);
@@ -110,7 +110,7 @@ describe("ServiceSlugPage — snapshot regression", () => {
110
110
  });
111
111
  const { container } = render(element);
112
112
  expect(container.innerHTML).toMatchInlineSnapshot(`
113
- "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Implementation consulting","description":"Help adapting AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Service","name":"Implementation consulting","description":"Help adapting AI Launch Kit."}},{"@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":"Services","item":"http://localhost:3000/en/services"},{"@type":"ListItem","position":3,"name":"Implementation consulting","item":"http://localhost:3000/en/services/consulting"}]}]}</script><main><h1>Implementation consulting</h1><p data-testid="service-description">Help adapting AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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="# Implementation consulting
113
+ "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Implementation consulting","description":"Help adapting AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Service","name":"Implementation consulting","description":"Help adapting AI Launch Kit."}},{"@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":"Services","item":"http://localhost:3000/en/services"},{"@type":"ListItem","position":3,"name":"Implementation consulting","item":"http://localhost:3000/en/services/consulting"}]}]}</script><main id="main"><h1>Implementation consulting</h1><p data-testid="service-description">Help adapting AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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="# Implementation consulting
114
114
 
115
115
  Body."></div></article></div></div></main>"
116
116
  `);
@@ -1,8 +1,11 @@
1
1
  // SoftwareProduct detail page. Reads the SoftwareProduct MDX for the given slug,
2
2
  // renders via @working-theory/page-renderer, and emits a JSON-LD <script>. Unknown slugs 404.
3
+ // A page that composes `sections[]` (the starter's /software/product, #6658 D3)
4
+ // renders them; a prose-only page keeps the MDX-body render.
3
5
  import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
4
6
  import {
5
7
  getMdxSlugs,
8
+ getMdxSource as loadMdxSource,
6
9
  renderPage,
7
10
  renderPageMetadata,
8
11
  } from "@working-theory/page-renderer";
@@ -35,11 +38,21 @@ export default async function Page({
35
38
  getMdxSource,
36
39
  }: Props): Promise<ReactElement> {
37
40
  const { locale, slug } = await params;
41
+ const source = await (getMdxSource ?? loadMdxSource)(
42
+ "SoftwareProduct",
43
+ slug,
44
+ locale,
45
+ );
46
+ const composed =
47
+ source !== null &&
48
+ Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
49
+ ((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) >
50
+ 0;
38
51
  return renderPage({
39
52
  type: "SoftwareProduct",
40
53
  slug,
41
54
  locale,
42
- bodyMode: "mdx-body",
55
+ ...(composed ? {} : { bodyMode: "mdx-body" as const }),
43
56
  populatedDescriptionTestId: "software-description",
44
57
  ...(getMdxSource !== undefined && { getMdxSource }),
45
58
  });
@@ -61,7 +61,7 @@ describe("software/[slug] — snapshot regression", () => {
61
61
  });
62
62
  const { container } = render(element);
63
63
  expect(container.innerHTML).toMatchInlineSnapshot(`
64
- "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"@working-theory/page-renderer","description":"Single rendering primitive for MDX-wired routes.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"@working-theory/page-renderer","description":"Single rendering primitive for MDX-wired routes."}}</script><main><h1>@working-theory/page-renderer</h1><p data-testid="software-description">Single rendering primitive for MDX-wired routes.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-rendered">## Overview
64
+ "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"@working-theory/page-renderer","description":"Single rendering primitive for MDX-wired routes.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"@working-theory/page-renderer","description":"Single rendering primitive for MDX-wired routes."}}</script><main id="main"><h1>@working-theory/page-renderer</h1><p data-testid="software-description">Single rendering primitive for MDX-wired routes.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-rendered">## Overview
65
65
 
66
66
  Body content.</div></article></div></div></main>"
67
67
  `);
@@ -1,6 +1,8 @@
1
1
  // Software list page. Enumerates SoftwareProduct slugs via the content adapter
2
2
  // and renders a directory listing. No dedicated list MDX type exists for SoftwareProduct.
3
3
  import { getMdxSlugs as defaultGetMdxSlugs } from "@working-theory/page-renderer";
4
+ import { LinkListItem, RowGroup } from "@working-theory/ui/blocks";
5
+ import { Container, Heading, Text } from "@working-theory/ui/primitives";
4
6
  import { getTranslations } from "next-intl/server";
5
7
  import type { ReactElement } from "react";
6
8
 
@@ -19,21 +21,29 @@ export default async function Page({
19
21
 
20
22
  return (
21
23
  <main>
22
- <h1>{t("heading")}</h1>
23
- <p data-testid="software-description">
24
- {t("description")}
25
- </p>
26
- {slugs.length === 0 ? (
27
- <p data-testid="section-empty-state">{t("emptyState")}</p>
28
- ) : (
29
- <ul data-testid="software-list">
30
- {slugs.map((slug) => (
31
- <li key={slug}>
32
- <a href={`/${locale}/software/${slug}`}>{slug}</a>
33
- </li>
34
- ))}
35
- </ul>
36
- )}
24
+ <Container className="py-section">
25
+ <Heading level={1}>{t("heading")}</Heading>
26
+ <div data-testid="software-description">
27
+ <Text>{t("description")}</Text>
28
+ </div>
29
+ {slugs.length === 0 ? (
30
+ <div data-testid="section-empty-state">
31
+ <Text>{t("emptyState")}</Text>
32
+ </div>
33
+ ) : (
34
+ <RowGroup data-testid="software-list">
35
+ {slugs.map((slug) => (
36
+ <LinkListItem
37
+ key={slug}
38
+ href={`/${locale}/software/${slug}`}
39
+ label={slug}
40
+ className="px-4"
41
+ data-testid={`software-list-item-${slug}`}
42
+ />
43
+ ))}
44
+ </RowGroup>
45
+ )}
46
+ </Container>
37
47
  </main>
38
48
  );
39
49
  }
@@ -62,7 +62,7 @@ describe("tech-articles/[slug] — snapshot regression", () => {
62
62
  });
63
63
  const { container } = render(element);
64
64
  expect(container.innerHTML).toMatchInlineSnapshot(`
65
- "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AEO Layer 1 Scoring","description":"Deep dive into AEO Layer 1 scoring and AILK audit internals.","mainEntity":{"@context":"https://schema.org","@type":"TechArticle","headline":"AEO Layer 1 Scoring","description":"Deep dive into AEO Layer 1 scoring and AILK audit internals."}},{"@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":"Tech Articles","item":"http://localhost:3000/en/tech-articles"},{"@type":"ListItem","position":3,"name":"AEO Layer 1 Scoring","item":"http://localhost:3000/en/tech-articles/aeo-layer1-scoring"}]}]}</script><main><h1>AEO Layer 1 Scoring</h1><p data-testid="tech-article-description">Deep dive into AEO Layer 1 scoring and AILK audit internals.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-rendered">## AEO Layer 1
65
+ "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AEO Layer 1 Scoring","description":"Deep dive into AEO Layer 1 scoring and AILK audit internals.","mainEntity":{"@context":"https://schema.org","@type":"TechArticle","headline":"AEO Layer 1 Scoring","description":"Deep dive into AEO Layer 1 scoring and AILK audit internals."}},{"@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":"Tech Articles","item":"http://localhost:3000/en/tech-articles"},{"@type":"ListItem","position":3,"name":"AEO Layer 1 Scoring","item":"http://localhost:3000/en/tech-articles/aeo-layer1-scoring"}]}]}</script><main id="main"><h1>AEO Layer 1 Scoring</h1><p data-testid="tech-article-description">Deep dive into AEO Layer 1 scoring and AILK audit internals.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-rendered">## AEO Layer 1
66
66
 
67
67
  Body content.</div></article></div></div></main>"
68
68
  `);
@@ -1,6 +1,8 @@
1
1
  // Tech Articles list page. Enumerates TechArticle slugs via the content adapter
2
2
  // and renders a directory listing. No dedicated list MDX type exists for TechArticle.
3
3
  import { getMdxSlugs as defaultGetMdxSlugs } from "@working-theory/page-renderer";
4
+ import { LinkListItem, RowGroup } from "@working-theory/ui/blocks";
5
+ import { Container, Heading, Text } from "@working-theory/ui/primitives";
4
6
  import { getTranslations } from "next-intl/server";
5
7
  import type { ReactElement } from "react";
6
8
 
@@ -19,21 +21,29 @@ export default async function Page({
19
21
 
20
22
  return (
21
23
  <main>
22
- <h1>{t("heading")}</h1>
23
- <p data-testid="tech-articles-description">
24
- {t("description")}
25
- </p>
26
- {slugs.length === 0 ? (
27
- <p data-testid="section-empty-state">{t("emptyState")}</p>
28
- ) : (
29
- <ul data-testid="tech-articles-list">
30
- {slugs.map((slug) => (
31
- <li key={slug}>
32
- <a href={`/${locale}/tech-articles/${slug}`}>{slug}</a>
33
- </li>
34
- ))}
35
- </ul>
36
- )}
24
+ <Container className="py-section">
25
+ <Heading level={1}>{t("heading")}</Heading>
26
+ <div data-testid="tech-articles-description">
27
+ <Text>{t("description")}</Text>
28
+ </div>
29
+ {slugs.length === 0 ? (
30
+ <div data-testid="section-empty-state">
31
+ <Text>{t("emptyState")}</Text>
32
+ </div>
33
+ ) : (
34
+ <RowGroup data-testid="tech-articles-list">
35
+ {slugs.map((slug) => (
36
+ <LinkListItem
37
+ key={slug}
38
+ href={`/${locale}/tech-articles/${slug}`}
39
+ label={slug}
40
+ className="px-4"
41
+ data-testid={`tech-articles-list-item-${slug}`}
42
+ />
43
+ ))}
44
+ </RowGroup>
45
+ )}
46
+ </Container>
37
47
  </main>
38
48
  );
39
49
  }
@@ -24,8 +24,7 @@ jest.mock("@working-theory/ui", () => ({
24
24
  }: {
25
25
  children: React.ReactNode;
26
26
  level: number;
27
- }): React.ReactElement =>
28
- React.createElement(`h${level}`, null, children),
27
+ }): React.ReactElement => React.createElement(`h${level}`, null, children),
29
28
  SectionRenderer: ({ sections }: { sections: unknown[] }) =>
30
29
  React.createElement("div", {
31
30
  "data-testid": "section-renderer",
@@ -45,7 +44,7 @@ describe("TestimonialsPage — snapshot regression", () => {
45
44
  });
46
45
  const { container } = render(element);
47
46
  expect(container.innerHTML).toMatchInlineSnapshot(
48
- `"<main><h1>Testimonials</h1><p data-testid="section-empty-state">Testimonials coming soon.</p></main>"`,
47
+ `"<main id="main"><h1>Testimonials</h1><p data-testid="section-empty-state">Testimonials coming soon.</p></main>"`,
49
48
  );
50
49
  });
51
50
 
@@ -67,7 +66,7 @@ describe("TestimonialsPage — snapshot regression", () => {
67
66
  });
68
67
  const { container } = render(element);
69
68
  expect(container.innerHTML).toMatchInlineSnapshot(
70
- `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Customer Testimonials","description":"What our customers say.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Review"}]},"aggregateRating":{"@context":"https://schema.org","@type":"AggregateRating","ratingValue":4.9,"reviewCount":0,"bestRating":5,"worstRating":1,"itemReviewed":{"@type":"SoftwareApplication"}}}</script><main><h1>Customer Testimonials</h1><p data-testid="testimonials-description">What our customers say.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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></main>"`,
69
+ `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Customer Testimonials","description":"What our customers say.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Review"}]},"aggregateRating":{"@context":"https://schema.org","@type":"AggregateRating","ratingValue":4.9,"reviewCount":0,"bestRating":5,"worstRating":1,"itemReviewed":{"@type":"SoftwareApplication"}}}</script><main id="main"><h1>Customer Testimonials</h1><p data-testid="testimonials-description">What our customers say.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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></main>"`,
71
70
  );
72
71
  });
73
72
  });
@@ -0,0 +1,48 @@
1
+ // UseCase detail page (#6658 D4) — one use case at /use-cases/<slug>, listed by
2
+ // the /use-cases page. Reads the UseCase MDX for the given slug, renders via
3
+ // @working-theory/page-renderer, and emits a JSON-LD WebPage whose mainEntity
4
+ // is an Article. Unknown slugs 404. Same shape as software/[slug]/page.tsx.
5
+ import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
6
+ import {
7
+ getMdxSlugs,
8
+ renderPage,
9
+ renderPageMetadata,
10
+ } from "@working-theory/page-renderer";
11
+ import type { Metadata } from "next";
12
+ import type { ReactElement } from "react";
13
+
14
+ interface Props {
15
+ params: Promise<{ locale: string; slug: string }>;
16
+ getMdxSource?: typeof defaultGetMdxSource;
17
+ }
18
+
19
+ export async function generateMetadata({
20
+ params,
21
+ }: Pick<Props, "params">): Promise<Metadata> {
22
+ const { locale, slug } = await params;
23
+ return renderPageMetadata({ type: "UseCase", slug, locale });
24
+ }
25
+
26
+ export async function generateStaticParams({
27
+ params,
28
+ }: {
29
+ params: { locale: string };
30
+ }): Promise<{ slug: string }[]> {
31
+ const slugs = await getMdxSlugs("UseCase", params.locale);
32
+ return slugs.map((slug) => ({ slug }));
33
+ }
34
+
35
+ export default async function Page({
36
+ params,
37
+ getMdxSource,
38
+ }: Props): Promise<ReactElement> {
39
+ const { locale, slug } = await params;
40
+ return renderPage({
41
+ type: "UseCase",
42
+ slug,
43
+ locale,
44
+ bodyMode: "mdx-body",
45
+ populatedDescriptionTestId: "use-case-description",
46
+ ...(getMdxSource !== undefined && { getMdxSource }),
47
+ });
48
+ }
@@ -24,8 +24,7 @@ jest.mock("@working-theory/ui", () => ({
24
24
  }: {
25
25
  children: React.ReactNode;
26
26
  level: number;
27
- }): React.ReactElement =>
28
- React.createElement(`h${level}`, null, children),
27
+ }): React.ReactElement => React.createElement(`h${level}`, null, children),
29
28
  SectionRenderer: ({ sections }: { sections: unknown[] }) =>
30
29
  React.createElement("div", {
31
30
  "data-testid": "section-renderer",
@@ -45,7 +44,7 @@ describe("UseCasesPage — snapshot regression", () => {
45
44
  });
46
45
  const { container } = render(element);
47
46
  expect(container.innerHTML).toMatchInlineSnapshot(
48
- `"<main><h1>Use Cases</h1><p data-testid="section-empty-state">Use cases listing coming soon.</p></main>"`,
47
+ `"<main id="main"><h1>Use Cases</h1><p data-testid="section-empty-state">Use cases listing coming soon.</p></main>"`,
49
48
  );
50
49
  });
51
50
 
@@ -67,7 +66,7 @@ describe("UseCasesPage — snapshot regression", () => {
67
66
  });
68
67
  const { container } = render(element);
69
68
  expect(container.innerHTML).toMatchInlineSnapshot(
70
- `"<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><h1>Use Cases</h1><p data-testid="use-cases-description">How teams use our platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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></main>"`,
69
+ `"<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"><h1>Use Cases</h1><p data-testid="use-cases-description">How teams use our platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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></main>"`,
71
70
  );
72
71
  });
73
72
  });
@@ -3,7 +3,11 @@
3
3
  import type {
4
4
  getMdxSource as defaultGetMdxSource,
5
5
  } from "@working-theory/page-renderer";
6
- import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
6
+ import {
7
+ getMdxSource as loadMdxSource,
8
+ renderPage,
9
+ renderPageMetadata,
10
+ } from "@working-theory/page-renderer";
7
11
  import type { Metadata } from "next";
8
12
  import type { ReactElement } from "react";
9
13
 
@@ -28,12 +32,21 @@ export default async function Page({
28
32
  getMdxSource,
29
33
  }: Props): Promise<ReactElement> {
30
34
  const { locale } = await params;
35
+ // A UseCasesList page that composes `sections[]` (the Free template's
36
+ // /use-cases, operator ruling 2026-09-23) renders them; a prose-only page
37
+ // keeps the MDX-body render. The CollectionPage JSON-LD is built from the
38
+ // sections either way (buildUseCasesList).
39
+ const source = await (getMdxSource ?? loadMdxSource)("UseCasesList", "use-cases", locale);
40
+ const composed =
41
+ source !== null &&
42
+ Array.isArray((source.frontmatter as { sections?: unknown[] }).sections) &&
43
+ ((source.frontmatter as { sections?: unknown[] }).sections?.length ?? 0) > 0;
31
44
  return renderPage({
32
45
  type: "UseCasesList",
33
46
  slug: "use-cases",
34
47
  locale,
35
48
  translationsNamespace: "use-cases",
36
- bodyMode: "mdx-body",
49
+ ...(composed ? {} : { bodyMode: "mdx-body" as const }),
37
50
  ...(getMdxSource !== undefined && { getMdxSource }),
38
51
  });
39
52
  }
@@ -62,7 +62,7 @@ describe("whitepapers/[slug] — snapshot regression", () => {
62
62
  });
63
63
  const { container } = render(element);
64
64
  expect(container.innerHTML).toMatchInlineSnapshot(`
65
- "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AILK AEO Strategy 2026","description":"In-depth research on answer engine optimization for AILK products.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"AILK AEO Strategy 2026","description":"In-depth research on answer engine optimization for AILK products."}},{"@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":"Whitepapers","item":"http://localhost:3000/en/whitepapers"},{"@type":"ListItem","position":3,"name":"AILK AEO Strategy 2026","item":"http://localhost:3000/en/whitepapers/ailk-aeo-strategy-2026"}]}]}</script><main><h1>AILK AEO Strategy 2026</h1><p data-testid="whitepaper-description">In-depth research on answer engine optimization for AILK products.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-rendered">## Executive Summary
65
+ "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AILK AEO Strategy 2026","description":"In-depth research on answer engine optimization for AILK products.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"AILK AEO Strategy 2026","description":"In-depth research on answer engine optimization for AILK products."}},{"@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":"Whitepapers","item":"http://localhost:3000/en/whitepapers"},{"@type":"ListItem","position":3,"name":"AILK AEO Strategy 2026","item":"http://localhost:3000/en/whitepapers/ailk-aeo-strategy-2026"}]}]}</script><main id="main"><h1>AILK AEO Strategy 2026</h1><p data-testid="whitepaper-description">In-depth research on answer engine optimization for AILK products.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><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-rendered">## Executive Summary
66
66
 
67
67
  Body content.</div></article></div></div></main>"
68
68
  `);
@@ -1,6 +1,8 @@
1
1
  // Whitepapers list page. Enumerates Whitepaper slugs via the content adapter
2
2
  // and renders a directory listing. No dedicated list MDX type exists for Whitepaper.
3
3
  import { getMdxSlugs as defaultGetMdxSlugs } from "@working-theory/page-renderer";
4
+ import { LinkListItem, RowGroup } from "@working-theory/ui/blocks";
5
+ import { Container, Heading, Text } from "@working-theory/ui/primitives";
4
6
  import { getTranslations } from "next-intl/server";
5
7
  import type { ReactElement } from "react";
6
8
 
@@ -19,21 +21,29 @@ export default async function Page({
19
21
 
20
22
  return (
21
23
  <main>
22
- <h1>{t("heading")}</h1>
23
- <p data-testid="whitepapers-description">
24
- {t("description")}
25
- </p>
26
- {slugs.length === 0 ? (
27
- <p data-testid="section-empty-state">{t("emptyState")}</p>
28
- ) : (
29
- <ul data-testid="whitepapers-list">
30
- {slugs.map((slug) => (
31
- <li key={slug}>
32
- <a href={`/${locale}/whitepapers/${slug}`}>{slug}</a>
33
- </li>
34
- ))}
35
- </ul>
36
- )}
24
+ <Container className="py-section">
25
+ <Heading level={1}>{t("heading")}</Heading>
26
+ <div data-testid="whitepapers-description">
27
+ <Text>{t("description")}</Text>
28
+ </div>
29
+ {slugs.length === 0 ? (
30
+ <div data-testid="section-empty-state">
31
+ <Text>{t("emptyState")}</Text>
32
+ </div>
33
+ ) : (
34
+ <RowGroup data-testid="whitepapers-list">
35
+ {slugs.map((slug) => (
36
+ <LinkListItem
37
+ key={slug}
38
+ href={`/${locale}/whitepapers/${slug}`}
39
+ label={slug}
40
+ className="px-4"
41
+ data-testid={`whitepapers-list-item-${slug}`}
42
+ />
43
+ ))}
44
+ </RowGroup>
45
+ )}
46
+ </Container>
37
47
  </main>
38
48
  );
39
49
  }