create-ailk 0.26.0 → 0.28.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 (224) hide show
  1. package/README.md +86 -9
  2. package/dist/add-module-core.js +2 -1
  3. package/dist/add-module.d.ts +2 -1
  4. package/dist/add-module.js +9 -4
  5. package/dist/add-route.js +46 -19
  6. package/dist/body-heading.d.ts +56 -0
  7. package/dist/body-heading.js +394 -0
  8. package/dist/cli.js +28 -1
  9. package/dist/content-payloads.js +2 -2
  10. package/dist/derive-architecture.js +8 -2
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.js +4 -1
  13. package/dist/lib/auth-secret-file.d.ts +6 -2
  14. package/dist/lib/auth-secret-file.js +46 -20
  15. package/dist/lib/env-catalog.js +20 -0
  16. package/dist/lib/site-modules.d.ts +8 -0
  17. package/dist/lib/site-modules.js +34 -0
  18. package/dist/module-architecture.js +25 -14
  19. package/dist/paid-sections.d.ts +17 -0
  20. package/dist/paid-sections.js +39 -0
  21. package/dist/programmatic.d.ts +16 -0
  22. package/dist/programmatic.js +73 -4
  23. package/dist/remove-module.d.ts +98 -0
  24. package/dist/remove-module.js +620 -0
  25. package/dist/seed-flows.d.ts +2 -2
  26. package/dist/seed-flows.js +16 -9
  27. package/dist/site-roster.js +4 -1
  28. package/dist/starter-grid.d.ts +26 -0
  29. package/dist/starter-grid.js +34 -0
  30. package/dist/surfaces.d.ts +26 -0
  31. package/dist/surfaces.js +112 -11
  32. package/dist/sync-routes.d.ts +7 -0
  33. package/dist/sync-routes.js +53 -6
  34. package/dist/template-pages.d.ts +36 -0
  35. package/dist/template-pages.js +129 -0
  36. package/dist/topology.d.ts +2 -1
  37. package/dist/topology.js +4 -2
  38. package/dist/update.d.ts +3 -1
  39. package/dist/update.js +90 -6
  40. package/package.json +3 -3
  41. package/release-scope.json +2 -1
  42. package/site-templates/aliases/hub.TEMPLATE-MANIFEST.json +115 -0
  43. package/templates/.env.example +12 -2
  44. package/templates/CLAUDE.md +20 -20
  45. package/templates/README.md +19 -19
  46. package/templates/apps/api/.env.example +16 -5
  47. package/templates/apps/api/CLAUDE.md +32 -11
  48. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +37 -0
  49. package/templates/apps/api/src/vercel-handler.ts +8 -1
  50. package/templates/apps/api/vercel.json +7 -1
  51. package/templates/apps/mcp/CLAUDE.md +18 -10
  52. package/templates/apps/web/.env.example +27 -0
  53. package/templates/apps/web/CLAUDE.md +56 -8
  54. package/templates/apps/web/__tests__/setup.ts +7 -0
  55. package/templates/apps/web/app/.well-known/mcp.json/route.ts +7 -7
  56. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/next-path.test.ts +24 -1
  57. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +51 -0
  58. package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +13 -0
  59. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +10 -4
  60. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/page.test.tsx +197 -4
  61. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +23 -7
  62. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +61 -2
  63. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +81 -0
  64. package/templates/apps/web/app/[locale]/(authed)/account/connect/__tests__/page.test.tsx +197 -0
  65. package/templates/apps/web/app/[locale]/(authed)/account/connect/page.tsx +152 -0
  66. package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +2 -2
  67. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +76 -21
  68. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +46 -16
  69. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +20 -3
  70. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +180 -2
  71. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +6 -2
  72. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +122 -0
  73. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +1 -1
  74. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +20 -3
  75. package/templates/apps/web/app/[locale]/case-studies/__tests__/hub-children.test.tsx +65 -0
  76. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +6 -2
  77. package/templates/apps/web/app/[locale]/case-studies/page.tsx +23 -11
  78. package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +11 -1
  79. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
  80. package/templates/apps/web/app/[locale]/consent.actions.ts +44 -6
  81. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +4 -4
  82. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +269 -42
  83. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +88 -30
  84. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +20 -3
  85. package/templates/apps/web/app/[locale]/courses/__tests__/hub-children.test.tsx +32 -0
  86. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +6 -2
  87. package/templates/apps/web/app/[locale]/courses/page.tsx +22 -6
  88. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +15 -2
  89. package/templates/apps/web/app/[locale]/events/__tests__/hub-children.test.tsx +32 -0
  90. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +8 -2
  91. package/templates/apps/web/app/[locale]/events/page.tsx +23 -7
  92. package/templates/apps/web/app/[locale]/features/__tests__/hub-children.test.tsx +32 -0
  93. package/templates/apps/web/app/[locale]/features/page.tsx +23 -25
  94. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +95 -0
  95. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/save.actions.test.ts +193 -0
  96. package/templates/apps/web/app/[locale]/flows/[slug]/email-moment.ts +92 -0
  97. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +10 -0
  98. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +513 -0
  99. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +122 -0
  100. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.no-commerce.test.tsx +195 -0
  101. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/landing.test.tsx +293 -0
  102. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +397 -0
  103. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/checkout-seam.ts +74 -0
  104. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +264 -0
  105. package/templates/apps/web/app/[locale]/flows/[slug]/save.actions.ts +75 -0
  106. package/templates/apps/web/app/[locale]/for/__tests__/hub-children.test.tsx +32 -0
  107. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +5 -1
  108. package/templates/apps/web/app/[locale]/for/page.tsx +23 -25
  109. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +20 -3
  110. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  111. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +25 -4
  112. package/templates/apps/web/app/[locale]/integrations/__tests__/hub-children.test.tsx +38 -0
  113. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +8 -2
  114. package/templates/apps/web/app/[locale]/integrations/page.tsx +23 -11
  115. package/templates/apps/web/app/[locale]/layout.tsx +161 -128
  116. package/templates/apps/web/app/[locale]/page.tsx +51 -3
  117. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +16 -3
  118. package/templates/apps/web/app/[locale]/partners/__tests__/hub-children.test.tsx +32 -0
  119. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +6 -2
  120. package/templates/apps/web/app/[locale]/partners/page.tsx +22 -6
  121. package/templates/apps/web/app/[locale]/portfolio/__tests__/hub-children.test.tsx +38 -0
  122. package/templates/apps/web/app/[locale]/portfolio/page.tsx +25 -24
  123. package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +321 -0
  124. package/templates/apps/web/app/[locale]/pricing/[slug]/page.tsx +87 -0
  125. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
  126. package/templates/apps/web/app/[locale]/pricing/__tests__/pricing-pages.test.tsx +215 -0
  127. package/templates/apps/web/app/[locale]/pricing/page.tsx +56 -341
  128. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +21 -4
  129. package/templates/apps/web/app/[locale]/products/__tests__/hub-children.test.tsx +32 -0
  130. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +8 -2
  131. package/templates/apps/web/app/[locale]/products/page.tsx +22 -6
  132. package/templates/apps/web/app/[locale]/projects/__tests__/hub-children.test.tsx +32 -0
  133. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -0
  134. package/templates/apps/web/app/[locale]/projects/page.tsx +18 -11
  135. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +25 -4
  136. package/templates/apps/web/app/[locale]/resources/__tests__/hub-children.test.tsx +38 -0
  137. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +6 -2
  138. package/templates/apps/web/app/[locale]/resources/page.tsx +20 -10
  139. package/templates/apps/web/app/[locale]/services/[slug]/[child]/page.tsx +68 -0
  140. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +64 -9
  141. package/templates/apps/web/app/[locale]/services/__tests__/hub-children.test.tsx +158 -0
  142. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +12 -2
  143. package/templates/apps/web/app/[locale]/services/__tests__/service-detail-sections.test.tsx +223 -0
  144. package/templates/apps/web/app/[locale]/services/__tests__/service-families.test.tsx +368 -0
  145. package/templates/apps/web/app/[locale]/services/page.tsx +45 -19
  146. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  147. package/templates/apps/web/app/[locale]/team/__tests__/hub-children.test.tsx +32 -0
  148. package/templates/apps/web/app/[locale]/team/page.tsx +25 -24
  149. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +24 -3
  150. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  151. package/templates/apps/web/app/[locale]/templates/[slug]/__tests__/page.test.tsx +203 -0
  152. package/templates/apps/web/app/[locale]/templates/[slug]/page.tsx +81 -0
  153. package/templates/apps/web/app/[locale]/templates/__tests__/page.test.tsx +236 -0
  154. package/templates/apps/web/app/[locale]/templates/page.tsx +108 -0
  155. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
  156. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +24 -3
  157. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  158. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +322 -2
  159. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +161 -29
  160. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +51 -0
  161. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +13 -0
  162. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +233 -7
  163. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +92 -41
  164. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +77 -39
  165. package/templates/apps/web/app/llms.txt/route.ts +41 -3
  166. package/templates/apps/web/app/robots.txt/route.ts +5 -1
  167. package/templates/apps/web/app/sitemap.xml/route.ts +45 -3
  168. package/templates/apps/web/auth.ts +102 -50
  169. package/templates/apps/web/components/signed-in-nav.tsx +8 -1
  170. package/templates/apps/web/lib/__tests__/bot-check.test.ts +254 -0
  171. package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +212 -13
  172. package/templates/apps/web/lib/__tests__/deck-page.test.ts +13 -1
  173. package/templates/apps/web/lib/__tests__/docs-index.test.ts +93 -0
  174. package/templates/apps/web/lib/__tests__/docs-path.test.ts +168 -0
  175. package/templates/apps/web/lib/__tests__/hub-children.fixtures.ts +278 -0
  176. package/templates/apps/web/lib/__tests__/nav-label-keys.test.ts +138 -0
  177. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +178 -2
  178. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +183 -0
  179. package/templates/apps/web/lib/__tests__/page-chrome.test.ts +412 -0
  180. package/templates/apps/web/lib/__tests__/pricing-page.test.ts +142 -0
  181. package/templates/apps/web/lib/__tests__/relay.test.ts +484 -4
  182. package/templates/apps/web/lib/__tests__/route-family.test.ts +57 -0
  183. package/templates/apps/web/lib/__tests__/service-listing.test.ts +111 -0
  184. package/templates/apps/web/lib/__tests__/services-path.test.ts +96 -0
  185. package/templates/apps/web/lib/__tests__/site-header.test.ts +151 -0
  186. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +148 -6
  187. package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +84 -0
  188. package/templates/apps/web/lib/__tests__/template-listing.test.ts +158 -0
  189. package/templates/apps/web/lib/auth-secret.ts +57 -0
  190. package/templates/apps/web/lib/bot-check.ts +138 -0
  191. package/templates/apps/web/lib/consent-visitor.ts +81 -7
  192. package/templates/apps/web/lib/deck-page.ts +6 -3
  193. package/templates/apps/web/lib/docs-index.ts +100 -0
  194. package/templates/apps/web/lib/docs-path.ts +94 -0
  195. package/templates/apps/web/lib/hub-children.tsx +318 -0
  196. package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +22 -2
  197. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +11 -8
  198. package/templates/apps/web/lib/modules.generated.ts +9 -0
  199. package/templates/apps/web/lib/nav-label-check.ts +61 -0
  200. package/templates/apps/web/lib/nav-label-keys.ts +78 -0
  201. package/templates/apps/web/lib/newsletter.actions.ts +31 -6
  202. package/templates/apps/web/lib/offerings-pricing.ts +135 -6
  203. package/templates/apps/web/lib/page-chrome.ts +367 -0
  204. package/templates/apps/web/lib/posting-title.ts +26 -4
  205. package/templates/apps/web/lib/pricing-page.ts +430 -0
  206. package/templates/apps/web/lib/relay.ts +44 -4
  207. package/templates/apps/web/lib/route-family.ts +69 -0
  208. package/templates/apps/web/lib/service-listing.ts +89 -0
  209. package/templates/apps/web/lib/services-path.ts +97 -0
  210. package/templates/apps/web/lib/sign-up-agreements.ts +17 -6
  211. package/templates/apps/web/lib/site-header.ts +94 -0
  212. package/templates/apps/web/lib/telemetry-scrub.ts +68 -3
  213. package/templates/apps/web/lib/template-listing.ts +169 -0
  214. package/templates/apps/web/messages/en.json +144 -3
  215. package/templates/apps/web/middleware.ts +179 -12
  216. package/templates/apps/web/next.config.mjs +220 -3
  217. package/templates/apps/web/package.json +3 -2
  218. package/templates/apps/web/scripts/assert-content-bundled.ts +28 -6
  219. package/templates/apps/web/scripts/check-nav-labels.ts +39 -0
  220. package/templates/apps/web/sentry.server.config.ts +30 -2
  221. package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -9
  222. package/templates/apps/web/src/lib/rate-limit.ts +68 -27
  223. package/templates/apps/web/src/lib/routes.ts +15 -1
  224. package/versions.json +48 -48
@@ -70,6 +70,8 @@ describe("IntegrationsPage — snapshot regression", () => {
70
70
  const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
71
71
  const element = await IntegrationsPage({
72
72
  params: Promise.resolve({ locale: "en" }),
73
+ // No child pages: this snapshot pins the hub's own render (#10121).
74
+ getMdxSlugs: async () => [],
73
75
  getMdxSource: getMdxSourceEmpty,
74
76
  });
75
77
  const { container } = render(element);
@@ -83,11 +85,14 @@ describe("IntegrationsPage — snapshot regression", () => {
83
85
  const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
84
86
  const element = await IntegrationsPage({
85
87
  params: Promise.resolve({ locale: "en" }),
88
+ // No child pages: this snapshot pins the hub's own render (#10121).
89
+ getMdxSlugs: async () => [],
86
90
  getMdxSource: getMdxSourcePopulated,
87
91
  });
88
92
  const { container } = render(element);
89
93
  expect(container.innerHTML).toMatchInlineSnapshot(`
90
- "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Integrations","description":"First-party integrations for AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"SoftwareApplication"}]}}</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">Integrations</h1><p data-testid="integrations-description" class="mt-block-tight text-lead">First-party integrations for AI Launch Kit.</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-remote" data-source="
94
+ "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Integrations","description":"First-party integrations for AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"SoftwareApplication"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><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 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-remote" data-source="# Integrations
95
+
91
96
  Body."></div></div></article></div></div></div></main>"
92
97
  `);
93
98
  });
@@ -110,7 +115,8 @@ describe("IntegrationSlugPage — snapshot regression", () => {
110
115
  });
111
116
  const { container } = render(element);
112
117
  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":"OpenAI API","description":"Connect AILK to OpenAI.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"OpenAI API","description":"Connect AILK to OpenAI."}},{"@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":"Integrations","item":"http://localhost:3000/en/integrations"},{"@type":"ListItem","position":3,"name":"OpenAI API","item":"http://localhost:3000/en/integrations/openai-api"}]}]}</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">OpenAI API</h1><p data-testid="integration-description" class="mt-block-tight text-lead">Connect AILK to OpenAI.</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-remote" data-source="
118
+ "<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"OpenAI API","description":"Connect AILK to OpenAI.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"OpenAI API","description":"Connect AILK to OpenAI."}},{"@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":"Integrations","item":"http://localhost:3000/en/integrations"},{"@type":"ListItem","position":3,"name":"OpenAI API","item":"http://localhost:3000/en/integrations/openai-api"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><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 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-remote" data-source="# OpenAI API
119
+
114
120
  Body."></div></div></article></div></div></div></main>"
115
121
  `);
116
122
  });
@@ -1,37 +1,49 @@
1
1
  // Integrations list page. Reads IntegrationsList MDX via the content adapter and
2
2
  // emits a JSON-LD CollectionPage via the dispatcher.
3
- import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
3
+ // It lists every Integration page through one generated card section (#10121,
4
+ // apps/web/lib/hub-children.tsx — the /alternatives and /templates shape).
5
+ import type {
6
+ getMdxSlugs as defaultGetMdxSlugs,
7
+ getMdxSource as defaultGetMdxSource,
8
+ getPageSections as defaultGetPageSections,
9
+ } from "@working-theory/page-renderer";
4
10
  import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
5
11
  import type { Metadata } from "next";
6
12
  import type { ReactElement } from "react";
7
13
 
14
+ import { type HubConfig, hubPageInput } from "../../../lib/hub-children";
15
+
8
16
  interface Props {
9
17
  params: Promise<{ locale: string }>;
10
18
  getMdxSource?: typeof defaultGetMdxSource;
19
+ getPageSections?: typeof defaultGetPageSections;
20
+ getMdxSlugs?: typeof defaultGetMdxSlugs;
11
21
  }
12
22
 
23
+ const HUB: HubConfig = {
24
+ listType: "IntegrationsList",
25
+ slug: "integrations",
26
+ childType: "Integration",
27
+ linkLabel: "View the integration",
28
+ sectionId: "integration-cards",
29
+ body: "mdx-body",
30
+ };
31
+
13
32
  export async function generateMetadata({
14
33
  params,
15
34
  }: Pick<Props, "params">): Promise<Metadata> {
16
35
  const { locale } = await params;
17
- return renderPageMetadata({
18
- type: "IntegrationsList",
19
- slug: "integrations",
20
- locale,
21
- });
36
+ return renderPageMetadata({ type: "IntegrationsList", slug: "integrations", locale });
22
37
  }
23
38
 
24
39
  export default async function Page({
25
40
  params,
26
- getMdxSource,
41
+ ...loaders
27
42
  }: Props): Promise<ReactElement> {
28
43
  const { locale } = await params;
29
44
  return renderPage({
30
45
  type: "IntegrationsList",
31
46
  slug: "integrations",
32
- locale,
33
- translationsNamespace: "integrations",
34
- bodyMode: "mdx-body",
35
- ...(getMdxSource !== undefined && { getMdxSource }),
47
+ ...(await hubPageInput(HUB, locale, loaders)),
36
48
  });
37
49
  }
@@ -48,6 +48,7 @@ import {
48
48
  getStealthRobots,
49
49
  readLocales,
50
50
  } from "@working-theory/web-i18n";
51
+ import { BotIdClient } from "botid/client";
51
52
  import type { Metadata, Viewport } from "next";
52
53
  import { cookies, headers } from "next/headers";
53
54
  import { NextIntlClientProvider } from "next-intl";
@@ -60,8 +61,13 @@ import {
60
61
  type SignedInNavProps,
61
62
  } from "../../components/signed-in-nav";
62
63
  import { isAccountPath, isAuthPath, isBookingPath } from "../../lib/auth-routes";
64
+ import { BOT_CLIENT_PROTECT, botCheckOn } from "../../lib/bot-check";
63
65
  import { isClosingFooterPath } from "../../lib/closing-footer-path";
64
66
  import { pageOptsIntoDeck } from "../../lib/deck-page";
67
+ import {
68
+ withPublicDocsColumns,
69
+ withPublicDocsPaths,
70
+ } from "../../lib/docs-path";
65
71
  import {
66
72
  inPageView,
67
73
  newsletterSignup,
@@ -75,14 +81,30 @@ import { assertEnabledLocale } from "../../lib/locale-guard";
75
81
  import { isMagnetWalkPath } from "../../lib/magnet-routes";
76
82
  import { SCHEDULING_PAGES } from "../../lib/modules.generated";
77
83
  import { footerColumns, navItems } from "../../lib/nav-config";
84
+ import { navMessageKeys, usedNavLabelKeys } from "../../lib/nav-label-keys";
78
85
  import { getOgImageUrl } from "../../lib/og";
86
+ import {
87
+ resolveChromePage,
88
+ resolveHomeChrome,
89
+ resolvePageChrome,
90
+ routeFamilyOf,
91
+ } from "../../lib/page-chrome";
79
92
  import { backLinkHref } from "../../lib/profile-back-link";
93
+ import {
94
+ withPublicServicesColumns,
95
+ withPublicServicesPaths,
96
+ } from "../../lib/services-path";
80
97
  import {
81
98
  COPYRIGHT_HOLDER,
82
99
  SITE_DESCRIPTION,
83
100
  SITE_NAME,
84
101
  } from "../../lib/site-brand";
85
102
  import { siteChrome, type SiteChrome } from "../../lib/site-chrome";
103
+ import {
104
+ HEADER_CTA_LABEL_KEY,
105
+ readSiteHeader,
106
+ signedOutHeaderActions,
107
+ } from "../../lib/site-header";
86
108
  import { siteLogo } from "../../lib/site-logo";
87
109
  // Registers the `_site.mdx` identity with page-renderer at module load, so
88
110
  // every page rendered under this layout carries the sitewide JSON-LD (#6597).
@@ -326,76 +348,36 @@ export default async function LocaleLayout({
326
348
  // Resolve nav labels server-side so Nav + Footer components stay
327
349
  // i18n-hook-free and are testable without a NextIntlClientProvider wrapper.
328
350
  const tNav = await getTranslations({ locale, namespace: "nav" });
351
+ // #10045 D1 — every key resolves from the `nav` namespace by name: the keys
352
+ // the merged catalog carries (template + site overlay), every key
353
+ // `nav-config.ts` uses and the ones this layout names. There is no allow-list
354
+ // to extend; a key used but missing fails the build
355
+ // (`scripts/check-nav-labels.ts`). Entries for surfaces this site did not
356
+ // scaffold are never reached — `nav-config.ts` only lists surfaces the
357
+ // declared architecture carries.
358
+ const navLabelKeys = new Set([...navMessageKeys(messages), ...usedNavLabelKeys()]);
359
+ // The typed catalog knows only the template's keys; a site's own keys (in
360
+ // nav-config.ts or messages/site/<locale>.json) are plain strings, so look
361
+ // them up untyped — the build check is what guarantees each one exists.
362
+ const navText = tNav as unknown as (key: string) => string;
363
+ const navItemLabels: Record<string, string> = Object.fromEntries(
364
+ [...navLabelKeys].map((key) => [key, navText(key)]),
365
+ );
366
+ // Session-aware action labels (#4984). `account` carries the signed-in
367
+ // user's display name (name, else email) so the Nav shows WHO is signed
368
+ // in; it keeps the generic label when there is no session.
369
+ if (session?.user) {
370
+ navItemLabels["account"] = session.user.name ?? session.user.email;
371
+ }
372
+ // The site's own signed-out header button (#10046) — `_site.mdx`'s
373
+ // `header.cta`. Its label is site content, written straight into the map
374
+ // as `account` is; the composition is `signedOutHeaderActions` below.
375
+ const siteHeader = readSiteHeader();
376
+ if (siteHeader?.cta) {
377
+ navItemLabels[HEADER_CTA_LABEL_KEY] = siteHeader.cta.label;
378
+ }
329
379
  const navLabels = {
330
- items: {
331
- home: tNav("home"),
332
- // The Free marketing-site chrome (#5469 D5): the Product flyout's three
333
- // entries + descriptions, the top-level items, and the Get started CTA.
334
- product: tNav("product"),
335
- services: tNav("services"),
336
- useCases: tNav("useCases"),
337
- useCasesDesc: tNav("useCasesDesc"),
338
- footerSitePagesHeading: tNav("footerSitePagesHeading"),
339
- footerFeaturesHeading: tNav("footerFeaturesHeading"),
340
- footerResourcesHeading: tNav("footerResourcesHeading"),
341
- feature1: tNav("feature1"),
342
- feature2: tNav("feature2"),
343
- feature3: tNav("feature3"),
344
- feature4: tNav("feature4"),
345
- feature5: tNav("feature5"),
346
- allFeatures: tNav("allFeatures"),
347
- // #6878 D6 — one flyout rule for every shape: the offering column
348
- // (Features or Services) beside the Customers column (the /for
349
- // persona pages), plus the Customers footer column.
350
- flyoutFeaturesHeading: tNav("flyoutFeaturesHeading"),
351
- flyoutServicesHeading: tNav("flyoutServicesHeading"),
352
- flyoutCustomersHeading: tNav("flyoutCustomersHeading"),
353
- footerCustomersHeading: tNav("footerCustomersHeading"),
354
- allCustomers: tNav("allCustomers"),
355
- customer1: tNav("customer1"),
356
- customer2: tNav("customer2"),
357
- customer3: tNav("customer3"),
358
- service1: tNav("service1"),
359
- service2: tNav("service2"),
360
- service3: tNav("service3"),
361
- howItWorks: tNav("howItWorks"),
362
- howItWorksDesc: tNav("howItWorksDesc"),
363
- features: tNav("features"),
364
- featuresDesc: tNav("featuresDesc"),
365
- faq: tNav("faq"),
366
- faqDesc: tNav("faqDesc"),
367
- pricing: tNav("pricing"),
368
- portfolio: tNav("portfolio"),
369
- about: tNav("about"),
370
- // The footer's column groups (#6468): their two headings + Contact.
371
- contact: tNav("contact"),
372
- // The three pages every starter's roster names (#6658 D8), linked from
373
- // the footer when their surfaces are on.
374
- bookACall: tNav("bookACall"),
375
- glossary: tNav("glossary"),
376
- newsletter: tNav("newsletter"),
377
- footerProductHeading: tNav("footerProductHeading"),
378
- footerCompanyHeading: tNav("footerCompanyHeading"),
379
- getStarted: tNav("getStarted"),
380
- signUp: tNav("signUp"),
381
- docs: tNav("docs"),
382
- blog: tNav("blog"),
383
- // The scheduling module's booking page, /meet (#4633; renamed from the
384
- // schedule/ route by #5563 — the label key itself keeps its name, D1
385
- // R2: it names copy, not a route). Present in the map unconditionally,
386
- // like `docs` and `blog`: the map is a label lookup, and an entry for
387
- // a surface this site did not scaffold is never reached — `nav-config.ts`
388
- // only lists surfaces the declared architecture carries.
389
- schedule: tNav("schedule"),
390
- // Session-aware action labels (#4984). `account` carries the signed-in
391
- // user's display name (name, else email) so the Nav shows WHO is signed
392
- // in; it falls back to the generic label when there is no session.
393
- signIn: tNav("signIn"),
394
- signOut: tNav("signOut"),
395
- account: session?.user
396
- ? (session.user.name ?? session.user.email)
397
- : tNav("account"),
398
- },
380
+ items: navItemLabels,
399
381
  openMenu: tNav("openMenu"),
400
382
  closeMenu: tNav("closeMenu"),
401
383
  };
@@ -453,7 +435,7 @@ export default async function LocaleLayout({
453
435
  variant: "outline-strong",
454
436
  },
455
437
  ]
456
- : [...authActions];
438
+ : signedOutHeaderActions(siteHeader, authActions);
457
439
 
458
440
  // The announcement bar is HOME-ONLY (#5469 D5): its route gate matches the
459
441
  // pathname literally, and under next-intl's `as-needed` prefixing the home
@@ -466,25 +448,49 @@ export default async function LocaleLayout({
466
448
  // same header `generateMetadata` above reads (forwarded by middleware.ts);
467
449
  // Next.js's `headers()` is request-scoped and cheap to call again here.
468
450
  const pathname = (await headers()).get("x-pathname") ?? homePath;
469
- const isDeckHome = siteChrome.home === "deck" && pathname === homePath;
470
- // Inner pages (#7721 D4): under `innerPages: "bare"` every page but the
471
- // home wears the bare header (back arrow + the mark) and the one-bar
472
- // footer. Fail-safe like the deck gate: a missing `x-pathname` resolves to
473
- // the home path, so the full bar and its auth buttons render.
474
- const isBareInner = siteChrome.innerPages === "bare" && pathname !== homePath;
475
-
476
- // The deck home's header is bare (#7718 D1): no actions, signed in or out.
477
- // Its auth links move to the footer, the deck's closing slide (D4) — the
478
- // auth pair signed out, an Account link signed in (the route where Sign
479
- // out lives; its label is the user's name, `navLabels.items.account`).
480
- const headerActions: NavAction[] = isDeckHome || isBareInner ? [] : navActions;
451
+ // #10049 D5 — the home's layout and each page's header and footer resolve
452
+ // page frontmatter → route family → `_site.mdx` → the design's knobs
453
+ // (`lib/page-chrome.ts`). A site that sets none of them resolves to the
454
+ // design's chrome exactly as before: the deck home and `innerPages: "bare"`
455
+ // (#7721 D4 — bare header + one-bar footer on every page but the home) are
456
+ // that resolver's design level. Fail-safe: a missing `x-pathname`
457
+ // resolves to the home path.
458
+ const isHome = pathname === homePath;
459
+ const home = await resolveHomeChrome(locale, chrome);
460
+ const homeLayout = home.layout;
461
+ const isDeckHome = homeLayout === "deck" && isHome;
462
+ const pageChrome = isHome
463
+ ? home.chrome
464
+ : resolvePageChrome({
465
+ isHome: false,
466
+ homeIsDeck: homeLayout === "deck",
467
+ family: routeFamilyOf(pathname, readLocales()),
468
+ page: await resolveChromePage(pathname, locale),
469
+ site: home.site,
470
+ chrome,
471
+ // #7797 — an inner page the design lists in `closingFooterPaths`
472
+ // ends on the closing footer unless a site setting says otherwise.
473
+ closingListed: isClosingFooterPath({
474
+ pathname,
475
+ paths: chrome.closingFooterPaths,
476
+ locales: readLocales(),
477
+ }),
478
+ });
479
+ const headerIsFull = pageChrome.header === "full";
480
+
481
+ // Only a full header carries actions (#7718 D1, #10049). Under a bare or no
482
+ // header the auth links move to the footer — the auth pair signed out, an
483
+ // Account link signed in (the route where Sign out lives; its label is the
484
+ // user's name, `navLabels.items.account`).
485
+ const headerActions: NavAction[] = headerIsFull ? navActions : [];
481
486
  const deckFooterActions = session
482
487
  ? [{ labelKey: "account", path: "/account", variant: "outline-strong" as const }]
483
488
  : [...authActions];
484
489
  // A bare inner page's auth (#7721 D3/D4) rides the one-bar footer's link
485
490
  // group: Sign in / Sign up signed out, Account signed in (where Sign out
486
- // lives). Labels from the existing `nav` namespace.
487
- const accountLinks = !isBareInner
491
+ // lives). Labels from the existing `nav` namespace. Under a full header
492
+ // they are in the header instead (#10049).
493
+ const accountLinks = !(pageChrome.footer === "line" && !headerIsFull)
488
494
  ? []
489
495
  : session
490
496
  ? [{ label: tNav("account"), href: localeHref(locale, "/account") }]
@@ -528,18 +534,6 @@ export default async function LocaleLayout({
528
534
  // middleware and hreflang alternates read (NEXT_PUBLIC_ENABLED_LOCALES).
529
535
  const enabledLocales = readLocales();
530
536
 
531
- // #7797 — an inner page the site lists in `closingFooterPaths` ends on the
532
- // closing footer: the wordmark, the contact lines and the legal line. The
533
- // home is never an inner page, and an absent or empty list is false for
534
- // every page, so a site that lists none renders exactly as before.
535
- const isClosingInner =
536
- pathname !== homePath &&
537
- isClosingFooterPath({
538
- pathname,
539
- paths: chrome.closingFooterPaths,
540
- locales: enabledLocales,
541
- });
542
-
543
537
  // The (auth) route group's chrome branch (#5963).
544
538
  //
545
539
  // A route group's layout nests INSIDE its parent segment's layout, so
@@ -611,8 +605,14 @@ export default async function LocaleLayout({
611
605
  // #7721 D4 (gap F) — `mark-only` means the mark alone: no wordmark beside
612
606
  // it on any page. On the deck home `deckMorph` still governs the brand:
613
607
  // Nav spreads its `full-logo` composition after `logoDisplay`'s.
608
+ // #10049 D5 — the hybrid deck home (a full header) keeps the full bar,
609
+ // pinned, with no `deckMorph`; a bare header on a non-deck home is the
610
+ // bare bar, unpinned.
614
611
  ...(siteChrome.nav === "mark-only" ? { logoDisplay: "mark" as const } : {}),
615
- ...(isDeckHome ? { sticky: true, deckMorph: true, bar: "bare" as const } : {}),
612
+ ...(isHome && pageChrome.header === "bare"
613
+ ? { ...(isDeckHome ? { sticky: true, deckMorph: true } : {}), bar: "bare" as const }
614
+ : {}),
615
+ ...(isDeckHome && headerIsFull ? { sticky: true } : {}),
616
616
  // #8687 D5 — a deck page's header is pinned, as the deck home's is: under
617
617
  // snap an unpinned bar scrolls away with slide 1 (#5697). It keeps its
618
618
  // own actions, mark and bar style — only `sticky` is added.
@@ -646,9 +646,12 @@ export default async function LocaleLayout({
646
646
  ] as const;
647
647
  // Footer column groups (#6468), generated beside `navItems` as label keys;
648
648
  // `Footer` takes translated strings, so resolve them through the same map.
649
- const labelFor = (key: string): string =>
650
- (navLabels.items as Record<string, string>)[key] ?? key;
651
- const footerColumnDefs = footerColumns.map((col) => ({
649
+ const labelFor = (key: string): string => navItemLabels[key] ?? navText(key);
650
+ // #10041 D10 — every nav and footer path at `/docs` is rendered at the
651
+ // site's docs path; `nav-config.ts` itself is never edited.
652
+ // #10043 D9 — likewise every path at `/services`, at the services base.
653
+ const publicNavItems = withPublicServicesPaths(withPublicDocsPaths(navItems));
654
+ const footerColumnDefs = withPublicServicesColumns(withPublicDocsColumns(footerColumns)).map((col) => ({
652
655
  heading: labelFor(col.headingKey),
653
656
  headingKey: col.headingKey,
654
657
  items: col.items.map((item) => ({ ...item, label: labelFor(item.labelKey) })),
@@ -672,7 +675,7 @@ export default async function LocaleLayout({
672
675
  // reopens the consent banner (BannerConsentIsland).
673
676
  { label: tConsent("settingsLabel"), href: "/privacy#cookie-settings" },
674
677
  ...(siteChrome.footer === "centered"
675
- ? [{ label: navLabels.items.faq, href: "/faq" }]
678
+ ? [{ label: tNav("faq"), href: "/faq" }]
676
679
  : []),
677
680
  ];
678
681
 
@@ -704,6 +707,13 @@ export default async function LocaleLayout({
704
707
  dangerouslySetInnerHTML={{ __html: FLASH_PREVENTION_SCRIPT }}
705
708
  />
706
709
  )}
710
+ {/* #9889 D5, #9255 D3 — BotID's browser half: one inline script,
711
+ nothing visible, protecting every same-origin POST (a Server
712
+ Action posts to the page it was submitted from). Mounted only
713
+ where the server check runs, so a site with it off is
714
+ byte-identical and one off Vercel never loads a challenge it
715
+ cannot complete. */}
716
+ {botCheckOn("write") ? <BotIdClient protect={BOT_CLIENT_PROTECT} /> : null}
707
717
  {/* Inline CSS-variable declarations for the active theme mode */}
708
718
  <style
709
719
  data-ailk="theme-vars"
@@ -834,9 +844,17 @@ export default async function LocaleLayout({
834
844
  // #8687 D5/C3 — the home's brand landing stays home-only;
835
845
  // an opted-in page gets the deck's mechanics, never the
836
846
  // wordmark-in-the-headline treatment.
837
- ...(isDeckHome ? { deckHero: true } : {}),
838
- // #7740 D8 — the design's deck presentation dial.
839
- ...(chrome.deckPresentation === true ? { presentation: true } : {}),
847
+ // #10049 D5 — a full header keeps the home's own bar, so
848
+ // the bare-bar landing (the brand in the headline) is off.
849
+ ...(isDeckHome && !headerIsFull ? { deckHero: true } : {}),
850
+ // #10049 (operator, 2026-10-27) — the pinned full bar
851
+ // never covers a slide's own top spacing.
852
+ ...(isDeckHome && headerIsFull ? { navScrollOffset: true } : {}),
853
+ // #7740 D8 — the design's deck presentation dial. A full
854
+ // header keeps one colour across slides (#10049 D5/D7).
855
+ ...(chrome.deckPresentation === true
856
+ ? { presentation: true, ...(headerIsFull ? { navFollowsGround: false } : {}) }
857
+ : {}),
840
858
  // The deck's slide-advance controls (#7732) — named from
841
859
  // the `nav` catalog, never the shell's English default.
842
860
  slideAdvanceLabels: {
@@ -851,7 +869,10 @@ export default async function LocaleLayout({
851
869
  // server Nav (nothing callable crosses the RSC boundary).
852
870
  // #7744 D7 — on a deck home a team profile's arrow returns
853
871
  // to the people slide (`/#slide-people`) instead.
854
- isBareInner ? (
872
+ // #10049 D1 — `none` renders no header; a site-set `full`
873
+ // header shows the menus even on a `mark-only` design.
874
+ pageChrome.header === "none" ? undefined : !isHome &&
875
+ pageChrome.header === "bare" ? (
855
876
  <Nav
856
877
  {...navProps}
857
878
  navItems={[]}
@@ -860,7 +881,7 @@ export default async function LocaleLayout({
860
881
  href: backLinkHref({
861
882
  pathname,
862
883
  homePath,
863
- home: siteChrome.home,
884
+ home: homeLayout,
864
885
  locales: enabledLocales,
865
886
  }),
866
887
  label: tNav("home"),
@@ -871,15 +892,21 @@ export default async function LocaleLayout({
871
892
  ) : (
872
893
  <Nav
873
894
  {...navProps}
874
- navItems={siteChrome.nav === "mark-only" ? [] : navItems}
895
+ navItems={
896
+ siteChrome.nav === "mark-only" &&
897
+ (pageChrome.header === "bare" || pageChrome.headerFromTemplate)
898
+ ? []
899
+ : publicNavItems
900
+ }
875
901
  />
876
902
  )
877
903
  }
878
904
  footer={
905
+ pageChrome.footer === "none" ? undefined : (
879
906
  <Footer
880
907
  locale={locale}
881
908
  itemLabels={navLabels.items}
882
- navItems={navItems}
909
+ navItems={publicNavItems}
883
910
  columns={footerColumnDefs}
884
911
  siteName={SITE_NAME}
885
912
  logo={brandLogo}
@@ -892,27 +919,32 @@ export default async function LocaleLayout({
892
919
  // #7721 D4 — a bare inner page's footer is the one bar, its
893
920
  // link group carrying the account links the header drops.
894
921
  // #7797 — a listed inner page takes the same closing footer.
895
- // Under `innerPages: bare` the one-bar footer is where the
896
- // account links live (the header drops them), so the closing
897
- // footer carries the deck's auth actions in their place; on
898
- // any other inner page the header still holds them.
899
- {...(isDeckHome && isMinimalChrome
900
- ? { layout: "closing" as const, contactLines: readSiteContactLines() }
901
- : isClosingInner
902
- ? {
903
- layout: "closing" as const,
904
- contactLines: readSiteContactLines(),
905
- ...(isBareInner ? { actions: deckFooterActions } : {}),
906
- }
907
- : isBareInner
908
- ? { layout: "line" as const, accountLinks }
909
- : siteChrome.footer === "centered"
910
- ? { layout: "centered" as const }
911
- : {})}
922
+ // Under a bare header the closing footer carries the auth
923
+ // actions in its place; under a full header the header still
924
+ // holds them.
925
+ // #10049 D5 — the style is `pageChrome.footer`, and every
926
+ // footer with an actions row takes the auth links when the
927
+ // header is not full.
928
+ {...(pageChrome.footer === "closing"
929
+ ? {
930
+ layout: "closing" as const,
931
+ contactLines: readSiteContactLines(),
932
+ ...(!headerIsFull && !(isHome && isMinimalChrome)
933
+ ? { actions: deckFooterActions }
934
+ : {}),
935
+ }
936
+ : pageChrome.footer === "line"
937
+ ? { layout: "line" as const, accountLinks }
938
+ : {
939
+ ...(pageChrome.footer === "centered"
940
+ ? { layout: "centered" as const }
941
+ : {}),
942
+ ...(!headerIsFull ? { actions: deckFooterActions } : {}),
943
+ })}
912
944
  // #7718 D4 — the deck home's closing slide carries the
913
- // centred brand and the auth links its bare header drops.
914
- {...(isDeckHome && !isMinimalChrome
915
- ? { centeredBrand: true, actions: deckFooterActions }
945
+ // centred brand beside the auth links its bare header drops.
946
+ {...(isDeckHome && !headerIsFull && pageChrome.footer !== "closing"
947
+ ? { centeredBrand: true }
916
948
  : {})}
917
949
  // #7823 — the minimal closing slide's actions are a button,
918
950
  // then a text link; Footer reads the style from this.
@@ -932,6 +964,7 @@ export default async function LocaleLayout({
932
964
  />
933
965
  }
934
966
  />
967
+ )
935
968
  }
936
969
  >
937
970
  {children}