create-ailk 0.22.0 → 0.23.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/component-catalog.json +149 -5
- package/component-catalog.md +7 -6
- package/dist/add-module.js +11 -9
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +294 -12
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +7 -5
- package/dist/derive-architecture.js +9 -32
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +0 -4
- package/dist/lib/npm-auth.d.ts +9 -0
- package/dist/lib/npm-auth.js +48 -0
- package/dist/lib/scaffold-render-sweep.d.ts +3 -2
- package/dist/lib/scaffold-render-sweep.js +5 -8
- package/dist/module-architecture.js +98 -10
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/spoke-checkout.d.ts +38 -35
- package/dist/spoke-checkout.js +66 -229
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +33 -12
- package/dist/sync-routes.d.ts +38 -1
- package/dist/sync-routes.js +184 -11
- package/dist/template-record.d.ts +1 -1
- package/dist/template-record.js +3 -2
- package/dist/update.js +10 -0
- package/package.json +3 -3
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +4 -49
- package/site-templates/_base/content/en/Pricing/pricing.mdx +3 -21
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +23 -20
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +28 -14
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +15 -39
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +38 -34
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +14 -31
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +26 -25
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +6 -43
- package/templates/.env.example +9 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +27 -0
- package/templates/apps/api/CLAUDE.md +77 -16
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +2 -0
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +45 -0
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +185 -0
- package/templates/apps/api/src/config/index.ts +109 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/stripe.ts +74 -4
- package/templates/apps/api/src/lib/webhook-idempotency.ts +66 -2
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +218 -0
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +174 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/spec.ts +10 -7
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +325 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +1300 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +11 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +49 -0
- package/templates/apps/api/src/routes/checkout/session.ts +57 -25
- package/templates/apps/api/src/routes/checkout/sessions.ts +232 -82
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +5 -5
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +160 -0
- package/templates/apps/api/src/routes/consent/account.ts +4 -2
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +2 -2
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +731 -0
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +753 -0
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +762 -0
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- package/templates/apps/api/src/routes/content/offerings.ts +184 -0
- package/templates/apps/api/src/routes/content/pricing.ts +176 -0
- package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/index.ts +17 -14
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +814 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +80 -68
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +5 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +8 -2
- package/templates/apps/api/src/routes/webhooks/index.ts +23 -0
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +384 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +91 -17
- package/templates/apps/api/src/server.ts +140 -51
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +51 -2
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +234 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +374 -44
- package/templates/apps/api/src/services/__tests__/person.test.ts +232 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +444 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +13 -5
- package/templates/apps/api/src/services/entitlements.ts +157 -1
- package/templates/apps/api/src/services/lead-routing.ts +113 -58
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +295 -0
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +268 -0
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +868 -0
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +416 -0
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +396 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +681 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +308 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +385 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1003 -0
- package/templates/apps/api/src/services/offerings/index.ts +614 -0
- package/templates/apps/api/src/services/offerings/source.ts +140 -0
- package/templates/apps/api/src/services/offerings/store.ts +150 -0
- package/templates/apps/api/src/services/person.ts +124 -9
- package/templates/apps/api/src/services/plan-offerings.ts +96 -30
- package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
- package/templates/apps/mcp/CLAUDE.md +22 -14
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +9 -4
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +358 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +364 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +4 -4
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- package/templates/apps/mcp/__tests__/server.test.ts +13 -11
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +208 -12
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- package/templates/apps/mcp/src/tools/get_offerings.ts +47 -30
- package/templates/apps/mcp/src/tools/get_pricing.ts +40 -40
- package/templates/apps/mcp/src/tools/purchase.ts +30 -2
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +10 -31
- package/templates/apps/web/CLAUDE.md +315 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +4 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +8 -3
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +22 -144
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-hub.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +149 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-application-form.test.ts +22 -24
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +140 -66
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +57 -22
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +119 -11
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +28 -5
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +6 -2
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -92
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +237 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.count.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.deliverable.test.tsx +200 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.ending.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +430 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +21 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +352 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +12 -73
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +10 -31
- package/templates/apps/web/app/[locale]/layout.tsx +55 -4
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +174 -104
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +95 -58
- package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap +7 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +599 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/page.test.tsx +216 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +367 -6
- package/templates/apps/web/app/[locale]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +17 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +62 -3
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +70 -18
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +30 -9
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +316 -0
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +810 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +2 -1
- package/templates/apps/web/lib/__tests__/in-page-magnet.test.ts +265 -0
- package/templates/apps/web/lib/__tests__/magnet-routes.test.ts +57 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +293 -0
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +601 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +67 -8
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +9 -20
- package/templates/apps/web/lib/deck-page.ts +1 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/in-page-magnet.ts +416 -0
- package/templates/apps/web/lib/magnet-routes.ts +42 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +202 -0
- package/templates/apps/web/lib/offerings-pricing.ts +326 -0
- package/templates/apps/web/lib/posting-title.ts +17 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +4 -2
- package/templates/apps/web/messages/en.json +49 -15
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/content/en/BlogList/blog.mdx +4 -71
- package/templates/content/en/BookCall/book-a-call.mdx +1 -41
- package/templates/content/en/BookCall/onsite-estimate.mdx +1 -17
- package/templates/content/en/BookCall/onsite-service.mdx +1 -17
- package/templates/content/en/BookCall/start-project.mdx +1 -17
- package/templates/content/en/ContactPage/contact.mdx +6 -63
- package/templates/content/en/HomePage/home.mdx +3 -21
- package/templates/database/CHANGELOG.md +31 -0
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +161 -0
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +270 -0
- package/templates/database/__tests__/offerings-migration.test.ts +197 -0
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +86 -0
- package/templates/database/__tests__/offerings.db.test.ts +303 -0
- package/templates/database/__tests__/seed.test.ts +31 -1
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +146 -0
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +77 -0
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +45 -0
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +47 -0
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +16 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +30 -2
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +0 -93
- package/templates/apps/web/app/[locale]/downloads/page.tsx +0 -37
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +0 -139
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +0 -93
- package/templates/apps/web/app/api/checkout/[offering]/route.ts +0 -48
- package/templates/apps/web/app/api/checkout/__tests__/route.test.ts +0 -120
- package/templates/apps/web/app/api/checkout/__tests__/success-path.test.ts +0 -86
- package/templates/apps/web/app/api/webhooks/stripe/__tests__/route.test.ts +0 -92
- package/templates/apps/web/app/api/webhooks/stripe/route.ts +0 -31
- package/templates/apps/web/lib/__tests__/checkout.test.ts +0 -243
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/checkout.ts +0 -93
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
|
@@ -33,9 +33,10 @@ jest.mock("next-mdx-remote/rsc", () => ({
|
|
|
33
33
|
React.createElement("div", { "data-testid": "mdx-rendered" }, source),
|
|
34
34
|
}));
|
|
35
35
|
|
|
36
|
-
// ── #8746 — the
|
|
37
|
-
// Stubbed so the migration snapshot stays offline and deterministic
|
|
38
|
-
//
|
|
36
|
+
// ── #8746 — the careers routes once read the API and _site.mdx ────────────────
|
|
37
|
+
// Stubbed so the migration snapshot stays offline and deterministic. Since
|
|
38
|
+
// #8849 neither page reads them (the form is on the apply page, covered by
|
|
39
|
+
// job-apply-page.test.tsx).
|
|
39
40
|
jest.mock("@working-theory/api-client", () => ({
|
|
40
41
|
flows: {
|
|
41
42
|
get: jest.fn().mockResolvedValue({ ok: false, status: 404, body: null }),
|
|
@@ -75,23 +76,62 @@ describe("careers/page — snapshot regression", () => {
|
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
it("populated state renders deterministic HTML", async () => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
// #8849 D10 — sections mode: one generated items section per posting,
|
|
80
|
+
// first, with the search box; the ItemList carries ListItems.
|
|
81
|
+
const getMdxSourcePopulated = jest.fn<
|
|
82
|
+
ReturnType<typeof pageRenderer.getMdxSource>,
|
|
83
|
+
Parameters<typeof pageRenderer.getMdxSource>
|
|
84
|
+
>(async (type) =>
|
|
85
|
+
type === "CareersList"
|
|
86
|
+
? {
|
|
87
|
+
frontmatter: {
|
|
88
|
+
type: "CareersList" as const,
|
|
89
|
+
slug: "careers",
|
|
90
|
+
title: "Careers at AILK",
|
|
91
|
+
description: "Join the team building the AX-first platform.",
|
|
92
|
+
},
|
|
93
|
+
body: "mdx-body-content",
|
|
94
|
+
}
|
|
95
|
+
: {
|
|
96
|
+
frontmatter: {
|
|
97
|
+
type: "JobPosting" as const,
|
|
98
|
+
slug: "senior-engineer",
|
|
99
|
+
title: "Senior Engineer",
|
|
100
|
+
description: "Build the AILK platform.",
|
|
101
|
+
employmentType: "FULL_TIME" as const,
|
|
102
|
+
location: "Remote (US)",
|
|
103
|
+
},
|
|
104
|
+
body: "",
|
|
105
|
+
},
|
|
106
|
+
);
|
|
87
107
|
|
|
88
108
|
const element = await CareersPage({
|
|
89
109
|
params: Promise.resolve({ locale: "en" }),
|
|
90
110
|
getMdxSource: getMdxSourcePopulated,
|
|
111
|
+
getMdxSlugs: jest.fn().mockResolvedValue(["senior-engineer"]),
|
|
91
112
|
});
|
|
92
113
|
const { container } = render(element);
|
|
114
|
+
const jsonLd = JSON.parse(
|
|
115
|
+
container.querySelector("[data-testid='json-ld']")!.textContent!,
|
|
116
|
+
) as { mainEntity: { itemListElement: Record<string, unknown>[] } };
|
|
117
|
+
expect(jsonLd.mainEntity.itemListElement).toEqual([
|
|
118
|
+
{
|
|
119
|
+
"@type": "ListItem",
|
|
120
|
+
position: 1,
|
|
121
|
+
url: "http://localhost:3000/en/careers/senior-engineer",
|
|
122
|
+
item: {
|
|
123
|
+
"@type": "JobPosting",
|
|
124
|
+
name: "Senior Engineer",
|
|
125
|
+
description: "Build the AILK platform.",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
expect(
|
|
130
|
+
container.querySelector("[data-testid='items-filter-search']"),
|
|
131
|
+
).not.toBeNull();
|
|
132
|
+
expect(container.querySelector("[data-testid='mdx-body']")).toBeNull();
|
|
93
133
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
94
|
-
`"<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="
|
|
134
|
+
`"<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":"ListItem","position":1,"url":"http://localhost:3000/en/careers/senior-engineer","item":{"@type":"JobPosting","name":"Senior Engineer","description":"Build the AILK platform."}}]}}</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><div class="ailk-section-renderer" data-testid="section-renderer"><span id="section-open-positions" class="ailk-section-anchor" aria-hidden="true" data-testid="section-anchor"></span><section class="py-section bg-(--color-bg)" data-testid="items" data-section="items"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8"><div class="mb-block-loose" data-testid="section-frame-header"><div><h2 class="text-heading-1 lg:text-display-sm font-display text-fg whitespace-pre-line max-w-3xl">Open positions</h2></div></div><div data-testid="items-filter-island" class="mt-10"><div class="flex flex-col gap-4 md:flex-row md:items-end" data-testid="items-filter"><div class="flex flex-col gap-1.5 md:flex-1"><div class="flex items-center justify-between gap-2"><label class="text-body-sm font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" for="_r_0_-search">Search roles</label></div><input data-testid="items-filter-search" class="flex rounded-control border-hairline text-body-sm text-(--color-fg) file:border-0 file:bg-transparent file:text-body-sm file:font-medium file:text-(--color-fg) placeholder:text-(--color-text-muted) focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 motion-safe:transition-colors bg-(--color-surface-raised) border-(--color-border-strong) hover:enabled:border-transparent hover:enabled:ring-1 hover:enabled:ring-(--color-border-strong) focus-visible:border-transparent focus-visible:ring-(--color-brand) h-(--control-height-default) px-3 py-1.5 w-full" id="_r_0_-search" type="search" value=""></div><div class="flex flex-col gap-1.5 md:w-56"><div class="flex items-center justify-between gap-2"><label class="text-body-sm font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" for="_r_0_-type">Type</label></div><button type="button" role="combobox" aria-expanded="false" aria-autocomplete="none" dir="ltr" data-state="closed" data-testid="items-filter-facet-type" class="flex w-full items-center justify-between rounded-control border-hairline border-(--color-border) bg-(--color-surface-secondary) text-body-sm text-(--color-fg) placeholder:text-(--color-text-muted) focus-visible:outline-none focus-visible:border-transparent focus-visible:ring-1 focus-visible:ring-(--color-accent) hover:enabled:border-transparent hover:enabled:ring-1 hover:enabled:ring-(--color-border) disabled:cursor-not-allowed disabled:opacity-50 h-(--control-height-default) px-3 py-1.5 w-full" id="_r_0_-type"><span style="pointer-events: none;">All types</span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down size-4 opacity-50" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg></button></div><div class="flex flex-col gap-1.5 md:w-56"><div class="flex items-center justify-between gap-2"><label class="text-body-sm font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" for="_r_0_-location">Location</label></div><button type="button" role="combobox" aria-expanded="false" aria-autocomplete="none" dir="ltr" data-state="closed" data-testid="items-filter-facet-location" class="flex w-full items-center justify-between rounded-control border-hairline border-(--color-border) bg-(--color-surface-secondary) text-body-sm text-(--color-fg) placeholder:text-(--color-text-muted) focus-visible:outline-none focus-visible:border-transparent focus-visible:ring-1 focus-visible:ring-(--color-accent) hover:enabled:border-transparent hover:enabled:ring-1 hover:enabled:ring-(--color-border) disabled:cursor-not-allowed disabled:opacity-50 h-(--control-height-default) px-3 py-1.5 w-full" id="_r_0_-location"><span style="pointer-events: none;">All locations</span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down size-4 opacity-50" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg></button></div></div><div role="status" aria-live="polite" class="mt-4 text-body-sm text-muted" data-testid="items-filter-status">1 open position</div><div class="grid gap-grid max-w-2xl justify-items-center mx-auto mt-6" data-testid="items-grid" data-separation="gapped"><div class="contents" data-testid="items-filter-slot"><div class="h-full rounded-card p-card flex flex-col border-hairline border-(--color-border) bg-(--color-surface-raised)" data-testid="card"><span class="text-body font-body leading-normal text-fg mb-1 block text-eyebrow eyebrow-case" style="color: var(--items-item-eyebrow-color, var(--color-brand));">Full-time · Remote (US)</span><h3 class="text-heading-3 font-display text-fg">Senior Engineer</h3><p class="text-body font-body leading-normal text-(--color-text-muted) mt-2 text-sm">Build the AILK platform.</p><div class="mt-auto pt-4" data-testid="card-footer"><a class="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-brand) rounded-sm mt-4 inline-block text-sm font-medium text-brand underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand" href="/careers/senior-engineer" data-testid="items-item-link-0">View role</a></div></div></div></div></div></div></section></div></main>"`,
|
|
95
135
|
);
|
|
96
136
|
});
|
|
97
137
|
});
|
|
@@ -115,15 +155,10 @@ describe("careers/[slug]/page — snapshot regression", () => {
|
|
|
115
155
|
getMdxSource: getMdxSourcePopulated,
|
|
116
156
|
});
|
|
117
157
|
const { container } = render(element);
|
|
118
|
-
// #
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// snapshot keeps pinning renderPage's output byte for byte.
|
|
123
|
-
const pinned = container.cloneNode(true) as HTMLElement;
|
|
124
|
-
pinned.querySelector("#apply")?.remove();
|
|
125
|
-
expect(pinned.innerHTML).toMatchInlineSnapshot(
|
|
126
|
-
`"<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.","potentialAction":{"@type":"ApplyAction","target":"http://localhost:3000/en/careers/senior-engineer#apply"}}},{"@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 motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&>*: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>"`,
|
|
158
|
+
// #8849 D3, D8 — the JobPosting's ApplyAction targets the apply page, and
|
|
159
|
+
// the page passes renderPage an Apply button inside <main>.
|
|
160
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
161
|
+
`"<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.","potentialAction":{"@type":"ApplyAction","target":"http://localhost:3000/en/careers/senior-engineer/apply"}}},{"@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 motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) not-disabled:hover:bg-(--button-hover-quiet-bg) not-disabled:hover:text-(--button-hover-quiet-fg) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:bg-gray-200 not-disabled:active:text-(--color-fg) rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div class="ailk-prose max-w-none text-(--color-text-muted) text-body-sm leading-relaxed [&>*: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><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 pb-section"><a class="inline-flex items-center justify-center gap-2 font-medium motion-safe:transition motion-safe:duration-(--button-hover-duration) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 bg-(--color-brand) text-(--color-fg-on-brand) not-disabled:hover:bg-(--button-hover-bg) not-disabled:hover:text-(--button-hover-fg) not-disabled:hover:outline not-disabled:hover:outline-1 not-disabled:hover:-outline-offset-1 not-disabled:hover:outline-(--button-hover-border) not-disabled:hover:opacity-(--button-hover-opacity) not-disabled:hover:transform-(--button-hover-transform) not-disabled:hover:shadow-(--button-hover-shadow) not-disabled:active:opacity-80 rounded-control px-control-x min-h-(--control-height-big)" href="/careers/senior-engineer/apply" data-testid="job-apply-cta">Apply for this role</a></div></main>"`,
|
|
127
162
|
);
|
|
128
163
|
});
|
|
129
164
|
|
|
@@ -1,13 +1,59 @@
|
|
|
1
|
-
// Careers list page
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// Careers list page (#8849 D10) — the /alternatives pattern. Reads CareersList
|
|
2
|
+
// MDX via the content adapter and puts one generated `items` section first,
|
|
3
|
+
// one card per JobPosting, so the visible card grid and the CollectionPage's
|
|
4
|
+
// ItemList JSON-LD (built from the same sections) always agree. Each card
|
|
5
|
+
// links to /careers/<slug>. The section carries a search box and employment-
|
|
6
|
+
// type and location filters (the Items filter, #8849 D6) whose facets read the
|
|
7
|
+
// postings' first-class `employmentType` and `location` frontmatter, never
|
|
8
|
+
// `schema.mainEntity`. Every posting stays in the server HTML.
|
|
9
|
+
//
|
|
10
|
+
// A missing seed renders the `careers` empty state; a seed with no postings
|
|
11
|
+
// renders its declared sections alone.
|
|
12
|
+
import type {
|
|
13
|
+
getMdxSource as defaultGetMdxSource,
|
|
14
|
+
getMdxSlugs as defaultGetMdxSlugs,
|
|
15
|
+
getPageSections as defaultGetPageSections,
|
|
16
|
+
} from "@working-theory/page-renderer";
|
|
17
|
+
import {
|
|
18
|
+
getMdxSlugs as loadMdxSlugs,
|
|
19
|
+
getMdxSource as loadMdxSource,
|
|
20
|
+
getPageSections as loadPageSections,
|
|
21
|
+
renderPage,
|
|
22
|
+
renderPageMetadata,
|
|
23
|
+
} from "@working-theory/page-renderer";
|
|
5
24
|
import type { Metadata } from "next";
|
|
25
|
+
import { getTranslations } from "next-intl/server";
|
|
6
26
|
import type { ReactElement } from "react";
|
|
7
27
|
|
|
8
28
|
interface Props {
|
|
9
29
|
params: Promise<{ locale: string }>;
|
|
10
30
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
31
|
+
getMdxSlugs?: typeof defaultGetMdxSlugs;
|
|
32
|
+
getPageSections?: typeof defaultGetPageSections;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Section = Awaited<ReturnType<typeof defaultGetPageSections>>[number];
|
|
36
|
+
|
|
37
|
+
/** Each `employmentType` value's `careers` message key. */
|
|
38
|
+
const EMPLOYMENT_TYPE_KEYS = {
|
|
39
|
+
FULL_TIME: "employmentTypeFullTime",
|
|
40
|
+
PART_TIME: "employmentTypePartTime",
|
|
41
|
+
CONTRACTOR: "employmentTypeContractor",
|
|
42
|
+
TEMPORARY: "employmentTypeTemporary",
|
|
43
|
+
INTERN: "employmentTypeIntern",
|
|
44
|
+
VOLUNTEER: "employmentTypeVolunteer",
|
|
45
|
+
PER_DIEM: "employmentTypePerDiem",
|
|
46
|
+
OTHER: "employmentTypeOther",
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
type EmploymentType = keyof typeof EMPLOYMENT_TYPE_KEYS;
|
|
50
|
+
|
|
51
|
+
interface PostingFields {
|
|
52
|
+
title?: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
publishedAt?: string;
|
|
55
|
+
employmentType?: EmploymentType;
|
|
56
|
+
location?: string;
|
|
11
57
|
}
|
|
12
58
|
|
|
13
59
|
export async function generateMetadata({
|
|
@@ -20,14 +66,92 @@ export async function generateMetadata({
|
|
|
20
66
|
export default async function Page({
|
|
21
67
|
params,
|
|
22
68
|
getMdxSource,
|
|
69
|
+
getMdxSlugs,
|
|
70
|
+
getPageSections,
|
|
23
71
|
}: Props): Promise<ReactElement> {
|
|
24
72
|
const { locale } = await params;
|
|
73
|
+
const load = getMdxSource ?? loadMdxSource;
|
|
74
|
+
const listSlugs = getMdxSlugs ?? loadMdxSlugs;
|
|
75
|
+
const loadSections = getPageSections ?? loadPageSections;
|
|
76
|
+
const t = await getTranslations({ locale, namespace: "careers" });
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Put one generated `items` section first — the postings are the page's
|
|
80
|
+
* main content — followed by the declared sections. Absent when there are
|
|
81
|
+
* no postings.
|
|
82
|
+
*/
|
|
83
|
+
async function withPostings(frontmatter: unknown): Promise<Section[]> {
|
|
84
|
+
const declared = await loadSections(frontmatter);
|
|
85
|
+
const slugs = await listSlugs("JobPosting", locale);
|
|
86
|
+
if (slugs.length === 0) return declared;
|
|
87
|
+
|
|
88
|
+
const postings = await Promise.all(
|
|
89
|
+
slugs.map(async (slug) => {
|
|
90
|
+
const source = await load("JobPosting", slug, locale);
|
|
91
|
+
return { slug, fm: (source?.frontmatter ?? {}) as PostingFields };
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
// Newest `publishedAt` first, then by slug; no date sorts last.
|
|
95
|
+
postings.sort(
|
|
96
|
+
(a, b) =>
|
|
97
|
+
(b.fm.publishedAt ?? "").localeCompare(a.fm.publishedAt ?? "") ||
|
|
98
|
+
a.slug.localeCompare(b.slug),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const items = postings.map(({ slug, fm }) => {
|
|
102
|
+
const type =
|
|
103
|
+
fm.employmentType !== undefined
|
|
104
|
+
? t(EMPLOYMENT_TYPE_KEYS[fm.employmentType])
|
|
105
|
+
: undefined;
|
|
106
|
+
const eyebrow = [type, fm.location].filter(Boolean).join(" · ");
|
|
107
|
+
const facets = {
|
|
108
|
+
...(type !== undefined && { type }),
|
|
109
|
+
...(fm.location !== undefined && { location: fm.location }),
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
title: fm.title ?? slug,
|
|
113
|
+
description: fm.description ?? "",
|
|
114
|
+
...(eyebrow !== "" && { eyebrow }),
|
|
115
|
+
link: { label: t("viewRole"), href: `/careers/${slug}` },
|
|
116
|
+
...(Object.keys(facets).length > 0 && { facets }),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const filter = {
|
|
121
|
+
searchLabel: t("filterSearch"),
|
|
122
|
+
facets: [
|
|
123
|
+
{ key: "type", label: t("filterType"), allLabel: t("filterAllTypes") },
|
|
124
|
+
{
|
|
125
|
+
key: "location",
|
|
126
|
+
label: t("filterLocation"),
|
|
127
|
+
allLabel: t("filterAllLocations"),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
emptyText: t("filterEmpty"),
|
|
131
|
+
// The island fills `{count}` in the browser, so the template passes
|
|
132
|
+
// through with its placeholder intact.
|
|
133
|
+
countText: {
|
|
134
|
+
one: t("filterCountOne"),
|
|
135
|
+
other: t("filterCountOther", { count: "{count}" }),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
type: "items" as const,
|
|
142
|
+
id: "open-positions",
|
|
143
|
+
data: { headline: t("openPositions"), items, filter },
|
|
144
|
+
},
|
|
145
|
+
...declared,
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
|
|
25
149
|
return renderPage({
|
|
26
150
|
type: "CareersList",
|
|
27
151
|
slug: "careers",
|
|
28
152
|
locale,
|
|
29
153
|
translationsNamespace: "careers",
|
|
30
|
-
|
|
154
|
+
getPageSections: withPostings,
|
|
31
155
|
...(getMdxSource !== undefined && { getMdxSource }),
|
|
32
156
|
});
|
|
33
157
|
}
|
|
@@ -219,7 +219,7 @@ describe("contact/page — rendering (AC-3)", () => {
|
|
|
219
219
|
expect(mockCaptureStatus).toHaveBeenCalledTimes(1);
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
-
it("links Terms
|
|
222
|
+
it("links Terms and Privacy Policy in the two-link statement (#8847 D2)", async () => {
|
|
223
223
|
mockFlowsGet.mockResolvedValue(served(contactFormFlowConfig));
|
|
224
224
|
|
|
225
225
|
await renderPage();
|
|
@@ -232,10 +232,7 @@ describe("contact/page — rendering (AC-3)", () => {
|
|
|
232
232
|
"href",
|
|
233
233
|
localeHref("en", "/privacy"),
|
|
234
234
|
);
|
|
235
|
-
expect(screen.
|
|
236
|
-
"href",
|
|
237
|
-
localeHref("en", "/working-agreement"),
|
|
238
|
-
);
|
|
235
|
+
expect(screen.queryByTestId("consent-policy-link-working_agreement")).toBeNull();
|
|
239
236
|
});
|
|
240
237
|
|
|
241
238
|
it("a magnet-driven post reaches leads.submit as flow:contact with its subject", async () => {
|
|
@@ -315,7 +312,7 @@ describe("contact/page — fallback (AC-5)", () => {
|
|
|
315
312
|
expect(screen.getByTestId("contact-submit")).toHaveTextContent("Request a demo");
|
|
316
313
|
});
|
|
317
314
|
|
|
318
|
-
it("the built-in form takes its labels from the contact catalog and the
|
|
315
|
+
it("the built-in form takes its labels from the contact catalog and ends on the built-in ending (#8847 D7)", async () => {
|
|
319
316
|
mockFlowsGet.mockResolvedValue({ ok: false, status: 404, body: {} });
|
|
320
317
|
const messages = jest.requireActual("../../../../messages/en.json") as {
|
|
321
318
|
contact: Record<string, string>;
|
|
@@ -332,13 +329,16 @@ describe("contact/page — fallback (AC-5)", () => {
|
|
|
332
329
|
const form = screen.getByTestId("lead-capture-form");
|
|
333
330
|
expect(form).toHaveTextContent(messages.contact.nameLabel!);
|
|
334
331
|
expect(form).toHaveTextContent(messages.contact.messageLabel!);
|
|
335
|
-
expect(screen.getByTestId("contact-submit")).toHaveTextContent(
|
|
332
|
+
expect(screen.getByTestId("contact-submit")).toHaveTextContent(messages.contact.sendMessage!);
|
|
336
333
|
|
|
337
334
|
await tickAndSubmit({ name: "Ada", email: "ada@example.com" });
|
|
338
|
-
await waitFor(() =>
|
|
335
|
+
await waitFor(() =>
|
|
336
|
+
expect(screen.getByTestId("contact-success-headline")).toHaveTextContent("Message sent."),
|
|
337
|
+
);
|
|
338
|
+
expect(document.body).not.toHaveTextContent("Got it, thanks.");
|
|
339
339
|
});
|
|
340
340
|
|
|
341
|
-
it("a magnet's submit label
|
|
341
|
+
it("a magnet's submit label and ending win over the section's copy (#8847 D7)", async () => {
|
|
342
342
|
mockFlowsGet.mockResolvedValue(served(contactFormFlowConfig));
|
|
343
343
|
|
|
344
344
|
await renderPage([
|
|
@@ -349,13 +349,121 @@ describe("contact/page — fallback (AC-5)", () => {
|
|
|
349
349
|
},
|
|
350
350
|
]);
|
|
351
351
|
|
|
352
|
-
const form = screen.getByTestId("lead-capture-form");
|
|
353
352
|
expect(screen.getByTestId("contact-submit")).toHaveTextContent(
|
|
354
353
|
contactFormFromConfig(contactFormFlowConfig, "en")!.submitLabel!,
|
|
355
354
|
);
|
|
356
355
|
|
|
357
356
|
await tickAndSubmit({ name: "Ada", email: "ada@example.com" });
|
|
358
|
-
await waitFor(() =>
|
|
357
|
+
await waitFor(() =>
|
|
358
|
+
expect(screen.getByTestId("contact-success-headline")).toHaveTextContent("Message sent."),
|
|
359
|
+
);
|
|
360
|
+
expect(document.body).not.toHaveTextContent("Got it, thanks.");
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ── #8847 AC-2 — /contact is the magnet ─────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
/** A served Contact form whose every visible fact differs from the default. */
|
|
367
|
+
function sayHelloConfig(): FlowConfig {
|
|
368
|
+
const config = JSON.parse(JSON.stringify(contactFormFlowConfig)) as FlowConfig;
|
|
369
|
+
const step = config.steps[0] as FieldStep;
|
|
370
|
+
step.copy = { title: { en: "Say hello" }, body: { en: "Two days, tops." }, cta: { en: "Send" } };
|
|
371
|
+
step.fields.find((f) => f.name === "terms")!.copy.label = { en: "I agree to the [Terms](terms)." };
|
|
372
|
+
config.ending = { default: { kind: "screen", headline: { en: "Got it, {name}." } } };
|
|
373
|
+
return config;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const COLUMN_SECTION = {
|
|
377
|
+
id: "contact-form",
|
|
378
|
+
type: "contact-form",
|
|
379
|
+
data: { variant: "column", headline: "Old heading", description: "Old line", fields: [] },
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
async function renderColumnPage() {
|
|
383
|
+
const element = await ContactPage({
|
|
384
|
+
params: Promise.resolve({ locale: "en" }),
|
|
385
|
+
getMdxSource: jest.fn().mockResolvedValue({
|
|
386
|
+
...source,
|
|
387
|
+
frontmatter: { ...source.frontmatter, title: "Old title" },
|
|
388
|
+
}),
|
|
389
|
+
getPageSections: jest.fn().mockResolvedValue([COLUMN_SECTION]),
|
|
390
|
+
});
|
|
391
|
+
return render(element);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
describe("contact/page — every visible fact comes from the magnet (#8847 AC-2)", () => {
|
|
395
|
+
it("the H1, the line, the consent and the ending are the served magnet's", async () => {
|
|
396
|
+
mockFlowsGet.mockResolvedValue(served(sayHelloConfig()));
|
|
397
|
+
|
|
398
|
+
await renderColumnPage();
|
|
399
|
+
|
|
400
|
+
const headings = screen.getAllByRole("heading", { level: 1 });
|
|
401
|
+
expect(headings).toHaveLength(1);
|
|
402
|
+
expect(headings[0]).toHaveTextContent("Say hello");
|
|
403
|
+
expect(screen.getByTestId("contact-form-column")).toHaveTextContent("Two days, tops.");
|
|
404
|
+
expect(
|
|
405
|
+
document.getElementById("lead-capture-consent-statement")!.textContent,
|
|
406
|
+
).toBe("I agree to the Terms.");
|
|
407
|
+
expect(screen.getByTestId("consent-policy-link-terms")).toBeInTheDocument();
|
|
408
|
+
for (const old of ["Old heading", "Old line", "Old title"]) {
|
|
409
|
+
expect(document.body).not.toHaveTextContent(old);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
await tickAndSubmit({ name: "Ada", email: "ada@example.com" });
|
|
413
|
+
await waitFor(() =>
|
|
414
|
+
expect(screen.getByTestId("contact-success-headline")).toHaveTextContent("Got it, Ada."),
|
|
415
|
+
);
|
|
416
|
+
const messages = jest.requireActual("../../../../messages/en.json") as {
|
|
417
|
+
contact: Record<string, string>;
|
|
418
|
+
};
|
|
419
|
+
expect(document.body).not.toHaveTextContent(messages.contact.successMessage!);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("generateMetadata's title is the magnet's step title", async () => {
|
|
423
|
+
mockFlowsGet.mockResolvedValue(served(sayHelloConfig()));
|
|
424
|
+
|
|
425
|
+
const metadata = await pageModule.generateMetadata({
|
|
426
|
+
params: Promise.resolve({ locale: "en" }),
|
|
427
|
+
getMdxSource: jest.fn().mockResolvedValue({
|
|
428
|
+
...source,
|
|
429
|
+
frontmatter: { ...source.frontmatter, title: "Old title" },
|
|
430
|
+
}),
|
|
431
|
+
getPageSections: jest.fn().mockResolvedValue([COLUMN_SECTION]),
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(String((metadata.title as { absolute?: string })?.absolute ?? metadata.title)).toContain(
|
|
435
|
+
"Say hello",
|
|
436
|
+
);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("with the hub unreadable, the built-in config's title and two-link statement render", async () => {
|
|
440
|
+
mockFlowsGet.mockRejectedValue(new Error("hub down"));
|
|
441
|
+
|
|
442
|
+
await renderColumnPage();
|
|
443
|
+
|
|
444
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Get in touch");
|
|
445
|
+
expect(
|
|
446
|
+
document.getElementById("lead-capture-consent-statement")!.textContent,
|
|
447
|
+
).toBe("I accept the Terms and Privacy Policy.");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("under the form, 'Prefer email?' links the _site.mdx contact email; absent with none", async () => {
|
|
451
|
+
const { readSiteContactEmail } = jest.requireMock("../site-contact-email") as {
|
|
452
|
+
readSiteContactEmail: jest.Mock;
|
|
453
|
+
};
|
|
454
|
+
mockFlowsGet.mockResolvedValue(served(contactFormFlowConfig));
|
|
455
|
+
readSiteContactEmail.mockReturnValue("hi@x.co");
|
|
456
|
+
|
|
457
|
+
const view = await renderColumnPage();
|
|
458
|
+
|
|
459
|
+
const line = screen.getByTestId("contact-prefer-email");
|
|
460
|
+
expect(line).toHaveTextContent("Prefer email? hi@x.co");
|
|
461
|
+
expect(line.querySelector("a")).toHaveAttribute("href", "mailto:hi@x.co");
|
|
462
|
+
view.unmount();
|
|
463
|
+
|
|
464
|
+
readSiteContactEmail.mockReturnValue(undefined);
|
|
465
|
+
await renderColumnPage();
|
|
466
|
+
expect(screen.queryByTestId("contact-prefer-email")).toBeNull();
|
|
359
467
|
});
|
|
360
468
|
});
|
|
361
469
|
|