create-ailk 0.20.0 → 0.21.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 +21 -1
- package/component-catalog.json +184 -6
- package/component-catalog.md +20 -9
- package/dist/apply-design.d.ts +9 -0
- package/dist/apply-design.js +78 -31
- package/dist/design-home.d.ts +54 -11
- package/dist/design-home.js +160 -22
- package/dist/lib/design-checks.d.ts +6 -1
- package/dist/lib/design-checks.js +23 -2
- package/dist/programmatic.js +44 -20
- package/dist/site-roster.d.ts +4 -2
- package/dist/site-roster.js +7 -5
- package/dist/starter-grid.js +16 -0
- package/dist/surfaces.js +11 -0
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.js +123 -1
- package/dist/update.d.ts +6 -1
- package/dist/update.js +120 -18
- package/package.json +3 -3
- package/site-templates/_base/ailk-template.json +2 -2
- package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
- package/templates/README.md +26 -25
- package/templates/apps/api/CLAUDE.md +7 -3
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
- package/templates/apps/api/src/services/project-listings.ts +6 -1
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +69 -61
- package/templates/apps/mcp/jest.config.cjs +1 -0
- package/templates/apps/web/CLAUDE.md +26 -1
- package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
- 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]/contact/ConversationalIntakeWrapper.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +347 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +73 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -3
- package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +35 -3
- package/templates/apps/web/app/[locale]/contact/page.tsx +81 -9
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +23 -2
- 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 +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +244 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +4 -2
- package/templates/apps/web/lib/deck-page.ts +186 -0
- package/templates/apps/web/messages/en.json +16 -5
- package/templates/database/CHANGELOG.md +9 -0
- package/templates/database/__tests__/seed.test.ts +39 -2
- package/templates/database/jest.config.cjs +1 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +29 -0
- package/templates/tools/jest/pin-timezone.cjs +34 -0
- package/versions.json +34 -34
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contactFormFromConfig — a Contact form magnet's config, as the shared
|
|
3
|
+
* `LeadCaptureForm` block renders it (#8661 D6).
|
|
4
|
+
*
|
|
5
|
+
* Reads the config's first step, which must be a `field` step (a config that
|
|
6
|
+
* opens with any other step is not a contact form). Answers `null` unless that
|
|
7
|
+
* step holds fields named `name` and `email`, because `captureLeadAction` needs both;
|
|
8
|
+
* the page then falls back (section fields, else the built-in config).
|
|
9
|
+
*
|
|
10
|
+
* Each `text`, `email`, `tel`, `url`, `textarea` or `select` field is mapped,
|
|
11
|
+
* in order. `name` and `email` sit side by side (`colSpan: 1`, the built-in
|
|
12
|
+
* pairing); every other field runs full width. The step's first `consent`
|
|
13
|
+
* field is the terms statement (its label), and its `copy.cta` the submit
|
|
14
|
+
* label. Other `consent` fields and `address` fields are not rendered.
|
|
15
|
+
*
|
|
16
|
+
* Copy resolves with the one LocalizedString rule every flow surface shares
|
|
17
|
+
* (`resolveLocalizedString`). Pure: no fetch, no React.
|
|
18
|
+
*/
|
|
19
|
+
import type { ContactFormField } from "@working-theory/ui";
|
|
20
|
+
import { resolveLocalizedString, type FlowConfig } from "@working-theory/validation";
|
|
21
|
+
|
|
22
|
+
export interface ContactFormView {
|
|
23
|
+
fields: ContactFormField[];
|
|
24
|
+
/** The step's first consent field's label — `[Label](key)` tokens intact. */
|
|
25
|
+
consentStatement?: string;
|
|
26
|
+
/** The step's `copy.cta`. */
|
|
27
|
+
submitLabel?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type FieldStep = Extract<FlowConfig["steps"][number], { type: "field" }>;
|
|
31
|
+
type FieldDef = FieldStep["fields"][number];
|
|
32
|
+
|
|
33
|
+
const RENDERED_TYPES = ["text", "email", "tel", "url", "textarea", "select"] as const;
|
|
34
|
+
type RenderedType = (typeof RENDERED_TYPES)[number];
|
|
35
|
+
|
|
36
|
+
function isRendered(type: FieldDef["inputType"]): type is RenderedType {
|
|
37
|
+
return (RENDERED_TYPES as readonly string[]).includes(type);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function contactFormFromConfig(
|
|
41
|
+
config: FlowConfig,
|
|
42
|
+
locale: string,
|
|
43
|
+
): ContactFormView | null {
|
|
44
|
+
const step = config.steps[0];
|
|
45
|
+
if (step?.type !== "field") return null;
|
|
46
|
+
const names = new Set(step.fields.map((f) => f.name));
|
|
47
|
+
if (!names.has("name") || !names.has("email")) return null;
|
|
48
|
+
|
|
49
|
+
const resolve = (value: Record<string, string>): string =>
|
|
50
|
+
resolveLocalizedString(value, locale, config.defaultLocale);
|
|
51
|
+
|
|
52
|
+
const fields: ContactFormField[] = [];
|
|
53
|
+
for (const field of step.fields) {
|
|
54
|
+
if (!isRendered(field.inputType)) continue;
|
|
55
|
+
fields.push({
|
|
56
|
+
name: field.name,
|
|
57
|
+
type: field.inputType,
|
|
58
|
+
label: resolve(field.copy.label),
|
|
59
|
+
required: field.required ?? false,
|
|
60
|
+
...(field.copy.placeholder !== undefined && {
|
|
61
|
+
placeholder: resolve(field.copy.placeholder),
|
|
62
|
+
}),
|
|
63
|
+
...(field.copy.help !== undefined && { helpText: resolve(field.copy.help) }),
|
|
64
|
+
...(field.options !== undefined && {
|
|
65
|
+
options: field.options.map((o) => ({ value: o.value, label: resolve(o.label) })),
|
|
66
|
+
}),
|
|
67
|
+
colSpan: field.name === "name" || field.name === "email" ? 1 : 2,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const consent = step.fields.find((f) => f.inputType === "consent");
|
|
72
|
+
return {
|
|
73
|
+
fields,
|
|
74
|
+
...(consent !== undefined && { consentStatement: resolve(consent.copy.label) }),
|
|
75
|
+
...(step.copy.cta !== undefined && { submitLabel: resolve(step.copy.cta) }),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -20,6 +20,7 @@ type ContactTextKey =
|
|
|
20
20
|
| "successMessage"
|
|
21
21
|
| "genericError"
|
|
22
22
|
| "consentStatement"
|
|
23
|
+
| "consentRequired"
|
|
23
24
|
| "unavailableHeading"
|
|
24
25
|
| "unavailableBody"
|
|
25
26
|
| "unavailableNoEmail";
|
|
@@ -43,6 +44,7 @@ export function contactFormText(t: ContactTranslator): LeadCaptureFormText {
|
|
|
43
44
|
success: t("successMessage"),
|
|
44
45
|
genericError: t("genericError"),
|
|
45
46
|
consentStatement: t("consentStatement"),
|
|
47
|
+
consentRequired: t("consentRequired"),
|
|
46
48
|
unavailableHeading: t("unavailableHeading"),
|
|
47
49
|
unavailableBody: t("unavailableBody", KEEP_EMAIL_PLACEHOLDER),
|
|
48
50
|
unavailableNoEmail: t("unavailableNoEmail"),
|
|
@@ -36,6 +36,15 @@
|
|
|
36
36
|
* (`ailkCaptureLead`). Absent (the real `<form>` never sets it) → defaults
|
|
37
37
|
* to `"web"`, unchanged from before.
|
|
38
38
|
*
|
|
39
|
+
* Source allow-list and terms (#8661 D8): `/contact` stamps a submission it
|
|
40
|
+
* drew from the Contact form magnet with a hidden `source` of
|
|
41
|
+
* `"flow:contact"`, so it lands as an Inbox submission of that magnet. The
|
|
42
|
+
* field is visitor-controlled, so only `"web"`, `"conversational-intake"` and
|
|
43
|
+
* `"flow:contact"` are kept; anything else is sent as `"web"`. And the terms
|
|
44
|
+
* box is checked here, not only by the client: unless the source is the
|
|
45
|
+
* intake (which renders no box), a post without `consent=true` answers a
|
|
46
|
+
* `consent` field error, merged with the schema's, and nothing is sent.
|
|
47
|
+
*
|
|
39
48
|
* Visitor forwarding (#7394): on a spoke whose API is a hub, the call
|
|
40
49
|
* carries the visitor's IP and the spoke's `AILK_VISITOR_FORWARD_KEY`
|
|
41
50
|
* (server-only, never `NEXT_PUBLIC_`) via `visitorForwardHeaders`, so the
|
|
@@ -47,7 +56,11 @@
|
|
|
47
56
|
|
|
48
57
|
import { leads, visitorForwardHeaders } from "@working-theory/api-client";
|
|
49
58
|
import { LEAD_CAPTURE_HONEYPOT, type LeadCaptureFormState } from "@working-theory/ui";
|
|
50
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
CONTACT_FORM_FLOW_SLUG,
|
|
61
|
+
flowLeadSource,
|
|
62
|
+
submitLeadInputSchema,
|
|
63
|
+
} from "@working-theory/validation";
|
|
51
64
|
import { cookies, headers } from "next/headers";
|
|
52
65
|
|
|
53
66
|
import { consentVisitorHeader, readConsentVisitor } from "../../../lib/consent-visitor";
|
|
@@ -65,6 +78,16 @@ const RESERVED_FIELDS = new Set([
|
|
|
65
78
|
"source",
|
|
66
79
|
]);
|
|
67
80
|
const MAX_EXTRA_FIELDS = 20;
|
|
81
|
+
|
|
82
|
+
/** The only sources a form post may stamp (#8661 D8); anything else is `"web"`. */
|
|
83
|
+
const ALLOWED_SOURCES = new Set([
|
|
84
|
+
"web",
|
|
85
|
+
"conversational-intake",
|
|
86
|
+
flowLeadSource(CONTACT_FORM_FLOW_SLUG),
|
|
87
|
+
]);
|
|
88
|
+
const INTAKE_SOURCE = "conversational-intake";
|
|
89
|
+
/** The server's terms-error text. The form block shows its own copy for this key. */
|
|
90
|
+
const CONSENT_REQUIRED = "Please accept the terms to send your message.";
|
|
68
91
|
const MAX_EXTRA_FIELD_LENGTH = 2000;
|
|
69
92
|
|
|
70
93
|
/**
|
|
@@ -112,15 +135,25 @@ export async function captureLeadAction(
|
|
|
112
135
|
phone: formData.get("phone") ?? undefined,
|
|
113
136
|
};
|
|
114
137
|
|
|
138
|
+
const posted = formData.get("source");
|
|
139
|
+
const source =
|
|
140
|
+
typeof posted === "string" && ALLOWED_SOURCES.has(posted) ? posted : "web";
|
|
141
|
+
|
|
142
|
+
const fieldErrors: Record<string, string> = {};
|
|
143
|
+
if (source !== INTAKE_SOURCE && formData.get("consent") !== "true") {
|
|
144
|
+
fieldErrors.consent = CONSENT_REQUIRED;
|
|
145
|
+
}
|
|
146
|
+
|
|
115
147
|
const parsed = submitLeadInputSchema.safeParse(raw);
|
|
116
148
|
if (!parsed.success) {
|
|
117
|
-
const fieldErrors: Record<string, string> = {};
|
|
118
149
|
for (const issue of parsed.error.issues) {
|
|
119
150
|
const field = String(issue.path[0] ?? "form");
|
|
120
151
|
if (!fieldErrors[field]) {
|
|
121
152
|
fieldErrors[field] = issue.message;
|
|
122
153
|
}
|
|
123
154
|
}
|
|
155
|
+
}
|
|
156
|
+
if (!parsed.success || Object.keys(fieldErrors).length > 0) {
|
|
124
157
|
return { kind: "error", fieldErrors };
|
|
125
158
|
}
|
|
126
159
|
|
|
@@ -129,7 +162,6 @@ export async function captureLeadAction(
|
|
|
129
162
|
|
|
130
163
|
const { name, email, message, phone } = parsed.data;
|
|
131
164
|
const siteId = process.env.AILK_SITE_ID ?? "default";
|
|
132
|
-
const source = (formData.get("source") as string | null) || "web";
|
|
133
165
|
const body = {
|
|
134
166
|
siteId,
|
|
135
167
|
source,
|
|
@@ -11,10 +11,27 @@
|
|
|
11
11
|
// API, an older API without the status route, or any other failed read is
|
|
12
12
|
// treated as accepting: the form renders, and its submit reports honestly.
|
|
13
13
|
//
|
|
14
|
+
// The form's fields (#8661 D6), first usable wins: the workspace's Contact
|
|
15
|
+
// form magnet (`flows.get("contact")`, the public flow read — the API resolves
|
|
16
|
+
// the workspace, the site alias keyless and a spoke by its key); else the
|
|
17
|
+
// page's `contact-form` section fields when it declares any; else the
|
|
18
|
+
// built-in form: the seeded magnet's own config, its name, email and message
|
|
19
|
+
// labels from the `contact` catalog. The submit label is the magnet's, else
|
|
20
|
+
// the section's; the success message is the section's. A magnet-driven
|
|
21
|
+
// post is stamped `source: "flow:contact"`, so it is an Inbox submission of
|
|
22
|
+
// the Contact form; the other two post as `web`. The page is rendered on
|
|
23
|
+
// every request, so an Admin edit shows on the next one. The terms statement
|
|
24
|
+
// links Terms, Privacy Policy and Working Agreement from a page-built map —
|
|
25
|
+
// a magnet label can name a link key, never an href.
|
|
26
|
+
//
|
|
27
|
+
// A `contact-form` section in the `column` variant (the minimalism Contact
|
|
28
|
+
// page, #8661 D12) gets the stacked form look and a Send that stays enabled,
|
|
29
|
+
// gating the terms box on submit instead.
|
|
30
|
+
//
|
|
14
31
|
// The chat-style intake (ConversationalIntakeWrapper, #4534) remains
|
|
15
32
|
// available to a site that prefers it. JSON-LD and translations handled by
|
|
16
33
|
// renderPage.
|
|
17
|
-
import { leads } from "@working-theory/api-client";
|
|
34
|
+
import { flows, leads } from "@working-theory/api-client";
|
|
18
35
|
import type {
|
|
19
36
|
getMdxSource as defaultGetMdxSource,
|
|
20
37
|
getPageSections as defaultGetPageSections,
|
|
@@ -29,17 +46,26 @@ import {
|
|
|
29
46
|
ContactFallback,
|
|
30
47
|
type ContactFormField,
|
|
31
48
|
LeadCaptureForm,
|
|
49
|
+
type LeadCaptureFormText,
|
|
32
50
|
localeHref,
|
|
33
51
|
} from "@working-theory/ui";
|
|
52
|
+
import {
|
|
53
|
+
CONTACT_FORM_FLOW_SLUG,
|
|
54
|
+
contactFormFlowConfig,
|
|
55
|
+
flowLeadSource,
|
|
56
|
+
type FlowConfig,
|
|
57
|
+
} from "@working-theory/validation";
|
|
34
58
|
import type { Metadata } from "next";
|
|
35
59
|
import { getTranslations } from "next-intl/server";
|
|
36
60
|
import type { ReactElement } from "react";
|
|
37
61
|
|
|
62
|
+
import { contactFormFromConfig } from "./contact-form-magnet";
|
|
38
63
|
import { contactFormText } from "./contact-form-text";
|
|
39
64
|
import { captureLeadAction } from "./contact.actions";
|
|
40
65
|
import { readSiteContactEmail } from "./site-contact-email";
|
|
41
66
|
|
|
42
|
-
|
|
67
|
+
// #8661 D6 — the Contact form magnet is read on every request.
|
|
68
|
+
export const dynamic = "force-dynamic";
|
|
43
69
|
|
|
44
70
|
interface Props {
|
|
45
71
|
params: Promise<{ locale: string }>;
|
|
@@ -72,6 +98,21 @@ function sectionFields(section: ContactFormSection): ContactFormField[] {
|
|
|
72
98
|
}));
|
|
73
99
|
}
|
|
74
100
|
|
|
101
|
+
const BUILT_IN_LABELS = { name: "nameLabel", email: "emailLabel", message: "messageLabel" } as const;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The built-in form: the seeded magnet's own fields, with the labels the
|
|
105
|
+
* `contact` catalog carries (name, email, message) taken from it, so a
|
|
106
|
+
* translated site keeps its translated form when the magnet is absent.
|
|
107
|
+
*/
|
|
108
|
+
function builtInFields(text: LeadCaptureFormText, locale: string): ContactFormField[] {
|
|
109
|
+
const view = contactFormFromConfig(contactFormFlowConfig, locale);
|
|
110
|
+
return (view?.fields ?? []).map((f) => {
|
|
111
|
+
const key = BUILT_IN_LABELS[f.name as keyof typeof BUILT_IN_LABELS];
|
|
112
|
+
return key ? { ...f, label: text[key] } : f;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
75
116
|
/** The API's capture status, or `null` for any failed read (see header). */
|
|
76
117
|
async function readCaptureStatus(): Promise<CaptureStatus | null> {
|
|
77
118
|
try {
|
|
@@ -82,6 +123,21 @@ async function readCaptureStatus(): Promise<CaptureStatus | null> {
|
|
|
82
123
|
}
|
|
83
124
|
}
|
|
84
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The served Contact form magnet's config, or `null` for any failed read: a
|
|
128
|
+
* non-2xx (absent, draft, a build with no flow route) or a throw. The
|
|
129
|
+
* property access sits inside the `try`, so a client with no `flows` is a
|
|
130
|
+
* failed read too.
|
|
131
|
+
*/
|
|
132
|
+
async function readContactMagnet(): Promise<FlowConfig | null> {
|
|
133
|
+
try {
|
|
134
|
+
const res = await flows.get(CONTACT_FORM_FLOW_SLUG);
|
|
135
|
+
return res.ok ? res.body : null;
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
85
141
|
export async function generateMetadata({
|
|
86
142
|
params,
|
|
87
143
|
}: Pick<Props, "params">): Promise<Metadata> {
|
|
@@ -95,9 +151,10 @@ export default async function Page({
|
|
|
95
151
|
getPageSections,
|
|
96
152
|
}: Props): Promise<ReactElement> {
|
|
97
153
|
const { locale } = await params;
|
|
98
|
-
const [status, t] = await Promise.all([
|
|
154
|
+
const [status, t, magnet] = await Promise.all([
|
|
99
155
|
readCaptureStatus(),
|
|
100
156
|
getTranslations({ locale, namespace: "contact" }),
|
|
157
|
+
readContactMagnet(),
|
|
101
158
|
]);
|
|
102
159
|
const accepting = status?.contact ?? true;
|
|
103
160
|
const contactEmail = readSiteContactEmail();
|
|
@@ -115,17 +172,32 @@ export default async function Page({
|
|
|
115
172
|
formSection?.id ??
|
|
116
173
|
(lead?.type === "hero" && lead.data.variant === "split" ? lead.id : undefined);
|
|
117
174
|
|
|
175
|
+
// #8661 D6 — the magnet's fields, else the section's, else the built-in form.
|
|
176
|
+
const served = magnet ? contactFormFromConfig(magnet, locale) : null;
|
|
177
|
+
const declared = formSection && formSection.data.fields.length > 0 ? formSection : undefined;
|
|
178
|
+
const fields: ContactFormField[] =
|
|
179
|
+
served?.fields ?? (declared ? sectionFields(declared) : builtInFields(text, locale));
|
|
180
|
+
// A magnet's own submit label wins; the section's copy fills what it leaves.
|
|
181
|
+
const submitLabel = served?.submitLabel ?? formSection?.data.submitLabel;
|
|
182
|
+
const successMessage = formSection?.data.successMessage;
|
|
183
|
+
const column = formSection?.data.variant === "column";
|
|
184
|
+
|
|
118
185
|
const surface = accepting ? (
|
|
119
186
|
<LeadCaptureForm
|
|
120
187
|
action={captureLeadAction}
|
|
121
|
-
text={text}
|
|
188
|
+
text={served?.consentStatement ? { ...text, consentStatement: served.consentStatement } : text}
|
|
122
189
|
termsHref={localeHref(locale, "/terms")}
|
|
190
|
+
policies={{
|
|
191
|
+
terms: localeHref(locale, "/terms"),
|
|
192
|
+
privacy: localeHref(locale, "/privacy"),
|
|
193
|
+
working_agreement: localeHref(locale, "/working-agreement"),
|
|
194
|
+
}}
|
|
123
195
|
fallbackEmail={fallbackEmail}
|
|
124
|
-
{
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
})}
|
|
196
|
+
fields={fields}
|
|
197
|
+
submitLabel={submitLabel}
|
|
198
|
+
successMessage={successMessage}
|
|
199
|
+
{...(served && { source: flowLeadSource(CONTACT_FORM_FLOW_SLUG) })}
|
|
200
|
+
{...(column && { look: "column" as const, consentGate: "on-submit" as const })}
|
|
129
201
|
/>
|
|
130
202
|
) : (
|
|
131
203
|
<ContactFallback
|
|
@@ -70,7 +70,7 @@ describe("courses/page — snapshot regression", () => {
|
|
|
70
70
|
});
|
|
71
71
|
const { container } = render(element);
|
|
72
72
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
73
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Courses","description":"Structured learning paths for teams adopting AILK.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Course"}]}}</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">Courses</h1><p data-testid="courses-description" class="mt-block-tight text-lead">Structured learning paths for teams adopting AILK.</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-
|
|
73
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Courses","description":"Structured learning paths for teams adopting AILK.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Course"}]}}</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">Courses</h1><p data-testid="courses-description" class="mt-block-tight text-lead">Structured learning paths for teams adopting AILK.</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-text-muted) 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">## Courses
|
|
74
74
|
|
|
75
75
|
Body content.</div></div></article></div></div></div></main>"
|
|
76
76
|
`);
|
|
@@ -99,7 +99,7 @@ describe("courses/[slug] — snapshot regression", () => {
|
|
|
99
99
|
});
|
|
100
100
|
const { container } = render(element);
|
|
101
101
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
102
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AEO Fundamentals","description":"A practical course covering Layer 1 AEO rules.","mainEntity":{"@context":"https://schema.org","@type":"Course","name":"AEO Fundamentals","description":"A practical course covering Layer 1 AEO rules."}},{"@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":"Courses","item":"http://localhost:3000/en/courses"},{"@type":"ListItem","position":3,"name":"AEO Fundamentals","item":"http://localhost:3000/en/courses/aeo-fundamentals"}]}]}</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">AEO Fundamentals</h1><p data-testid="course-description" class="mt-block-tight text-lead">A practical course covering Layer 1 AEO rules.</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-
|
|
102
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AEO Fundamentals","description":"A practical course covering Layer 1 AEO rules.","mainEntity":{"@context":"https://schema.org","@type":"Course","name":"AEO Fundamentals","description":"A practical course covering Layer 1 AEO rules."}},{"@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":"Courses","item":"http://localhost:3000/en/courses"},{"@type":"ListItem","position":3,"name":"AEO Fundamentals","item":"http://localhost:3000/en/courses/aeo-fundamentals"}]}]}</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">AEO Fundamentals</h1><p data-testid="course-description" class="mt-block-tight text-lead">A practical course covering Layer 1 AEO rules.</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-text-muted) 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">## AEO Fundamentals
|
|
103
103
|
|
|
104
104
|
Body content.</div></div></article></div></div></div></main>"
|
|
105
105
|
`);
|
|
@@ -58,7 +58,7 @@ describe("how-to/[slug] — snapshot regression", () => {
|
|
|
58
58
|
});
|
|
59
59
|
const { container } = render(element);
|
|
60
60
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
61
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Deploy AILK to Vercel","description":"Step-by-step guide to deploying AI Launch Kit on Vercel.","mainEntity":{"@context":"https://schema.org","@type":"HowTo","name":"Deploy AILK to Vercel","description":"Step-by-step guide to deploying AI Launch Kit on Vercel."}},{"@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":"How-To Guides","item":"http://localhost:3000/en/how-to"},{"@type":"ListItem","position":3,"name":"Deploy AILK to Vercel","item":"http://localhost:3000/en/how-to/deploy-ailk-vercel"}]}]}</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">Deploy AILK to Vercel</h1><p data-testid="how-to-description" class="mt-block-tight text-lead">Step-by-step guide to deploying AI Launch Kit on Vercel.</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-
|
|
61
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Deploy AILK to Vercel","description":"Step-by-step guide to deploying AI Launch Kit on Vercel.","mainEntity":{"@context":"https://schema.org","@type":"HowTo","name":"Deploy AILK to Vercel","description":"Step-by-step guide to deploying AI Launch Kit on Vercel."}},{"@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":"How-To Guides","item":"http://localhost:3000/en/how-to"},{"@type":"ListItem","position":3,"name":"Deploy AILK to Vercel","item":"http://localhost:3000/en/how-to/deploy-ailk-vercel"}]}]}</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">Deploy AILK to Vercel</h1><p data-testid="how-to-description" class="mt-block-tight text-lead">Step-by-step guide to deploying AI Launch Kit on Vercel.</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-text-muted) 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">## Steps
|
|
62
62
|
|
|
63
63
|
Body content.</div></div></article></div></div></div></main>"
|
|
64
64
|
`);
|
package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx
CHANGED
|
@@ -87,7 +87,7 @@ describe("IntegrationsPage — snapshot regression", () => {
|
|
|
87
87
|
});
|
|
88
88
|
const { container } = render(element);
|
|
89
89
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
90
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Integrations","description":"First-party integrations for AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"SoftwareApplication"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Integrations</h1><p data-testid="integrations-description" class="mt-block-tight text-lead">First-party integrations for AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium 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-
|
|
90
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Integrations","description":"First-party integrations for AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"SoftwareApplication"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Integrations</h1><p data-testid="integrations-description" class="mt-block-tight text-lead">First-party integrations for AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium 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-text-muted) 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-remote" data-source="
|
|
91
91
|
Body."></div></div></article></div></div></div></main>"
|
|
92
92
|
`);
|
|
93
93
|
});
|
|
@@ -110,7 +110,7 @@ describe("IntegrationSlugPage — snapshot regression", () => {
|
|
|
110
110
|
});
|
|
111
111
|
const { container } = render(element);
|
|
112
112
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
113
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"OpenAI API","description":"Connect AILK to OpenAI.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"OpenAI API","description":"Connect AILK to OpenAI."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Integrations","item":"http://localhost:3000/en/integrations"},{"@type":"ListItem","position":3,"name":"OpenAI API","item":"http://localhost:3000/en/integrations/openai-api"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">OpenAI API</h1><p data-testid="integration-description" class="mt-block-tight text-lead">Connect AILK to OpenAI.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium 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-
|
|
113
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"OpenAI API","description":"Connect AILK to OpenAI.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"OpenAI API","description":"Connect AILK to OpenAI."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Integrations","item":"http://localhost:3000/en/integrations"},{"@type":"ListItem","position":3,"name":"OpenAI API","item":"http://localhost:3000/en/integrations/openai-api"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">OpenAI API</h1><p data-testid="integration-description" class="mt-block-tight text-lead">Connect AILK to OpenAI.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium 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-text-muted) 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-remote" data-source="
|
|
114
114
|
Body."></div></div></article></div></div></div></main>"
|
|
115
115
|
`);
|
|
116
116
|
});
|
|
@@ -60,6 +60,10 @@ import {
|
|
|
60
60
|
} from "../../components/signed-in-nav";
|
|
61
61
|
import { isAccountPath, isAuthPath } from "../../lib/auth-routes";
|
|
62
62
|
import { isClosingFooterPath } from "../../lib/closing-footer-path";
|
|
63
|
+
// #8687 D5 — the per-page deck opt-in. Importing this module registers the
|
|
64
|
+
// deck-page seam with page-renderer at module load (D3), the same
|
|
65
|
+
// precedent as `../../lib/site-identity` and `../../lib/listings` below.
|
|
66
|
+
import { pageOptsIntoDeck } from "../../lib/deck-page";
|
|
63
67
|
import { assertEnabledLocale } from "../../lib/locale-guard";
|
|
64
68
|
import { footerColumns, navItems } from "../../lib/nav-config";
|
|
65
69
|
import { getOgImageUrl } from "../../lib/og";
|
|
@@ -527,6 +531,16 @@ export default async function LocaleLayout({
|
|
|
527
531
|
// fail-safe direction: an absent `x-pathname` keeps the full frame.
|
|
528
532
|
const isAccountRoute = isAccountPath(pathname, enabledLocales);
|
|
529
533
|
|
|
534
|
+
// #8687 D5 — a page whose frontmatter says `layout: deck` presents as the
|
|
535
|
+
// deck home does, without the home's landing (no deckHero, no bare bar).
|
|
536
|
+
// Never the home itself, and never on the auth or account branches, which
|
|
537
|
+
// render their own shell above and never reach MarketingShell at all.
|
|
538
|
+
const isDeckPage =
|
|
539
|
+
!isDeckHome &&
|
|
540
|
+
!isAuthRoute &&
|
|
541
|
+
!isAccountRoute &&
|
|
542
|
+
(await pageOptsIntoDeck({ pathname, locale }));
|
|
543
|
+
|
|
530
544
|
// Every Nav prop that is safe to serialize — shared verbatim by the
|
|
531
545
|
// server-rendered Nav (signed out) and `SignedInNav` (signed in), which adds
|
|
532
546
|
// `navItems` and `renderFlowTrigger` on its own side of the RSC boundary.
|
|
@@ -550,6 +564,10 @@ export default async function LocaleLayout({
|
|
|
550
564
|
// Nav spreads its `full-logo` composition after `logoDisplay`'s.
|
|
551
565
|
...(siteChrome.nav === "mark-only" ? { logoDisplay: "mark" as const } : {}),
|
|
552
566
|
...(isDeckHome ? { sticky: true, deckMorph: true, bar: "bare" as const } : {}),
|
|
567
|
+
// #8687 D5 — a deck page's header is pinned, as the deck home's is: under
|
|
568
|
+
// snap an unpinned bar scrolls away with slide 1 (#5697). It keeps its
|
|
569
|
+
// own actions, mark and bar style — only `sticky` is added.
|
|
570
|
+
...(isDeckPage ? { sticky: true } : {}),
|
|
553
571
|
siteName: SITE_NAME,
|
|
554
572
|
frame: "contained",
|
|
555
573
|
navAlignment: "right",
|
|
@@ -760,10 +778,13 @@ export default async function LocaleLayout({
|
|
|
760
778
|
),
|
|
761
779
|
}
|
|
762
780
|
: {})}
|
|
763
|
-
{...(isDeckHome
|
|
781
|
+
{...(isDeckHome || isDeckPage
|
|
764
782
|
? {
|
|
765
783
|
snap: "mandatory" as const,
|
|
766
|
-
|
|
784
|
+
// #8687 D5/C3 — the home's brand landing stays home-only;
|
|
785
|
+
// an opted-in page gets the deck's mechanics, never the
|
|
786
|
+
// wordmark-in-the-headline treatment.
|
|
787
|
+
...(isDeckHome ? { deckHero: true } : {}),
|
|
767
788
|
// #7740 D8 — the design's deck presentation dial.
|
|
768
789
|
...(chrome.deckPresentation === true ? { presentation: true } : {}),
|
|
769
790
|
// The deck's slide-advance controls (#7732) — named from
|