create-ailk 0.21.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 +186 -8
- package/component-catalog.md +9 -7
- package/dist/add-module.js +20 -14
- 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 +12 -3
- package/dist/derive-architecture.js +20 -8
- package/dist/design-package.js +7 -2
- package/dist/lib/design-checks.js +4 -2
- package/dist/lib/env-catalog.js +5 -5
- 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 +99 -9
- package/dist/paid-sections.d.ts +21 -2
- package/dist/paid-sections.js +69 -7
- package/dist/programmatic.js +19 -5
- package/dist/seed-flows.d.ts +4 -1
- package/dist/seed-flows.js +50 -4
- package/dist/site-chrome.d.ts +8 -0
- package/dist/site-chrome.js +17 -1
- package/dist/spoke-checkout.d.ts +65 -41
- package/dist/spoke-checkout.js +208 -430
- package/dist/starter-grid.d.ts +0 -3
- package/dist/starter-grid.js +24 -14
- package/dist/surfaces.js +52 -26
- package/dist/sync-routes.d.ts +38 -1
- package/dist/sync-routes.js +187 -13
- package/dist/template-record.d.ts +4 -3
- package/dist/template-record.js +8 -4
- package/dist/topology.js +25 -1
- package/dist/update.js +15 -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/designs/minimalism/DESIGN.md +1 -1
- 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 +87 -17
- 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__/consent-store.test.ts +19 -0
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +149 -0
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/consent-store.ts +28 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/flow-completion-store.ts +136 -0
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +38 -2
- package/templates/apps/api/src/lib/proposal-paid-store.ts +21 -4
- 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__/auth.hub-session-path.test.ts +14 -3
- 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/auth.ts +13 -6
- package/templates/apps/api/src/middleware/rate-limit.ts +226 -4
- package/templates/apps/api/src/openapi/spec.ts +33 -3
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +49 -0
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +41 -0
- 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 +386 -0
- 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 +268 -0
- package/templates/apps/api/src/routes/consent/index.ts +15 -7
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +36 -0
- 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/get-page.ts +4 -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/__tests__/magnet-completion.test.ts +515 -0
- package/templates/apps/api/src/routes/leads/index.ts +164 -7
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +649 -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 +245 -36
- package/templates/apps/api/src/server.ts +163 -46
- 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 +1383 -133
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +389 -0
- 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/checkout-buyer.ts +100 -4
- package/templates/apps/api/src/services/consent.ts +95 -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 +431 -47
- package/templates/apps/api/src/services/magnet-completion.ts +242 -0
- 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 +164 -17
- package/templates/apps/api/src/services/plan-offerings.ts +125 -0
- package/templates/apps/api/src/services/public-site-resolver.ts +27 -15
- 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 +24 -31
- package/templates/apps/web/CLAUDE.md +393 -9
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +6 -2
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +5 -4
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +3 -14
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +2 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +5 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +160 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +12 -9
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +105 -2
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +36 -3
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +47 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +60 -22
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +16 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +85 -31
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +18 -4
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +3 -1
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +7 -4
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +227 -207
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +17 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +143 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +385 -89
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +5 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +44 -5
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +68 -34
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +0 -8
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +39 -151
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-gate.ts +61 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +143 -140
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +40 -6
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +68 -54
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +5 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +189 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +295 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/job-application-form.ts +120 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +23 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +319 -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 +141 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +424 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +72 -11
- package/templates/apps/web/app/[locale]/careers/page.tsx +129 -5
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/checkout/__tests__/pages.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +136 -0
- package/templates/apps/web/app/[locale]/checkout/thank-you/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/checkout/unavailable/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +17 -1
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +79 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +160 -13
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +466 -36
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +172 -57
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +26 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +99 -60
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +341 -4
- 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__/FlowStepperClient.invite.test.tsx +257 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/account-invite.actions.test.ts +69 -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]/account-invite.actions.ts +35 -0
- 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]/for/__tests__/page-renderer-migration.test.tsx +1 -1
- 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 +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]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- 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]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/resources/__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]/team/__tests__/team-routes.test.tsx +4 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +89 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +104 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +9 -0
- 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/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +114 -0
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +20 -3
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +44 -1
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +22 -3
- 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/auth.ts +28 -2
- package/templates/apps/web/lib/__tests__/account-invite.test.ts +63 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +27 -1
- package/templates/apps/web/lib/__tests__/consent-policies.test.ts +23 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +7 -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 +101 -25
- package/templates/apps/web/lib/account-invite.ts +37 -0
- package/templates/apps/web/lib/auth-routes.ts +36 -2
- package/templates/apps/web/lib/checkout-config.ts +16 -0
- package/templates/apps/web/lib/consent-policies.ts +22 -0
- package/templates/apps/web/lib/deck-page.ts +7 -0
- package/templates/apps/web/lib/deliverable-grant.ts +18 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +111 -0
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +88 -32
- 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/prisma-for-auth.ts +8 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +449 -0
- package/templates/apps/web/lib/site-chrome.ts +2 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +71 -20
- package/templates/apps/web/middleware.ts +12 -2
- package/templates/apps/web/package.json +1 -0
- package/templates/apps/web/security-headers.config.ts +17 -0
- package/templates/apps/web/src/lib/__tests__/lead-write.test.ts +187 -0
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -0
- package/templates/apps/web/src/lib/lead-write.ts +90 -0
- package/templates/apps/web/src/lib/rate-limit.ts +79 -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 +69 -0
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +79 -0
- package/templates/database/__tests__/flow-completion-migration.test.ts +115 -0
- package/templates/database/__tests__/flow-completion.db.test.ts +136 -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 +64 -1
- package/templates/database/auth/schema.prisma +4 -0
- package/templates/database/config/schema.prisma +91 -0
- package/templates/database/inbox/schema.prisma +17 -2
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +52 -0
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +27 -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 +62 -3
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +0 -125
- 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/lib/__tests__/meeting-slugs.test.ts +0 -120
- package/templates/apps/web/lib/meeting-slugs.ts +0 -63
|
@@ -33,6 +33,26 @@ jest.mock("next-mdx-remote/rsc", () => ({
|
|
|
33
33
|
React.createElement("div", { "data-testid": "mdx-rendered" }, source),
|
|
34
34
|
}));
|
|
35
35
|
|
|
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).
|
|
40
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
41
|
+
flows: {
|
|
42
|
+
get: jest.fn().mockResolvedValue({ ok: false, status: 404, body: null }),
|
|
43
|
+
},
|
|
44
|
+
leads: {
|
|
45
|
+
captureStatus: jest.fn().mockRejectedValue(new Error("offline")),
|
|
46
|
+
submit: jest.fn(),
|
|
47
|
+
},
|
|
48
|
+
visitorForwardHeaders: () => undefined,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// eslint-disable-next-line no-restricted-syntax -- test-seam mock: the real read is a node:fs frontmatter parse of content/_site.mdx; narrowest possible scope
|
|
52
|
+
jest.mock("../../contact/site-contact-email", () => ({
|
|
53
|
+
readSiteContactEmail: jest.fn().mockReturnValue(undefined),
|
|
54
|
+
}));
|
|
55
|
+
|
|
36
56
|
// ── Import page components ─────────────────────────────────────────────────────
|
|
37
57
|
import CareersPage from "../page";
|
|
38
58
|
import CareersSlugPage from "../[slug]/page";
|
|
@@ -56,23 +76,62 @@ describe("careers/page — snapshot regression", () => {
|
|
|
56
76
|
});
|
|
57
77
|
|
|
58
78
|
it("populated state renders deterministic HTML", async () => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
);
|
|
68
107
|
|
|
69
108
|
const element = await CareersPage({
|
|
70
109
|
params: Promise.resolve({ locale: "en" }),
|
|
71
110
|
getMdxSource: getMdxSourcePopulated,
|
|
111
|
+
getMdxSlugs: jest.fn().mockResolvedValue(["senior-engineer"]),
|
|
72
112
|
});
|
|
73
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();
|
|
74
133
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
75
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Careers at AILK","description":"Join the team building the AX-first platform.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"JobPosting"}]}}</script><main id="main"><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>"`,
|
|
76
135
|
);
|
|
77
136
|
});
|
|
78
137
|
});
|
|
@@ -96,8 +155,10 @@ describe("careers/[slug]/page — snapshot regression", () => {
|
|
|
96
155
|
getMdxSource: getMdxSourcePopulated,
|
|
97
156
|
});
|
|
98
157
|
const { container } = render(element);
|
|
158
|
+
// #8849 D3, D8 — the JobPosting's ApplyAction targets the apply page, and
|
|
159
|
+
// the page passes renderPage an Apply button inside <main>.
|
|
99
160
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
100
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Senior Engineer","description":"Build the AILK platform.","mainEntity":{"@context":"https://schema.org","@type":"JobPosting","title":"Senior Engineer","description":"Build the AILK platform."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Careers","item":"http://localhost:3000/en/careers"},{"@type":"ListItem","position":3,"name":"Senior Engineer","item":"http://localhost:3000/en/careers/senior-engineer"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Senior Engineer</h1><p data-testid="senior-engineer-description" class="mt-block-tight text-lead">Build the AILK platform.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-
|
|
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>"`,
|
|
101
162
|
);
|
|
102
163
|
});
|
|
103
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
|
}
|
package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx
CHANGED
|
@@ -68,7 +68,7 @@ describe("case-studies/page — snapshot regression", () => {
|
|
|
68
68
|
});
|
|
69
69
|
const { container } = render(element);
|
|
70
70
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
71
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Case Studies","description":"Real-world accounts of teams using AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Case Studies</h1><p data-testid="case-studies-description" class="mt-block-tight text-lead">Real-world accounts of teams using AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-
|
|
71
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Case Studies","description":"Real-world accounts of teams using AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Case Studies</h1><p data-testid="case-studies-description" class="mt-block-tight text-lead">Real-world accounts of teams using AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium 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>"`,
|
|
72
72
|
);
|
|
73
73
|
});
|
|
74
74
|
});
|
|
@@ -93,7 +93,7 @@ describe("case-studies/[slug]/page — snapshot regression", () => {
|
|
|
93
93
|
});
|
|
94
94
|
const { container } = render(element);
|
|
95
95
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
96
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Case Studies","item":"http://localhost:3000/en/case-studies"},{"@type":"ListItem","position":3,"name":"AILK AEO Rollout","item":"http://localhost:3000/en/case-studies/ailk-aeo-rollout"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">AILK AEO Rollout</h1><p data-testid="ailk-aeo-rollout-description" class="mt-block-tight text-lead">How a team shipped AEO with AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-
|
|
96
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Case Studies","item":"http://localhost:3000/en/case-studies"},{"@type":"ListItem","position":3,"name":"AILK AEO Rollout","item":"http://localhost:3000/en/case-studies/ailk-aeo-rollout"}]}]}</script><main id="main"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8 py-section"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">AILK AEO Rollout</h1><p data-testid="ailk-aeo-rollout-description" class="mt-block-tight text-lead">How a team shipped AEO with AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle mt-block-loose"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium 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>"`,
|
|
97
97
|
);
|
|
98
98
|
});
|
|
99
99
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The standard checkout return pages (#8748 D1, D5, D11; spec
|
|
3
|
+
* docs/specs/8748-a-standard-spoke-checkout-route-spec.md AC-2).
|
|
4
|
+
*
|
|
5
|
+
* Both pages are noindex, render their namespace's copy, read no search
|
|
6
|
+
* parameter and never import the checkout standard. On any build that is
|
|
7
|
+
* not a spoke they call `notFound()` before reading a translation.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { render } from "@testing-library/react";
|
|
13
|
+
|
|
14
|
+
const mockNotFound = jest.fn(() => {
|
|
15
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
16
|
+
});
|
|
17
|
+
jest.mock("next/navigation", () => ({
|
|
18
|
+
__esModule: true,
|
|
19
|
+
notFound: () => mockNotFound(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const mockGetTranslations = jest.fn();
|
|
23
|
+
jest.mock("next-intl/server", () => {
|
|
24
|
+
const actual = jest.requireActual("next-intl/server");
|
|
25
|
+
return {
|
|
26
|
+
...actual,
|
|
27
|
+
getTranslations: (arg: unknown) => {
|
|
28
|
+
mockGetTranslations(arg);
|
|
29
|
+
return actual.getTranslations(arg);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const mockTopology: { value: "hub" | "spoke" } = { value: "spoke" };
|
|
35
|
+
// eslint-disable-next-line no-restricted-syntax -- #507: the topology is a generated constant (#7426), not a runtime input; the seam IS the boundary between a hub and a spoke build, and no env or HTTP surface selects it.
|
|
36
|
+
jest.mock("../../../../lib/topology.generated", () => ({
|
|
37
|
+
__esModule: true,
|
|
38
|
+
get TOPOLOGY() {
|
|
39
|
+
return mockTopology.value;
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
// The thank-you page reads a paid session through the package (#8757 D13);
|
|
44
|
+
// with no `session_id` it never calls it, so a stub is enough here.
|
|
45
|
+
// `virtual`: CI runs this suite before the package's dist is built.
|
|
46
|
+
jest.mock(
|
|
47
|
+
"@working-theory/spoke-checkout",
|
|
48
|
+
() => ({ __esModule: true, readPaidCheckoutEmail: jest.fn(async () => null) }),
|
|
49
|
+
{ virtual: true },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
import messages from "../../../../messages/en.json";
|
|
53
|
+
import ThankYouPage, {
|
|
54
|
+
generateMetadata as thankYouMetadata,
|
|
55
|
+
} from "../thank-you/page";
|
|
56
|
+
import UnavailablePage, {
|
|
57
|
+
generateMetadata as unavailableMetadata,
|
|
58
|
+
} from "../unavailable/page";
|
|
59
|
+
|
|
60
|
+
type PageModule = {
|
|
61
|
+
name: string;
|
|
62
|
+
dir: string;
|
|
63
|
+
namespace: "checkoutThankYou" | "checkoutUnavailable";
|
|
64
|
+
Page: typeof ThankYouPage;
|
|
65
|
+
generateMetadata: typeof thankYouMetadata;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const PAGES: PageModule[] = [
|
|
69
|
+
{
|
|
70
|
+
name: "thank-you",
|
|
71
|
+
dir: "thank-you",
|
|
72
|
+
namespace: "checkoutThankYou",
|
|
73
|
+
Page: ThankYouPage,
|
|
74
|
+
generateMetadata: thankYouMetadata,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "unavailable",
|
|
78
|
+
dir: "unavailable",
|
|
79
|
+
namespace: "checkoutUnavailable",
|
|
80
|
+
Page: UnavailablePage,
|
|
81
|
+
generateMetadata: unavailableMetadata,
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const params = (): Promise<{ locale: string }> =>
|
|
86
|
+
Promise.resolve({ locale: "en" });
|
|
87
|
+
|
|
88
|
+
function source(dir: string): string {
|
|
89
|
+
return readFileSync(join(__dirname, "..", dir, "page.tsx"), "utf8");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
mockNotFound.mockClear();
|
|
94
|
+
mockGetTranslations.mockClear();
|
|
95
|
+
mockTopology.value = "spoke";
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe.each(PAGES)("the checkout $name page (#8748 AC-2)", (page) => {
|
|
99
|
+
it("is noindex", async () => {
|
|
100
|
+
const metadata = await page.generateMetadata({ params: params() });
|
|
101
|
+
expect(metadata.robots).toEqual({ index: false, follow: false });
|
|
102
|
+
expect(metadata.title).toBe(messages[page.namespace].title);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("renders its namespace's title", async () => {
|
|
106
|
+
const { getByText } = render(await page.Page({ params: params() }));
|
|
107
|
+
expect(getByText(messages[page.namespace].title)).toBeTruthy();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("never imports lib/checkout, and only the thank-you page reads searchParams (#8757 D13)", () => {
|
|
111
|
+
const text = source(page.dir);
|
|
112
|
+
expect(text).not.toMatch(/lib\/checkout["']/);
|
|
113
|
+
if (page.dir !== "thank-you") expect(text).not.toContain("searchParams");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("calls notFound() before any translation read when the build is a hub (D11)", async () => {
|
|
117
|
+
mockTopology.value = "hub";
|
|
118
|
+
await expect(page.Page({ params: params() })).rejects.toThrow(
|
|
119
|
+
"NEXT_NOT_FOUND",
|
|
120
|
+
);
|
|
121
|
+
await expect(page.generateMetadata({ params: params() })).rejects.toThrow(
|
|
122
|
+
"NEXT_NOT_FOUND",
|
|
123
|
+
);
|
|
124
|
+
expect(mockNotFound).toHaveBeenCalledTimes(2);
|
|
125
|
+
expect(mockGetTranslations).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("the checkout unavailable page's way out", () => {
|
|
130
|
+
it("links /contact", async () => {
|
|
131
|
+
const { container } = render(await UnavailablePage({ params: params() }));
|
|
132
|
+
const hrefs = [...container.querySelectorAll("a")].map((a) =>
|
|
133
|
+
a.getAttribute("href"),
|
|
134
|
+
);
|
|
135
|
+
expect(hrefs.some((href) => href === "/contact" || href === "/en/contact")).toBe(
|
|
136
|
+
true,
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
});
|