create-ailk 0.27.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 (210) hide show
  1. package/README.md +84 -7
  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 +23 -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 +105 -9
  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 +38 -4
  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 +5 -2
  44. package/templates/apps/api/.env.example +9 -5
  45. package/templates/apps/api/CLAUDE.md +14 -9
  46. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +37 -0
  47. package/templates/apps/api/src/vercel-handler.ts +8 -1
  48. package/templates/apps/mcp/CLAUDE.md +18 -10
  49. package/templates/apps/web/.env.example +25 -11
  50. package/templates/apps/web/CLAUDE.md +45 -4
  51. package/templates/apps/web/__tests__/setup.ts +7 -0
  52. package/templates/apps/web/app/.well-known/mcp.json/route.ts +7 -7
  53. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +61 -2
  54. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +81 -0
  55. package/templates/apps/web/app/[locale]/(authed)/account/connect/__tests__/page.test.tsx +197 -0
  56. package/templates/apps/web/app/[locale]/(authed)/account/connect/page.tsx +152 -0
  57. package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +2 -2
  58. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +76 -21
  59. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +46 -16
  60. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +20 -3
  61. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +180 -2
  62. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +6 -2
  63. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +122 -0
  64. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +1 -1
  65. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +20 -3
  66. package/templates/apps/web/app/[locale]/case-studies/__tests__/hub-children.test.tsx +65 -0
  67. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +6 -2
  68. package/templates/apps/web/app/[locale]/case-studies/page.tsx +23 -11
  69. package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +11 -1
  70. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
  71. package/templates/apps/web/app/[locale]/consent.actions.ts +44 -6
  72. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +4 -4
  73. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +269 -42
  74. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +88 -30
  75. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +20 -3
  76. package/templates/apps/web/app/[locale]/courses/__tests__/hub-children.test.tsx +32 -0
  77. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +6 -2
  78. package/templates/apps/web/app/[locale]/courses/page.tsx +22 -6
  79. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +15 -2
  80. package/templates/apps/web/app/[locale]/events/__tests__/hub-children.test.tsx +32 -0
  81. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +8 -2
  82. package/templates/apps/web/app/[locale]/events/page.tsx +23 -7
  83. package/templates/apps/web/app/[locale]/features/__tests__/hub-children.test.tsx +32 -0
  84. package/templates/apps/web/app/[locale]/features/page.tsx +23 -25
  85. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +95 -0
  86. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/save.actions.test.ts +193 -0
  87. package/templates/apps/web/app/[locale]/flows/[slug]/email-moment.ts +92 -0
  88. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +10 -0
  89. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +513 -0
  90. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +122 -0
  91. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.no-commerce.test.tsx +195 -0
  92. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/landing.test.tsx +293 -0
  93. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +397 -0
  94. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/checkout-seam.ts +74 -0
  95. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +264 -0
  96. package/templates/apps/web/app/[locale]/flows/[slug]/save.actions.ts +75 -0
  97. package/templates/apps/web/app/[locale]/for/__tests__/hub-children.test.tsx +32 -0
  98. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +5 -1
  99. package/templates/apps/web/app/[locale]/for/page.tsx +23 -25
  100. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +20 -3
  101. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  102. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +25 -4
  103. package/templates/apps/web/app/[locale]/integrations/__tests__/hub-children.test.tsx +38 -0
  104. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +8 -2
  105. package/templates/apps/web/app/[locale]/integrations/page.tsx +23 -11
  106. package/templates/apps/web/app/[locale]/layout.tsx +157 -139
  107. package/templates/apps/web/app/[locale]/page.tsx +51 -3
  108. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +16 -3
  109. package/templates/apps/web/app/[locale]/partners/__tests__/hub-children.test.tsx +32 -0
  110. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +6 -2
  111. package/templates/apps/web/app/[locale]/partners/page.tsx +22 -6
  112. package/templates/apps/web/app/[locale]/portfolio/__tests__/hub-children.test.tsx +38 -0
  113. package/templates/apps/web/app/[locale]/portfolio/page.tsx +25 -24
  114. package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +321 -0
  115. package/templates/apps/web/app/[locale]/pricing/[slug]/page.tsx +87 -0
  116. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
  117. package/templates/apps/web/app/[locale]/pricing/__tests__/pricing-pages.test.tsx +215 -0
  118. package/templates/apps/web/app/[locale]/pricing/page.tsx +56 -341
  119. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +21 -4
  120. package/templates/apps/web/app/[locale]/products/__tests__/hub-children.test.tsx +32 -0
  121. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +8 -2
  122. package/templates/apps/web/app/[locale]/products/page.tsx +22 -6
  123. package/templates/apps/web/app/[locale]/projects/__tests__/hub-children.test.tsx +32 -0
  124. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -0
  125. package/templates/apps/web/app/[locale]/projects/page.tsx +18 -11
  126. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +25 -4
  127. package/templates/apps/web/app/[locale]/resources/__tests__/hub-children.test.tsx +38 -0
  128. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +6 -2
  129. package/templates/apps/web/app/[locale]/resources/page.tsx +20 -10
  130. package/templates/apps/web/app/[locale]/services/[slug]/[child]/page.tsx +68 -0
  131. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +64 -9
  132. package/templates/apps/web/app/[locale]/services/__tests__/hub-children.test.tsx +158 -0
  133. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +12 -2
  134. package/templates/apps/web/app/[locale]/services/__tests__/service-detail-sections.test.tsx +223 -0
  135. package/templates/apps/web/app/[locale]/services/__tests__/service-families.test.tsx +368 -0
  136. package/templates/apps/web/app/[locale]/services/page.tsx +45 -19
  137. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  138. package/templates/apps/web/app/[locale]/team/__tests__/hub-children.test.tsx +32 -0
  139. package/templates/apps/web/app/[locale]/team/page.tsx +25 -24
  140. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +24 -3
  141. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  142. package/templates/apps/web/app/[locale]/templates/[slug]/__tests__/page.test.tsx +203 -0
  143. package/templates/apps/web/app/[locale]/templates/[slug]/page.tsx +81 -0
  144. package/templates/apps/web/app/[locale]/templates/__tests__/page.test.tsx +236 -0
  145. package/templates/apps/web/app/[locale]/templates/page.tsx +108 -0
  146. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
  147. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +24 -3
  148. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  149. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +204 -2
  150. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +151 -33
  151. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +233 -7
  152. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +92 -41
  153. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +77 -39
  154. package/templates/apps/web/app/llms.txt/route.ts +41 -3
  155. package/templates/apps/web/app/robots.txt/route.ts +5 -1
  156. package/templates/apps/web/app/sitemap.xml/route.ts +45 -3
  157. package/templates/apps/web/auth.ts +102 -50
  158. package/templates/apps/web/components/signed-in-nav.tsx +8 -1
  159. package/templates/apps/web/lib/__tests__/bot-check.test.ts +254 -0
  160. package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +212 -13
  161. package/templates/apps/web/lib/__tests__/deck-page.test.ts +13 -1
  162. package/templates/apps/web/lib/__tests__/docs-index.test.ts +93 -0
  163. package/templates/apps/web/lib/__tests__/docs-path.test.ts +168 -0
  164. package/templates/apps/web/lib/__tests__/hub-children.fixtures.ts +278 -0
  165. package/templates/apps/web/lib/__tests__/nav-label-keys.test.ts +138 -0
  166. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +178 -2
  167. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +183 -0
  168. package/templates/apps/web/lib/__tests__/page-chrome.test.ts +412 -0
  169. package/templates/apps/web/lib/__tests__/pricing-page.test.ts +142 -0
  170. package/templates/apps/web/lib/__tests__/relay.test.ts +202 -10
  171. package/templates/apps/web/lib/__tests__/route-family.test.ts +57 -0
  172. package/templates/apps/web/lib/__tests__/service-listing.test.ts +111 -0
  173. package/templates/apps/web/lib/__tests__/services-path.test.ts +96 -0
  174. package/templates/apps/web/lib/__tests__/site-header.test.ts +151 -0
  175. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +148 -6
  176. package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +84 -0
  177. package/templates/apps/web/lib/__tests__/template-listing.test.ts +158 -0
  178. package/templates/apps/web/lib/auth-secret.ts +57 -0
  179. package/templates/apps/web/lib/bot-check.ts +138 -0
  180. package/templates/apps/web/lib/consent-visitor.ts +81 -7
  181. package/templates/apps/web/lib/deck-page.ts +6 -3
  182. package/templates/apps/web/lib/docs-index.ts +100 -0
  183. package/templates/apps/web/lib/docs-path.ts +94 -0
  184. package/templates/apps/web/lib/hub-children.tsx +318 -0
  185. package/templates/apps/web/lib/modules.generated.ts +9 -0
  186. package/templates/apps/web/lib/nav-label-check.ts +61 -0
  187. package/templates/apps/web/lib/nav-label-keys.ts +78 -0
  188. package/templates/apps/web/lib/newsletter.actions.ts +31 -6
  189. package/templates/apps/web/lib/offerings-pricing.ts +135 -6
  190. package/templates/apps/web/lib/page-chrome.ts +367 -0
  191. package/templates/apps/web/lib/posting-title.ts +26 -4
  192. package/templates/apps/web/lib/pricing-page.ts +430 -0
  193. package/templates/apps/web/lib/relay.ts +31 -106
  194. package/templates/apps/web/lib/route-family.ts +69 -0
  195. package/templates/apps/web/lib/service-listing.ts +89 -0
  196. package/templates/apps/web/lib/services-path.ts +97 -0
  197. package/templates/apps/web/lib/site-header.ts +94 -0
  198. package/templates/apps/web/lib/telemetry-scrub.ts +68 -3
  199. package/templates/apps/web/lib/template-listing.ts +169 -0
  200. package/templates/apps/web/messages/en.json +86 -30
  201. package/templates/apps/web/middleware.ts +171 -11
  202. package/templates/apps/web/next.config.mjs +211 -2
  203. package/templates/apps/web/package.json +1 -1
  204. package/templates/apps/web/scripts/assert-content-bundled.ts +28 -6
  205. package/templates/apps/web/scripts/check-nav-labels.ts +39 -0
  206. package/templates/apps/web/sentry.server.config.ts +30 -2
  207. package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -9
  208. package/templates/apps/web/src/lib/rate-limit.ts +68 -27
  209. package/templates/apps/web/src/lib/routes.ts +15 -1
  210. 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
  }
@@ -61,8 +61,13 @@ import {
61
61
  type SignedInNavProps,
62
62
  } from "../../components/signed-in-nav";
63
63
  import { isAccountPath, isAuthPath, isBookingPath } from "../../lib/auth-routes";
64
+ import { BOT_CLIENT_PROTECT, botCheckOn } from "../../lib/bot-check";
64
65
  import { isClosingFooterPath } from "../../lib/closing-footer-path";
65
66
  import { pageOptsIntoDeck } from "../../lib/deck-page";
67
+ import {
68
+ withPublicDocsColumns,
69
+ withPublicDocsPaths,
70
+ } from "../../lib/docs-path";
66
71
  import {
67
72
  inPageView,
68
73
  newsletterSignup,
@@ -76,15 +81,30 @@ import { assertEnabledLocale } from "../../lib/locale-guard";
76
81
  import { isMagnetWalkPath } from "../../lib/magnet-routes";
77
82
  import { SCHEDULING_PAGES } from "../../lib/modules.generated";
78
83
  import { footerColumns, navItems } from "../../lib/nav-config";
84
+ import { navMessageKeys, usedNavLabelKeys } from "../../lib/nav-label-keys";
79
85
  import { getOgImageUrl } from "../../lib/og";
86
+ import {
87
+ resolveChromePage,
88
+ resolveHomeChrome,
89
+ resolvePageChrome,
90
+ routeFamilyOf,
91
+ } from "../../lib/page-chrome";
80
92
  import { backLinkHref } from "../../lib/profile-back-link";
81
- import { BOT_CHECKED_ROUTES, bookingBotCheckOn } from "../../lib/relay";
93
+ import {
94
+ withPublicServicesColumns,
95
+ withPublicServicesPaths,
96
+ } from "../../lib/services-path";
82
97
  import {
83
98
  COPYRIGHT_HOLDER,
84
99
  SITE_DESCRIPTION,
85
100
  SITE_NAME,
86
101
  } from "../../lib/site-brand";
87
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";
88
108
  import { siteLogo } from "../../lib/site-logo";
89
109
  // Registers the `_site.mdx` identity with page-renderer at module load, so
90
110
  // every page rendered under this layout carries the sitewide JSON-LD (#6597).
@@ -328,76 +348,36 @@ export default async function LocaleLayout({
328
348
  // Resolve nav labels server-side so Nav + Footer components stay
329
349
  // i18n-hook-free and are testable without a NextIntlClientProvider wrapper.
330
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
+ }
331
379
  const navLabels = {
332
- items: {
333
- home: tNav("home"),
334
- // The Free marketing-site chrome (#5469 D5): the Product flyout's three
335
- // entries + descriptions, the top-level items, and the Get started CTA.
336
- product: tNav("product"),
337
- services: tNav("services"),
338
- useCases: tNav("useCases"),
339
- useCasesDesc: tNav("useCasesDesc"),
340
- footerSitePagesHeading: tNav("footerSitePagesHeading"),
341
- footerFeaturesHeading: tNav("footerFeaturesHeading"),
342
- footerResourcesHeading: tNav("footerResourcesHeading"),
343
- feature1: tNav("feature1"),
344
- feature2: tNav("feature2"),
345
- feature3: tNav("feature3"),
346
- feature4: tNav("feature4"),
347
- feature5: tNav("feature5"),
348
- allFeatures: tNav("allFeatures"),
349
- // #6878 D6 — one flyout rule for every shape: the offering column
350
- // (Features or Services) beside the Customers column (the /for
351
- // persona pages), plus the Customers footer column.
352
- flyoutFeaturesHeading: tNav("flyoutFeaturesHeading"),
353
- flyoutServicesHeading: tNav("flyoutServicesHeading"),
354
- flyoutCustomersHeading: tNav("flyoutCustomersHeading"),
355
- footerCustomersHeading: tNav("footerCustomersHeading"),
356
- allCustomers: tNav("allCustomers"),
357
- customer1: tNav("customer1"),
358
- customer2: tNav("customer2"),
359
- customer3: tNav("customer3"),
360
- service1: tNav("service1"),
361
- service2: tNav("service2"),
362
- service3: tNav("service3"),
363
- howItWorks: tNav("howItWorks"),
364
- howItWorksDesc: tNav("howItWorksDesc"),
365
- features: tNav("features"),
366
- featuresDesc: tNav("featuresDesc"),
367
- faq: tNav("faq"),
368
- faqDesc: tNav("faqDesc"),
369
- pricing: tNav("pricing"),
370
- portfolio: tNav("portfolio"),
371
- about: tNav("about"),
372
- // The footer's column groups (#6468): their two headings + Contact.
373
- contact: tNav("contact"),
374
- // The three pages every starter's roster names (#6658 D8), linked from
375
- // the footer when their surfaces are on.
376
- bookACall: tNav("bookACall"),
377
- glossary: tNav("glossary"),
378
- newsletter: tNav("newsletter"),
379
- footerProductHeading: tNav("footerProductHeading"),
380
- footerCompanyHeading: tNav("footerCompanyHeading"),
381
- getStarted: tNav("getStarted"),
382
- signUp: tNav("signUp"),
383
- docs: tNav("docs"),
384
- blog: tNav("blog"),
385
- // The scheduling module's booking page, /meet (#4633; renamed from the
386
- // schedule/ route by #5563 — the label key itself keeps its name, D1
387
- // R2: it names copy, not a route). Present in the map unconditionally,
388
- // like `docs` and `blog`: the map is a label lookup, and an entry for
389
- // a surface this site did not scaffold is never reached — `nav-config.ts`
390
- // only lists surfaces the declared architecture carries.
391
- schedule: tNav("schedule"),
392
- // Session-aware action labels (#4984). `account` carries the signed-in
393
- // user's display name (name, else email) so the Nav shows WHO is signed
394
- // in; it falls back to the generic label when there is no session.
395
- signIn: tNav("signIn"),
396
- signOut: tNav("signOut"),
397
- account: session?.user
398
- ? (session.user.name ?? session.user.email)
399
- : tNav("account"),
400
- },
380
+ items: navItemLabels,
401
381
  openMenu: tNav("openMenu"),
402
382
  closeMenu: tNav("closeMenu"),
403
383
  };
@@ -455,7 +435,7 @@ export default async function LocaleLayout({
455
435
  variant: "outline-strong",
456
436
  },
457
437
  ]
458
- : [...authActions];
438
+ : signedOutHeaderActions(siteHeader, authActions);
459
439
 
460
440
  // The announcement bar is HOME-ONLY (#5469 D5): its route gate matches the
461
441
  // pathname literally, and under next-intl's `as-needed` prefixing the home
@@ -468,25 +448,49 @@ export default async function LocaleLayout({
468
448
  // same header `generateMetadata` above reads (forwarded by middleware.ts);
469
449
  // Next.js's `headers()` is request-scoped and cheap to call again here.
470
450
  const pathname = (await headers()).get("x-pathname") ?? homePath;
471
- const isDeckHome = siteChrome.home === "deck" && pathname === homePath;
472
- // Inner pages (#7721 D4): under `innerPages: "bare"` every page but the
473
- // home wears the bare header (back arrow + the mark) and the one-bar
474
- // footer. Fail-safe like the deck gate: a missing `x-pathname` resolves to
475
- // the home path, so the full bar and its auth buttons render.
476
- const isBareInner = siteChrome.innerPages === "bare" && pathname !== homePath;
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";
477
480
 
478
- // The deck home's header is bare (#7718 D1): no actions, signed in or out.
479
- // Its auth links move to the footer, the deck's closing slide (D4) — the
480
- // auth pair signed out, an Account link signed in (the route where Sign
481
- // out lives; its label is the user's name, `navLabels.items.account`).
482
- const headerActions: NavAction[] = isDeckHome || isBareInner ? [] : navActions;
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 : [];
483
486
  const deckFooterActions = session
484
487
  ? [{ labelKey: "account", path: "/account", variant: "outline-strong" as const }]
485
488
  : [...authActions];
486
489
  // A bare inner page's auth (#7721 D3/D4) rides the one-bar footer's link
487
490
  // group: Sign in / Sign up signed out, Account signed in (where Sign out
488
- // lives). Labels from the existing `nav` namespace.
489
- 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)
490
494
  ? []
491
495
  : session
492
496
  ? [{ label: tNav("account"), href: localeHref(locale, "/account") }]
@@ -530,18 +534,6 @@ export default async function LocaleLayout({
530
534
  // middleware and hreflang alternates read (NEXT_PUBLIC_ENABLED_LOCALES).
531
535
  const enabledLocales = readLocales();
532
536
 
533
- // #7797 — an inner page the site lists in `closingFooterPaths` ends on the
534
- // closing footer: the wordmark, the contact lines and the legal line. The
535
- // home is never an inner page, and an absent or empty list is false for
536
- // every page, so a site that lists none renders exactly as before.
537
- const isClosingInner =
538
- pathname !== homePath &&
539
- isClosingFooterPath({
540
- pathname,
541
- paths: chrome.closingFooterPaths,
542
- locales: enabledLocales,
543
- });
544
-
545
537
  // The (auth) route group's chrome branch (#5963).
546
538
  //
547
539
  // A route group's layout nests INSIDE its parent segment's layout, so
@@ -613,8 +605,14 @@ export default async function LocaleLayout({
613
605
  // #7721 D4 (gap F) — `mark-only` means the mark alone: no wordmark beside
614
606
  // it on any page. On the deck home `deckMorph` still governs the brand:
615
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.
616
611
  ...(siteChrome.nav === "mark-only" ? { logoDisplay: "mark" as const } : {}),
617
- ...(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 } : {}),
618
616
  // #8687 D5 — a deck page's header is pinned, as the deck home's is: under
619
617
  // snap an unpinned bar scrolls away with slide 1 (#5697). It keeps its
620
618
  // own actions, mark and bar style — only `sticky` is added.
@@ -648,9 +646,12 @@ export default async function LocaleLayout({
648
646
  ] as const;
649
647
  // Footer column groups (#6468), generated beside `navItems` as label keys;
650
648
  // `Footer` takes translated strings, so resolve them through the same map.
651
- const labelFor = (key: string): string =>
652
- (navLabels.items as Record<string, string>)[key] ?? key;
653
- 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) => ({
654
655
  heading: labelFor(col.headingKey),
655
656
  headingKey: col.headingKey,
656
657
  items: col.items.map((item) => ({ ...item, label: labelFor(item.labelKey) })),
@@ -674,7 +675,7 @@ export default async function LocaleLayout({
674
675
  // reopens the consent banner (BannerConsentIsland).
675
676
  { label: tConsent("settingsLabel"), href: "/privacy#cookie-settings" },
676
677
  ...(siteChrome.footer === "centered"
677
- ? [{ label: navLabels.items.faq, href: "/faq" }]
678
+ ? [{ label: tNav("faq"), href: "/faq" }]
678
679
  : []),
679
680
  ];
680
681
 
@@ -706,19 +707,13 @@ export default async function LocaleLayout({
706
707
  dangerouslySetInnerHTML={{ __html: FLASH_PREVENTION_SCRIPT }}
707
708
  />
708
709
  )}
709
- {/* #9889 D5 — BotID's browser half: one inline script, nothing
710
- visible, protecting exactly the pairs the relay checks. Mounted
711
- only where the server check runs, so a site with it off is
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
712
714
  byte-identical and one off Vercel never loads a challenge it
713
715
  cannot complete. */}
714
- {bookingBotCheckOn() ? (
715
- <BotIdClient
716
- protect={BOT_CHECKED_ROUTES.map((r) => ({
717
- path: r.path,
718
- method: r.method,
719
- }))}
720
- />
721
- ) : null}
716
+ {botCheckOn("write") ? <BotIdClient protect={BOT_CLIENT_PROTECT} /> : null}
722
717
  {/* Inline CSS-variable declarations for the active theme mode */}
723
718
  <style
724
719
  data-ailk="theme-vars"
@@ -849,9 +844,17 @@ export default async function LocaleLayout({
849
844
  // #8687 D5/C3 — the home's brand landing stays home-only;
850
845
  // an opted-in page gets the deck's mechanics, never the
851
846
  // wordmark-in-the-headline treatment.
852
- ...(isDeckHome ? { deckHero: true } : {}),
853
- // #7740 D8 — the design's deck presentation dial.
854
- ...(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
+ : {}),
855
858
  // The deck's slide-advance controls (#7732) — named from
856
859
  // the `nav` catalog, never the shell's English default.
857
860
  slideAdvanceLabels: {
@@ -866,7 +869,10 @@ export default async function LocaleLayout({
866
869
  // server Nav (nothing callable crosses the RSC boundary).
867
870
  // #7744 D7 — on a deck home a team profile's arrow returns
868
871
  // to the people slide (`/#slide-people`) instead.
869
- 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" ? (
870
876
  <Nav
871
877
  {...navProps}
872
878
  navItems={[]}
@@ -875,7 +881,7 @@ export default async function LocaleLayout({
875
881
  href: backLinkHref({
876
882
  pathname,
877
883
  homePath,
878
- home: siteChrome.home,
884
+ home: homeLayout,
879
885
  locales: enabledLocales,
880
886
  }),
881
887
  label: tNav("home"),
@@ -886,15 +892,21 @@ export default async function LocaleLayout({
886
892
  ) : (
887
893
  <Nav
888
894
  {...navProps}
889
- navItems={siteChrome.nav === "mark-only" ? [] : navItems}
895
+ navItems={
896
+ siteChrome.nav === "mark-only" &&
897
+ (pageChrome.header === "bare" || pageChrome.headerFromTemplate)
898
+ ? []
899
+ : publicNavItems
900
+ }
890
901
  />
891
902
  )
892
903
  }
893
904
  footer={
905
+ pageChrome.footer === "none" ? undefined : (
894
906
  <Footer
895
907
  locale={locale}
896
908
  itemLabels={navLabels.items}
897
- navItems={navItems}
909
+ navItems={publicNavItems}
898
910
  columns={footerColumnDefs}
899
911
  siteName={SITE_NAME}
900
912
  logo={brandLogo}
@@ -907,27 +919,32 @@ export default async function LocaleLayout({
907
919
  // #7721 D4 — a bare inner page's footer is the one bar, its
908
920
  // link group carrying the account links the header drops.
909
921
  // #7797 — a listed inner page takes the same closing footer.
910
- // Under `innerPages: bare` the one-bar footer is where the
911
- // account links live (the header drops them), so the closing
912
- // footer carries the deck's auth actions in their place; on
913
- // any other inner page the header still holds them.
914
- {...(isDeckHome && isMinimalChrome
915
- ? { layout: "closing" as const, contactLines: readSiteContactLines() }
916
- : isClosingInner
917
- ? {
918
- layout: "closing" as const,
919
- contactLines: readSiteContactLines(),
920
- ...(isBareInner ? { actions: deckFooterActions } : {}),
921
- }
922
- : isBareInner
923
- ? { layout: "line" as const, accountLinks }
924
- : siteChrome.footer === "centered"
925
- ? { layout: "centered" as const }
926
- : {})}
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
+ })}
927
944
  // #7718 D4 — the deck home's closing slide carries the
928
- // centred brand and the auth links its bare header drops.
929
- {...(isDeckHome && !isMinimalChrome
930
- ? { centeredBrand: true, actions: deckFooterActions }
945
+ // centred brand beside the auth links its bare header drops.
946
+ {...(isDeckHome && !headerIsFull && pageChrome.footer !== "closing"
947
+ ? { centeredBrand: true }
931
948
  : {})}
932
949
  // #7823 — the minimal closing slide's actions are a button,
933
950
  // then a text link; Footer reads the style from this.
@@ -947,6 +964,7 @@ export default async function LocaleLayout({
947
964
  />
948
965
  }
949
966
  />
967
+ )
950
968
  }
951
969
  >
952
970
  {children}