create-ailk 0.12.1 → 0.14.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 (163) hide show
  1. package/README.md +12 -4
  2. package/component-catalog.json +247 -33
  3. package/component-catalog.md +10 -6
  4. package/dist/apply-design.d.ts +21 -2
  5. package/dist/apply-design.js +120 -19
  6. package/dist/cli.js +1 -0
  7. package/dist/design-compile.d.ts +70 -2
  8. package/dist/design-compile.js +97 -1
  9. package/dist/design-home.d.ts +74 -0
  10. package/dist/design-home.js +256 -0
  11. package/dist/design-package.d.ts +39 -2
  12. package/dist/design-package.js +223 -9
  13. package/dist/design-presets.d.ts +17 -1
  14. package/dist/design-presets.js +17 -5
  15. package/dist/design-scaffold.d.ts +6 -25
  16. package/dist/design-scaffold.js +26 -74
  17. package/dist/index.d.ts +1 -0
  18. package/dist/lib/design-checks.d.ts +12 -1
  19. package/dist/lib/design-checks.js +38 -2
  20. package/dist/lib/module-license-gate.d.ts +9 -0
  21. package/dist/lib/module-license-gate.js +18 -0
  22. package/dist/module-architecture.js +27 -0
  23. package/dist/programmatic.js +70 -26
  24. package/dist/site-chrome.d.ts +38 -0
  25. package/dist/site-chrome.js +109 -2
  26. package/dist/site-fonts.d.ts +21 -0
  27. package/dist/site-fonts.js +54 -0
  28. package/dist/site-roster.d.ts +16 -0
  29. package/dist/site-roster.js +81 -0
  30. package/dist/starter-grid.js +15 -0
  31. package/dist/surfaces.js +12 -0
  32. package/dist/team-profiles.d.ts +29 -0
  33. package/dist/team-profiles.js +43 -0
  34. package/package.json +3 -3
  35. package/release-scope.json +1 -1
  36. package/site-templates/_base/content/en/TeamList/team.mdx +68 -0
  37. package/site-templates/_base/content/en/TeamMember/first-member.mdx +22 -0
  38. package/site-templates/_base/content/en/TeamMember/second-member.mdx +22 -0
  39. package/site-templates/_base/content/en/TeamMember/third-member.mdx +22 -0
  40. package/site-templates/designs/minimalism/DESIGN.md +164 -34
  41. package/site-templates/designs/minimalism/home/local-business-service.mdx +119 -0
  42. package/site-templates/designs/minimalism/home/organization-product.mdx +132 -0
  43. package/site-templates/designs/minimalism/home/organization-service.mdx +131 -0
  44. package/site-templates/designs/minimalism/home/person-service.mdx +101 -0
  45. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +34 -1
  46. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +20 -0
  47. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +34 -1
  48. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +22 -1
  49. package/site-templates/marketing-site-free/ailk-template.json +1 -1
  50. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +34 -1
  51. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -7
  52. package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
  53. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +34 -1
  54. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -7
  55. package/site-templates/shapes/organization-service/ailk-template.json +1 -1
  56. package/templates/.env.example +23 -9
  57. package/templates/README.md +2 -0
  58. package/templates/apps/api/CLAUDE.md +24 -17
  59. package/templates/apps/api/src/__tests__/factories/agent-caller.ts +98 -0
  60. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +321 -0
  61. package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +117 -0
  62. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +39 -14
  63. package/templates/apps/api/src/config/index.ts +35 -20
  64. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +3 -0
  65. package/templates/apps/api/src/lib/__mocks__/prisma.ts +14 -0
  66. package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +468 -0
  67. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +32 -2
  68. package/templates/apps/api/src/lib/agent-store.ts +521 -0
  69. package/templates/apps/api/src/lib/spoke-store.ts +13 -2
  70. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +384 -1
  71. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +61 -0
  72. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +176 -0
  73. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +76 -0
  74. package/templates/apps/api/src/middleware/auth.ts +219 -5
  75. package/templates/apps/api/src/middleware/default-tenant.ts +29 -8
  76. package/templates/apps/api/src/middleware/rate-limit.ts +35 -13
  77. package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +76 -15
  78. package/templates/apps/api/src/routes/leads/index.ts +14 -6
  79. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -6
  80. package/templates/apps/api/src/routes/project-listings/index.ts +14 -9
  81. package/templates/apps/api/src/routes/project-listings/me.ts +4 -3
  82. package/templates/apps/api/src/routes/project-listings/patch-copy.ts +7 -5
  83. package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +198 -0
  84. package/templates/apps/api/src/server.ts +27 -0
  85. package/templates/apps/api/src/services/__tests__/audit-log.test.ts +374 -57
  86. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +25 -2
  87. package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +303 -17
  88. package/templates/apps/api/src/services/audit-log.ts +278 -47
  89. package/templates/apps/api/src/services/default-tenant.ts +55 -5
  90. package/templates/apps/api/src/services/leads-inbox.ts +12 -21
  91. package/templates/apps/api/src/services/project-listings.ts +1 -21
  92. package/templates/apps/api/src/services/public-site-resolver.ts +103 -13
  93. package/templates/apps/mcp/CLAUDE.md +44 -5
  94. package/templates/apps/mcp/__tests__/parity.test.ts +53 -1
  95. package/templates/apps/mcp/__tests__/workspace-surface.test.ts +364 -0
  96. package/templates/apps/mcp/package.json +1 -1
  97. package/templates/apps/mcp/src/http-transport.ts +107 -1
  98. package/templates/apps/mcp/src/workspace-server.ts +193 -0
  99. package/templates/apps/web/.env.example +64 -37
  100. package/templates/apps/web/CLAUDE.md +11 -0
  101. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +10 -0
  102. package/templates/apps/web/app/[locale]/(authed)/account/settings/__tests__/theme-modes.test.ts +42 -0
  103. package/templates/apps/web/app/[locale]/(authed)/account/settings/theme-modes.ts +8 -0
  104. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
  105. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
  106. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +14 -1
  107. package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +69 -0
  108. package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +64 -0
  109. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +4 -4
  110. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +15 -3
  111. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +17 -6
  112. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +14 -1
  113. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +2 -2
  114. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +4 -6
  115. package/templates/apps/web/app/[locale]/layout.tsx +156 -53
  116. package/templates/apps/web/app/[locale]/page.tsx +37 -0
  117. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
  118. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +4 -6
  119. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +15 -2
  120. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +43 -6
  121. package/templates/apps/web/app/[locale]/services/page.tsx +14 -2
  122. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +2 -2
  123. package/templates/apps/web/app/[locale]/team/[slug]/page.tsx +45 -0
  124. package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +155 -0
  125. package/templates/apps/web/app/[locale]/team/page.tsx +49 -0
  126. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +2 -2
  127. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +14 -1
  128. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +2 -2
  129. package/templates/apps/web/app/not-found.tsx +47 -0
  130. package/templates/apps/web/app/v1/[...path]/route.ts +16 -0
  131. package/templates/apps/web/lib/__tests__/profile-back-link.test.ts +95 -0
  132. package/templates/apps/web/lib/__tests__/relay.test.ts +515 -0
  133. package/templates/apps/web/lib/__tests__/site-theme-head.test.ts +70 -0
  134. package/templates/apps/web/lib/__tests__/site-theme.test.ts +64 -1
  135. package/templates/apps/web/lib/profile-back-link.ts +51 -0
  136. package/templates/apps/web/lib/relay.ts +279 -0
  137. package/templates/apps/web/lib/site-chrome.ts +12 -0
  138. package/templates/apps/web/lib/site-fonts.ts +15 -0
  139. package/templates/apps/web/lib/site-logo.tsx +39 -1
  140. package/templates/apps/web/lib/site-theme-head.ts +33 -0
  141. package/templates/apps/web/lib/site-theme.ts +13 -1
  142. package/templates/apps/web/messages/en.json +7 -1
  143. package/templates/apps/web/middleware.ts +5 -4
  144. package/templates/apps/web/next.config.mjs +6 -0
  145. package/templates/database/CHANGELOG.md +22 -0
  146. package/templates/database/__tests__/agents.db.test.ts +235 -0
  147. package/templates/database/auth/schema.prisma +61 -0
  148. package/templates/database/migrations/20261005120000_agents/migration.sql +84 -0
  149. package/templates/database/package.json +1 -1
  150. package/versions.json +35 -35
  151. package/site-templates/designs/minimalism/sample/README.md +0 -48
  152. package/site-templates/designs/minimalism/sample/content/en/HomePage/home.mdx +0 -244
  153. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-1.svg +0 -7
  154. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-2.svg +0 -7
  155. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-3.svg +0 -7
  156. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-4.svg +0 -7
  157. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-5.svg +0 -7
  158. package/site-templates/designs/minimalism/sample/public/avatar-placeholder-6.svg +0 -7
  159. package/site-templates/designs/minimalism/sample/public/hero-backdrop.svg +0 -17
  160. package/site-templates/designs/minimalism/sample/public/logo-placeholder-1.svg +0 -6
  161. package/site-templates/designs/minimalism/sample/public/logo-placeholder-2.svg +0 -6
  162. package/site-templates/designs/minimalism/sample/public/logo-placeholder-3.svg +0 -6
  163. package/site-templates/designs/minimalism/sample/public/wordmark-placeholder.svg +0 -28
@@ -0,0 +1,193 @@
1
+ /**
2
+ * The signed-in workspace MCP server (#3591 D15, D16).
3
+ *
4
+ * Serves the catalog's `surface: "workspace"` tools — the agent's own context
5
+ * and the inbox actions — at `/mcp/workspace` (`http-transport.ts`), one
6
+ * fresh server per request. Each tool is the twin of one apps/api route: its
7
+ * handler forwards the caller's agent key to that route through
8
+ * `@working-theory/api-client` and returns the route's `data`.
9
+ *
10
+ * The key reaches a handler only as `extra.authInfo.token`, which the
11
+ * transport sets from the request's `Authorization` header for that request
12
+ * alone. Nothing here logs or serialises `extra`, `extra.authInfo` or
13
+ * `extra.requestInfo` (the SDK puts the raw `authorization` header there,
14
+ * SEC-3591-12), and no result carries the key: a refusal from the route is
15
+ * an MCP tool error naming the route's `error.code` and HTTP status only.
16
+ * apps/mcp checks nothing about the key beyond its shape; apps/api is the one
17
+ * place it is checked.
18
+ *
19
+ * A tool registers exactly when its route does: when any module in its
20
+ * catalog `modulesAnyOf` is enabled, the same disjunction the route's
21
+ * `server.ts` gate in apps/api is. The anonymous server (`server.ts`) never
22
+ * sees these entries — the catalog returns them only for
23
+ * `surface: "workspace"`.
24
+ */
25
+
26
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
27
+ import {
28
+ getMcpToolRegistrations,
29
+ type McpToolRegistration,
30
+ } from "@working-theory/metadata";
31
+
32
+ import { isModuleEnabled } from "./config/modules.js";
33
+
34
+ /** What a twin call answers — `@working-theory/api-client`'s `ApiResult`. */
35
+ type ForwardResult =
36
+ | { ok: true; body: unknown }
37
+ | { ok: false; status: number; body: unknown };
38
+
39
+ type WorkspaceToolHandler = (
40
+ key: string,
41
+ input: unknown,
42
+ ) => Promise<ForwardResult>;
43
+
44
+ /**
45
+ * Tool name → its handler's file and export. Every file is a paid slice the
46
+ * OSS strip deletes, so — exactly as `server.ts`'s `MODULE_TOOL_ENTRIES` — the
47
+ * specifier is read from this table, never written at an `import()` call
48
+ * site, which `tsc` would resolve on the stripped tree.
49
+ */
50
+ const WORKSPACE_TOOL_ENTRIES: Record<
51
+ string,
52
+ { readonly specifier: string; readonly exportName: string }
53
+ > = {
54
+ get_agent_context: {
55
+ specifier: "./tools/agent_context.js",
56
+ exportName: "ailkGetAgentContext",
57
+ },
58
+ list_inbox_messages: {
59
+ specifier: "./tools/inbox_messages.js",
60
+ exportName: "ailkListInboxMessages",
61
+ },
62
+ get_inbox_message: {
63
+ specifier: "./tools/inbox_messages.js",
64
+ exportName: "ailkGetInboxMessage",
65
+ },
66
+ mark_inbox_message_done: {
67
+ specifier: "./tools/inbox_messages.js",
68
+ exportName: "ailkMarkInboxMessageDone",
69
+ },
70
+ accept_inbox_message: {
71
+ specifier: "./tools/inbox_messages.js",
72
+ exportName: "ailkAcceptInboxMessage",
73
+ },
74
+ list_inbox_submissions: {
75
+ specifier: "./tools/inbox_submissions.js",
76
+ exportName: "ailkListInboxSubmissions",
77
+ },
78
+ get_inbox_submission: {
79
+ specifier: "./tools/inbox_submissions.js",
80
+ exportName: "ailkGetInboxSubmission",
81
+ },
82
+ mark_inbox_submission_done: {
83
+ specifier: "./tools/inbox_submissions.js",
84
+ exportName: "ailkMarkInboxSubmissionDone",
85
+ },
86
+ list_inbox_applications: {
87
+ specifier: "./tools/inbox_applications.js",
88
+ exportName: "ailkListInboxApplications",
89
+ },
90
+ get_inbox_application: {
91
+ specifier: "./tools/inbox_applications.js",
92
+ exportName: "ailkGetInboxApplication",
93
+ },
94
+ decide_inbox_application: {
95
+ specifier: "./tools/inbox_applications.js",
96
+ exportName: "ailkDecideInboxApplication",
97
+ },
98
+ };
99
+
100
+ async function resolveWorkspaceHandler(
101
+ name: string,
102
+ ): Promise<WorkspaceToolHandler> {
103
+ const entry = WORKSPACE_TOOL_ENTRIES[name];
104
+ if (!entry) {
105
+ throw new Error(`MCP catalog drift: no handler for tool '${name}'`);
106
+ }
107
+ const mod = (await import(entry.specifier)) as Record<
108
+ string,
109
+ WorkspaceToolHandler | undefined
110
+ >;
111
+ const fn = mod[entry.exportName];
112
+ if (!fn) {
113
+ throw new Error(
114
+ `MCP slice drift: '${entry.specifier}' does not export '${entry.exportName}'`,
115
+ );
116
+ }
117
+ return fn;
118
+ }
119
+
120
+ /** Whether this build registers the tool: any of its modules is enabled. */
121
+ function isWorkspaceToolEnabled(reg: McpToolRegistration): boolean {
122
+ return (reg.modulesAnyOf ?? []).some((name) => isModuleEnabled(name));
123
+ }
124
+
125
+ /** The names {@link createWorkspaceMcpServer} registers on this build. */
126
+ export function listWorkspaceTools(): string[] {
127
+ return getMcpToolRegistrations({ surface: "workspace" })
128
+ .filter(isWorkspaceToolEnabled)
129
+ .map((reg) => reg.name);
130
+ }
131
+
132
+ /** The route's `error.code`, or a fixed code when the body carries none. */
133
+ function errorCodeOf(body: unknown): string {
134
+ if (typeof body === "object" && body !== null) {
135
+ const error = (body as { error?: unknown }).error;
136
+ if (typeof error === "object" && error !== null) {
137
+ const code = (error as { code?: unknown }).code;
138
+ if (typeof code === "string") return code;
139
+ }
140
+ }
141
+ return "api_error";
142
+ }
143
+
144
+ function toolError(code: string, status: number) {
145
+ return {
146
+ isError: true,
147
+ content: [
148
+ {
149
+ type: "text" as const,
150
+ text: JSON.stringify({ ok: false, error: { code, status } }),
151
+ },
152
+ ],
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Creates the signed-in workspace server: every `surface: "workspace"` catalog
158
+ * entry whose module is enabled, each forwarding the caller's key to its twin
159
+ * route. Unconnected — the transport attaches one per request.
160
+ */
161
+ export function createWorkspaceMcpServer(): McpServer {
162
+ const server = new McpServer({ name: "ailk-workspace", version: "0.1.0" });
163
+
164
+ for (const reg of getMcpToolRegistrations({ surface: "workspace" })) {
165
+ if (!isWorkspaceToolEnabled(reg)) continue;
166
+ // Boot-time drift detection, as `server.ts` does it.
167
+ if (!WORKSPACE_TOOL_ENTRIES[reg.name]) {
168
+ throw new Error(`MCP catalog drift: no handler for tool '${reg.name}'`);
169
+ }
170
+ server.tool(
171
+ reg.name,
172
+ reg.description,
173
+ reg.inputShape,
174
+ async (input, extra) => {
175
+ const key = extra.authInfo?.token;
176
+ if (!key) return toolError("unauthorized", 401);
177
+ const handler = await resolveWorkspaceHandler(reg.name);
178
+ const res = await handler(key, input);
179
+ if (!res.ok) return toolError(errorCodeOf(res.body), res.status);
180
+ return {
181
+ content: [
182
+ {
183
+ type: "text" as const,
184
+ text: JSON.stringify({ ok: true, data: res.body }),
185
+ },
186
+ ],
187
+ };
188
+ },
189
+ );
190
+ }
191
+
192
+ return server;
193
+ }
@@ -25,35 +25,71 @@ CONTENT_ADAPTER=mdx
25
25
  # CONTENT_REVALIDATE_SECRET=
26
26
 
27
27
  # =============================================================================
28
- # apps/api base URL (browser-side lead-capture forms) — #3965
29
- # =============================================================================
30
- # apps/web and apps/api are separate origins in every deployment shape that
31
- # doesn't front them with a rewrite/proxy. A browser-side block (e.g.
32
- # NewsletterFormInline, packages/ui/src/blocks/NewsletterFormInline.tsx)
33
- # posting a root-relative action (e.g. /v1/leads/subscribe) resolves it
34
- # against apps/web's OWN origin -- not apps/api's -- unless this is set.
35
- # Same convention as apps/workspace/lib/client-api.ts's identically-named var.
36
- # Should be apps/api's origin with NO trailing slash (a single trailing
37
- # slash is stripped before joining, but keep this clean regardless).
38
- #
39
- # THIS IS THE ONE BROWSER-SIDE NAME (#5397). All three browser readers of the
40
- # apps/api origin resolve it from this variable: resolveSubmitUrl
41
- # (packages/ui, the form blocks above), apps/workspace's client-api, and
28
+ # The same-origin relay, and the browser-side apps/api name — #7644, #5397
29
+ # =============================================================================
30
+ # THIS SITE'S OWN FORMS NEVER CALL apps/api FROM THE BROWSER (#7644). Lead
31
+ # capture, newsletter signup, step-flow submits, booking and the commerce
32
+ # invoice form post to THIS site's own /v1/... address; app/v1/[...path]/route.ts
33
+ # forwards each server-side to apps/api at AILK_API_URL, attaching this site's
34
+ # AILK_SPOKE_KEY when it is set and none when it is not (a single-site install
35
+ # takes the same path). next.config.mjs sets NEXT_PUBLIC_AILK_SAME_ORIGIN_RELAY
36
+ # for that — it is part of the source, not a variable to set here. The relay
37
+ # forwards exactly these, and refuses every other /v1 path with a 404:
38
+ #
39
+ # POST /v1/leads/submit POST /v1/leads/subscribe
40
+ # POST /v1/leads/callback GET /v1/schedule/availability
41
+ # POST /v1/schedule/book POST /v1/commerce/invoices
42
+ #
43
+ # An authored form `action` must name one of them. Adding one is a reviewed
44
+ # change to RELAY_ROUTES in lib/relay.ts and to this list. The visitor's
45
+ # x-forwarded-for and user-agent reach the hub as received (and the keyed
46
+ # visitor IP, with AILK_VISITOR_FORWARD_KEY — server-side only, never
47
+ # NEXT_PUBLIC_; see the repo-root .env.example, #7394). A cross-site post is
48
+ # refused before the hub is called.
49
+ #
50
+ # EDGE PRECONDITION: the relay forwards x-forwarded-for exactly as this site
51
+ # received it, so run the site behind an edge that OVERWRITES the client-address
52
+ # headers (as Vercel does), not one that appends to them. Otherwise a caller
53
+ # can choose its own rate-limit identity at the hub (spec SEC-7644-6). And when
54
+ # the HUB's edge overwrites x-forwarded-for with the address it saw (this
55
+ # site's server), set AILK_VISITOR_FORWARD_KEY here and on the hub: without it
56
+ # every visitor of this site shares one rate limit on the hub's lead routes.
57
+ #
58
+ # AILK_SPOKE_KEY ON A HUB WITH SPOKES (#7633). A hub whose spoke registry holds
59
+ # a row refuses every public call without a key by default, so EVERY site of it
60
+ # sets its own key — the hub's own site too, registered against the operator's
61
+ # own workspace. The relay above carries it on the browser's calls; the site's
62
+ # server-side reads (meeting types, the /meet pages, Server Actions) send it
63
+ # themselves. It is sent only when AILK_API_URL is https: or loopback.
64
+ # AILK_SPOKE_KEY=
65
+ #
66
+ # UPGRADE ORDER for a hub with spokes. 1) Redeploy every site on this release,
67
+ # the hub's own site included — a site still on the previous release keeps
68
+ # posting to the hub directly, and keeps working, until then. 2) Register a key
69
+ # for every site (the #7532 runbook) and set it as AILK_SPOKE_KEY. 3) Only then
70
+ # upgrade the hub to the release that refuses keyless callers (#7633; the
71
+ # runbook's "Before you upgrade a hub"). After that a spoke's origin can
72
+ # come off the hub's AILK_TRUSTED_ORIGINS — unless the site uses the commerce
73
+ # invoice form, a cookie-authenticated owner write whose Origin the hub still
74
+ # checks against that list.
75
+ #
76
+ # NEXT_PUBLIC_AILK_API_URL is still THE ONE BROWSER-SIDE NAME of apps/api's
77
+ # origin (#5397) for the two browser readers the relay does not cover:
42
78
  # @working-theory/api-client's getApiBase() — which runs in the browser
43
79
  # wherever a "use client" component binds it, e.g. the founder application's
44
- # ListingApplicationClient. getApiBase() previously read NEXT_PUBLIC_API_URL,
45
- # a name this file never documented, so a site configured exactly per these
46
- # docs sent its browser traffic to http://localhost:3001. That name is still
47
- # honoured as a deprecated fallback; do not set it in a new deployment.
80
+ # ListingApplicationClient — and the report reader's download link. It is also
81
+ # what packages/ui's resolveSubmitUrl falls back to in a build WITHOUT the
82
+ # relay (an older scaffold that upgrades @working-theory/ui alone). Should be
83
+ # apps/api's origin with NO trailing slash. getApiBase() previously read
84
+ # NEXT_PUBLIC_API_URL, a name this file never documented, so a site configured
85
+ # exactly per these docs sent its browser traffic to http://localhost:3001.
86
+ # That name is still honoured as a deprecated fallback; do not set it in a new
87
+ # deployment.
48
88
  #
49
89
  # The server-side counterpart is AILK_API_URL (see the repo-root .env.example)
50
90
  # — deliberately NOT NEXT_PUBLIC_, and it still wins over this one when both
51
- # are set, so an RSC/Server-Action call can target an internal origin the
52
- # browser never sees.
53
- #
54
- # A spoke calling a hub API server-to-server forwards its visitors for the
55
- # hub's per-visitor lead limit with AILK_VISITOR_FORWARD_KEY — server-side
56
- # only, never NEXT_PUBLIC_ (see the repo-root .env.example, #7394).
91
+ # are set, so an RSC/Server-Action call and the relay can target an internal
92
+ # origin the browser never sees.
57
93
  # NEXT_PUBLIC_AILK_API_URL=http://localhost:3002
58
94
 
59
95
  # =============================================================================
@@ -188,16 +224,7 @@ CONTENT_ADAPTER=mdx
188
224
  # boundary checkable rather than merely intended.
189
225
  # SCHEDULING_CAL_LINK=owner/event-slug
190
226
  #
191
- # SPLIT-ORIGIN DEPLOYS NEED A CSP EXTENSION. The booker calls this site's
192
- # /v1/schedule/* routes from the browser, resolved through
193
- # NEXT_PUBLIC_AILK_API_URL. Set that variable and the calls become
194
- # cross-origin, but the baseline CSP in security-headers.config.ts is
195
- # `connect-src 'self' https://api.stripe.com` — so the browser blocks them and
196
- # the booker shows its generic unavailable state with no explanation. Pass the
197
- # API origin through the customer extension `buildHeaders` accepts:
198
- #
199
- # buildHeaders({ csp: { "connect-src": ["'self'", "<your apps/api origin>"] } })
200
- #
201
- # Deploying apps/web and apps/api on one origin (leaving
202
- # NEXT_PUBLIC_AILK_API_URL unset) needs no extension — the calls stay
203
- # same-origin and 'self' already covers them.
227
+ # The booker's /v1/schedule/* calls go through this site's same-origin relay
228
+ # (see the relay block above, #7644), so the baseline CSP's
229
+ # `connect-src 'self'` already covers them — no CSP extension is needed for
230
+ # them, whatever NEXT_PUBLIC_AILK_API_URL is set to.
@@ -132,6 +132,15 @@ same `captureLeadAction`, and on Skip or a failed submit swaps to
132
132
  embedding for it is behind the hero CTA's lightbox gate
133
133
  (`packages/ui/src/sections/FormLightbox.tsx`, #4577), not a whole-page mount.
134
134
 
135
+ ## The same-origin relay (`/v1/*`)
136
+
137
+ A visitor's browser never calls the hub. Lead capture, newsletter signup, callback, scheduling and the commerce invoice call post to this site's own `/v1/...` address, and `app/v1/[...path]/route.ts` hands each request to `lib/relay.ts` (`handleRelay`), which forwards it server-side through `@working-theory/api-client`'s `relayToApi` with the site's own `AILK_SPOKE_KEY` when it has one (#7644). The browser never holds the key, so a hub can refuse unknown callers (#7633) without taking a site's forms down.
138
+
139
+ - **Only the list.** `RELAY_ROUTES` is the fixed set of (method, path) pairs: `POST /v1/leads/{submit,subscribe,callback}`, `GET /v1/schedule/availability`, `POST /v1/schedule/book`, `POST /v1/commerce/invoices`. Anything else is a 404 with no hub call. Adding a route is a reviewed change to that list, its test and `.env.example`.
140
+ - **No cross-site POST.** A `Sec-Fetch-Site` other than `same-origin`, or an `Origin` not on this site's own host, is refused 403 before the hub is called.
141
+ - **Headers are an allow-list.** The visitor's `x-forwarded-for` (else `x-real-ip`) and `user-agent` pass through so per-visitor rate limits still count each visitor; no `authorization`, client-sent `x-ailk-*` or `host` is forwarded, and a `cookie` goes only on `POST /v1/commerce/invoices`, the one session route.
142
+ - `/v1` is exempt from next-intl's locale rewrite in `middleware.ts`, as `/api` is.
143
+
135
144
  ## AEO & performance
136
145
 
137
146
  - Static rendering is the default. Don't introduce client rendering without a reason.
@@ -147,3 +156,5 @@ embedding for it is behind the hero CTA's lightbox gate
147
156
  - Don't use `transition-all`.
148
157
  - Don't reach into `apps/api` or `apps/mcp` from `apps/web`. They're peer apps — talk via HTTP/MCP.
149
158
  - Don't commit images to `screenshots/` — only the `.gitkeep` is tracked.
159
+
160
+ Last updated: 2026-10-05 — doc-sync: added § The same-origin relay (#7644) — verified against `apps/web/lib/relay.ts` and `apps/web/app/v1/[...path]/route.ts`.
@@ -89,6 +89,16 @@ jest.mock("next/navigation", () => ({
89
89
  redirect: jest.fn(),
90
90
  }));
91
91
 
92
+ // The colour-mode cases test the theme's modes, so they run against an unpinned
93
+ // chrome: a scaffold whose design pins light mode (minimalism, #7742 D2) would
94
+ // otherwise hide the control in every case. theme-modes.test.ts covers the pin.
95
+ // eslint-disable-next-line no-restricted-syntax -- #507: `site-chrome.ts` is a generated data module with no external boundary to mock instead (as theme-modes.test.ts).
96
+ jest.mock("../../../../../lib/site-chrome", () => {
97
+ const actual = jest.requireActual("../../../../../lib/site-chrome");
98
+ const { colorMode, ...unpinned } = actual.siteChrome;
99
+ return { ...actual, siteChrome: unpinned };
100
+ });
101
+
92
102
  import Page from "../settings/page";
93
103
 
94
104
  const SESSION = {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `siteThemeModeCount` (#7742 D2 / AC-2): a design pinned to light has one
3
+ * colour mode whatever its palette declares; without the pin, the declared
4
+ * palette decides, as before.
5
+ */
6
+ import type { SiteChrome } from "../../../../../../lib/site-chrome";
7
+
8
+ const mockSiteChrome: SiteChrome = {
9
+ nav: "full",
10
+ footer: "columns",
11
+ announcementBanner: false,
12
+ home: "default",
13
+ };
14
+ // eslint-disable-next-line no-restricted-syntax -- #507: `site-chrome.ts` is a generated data module with no external boundary to mock instead; this is the seam theme-modes.ts itself reads.
15
+ jest.mock("../../../../../../lib/site-chrome", () => ({
16
+ __esModule: true,
17
+ siteChrome: mockSiteChrome,
18
+ }));
19
+
20
+ // Both palettes declared — the count is 2 without the pin.
21
+ // eslint-disable-next-line no-restricted-syntax -- #507: the one seam onto `_site.mdx`; the palette is the test's to set.
22
+ jest.mock("../../../../../../lib/site-theme", () => ({
23
+ __esModule: true,
24
+ loadSiteBrandTokens: () => ({ colors: { light: { bg: "#ffffff" }, dark: { bg: "#000000" } } }),
25
+ }));
26
+
27
+ import { siteThemeModeCount } from "../theme-modes";
28
+
29
+ afterEach(() => {
30
+ delete mockSiteChrome.colorMode;
31
+ });
32
+
33
+ describe("siteThemeModeCount (#7742 D2)", () => {
34
+ it('colorMode: "light" gives 1 even when both palettes are declared', () => {
35
+ mockSiteChrome.colorMode = "light";
36
+ expect(siteThemeModeCount()).toBe(1);
37
+ });
38
+
39
+ it("without colorMode the same palette gives 2", () => {
40
+ expect(siteThemeModeCount()).toBe(2);
41
+ });
42
+ });
@@ -14,11 +14,19 @@
14
14
  *
15
15
  * A theme the loader rejects counts as the default (2): the layout surfaces
16
16
  * that error itself, and settings must not be where it first appears.
17
+ *
18
+ * A design that pins the mode (`colorMode: "light"`, #7742 D2) has one mode
19
+ * whatever its palette declares, so the settings control hides there too.
17
20
  */
18
21
 
22
+ import { siteChrome, type SiteChrome } from "../../../../../lib/site-chrome";
19
23
  import { loadSiteBrandTokens } from "../../../../../lib/site-theme";
20
24
 
25
+ /** Widened as the locale layout reads it: an older generated file has no `colorMode`. */
26
+ const chrome: SiteChrome & { colorMode?: "light" } = siteChrome;
27
+
21
28
  export function siteThemeModeCount(): 1 | 2 {
29
+ if (chrome.colorMode === "light") return 1;
22
30
  try {
23
31
  const colors = loadSiteBrandTokens()?.["colors"];
24
32
  if (typeof colors !== "object" || colors === null) return 2;
@@ -72,7 +72,7 @@ describe("careers/page — snapshot regression", () => {
72
72
  });
73
73
  const { container } = render(element);
74
74
  expect(container.innerHTML).toMatchInlineSnapshot(
75
- `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Careers at AILK","description":"Join the team building the AX-first platform.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"JobPosting"}]}}</script><main id="main"><h1>Careers at AILK</h1><p data-testid="careers-description">Join the team building the AX-first platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">mdx-body-content</div></article></div></div></main>"`,
75
+ `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Careers at AILK","description":"Join the team building the AX-first platform.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"JobPosting"}]}}</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">Careers at AILK</h1><p data-testid="careers-description" class="mt-block-tight text-lead">Join the team building the AX-first platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-fg) text-body-sm leading-relaxed [&amp;>*:first-child]:mt-0 [&amp;_h2]:mt-block-loose [&amp;_h2]:scroll-mt-24 [&amp;_h2]:text-heading-3 [&amp;_h2]:font-display [&amp;_h2]:text-(--color-fg) [&amp;_h3]:mt-block-loose [&amp;_h3]:scroll-mt-24 [&amp;_h3]:text-heading-4 [&amp;_h3]:font-display [&amp;_h3]:text-(--color-fg) [&amp;_h4]:mt-block-tight [&amp;_h4]:scroll-mt-24 [&amp;_h4]:text-heading-4 [&amp;_h4]:font-display [&amp;_h4]:text-(--color-fg) [&amp;_h5]:mt-block-tight [&amp;_h5]:scroll-mt-24 [&amp;_h5]:text-heading-4 [&amp;_h5]:font-display [&amp;_h5]:text-(--color-fg) [&amp;_h6]:mt-block-tight [&amp;_h6]:scroll-mt-24 [&amp;_h6]:text-heading-4 [&amp;_h6]:font-display [&amp;_h6]:text-(--color-fg) [&amp;_p]:mt-paragraph [&amp;_strong]:font-semibold [&amp;_strong]:text-(--color-fg) [&amp;_em]:italic [&amp;_ul]:mt-paragraph [&amp;_ul]:list-disc [&amp;_ul]:ps-6 [&amp;_ol]:mt-paragraph [&amp;_ol]:list-decimal [&amp;_ol]:ps-6 [&amp;_li]:mt-1 [&amp;_li]:ps-1 [&amp;_li]:marker:text-(--color-text-muted) [&amp;_li>ul]:mt-1 [&amp;_li>ol]:mt-1 [&amp;_blockquote]:mt-paragraph [&amp;_blockquote]:border-s-4 [&amp;_blockquote]:border-(--color-border-strong) [&amp;_blockquote]:ps-4 [&amp;_blockquote]:italic [&amp;_blockquote]:text-(--color-text-muted) [&amp;_hr]:my-block-loose [&amp;_hr]:border-(--color-border) [&amp;_a]:text-(--color-accent) [&amp;_a]:underline [&amp;_a]:underline-offset-4 [&amp;_:not(pre)>code]:rounded [&amp;_:not(pre)>code]:bg-(--color-surface-secondary) [&amp;_:not(pre)>code]:px-1.5 [&amp;_:not(pre)>code]:py-0.5 [&amp;_:not(pre)>code]:text-code [&amp;_:not(pre)>code]:font-mono [&amp;_:not(pre)>code]:text-(--color-fg) [&amp;_pre]:mt-paragraph [&amp;_pre]:overflow-auto [&amp;_pre]:rounded-md [&amp;_pre]:border-hairline [&amp;_pre]:border-(--color-border) [&amp;_pre]:bg-(--color-bg) [&amp;_pre]:p-4 [&amp;_pre]:font-mono [&amp;_pre]:text-body-sm [&amp;_img]:mt-paragraph [&amp;_img]:h-auto [&amp;_img]:max-w-full [&amp;_img]:rounded-lg [&amp;_table]:mt-paragraph [&amp;_table]:w-full [&amp;_table]:border-collapse [&amp;_table]:text-body-sm [&amp;_th]:border-b [&amp;_th]:border-(--color-border) [&amp;_th]:bg-(--color-surface-secondary) [&amp;_th]:px-3 [&amp;_th]:py-2 [&amp;_th]:text-start [&amp;_th]:font-semibold [&amp;_th]:text-(--color-fg) [&amp;_td]:border-b [&amp;_td]:border-(--color-border) [&amp;_td]:px-3 [&amp;_td]:py-2 [&amp;_kbd]:rounded [&amp;_kbd]:border-hairline [&amp;_kbd]:border-(--color-border) [&amp;_kbd]:bg-(--color-surface-secondary) [&amp;_kbd]:px-1.5 [&amp;_kbd]:py-0.5 [&amp;_kbd]:text-caption [&amp;_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">mdx-body-content</div></div></article></div></div></div></main>"`,
76
76
  );
77
77
  });
78
78
  });
@@ -97,7 +97,7 @@ describe("careers/[slug]/page — snapshot regression", () => {
97
97
  });
98
98
  const { container } = render(element);
99
99
  expect(container.innerHTML).toMatchInlineSnapshot(
100
- `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Senior Engineer","description":"Build the AILK platform.","mainEntity":{"@context":"https://schema.org","@type":"JobPosting","title":"Senior Engineer","description":"Build the AILK platform."}},{"@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":"Careers","item":"http://localhost:3000/en/careers"},{"@type":"ListItem","position":3,"name":"Senior Engineer","item":"http://localhost:3000/en/careers/senior-engineer"}]}]}</script><main id="main"><h1>Senior Engineer</h1><p data-testid="senior-engineer-description">Build the AILK platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">mdx-body-content</div></article></div></div></main>"`,
100
+ `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Senior Engineer","description":"Build the AILK platform.","mainEntity":{"@context":"https://schema.org","@type":"JobPosting","title":"Senior Engineer","description":"Build the AILK platform."}},{"@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":"Careers","item":"http://localhost:3000/en/careers"},{"@type":"ListItem","position":3,"name":"Senior Engineer","item":"http://localhost:3000/en/careers/senior-engineer"}]}]}</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">Senior Engineer</h1><p data-testid="senior-engineer-description" class="mt-block-tight text-lead">Build the AILK platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-fg) text-body-sm leading-relaxed [&amp;>*:first-child]:mt-0 [&amp;_h2]:mt-block-loose [&amp;_h2]:scroll-mt-24 [&amp;_h2]:text-heading-3 [&amp;_h2]:font-display [&amp;_h2]:text-(--color-fg) [&amp;_h3]:mt-block-loose [&amp;_h3]:scroll-mt-24 [&amp;_h3]:text-heading-4 [&amp;_h3]:font-display [&amp;_h3]:text-(--color-fg) [&amp;_h4]:mt-block-tight [&amp;_h4]:scroll-mt-24 [&amp;_h4]:text-heading-4 [&amp;_h4]:font-display [&amp;_h4]:text-(--color-fg) [&amp;_h5]:mt-block-tight [&amp;_h5]:scroll-mt-24 [&amp;_h5]:text-heading-4 [&amp;_h5]:font-display [&amp;_h5]:text-(--color-fg) [&amp;_h6]:mt-block-tight [&amp;_h6]:scroll-mt-24 [&amp;_h6]:text-heading-4 [&amp;_h6]:font-display [&amp;_h6]:text-(--color-fg) [&amp;_p]:mt-paragraph [&amp;_strong]:font-semibold [&amp;_strong]:text-(--color-fg) [&amp;_em]:italic [&amp;_ul]:mt-paragraph [&amp;_ul]:list-disc [&amp;_ul]:ps-6 [&amp;_ol]:mt-paragraph [&amp;_ol]:list-decimal [&amp;_ol]:ps-6 [&amp;_li]:mt-1 [&amp;_li]:ps-1 [&amp;_li]:marker:text-(--color-text-muted) [&amp;_li>ul]:mt-1 [&amp;_li>ol]:mt-1 [&amp;_blockquote]:mt-paragraph [&amp;_blockquote]:border-s-4 [&amp;_blockquote]:border-(--color-border-strong) [&amp;_blockquote]:ps-4 [&amp;_blockquote]:italic [&amp;_blockquote]:text-(--color-text-muted) [&amp;_hr]:my-block-loose [&amp;_hr]:border-(--color-border) [&amp;_a]:text-(--color-accent) [&amp;_a]:underline [&amp;_a]:underline-offset-4 [&amp;_:not(pre)>code]:rounded [&amp;_:not(pre)>code]:bg-(--color-surface-secondary) [&amp;_:not(pre)>code]:px-1.5 [&amp;_:not(pre)>code]:py-0.5 [&amp;_:not(pre)>code]:text-code [&amp;_:not(pre)>code]:font-mono [&amp;_:not(pre)>code]:text-(--color-fg) [&amp;_pre]:mt-paragraph [&amp;_pre]:overflow-auto [&amp;_pre]:rounded-md [&amp;_pre]:border-hairline [&amp;_pre]:border-(--color-border) [&amp;_pre]:bg-(--color-bg) [&amp;_pre]:p-4 [&amp;_pre]:font-mono [&amp;_pre]:text-body-sm [&amp;_img]:mt-paragraph [&amp;_img]:h-auto [&amp;_img]:max-w-full [&amp;_img]:rounded-lg [&amp;_table]:mt-paragraph [&amp;_table]:w-full [&amp;_table]:border-collapse [&amp;_table]:text-body-sm [&amp;_th]:border-b [&amp;_th]:border-(--color-border) [&amp;_th]:bg-(--color-surface-secondary) [&amp;_th]:px-3 [&amp;_th]:py-2 [&amp;_th]:text-start [&amp;_th]:font-semibold [&amp;_th]:text-(--color-fg) [&amp;_td]:border-b [&amp;_td]:border-(--color-border) [&amp;_td]:px-3 [&amp;_td]:py-2 [&amp;_kbd]:rounded [&amp;_kbd]:border-hairline [&amp;_kbd]:border-(--color-border) [&amp;_kbd]:bg-(--color-surface-secondary) [&amp;_kbd]:px-1.5 [&amp;_kbd]:py-0.5 [&amp;_kbd]:text-caption [&amp;_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">mdx-body-content</div></div></article></div></div></div></main>"`,
101
101
  );
102
102
  });
103
103
 
@@ -68,7 +68,7 @@ describe("case-studies/page — snapshot regression", () => {
68
68
  });
69
69
  const { container } = render(element);
70
70
  expect(container.innerHTML).toMatchInlineSnapshot(
71
- `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Case Studies","description":"Real-world accounts of teams using AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</script><main id="main"><h1>Case Studies</h1><p data-testid="case-studies-description">Real-world accounts of teams using AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">mdx-body-content</div></article></div></div></main>"`,
71
+ `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Case Studies","description":"Real-world accounts of teams using AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</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">Case Studies</h1><p data-testid="case-studies-description" class="mt-block-tight text-lead">Real-world accounts of teams using 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 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-fg) text-body-sm leading-relaxed [&amp;>*:first-child]:mt-0 [&amp;_h2]:mt-block-loose [&amp;_h2]:scroll-mt-24 [&amp;_h2]:text-heading-3 [&amp;_h2]:font-display [&amp;_h2]:text-(--color-fg) [&amp;_h3]:mt-block-loose [&amp;_h3]:scroll-mt-24 [&amp;_h3]:text-heading-4 [&amp;_h3]:font-display [&amp;_h3]:text-(--color-fg) [&amp;_h4]:mt-block-tight [&amp;_h4]:scroll-mt-24 [&amp;_h4]:text-heading-4 [&amp;_h4]:font-display [&amp;_h4]:text-(--color-fg) [&amp;_h5]:mt-block-tight [&amp;_h5]:scroll-mt-24 [&amp;_h5]:text-heading-4 [&amp;_h5]:font-display [&amp;_h5]:text-(--color-fg) [&amp;_h6]:mt-block-tight [&amp;_h6]:scroll-mt-24 [&amp;_h6]:text-heading-4 [&amp;_h6]:font-display [&amp;_h6]:text-(--color-fg) [&amp;_p]:mt-paragraph [&amp;_strong]:font-semibold [&amp;_strong]:text-(--color-fg) [&amp;_em]:italic [&amp;_ul]:mt-paragraph [&amp;_ul]:list-disc [&amp;_ul]:ps-6 [&amp;_ol]:mt-paragraph [&amp;_ol]:list-decimal [&amp;_ol]:ps-6 [&amp;_li]:mt-1 [&amp;_li]:ps-1 [&amp;_li]:marker:text-(--color-text-muted) [&amp;_li>ul]:mt-1 [&amp;_li>ol]:mt-1 [&amp;_blockquote]:mt-paragraph [&amp;_blockquote]:border-s-4 [&amp;_blockquote]:border-(--color-border-strong) [&amp;_blockquote]:ps-4 [&amp;_blockquote]:italic [&amp;_blockquote]:text-(--color-text-muted) [&amp;_hr]:my-block-loose [&amp;_hr]:border-(--color-border) [&amp;_a]:text-(--color-accent) [&amp;_a]:underline [&amp;_a]:underline-offset-4 [&amp;_:not(pre)>code]:rounded [&amp;_:not(pre)>code]:bg-(--color-surface-secondary) [&amp;_:not(pre)>code]:px-1.5 [&amp;_:not(pre)>code]:py-0.5 [&amp;_:not(pre)>code]:text-code [&amp;_:not(pre)>code]:font-mono [&amp;_:not(pre)>code]:text-(--color-fg) [&amp;_pre]:mt-paragraph [&amp;_pre]:overflow-auto [&amp;_pre]:rounded-md [&amp;_pre]:border-hairline [&amp;_pre]:border-(--color-border) [&amp;_pre]:bg-(--color-bg) [&amp;_pre]:p-4 [&amp;_pre]:font-mono [&amp;_pre]:text-body-sm [&amp;_img]:mt-paragraph [&amp;_img]:h-auto [&amp;_img]:max-w-full [&amp;_img]:rounded-lg [&amp;_table]:mt-paragraph [&amp;_table]:w-full [&amp;_table]:border-collapse [&amp;_table]:text-body-sm [&amp;_th]:border-b [&amp;_th]:border-(--color-border) [&amp;_th]:bg-(--color-surface-secondary) [&amp;_th]:px-3 [&amp;_th]:py-2 [&amp;_th]:text-start [&amp;_th]:font-semibold [&amp;_th]:text-(--color-fg) [&amp;_td]:border-b [&amp;_td]:border-(--color-border) [&amp;_td]:px-3 [&amp;_td]:py-2 [&amp;_kbd]:rounded [&amp;_kbd]:border-hairline [&amp;_kbd]:border-(--color-border) [&amp;_kbd]:bg-(--color-surface-secondary) [&amp;_kbd]:px-1.5 [&amp;_kbd]:py-0.5 [&amp;_kbd]:text-caption [&amp;_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">mdx-body-content</div></div></article></div></div></div></main>"`,
72
72
  );
73
73
  });
74
74
  });
@@ -93,7 +93,7 @@ describe("case-studies/[slug]/page — snapshot regression", () => {
93
93
  });
94
94
  const { container } = render(element);
95
95
  expect(container.innerHTML).toMatchInlineSnapshot(
96
- `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Case Studies","item":"http://localhost:3000/en/case-studies"},{"@type":"ListItem","position":3,"name":"AILK AEO Rollout","item":"http://localhost:3000/en/case-studies/ailk-aeo-rollout"}]}]}</script><main id="main"><h1>AILK AEO Rollout</h1><p data-testid="ailk-aeo-rollout-description">How a team shipped AEO with AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">mdx-body-content</div></article></div></div></main>"`,
96
+ `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Case Studies","item":"http://localhost:3000/en/case-studies"},{"@type":"ListItem","position":3,"name":"AILK AEO Rollout","item":"http://localhost:3000/en/case-studies/ailk-aeo-rollout"}]}]}</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">AILK AEO Rollout</h1><p data-testid="ailk-aeo-rollout-description" class="mt-block-tight text-lead">How a team shipped AEO with 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 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-fg) text-body-sm leading-relaxed [&amp;>*:first-child]:mt-0 [&amp;_h2]:mt-block-loose [&amp;_h2]:scroll-mt-24 [&amp;_h2]:text-heading-3 [&amp;_h2]:font-display [&amp;_h2]:text-(--color-fg) [&amp;_h3]:mt-block-loose [&amp;_h3]:scroll-mt-24 [&amp;_h3]:text-heading-4 [&amp;_h3]:font-display [&amp;_h3]:text-(--color-fg) [&amp;_h4]:mt-block-tight [&amp;_h4]:scroll-mt-24 [&amp;_h4]:text-heading-4 [&amp;_h4]:font-display [&amp;_h4]:text-(--color-fg) [&amp;_h5]:mt-block-tight [&amp;_h5]:scroll-mt-24 [&amp;_h5]:text-heading-4 [&amp;_h5]:font-display [&amp;_h5]:text-(--color-fg) [&amp;_h6]:mt-block-tight [&amp;_h6]:scroll-mt-24 [&amp;_h6]:text-heading-4 [&amp;_h6]:font-display [&amp;_h6]:text-(--color-fg) [&amp;_p]:mt-paragraph [&amp;_strong]:font-semibold [&amp;_strong]:text-(--color-fg) [&amp;_em]:italic [&amp;_ul]:mt-paragraph [&amp;_ul]:list-disc [&amp;_ul]:ps-6 [&amp;_ol]:mt-paragraph [&amp;_ol]:list-decimal [&amp;_ol]:ps-6 [&amp;_li]:mt-1 [&amp;_li]:ps-1 [&amp;_li]:marker:text-(--color-text-muted) [&amp;_li>ul]:mt-1 [&amp;_li>ol]:mt-1 [&amp;_blockquote]:mt-paragraph [&amp;_blockquote]:border-s-4 [&amp;_blockquote]:border-(--color-border-strong) [&amp;_blockquote]:ps-4 [&amp;_blockquote]:italic [&amp;_blockquote]:text-(--color-text-muted) [&amp;_hr]:my-block-loose [&amp;_hr]:border-(--color-border) [&amp;_a]:text-(--color-accent) [&amp;_a]:underline [&amp;_a]:underline-offset-4 [&amp;_:not(pre)>code]:rounded [&amp;_:not(pre)>code]:bg-(--color-surface-secondary) [&amp;_:not(pre)>code]:px-1.5 [&amp;_:not(pre)>code]:py-0.5 [&amp;_:not(pre)>code]:text-code [&amp;_:not(pre)>code]:font-mono [&amp;_:not(pre)>code]:text-(--color-fg) [&amp;_pre]:mt-paragraph [&amp;_pre]:overflow-auto [&amp;_pre]:rounded-md [&amp;_pre]:border-hairline [&amp;_pre]:border-(--color-border) [&amp;_pre]:bg-(--color-bg) [&amp;_pre]:p-4 [&amp;_pre]:font-mono [&amp;_pre]:text-body-sm [&amp;_img]:mt-paragraph [&amp;_img]:h-auto [&amp;_img]:max-w-full [&amp;_img]:rounded-lg [&amp;_table]:mt-paragraph [&amp;_table]:w-full [&amp;_table]:border-collapse [&amp;_table]:text-body-sm [&amp;_th]:border-b [&amp;_th]:border-(--color-border) [&amp;_th]:bg-(--color-surface-secondary) [&amp;_th]:px-3 [&amp;_th]:py-2 [&amp;_th]:text-start [&amp;_th]:font-semibold [&amp;_th]:text-(--color-fg) [&amp;_td]:border-b [&amp;_td]:border-(--color-border) [&amp;_td]:px-3 [&amp;_td]:py-2 [&amp;_kbd]:rounded [&amp;_kbd]:border-hairline [&amp;_kbd]:border-(--color-border) [&amp;_kbd]:bg-(--color-surface-secondary) [&amp;_kbd]:px-1.5 [&amp;_kbd]:py-0.5 [&amp;_kbd]:text-caption [&amp;_kbd]:font-mono" data-testid="mdx-content"><div data-testid="mdx-rendered">mdx-body-content</div></div></article></div></div></div></main>"`,
97
97
  );
98
98
  });
99
99
 
@@ -16,6 +16,19 @@ import { render } from "@testing-library/react";
16
16
 
17
17
  // ── @working-theory/ui mock ─────────────────────────────────────────────────────────────
18
18
  jest.mock("@working-theory/ui", () => ({
19
+ // #7745: renderPage's mdx-body branch frames its head and body in the
20
+ // section Container and renders the body through MDXContent + the shared map.
21
+ Container: ({
22
+ children,
23
+ }: {
24
+ children?: React.ReactNode;
25
+ }): React.ReactElement => React.createElement("div", null, children),
26
+ MDXContent: ({
27
+ children,
28
+ }: {
29
+ children?: React.ReactNode;
30
+ }): React.ReactElement => React.createElement(React.Fragment, null, children),
31
+ getMDXComponents: (overrides?: Record<string, unknown>) => ({ ...overrides }),
19
32
  // #4446/#4447: renderPage's own fallback/empty-state title now composes
20
33
  // Heading (mobile type-budget classes, #4417) instead of a bare <h1>.
21
34
  Heading: ({
@@ -66,7 +79,7 @@ describe("ClientsPage — snapshot regression", () => {
66
79
  });
67
80
  const { container } = render(element);
68
81
  expect(container.innerHTML).toMatchInlineSnapshot(
69
- `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Our Clients","description":"Companies that trust us.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Organization"}]}}</script><main id="main"><h1>Our Clients</h1><p data-testid="clients-description">Companies that trust us.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source=""></div></article></div></div></main>"`,
82
+ `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Our Clients","description":"Companies that trust us.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Organization"}]}}</script><main id="main"><div><h1>Our Clients</h1><p data-testid="clients-description" class="mt-block-tight text-lead">Companies that trust us.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&amp;>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source=""></div></article></div></div></div></main>"`,
70
83
  );
71
84
  });
72
85
  });