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.
- package/README.md +12 -4
- package/component-catalog.json +247 -33
- package/component-catalog.md +10 -6
- package/dist/apply-design.d.ts +21 -2
- package/dist/apply-design.js +120 -19
- package/dist/cli.js +1 -0
- package/dist/design-compile.d.ts +70 -2
- package/dist/design-compile.js +97 -1
- package/dist/design-home.d.ts +74 -0
- package/dist/design-home.js +256 -0
- package/dist/design-package.d.ts +39 -2
- package/dist/design-package.js +223 -9
- package/dist/design-presets.d.ts +17 -1
- package/dist/design-presets.js +17 -5
- package/dist/design-scaffold.d.ts +6 -25
- package/dist/design-scaffold.js +26 -74
- package/dist/index.d.ts +1 -0
- package/dist/lib/design-checks.d.ts +12 -1
- package/dist/lib/design-checks.js +38 -2
- package/dist/lib/module-license-gate.d.ts +9 -0
- package/dist/lib/module-license-gate.js +18 -0
- package/dist/module-architecture.js +27 -0
- package/dist/programmatic.js +70 -26
- package/dist/site-chrome.d.ts +38 -0
- package/dist/site-chrome.js +109 -2
- package/dist/site-fonts.d.ts +21 -0
- package/dist/site-fonts.js +54 -0
- package/dist/site-roster.d.ts +16 -0
- package/dist/site-roster.js +81 -0
- package/dist/starter-grid.js +15 -0
- package/dist/surfaces.js +12 -0
- package/dist/team-profiles.d.ts +29 -0
- package/dist/team-profiles.js +43 -0
- package/package.json +3 -3
- package/release-scope.json +1 -1
- package/site-templates/_base/content/en/TeamList/team.mdx +68 -0
- package/site-templates/_base/content/en/TeamMember/first-member.mdx +22 -0
- package/site-templates/_base/content/en/TeamMember/second-member.mdx +22 -0
- package/site-templates/_base/content/en/TeamMember/third-member.mdx +22 -0
- package/site-templates/designs/minimalism/DESIGN.md +164 -34
- package/site-templates/designs/minimalism/home/local-business-service.mdx +119 -0
- package/site-templates/designs/minimalism/home/organization-product.mdx +132 -0
- package/site-templates/designs/minimalism/home/organization-service.mdx +131 -0
- package/site-templates/designs/minimalism/home/person-service.mdx +101 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +20 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +22 -1
- package/site-templates/marketing-site-free/ailk-template.json +1 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -7
- package/site-templates/shapes/local-business-service/ailk-template.json +1 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +34 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -7
- package/site-templates/shapes/organization-service/ailk-template.json +1 -1
- package/templates/.env.example +23 -9
- package/templates/README.md +2 -0
- package/templates/apps/api/CLAUDE.md +24 -17
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +98 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +321 -0
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +117 -0
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +39 -14
- package/templates/apps/api/src/config/index.ts +35 -20
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +3 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +14 -0
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +468 -0
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +32 -2
- package/templates/apps/api/src/lib/agent-store.ts +521 -0
- package/templates/apps/api/src/lib/spoke-store.ts +13 -2
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +384 -1
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +61 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +176 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +76 -0
- package/templates/apps/api/src/middleware/auth.ts +219 -5
- package/templates/apps/api/src/middleware/default-tenant.ts +29 -8
- package/templates/apps/api/src/middleware/rate-limit.ts +35 -13
- package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +76 -15
- package/templates/apps/api/src/routes/leads/index.ts +14 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -6
- package/templates/apps/api/src/routes/project-listings/index.ts +14 -9
- package/templates/apps/api/src/routes/project-listings/me.ts +4 -3
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +7 -5
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +198 -0
- package/templates/apps/api/src/server.ts +27 -0
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +374 -57
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +25 -2
- package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +303 -17
- package/templates/apps/api/src/services/audit-log.ts +278 -47
- package/templates/apps/api/src/services/default-tenant.ts +55 -5
- package/templates/apps/api/src/services/leads-inbox.ts +12 -21
- package/templates/apps/api/src/services/project-listings.ts +1 -21
- package/templates/apps/api/src/services/public-site-resolver.ts +103 -13
- package/templates/apps/mcp/CLAUDE.md +44 -5
- package/templates/apps/mcp/__tests__/parity.test.ts +53 -1
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +364 -0
- package/templates/apps/mcp/package.json +1 -1
- package/templates/apps/mcp/src/http-transport.ts +107 -1
- package/templates/apps/mcp/src/workspace-server.ts +193 -0
- package/templates/apps/web/.env.example +64 -37
- package/templates/apps/web/CLAUDE.md +11 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +10 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/__tests__/theme-modes.test.ts +42 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/theme-modes.ts +8 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +14 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/site-contact-lines.test.ts +69 -0
- package/templates/apps/web/app/[locale]/contact/site-contact-lines.ts +64 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +15 -3
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +17 -6
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +14 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +4 -6
- package/templates/apps/web/app/[locale]/layout.tsx +156 -53
- package/templates/apps/web/app/[locale]/page.tsx +37 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +4 -6
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +15 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +43 -6
- package/templates/apps/web/app/[locale]/services/page.tsx +14 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/team/[slug]/page.tsx +45 -0
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +155 -0
- package/templates/apps/web/app/[locale]/team/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +14 -1
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/not-found.tsx +47 -0
- package/templates/apps/web/app/v1/[...path]/route.ts +16 -0
- package/templates/apps/web/lib/__tests__/profile-back-link.test.ts +95 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +515 -0
- package/templates/apps/web/lib/__tests__/site-theme-head.test.ts +70 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +64 -1
- package/templates/apps/web/lib/profile-back-link.ts +51 -0
- package/templates/apps/web/lib/relay.ts +279 -0
- package/templates/apps/web/lib/site-chrome.ts +12 -0
- package/templates/apps/web/lib/site-fonts.ts +15 -0
- package/templates/apps/web/lib/site-logo.tsx +39 -1
- package/templates/apps/web/lib/site-theme-head.ts +33 -0
- package/templates/apps/web/lib/site-theme.ts +13 -1
- package/templates/apps/web/messages/en.json +7 -1
- package/templates/apps/web/middleware.ts +5 -4
- package/templates/apps/web/next.config.mjs +6 -0
- package/templates/database/CHANGELOG.md +22 -0
- package/templates/database/__tests__/agents.db.test.ts +235 -0
- package/templates/database/auth/schema.prisma +61 -0
- package/templates/database/migrations/20261005120000_agents/migration.sql +84 -0
- package/templates/database/package.json +1 -1
- package/versions.json +35 -35
- package/site-templates/designs/minimalism/sample/README.md +0 -48
- package/site-templates/designs/minimalism/sample/content/en/HomePage/home.mdx +0 -244
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-1.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-2.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-3.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-4.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-5.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/avatar-placeholder-6.svg +0 -7
- package/site-templates/designs/minimalism/sample/public/hero-backdrop.svg +0 -17
- package/site-templates/designs/minimalism/sample/public/logo-placeholder-1.svg +0 -6
- package/site-templates/designs/minimalism/sample/public/logo-placeholder-2.svg +0 -6
- package/site-templates/designs/minimalism/sample/public/logo-placeholder-3.svg +0 -6
- 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
|
-
#
|
|
29
|
-
# =============================================================================
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
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.
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
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
|
|
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
|
-
#
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
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 = {
|
package/templates/apps/web/app/[locale]/(authed)/account/settings/__tests__/theme-modes.test.ts
ADDED
|
@@ -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 [&>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 [&>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 [&>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 [&>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 [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_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 [&>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 [&>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 [&>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 [&>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 [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_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
|
|
package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx
CHANGED
|
@@ -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 [&>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 [&>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 [&>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 [&>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 [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_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 [&>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 [&>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 [&>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 [&>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 [&>*:first-child]:mt-0 [&_h2]:mt-block-loose [&_h2]:scroll-mt-24 [&_h2]:text-heading-3 [&_h2]:font-display [&_h2]:text-(--color-fg) [&_h3]:mt-block-loose [&_h3]:scroll-mt-24 [&_h3]:text-heading-4 [&_h3]:font-display [&_h3]:text-(--color-fg) [&_h4]:mt-block-tight [&_h4]:scroll-mt-24 [&_h4]:text-heading-4 [&_h4]:font-display [&_h4]:text-(--color-fg) [&_h5]:mt-block-tight [&_h5]:scroll-mt-24 [&_h5]:text-heading-4 [&_h5]:font-display [&_h5]:text-(--color-fg) [&_h6]:mt-block-tight [&_h6]:scroll-mt-24 [&_h6]:text-heading-4 [&_h6]:font-display [&_h6]:text-(--color-fg) [&_p]:mt-paragraph [&_strong]:font-semibold [&_strong]:text-(--color-fg) [&_em]:italic [&_ul]:mt-paragraph [&_ul]:list-disc [&_ul]:ps-6 [&_ol]:mt-paragraph [&_ol]:list-decimal [&_ol]:ps-6 [&_li]:mt-1 [&_li]:ps-1 [&_li]:marker:text-(--color-text-muted) [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_blockquote]:mt-paragraph [&_blockquote]:border-s-4 [&_blockquote]:border-(--color-border-strong) [&_blockquote]:ps-4 [&_blockquote]:italic [&_blockquote]:text-(--color-text-muted) [&_hr]:my-block-loose [&_hr]:border-(--color-border) [&_a]:text-(--color-accent) [&_a]:underline [&_a]:underline-offset-4 [&_:not(pre)>code]:rounded [&_:not(pre)>code]:bg-(--color-surface-secondary) [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:text-code [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-(--color-fg) [&_pre]:mt-paragraph [&_pre]:overflow-auto [&_pre]:rounded-md [&_pre]:border-hairline [&_pre]:border-(--color-border) [&_pre]:bg-(--color-bg) [&_pre]:p-4 [&_pre]:font-mono [&_pre]:text-body-sm [&_img]:mt-paragraph [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-lg [&_table]:mt-paragraph [&_table]:w-full [&_table]:border-collapse [&_table]:text-body-sm [&_th]:border-b [&_th]:border-(--color-border) [&_th]:bg-(--color-surface-secondary) [&_th]:px-3 [&_th]:py-2 [&_th]:text-start [&_th]:font-semibold [&_th]:text-(--color-fg) [&_td]:border-b [&_td]:border-(--color-border) [&_td]:px-3 [&_td]:py-2 [&_kbd]:rounded [&_kbd]:border-hairline [&_kbd]:border-(--color-border) [&_kbd]:bg-(--color-surface-secondary) [&_kbd]:px-1.5 [&_kbd]:py-0.5 [&_kbd]:text-caption [&_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 [&>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 [&>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 [&>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 [&>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
|
});
|