create-ailk 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -14
- package/component-catalog.json +149 -5
- package/component-catalog.md +7 -6
- package/dist/add-module.js +17 -10
- package/dist/add-route.d.ts +55 -1
- package/dist/add-route.js +301 -13
- package/dist/cli.js +27 -10
- package/dist/derive-architecture.d.ts +7 -5
- package/dist/derive-architecture.js +26 -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 +36 -2
- package/dist/lib/scaffold-render-sweep.js +50 -8
- package/dist/module-architecture.js +115 -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 +68 -5
- package/dist/sync-routes.js +455 -29
- 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 +13 -7
- package/templates/.github/workflows/ci.yml +13 -0
- package/templates/CLAUDE.md +4 -2
- package/templates/README.md +1 -0
- package/templates/_gitignore +4 -0
- package/templates/apps/api/.env.example +34 -0
- package/templates/apps/api/CLAUDE.md +94 -19
- 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 +7 -1
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +116 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +16 -0
- package/templates/apps/api/src/lib/error-serializer.ts +74 -7
- package/templates/apps/api/src/lib/site.ts +4 -1
- package/templates/apps/api/src/lib/stripe.ts +77 -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/__tests__/openapi.test.ts +127 -0
- package/templates/apps/api/src/openapi/spec.ts +343 -7
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +332 -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 +1426 -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/get-page.ts +34 -119
- package/templates/apps/api/src/routes/content/index.ts +16 -2
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -36
- 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/search.ts +16 -45
- package/templates/apps/api/src/routes/content/shown-offerings.ts +229 -0
- package/templates/apps/api/src/routes/leads/index.ts +377 -308
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +20 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +17 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/README.md +66 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -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 +6 -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 +93 -17
- package/templates/apps/api/src/server.ts +165 -51
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +121 -4
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +313 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +61 -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 +260 -1
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +450 -0
- package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +202 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +15 -18
- package/templates/apps/api/src/services/abandon-email.ts +28 -5
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +15 -12
- 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 +348 -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 +975 -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 +400 -0
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +685 -0
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +231 -0
- package/templates/apps/api/src/services/offerings/catalog.ts +318 -0
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +403 -0
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +192 -0
- package/templates/apps/api/src/services/offerings/connection.ts +1083 -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 +147 -9
- package/templates/apps/api/src/services/plan-offerings.ts +96 -30
- package/templates/apps/api/src/services/project-listings.ts +43 -31
- package/templates/apps/api/src/services/public-site-resolver.ts +19 -14
- package/templates/apps/mcp/CLAUDE.md +32 -18
- package/templates/apps/mcp/__tests__/app-entry.test.ts +2 -2
- 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 +95 -14
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +8 -3
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -25
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +22 -7
- package/templates/apps/mcp/__tests__/list_pages.test.ts +84 -44
- package/templates/apps/mcp/__tests__/locale.test.ts +41 -56
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +2 -2
- package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +189 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +115 -78
- package/templates/apps/mcp/__tests__/request_callback.test.ts +63 -128
- package/templates/apps/mcp/__tests__/search_content.test.ts +76 -61
- 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__/submit_lead.test.ts +82 -167
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +59 -130
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +11 -11
- package/templates/apps/mcp/package.json +1 -0
- package/templates/apps/mcp/src/abuse/index.ts +18 -11
- package/templates/apps/mcp/src/http-transport.ts +47 -9
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -26
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +5 -4
- package/templates/apps/mcp/src/lib/shown-offerings.ts +105 -0
- package/templates/apps/mcp/src/lib/site-config.ts +53 -0
- package/templates/apps/mcp/src/server.ts +5 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +30 -28
- 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/index.ts +22 -21
- package/templates/apps/mcp/src/tools/list_pages.ts +27 -33
- package/templates/apps/mcp/src/tools/purchase.ts +30 -2
- package/templates/apps/mcp/src/tools/read_content.ts +46 -40
- package/templates/apps/mcp/src/tools/request_callback.ts +21 -28
- package/templates/apps/mcp/src/tools/search_content.ts +20 -24
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +23 -26
- package/templates/apps/mcp/src/workspace-server.ts +15 -0
- package/templates/apps/web/.env.example +10 -31
- package/templates/apps/web/CLAUDE.md +324 -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__/ConversationalIntakeWrapper.test.tsx +1 -1
- 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 +20 -6
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +1 -1
- 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 +369 -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 +64 -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 +44 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/newsletter.actions.ts +205 -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 +51 -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 +63 -1
- package/templates/database/__tests__/trial-magnet-migration.test.ts +186 -0
- 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/migrations/20261025120000_trial_magnet/migration.sql +28 -0
- package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +23 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +57 -2
- package/templates/tools/lint-architecture.ts +205 -1
- package/versions.json +35 -35
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -199
- package/templates/apps/api/src/routes/content/slug.ts +0 -9
- 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
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* magnet-page.tsx — the standard magnet page pair (#8840 D8).
|
|
4
|
+
*
|
|
5
|
+
* Every multi-step magnet is two pages, the canon's #5053 split moved to the
|
|
6
|
+
* magnet's own path: an indexable LANDING (`/<magnet>`, inside the site's
|
|
7
|
+
* chrome — a centred hero of the flow record's `landing` copy, a CTA to
|
|
8
|
+
* `/<magnet>/start`, and the `Assessment` page type's single-layer
|
|
9
|
+
* `WebApplication` JSON-LD) and a `noindex, follow` WALK (`/<magnet>/start`,
|
|
10
|
+
* or the `startPath` a landing names, as `/trial` names `/trial/request` —
|
|
11
|
+
* #9287 DD10; chrome-free by `lib/magnet-routes.ts`, the back arrow alone
|
|
12
|
+
* above step 1).
|
|
13
|
+
* This module is the only code that renders either half: `/flows/<slug>`,
|
|
14
|
+
* the careers apply pages and every pair `create-ailk add-route magnet`
|
|
15
|
+
* writes are callers of the factories below.
|
|
16
|
+
*
|
|
17
|
+
* The read: one `flows.get(slug)` shared per server instance for 15 seconds
|
|
18
|
+
* per slug (the /contact idiom, #8731), so a page's metadata, its body and a
|
|
19
|
+
* burst of views make one hub read. A slug the schema refuses is `absent`
|
|
20
|
+
* with no read and no cache entry, each insert first drops the stale
|
|
21
|
+
* entries, and the map holds at most 256 slugs, so random URLs cannot grow
|
|
22
|
+
* it (SEC-8840-G). A refused
|
|
23
|
+
* connection or a 5xx is `unavailable`: both pages then show the site's
|
|
24
|
+
* contact email instead of a 500, are `noindex`, and retry nothing. A site
|
|
25
|
+
* with no hub to ask — the read answers 503 `flows_not_configured`, or a
|
|
26
|
+
* failed read with no API origin configured — is `not-configured` (#9219);
|
|
27
|
+
* a pair renders it as `unavailable`, and `/reports/<slug>` uses it to 404
|
|
28
|
+
* an unknown slug where a hub outage shows the contact email; a landing that
|
|
29
|
+
* passes `notFoundWhenNotConfigured` (`/trial`, #9423) 404s on it too. A walk
|
|
30
|
+
* that exists apart from its flow (`/careers/apply`) passes a `title`: its
|
|
31
|
+
* fallback then carries that page heading, and an absent flow renders the
|
|
32
|
+
* same fallback rather than a 404 (#9219).
|
|
33
|
+
*
|
|
34
|
+
* A flow whose served config carries a `deliverable` (#8848 D7) has one
|
|
35
|
+
* route, `/reports/<slug>`: both halves redirect there (307) before
|
|
36
|
+
* rendering anything, so `/flows/<slug>` and every `add-route magnet` pair
|
|
37
|
+
* hand it over and an ungated deliverable never gets a form here. The
|
|
38
|
+
* deliverable's own walk calls `renderMagnetStart` with `asDeliverable`.
|
|
39
|
+
*/
|
|
40
|
+
import { flows } from "@working-theory/api-client";
|
|
41
|
+
import { JsonLd } from "@working-theory/jsonld";
|
|
42
|
+
import { flowRouteMetadata } from "@working-theory/metadata";
|
|
43
|
+
import { buildJsonLd } from "@working-theory/schema";
|
|
44
|
+
import { ContactFallback, Container, Heading, localeHref } from "@working-theory/ui";
|
|
45
|
+
import { NavBackLink } from "@working-theory/ui/chrome";
|
|
46
|
+
import { SectionRenderer } from "@working-theory/ui/sections";
|
|
47
|
+
import type { FlowConfig, PageSection } from "@working-theory/validation";
|
|
48
|
+
import { flowSlugSchema, resolveLocalizedString } from "@working-theory/validation";
|
|
49
|
+
import type { Metadata } from "next";
|
|
50
|
+
import { notFound, redirect } from "next/navigation";
|
|
51
|
+
import { getTranslations } from "next-intl/server";
|
|
52
|
+
import type { ReactElement } from "react";
|
|
53
|
+
|
|
54
|
+
import { readSiteContactEmail } from "../../contact/site-contact-email";
|
|
55
|
+
|
|
56
|
+
import { FlowStepperClient } from "./FlowStepperClient";
|
|
57
|
+
|
|
58
|
+
/** The marker every standard magnet page carries in its header comment (D13). */
|
|
59
|
+
export const MAGNET_PAGE_MARKER = "Standard magnet page (#8840)";
|
|
60
|
+
|
|
61
|
+
/** How long one slug's read is shared across requests (/contact's #8731 value). */
|
|
62
|
+
const MAGNET_READ_TTL_MS = 15_000;
|
|
63
|
+
|
|
64
|
+
/** At most this many slugs are held at once; past it a read is not shared. */
|
|
65
|
+
const MAGNET_READ_MAX_ENTRIES = 256;
|
|
66
|
+
|
|
67
|
+
export type MagnetRead =
|
|
68
|
+
| { kind: "ok"; config: FlowConfig }
|
|
69
|
+
| { kind: "absent" }
|
|
70
|
+
| { kind: "unavailable" }
|
|
71
|
+
| { kind: "not-configured" };
|
|
72
|
+
|
|
73
|
+
const magnetReads = new Map<string, { at: number; read: Promise<MagnetRead> }>();
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether an API origin is configured: the three names `getApiBase()` in
|
|
77
|
+
* packages/api-client/src/http.ts reads, in step with it (as `/pricing`
|
|
78
|
+
* checks them). Without one that function answers `localhost:3001`.
|
|
79
|
+
*/
|
|
80
|
+
function apiOriginConfigured(): boolean {
|
|
81
|
+
return Boolean(
|
|
82
|
+
process.env.AILK_API_URL ||
|
|
83
|
+
process.env.NEXT_PUBLIC_AILK_API_URL ||
|
|
84
|
+
process.env.NEXT_PUBLIC_API_URL,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The flows route's own answer when its hub has no database (apps/api `flows_not_configured`). */
|
|
89
|
+
function isFlowsNotConfigured(body: unknown): boolean {
|
|
90
|
+
const error = (body as { error?: { code?: unknown } } | null)?.error;
|
|
91
|
+
return error?.code === "flows_not_configured";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A read that failed: `not-configured` when there is no hub to ask, else `unavailable`. */
|
|
95
|
+
function failedRead(): MagnetRead {
|
|
96
|
+
return apiOriginConfigured() ? { kind: "unavailable" } : { kind: "not-configured" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function fetchMagnet(slug: string): Promise<MagnetRead> {
|
|
100
|
+
try {
|
|
101
|
+
const res = await flows.get(slug);
|
|
102
|
+
if (res.ok) return { kind: "ok", config: res.body };
|
|
103
|
+
if (res.status === 503 && isFlowsNotConfigured(res.body)) return { kind: "not-configured" };
|
|
104
|
+
return res.status >= 500 ? failedRead() : { kind: "absent" };
|
|
105
|
+
} catch {
|
|
106
|
+
return failedRead();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The served flow config for `slug`, shared per instance for 15 seconds. */
|
|
111
|
+
export function readMagnet(slug: string): Promise<MagnetRead> {
|
|
112
|
+
if (!flowSlugSchema.safeParse(slug).success) {
|
|
113
|
+
return Promise.resolve({ kind: "absent" });
|
|
114
|
+
}
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
const hit = magnetReads.get(slug);
|
|
117
|
+
if (hit && now - hit.at < MAGNET_READ_TTL_MS) return hit.read;
|
|
118
|
+
for (const [key, entry] of magnetReads) {
|
|
119
|
+
if (now - entry.at >= MAGNET_READ_TTL_MS) magnetReads.delete(key);
|
|
120
|
+
}
|
|
121
|
+
const read = fetchMagnet(slug);
|
|
122
|
+
if (magnetReads.size < MAGNET_READ_MAX_ENTRIES) magnetReads.set(slug, { at: now, read });
|
|
123
|
+
return read;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The landing's title and line: the record's `landing` copy, else today's. */
|
|
127
|
+
async function landingCopy(
|
|
128
|
+
config: FlowConfig,
|
|
129
|
+
locale: string,
|
|
130
|
+
): Promise<{ title: string; description: string; meta?: string; cta: string }> {
|
|
131
|
+
const t = await getTranslations({ locale, namespace: "flow" });
|
|
132
|
+
const pick = (value: Record<string, string> | undefined): string | undefined =>
|
|
133
|
+
value === undefined ? undefined : resolveLocalizedString(value, locale, config.defaultLocale);
|
|
134
|
+
const { landing } = config;
|
|
135
|
+
const meta = pick(landing?.meta);
|
|
136
|
+
return {
|
|
137
|
+
title: pick(landing?.title) ?? pick(config.steps[0]!.copy.title)!,
|
|
138
|
+
description: pick(landing?.description) ?? t("landingIntro"),
|
|
139
|
+
...(meta !== undefined ? { meta } : {}),
|
|
140
|
+
cta: pick(landing?.cta) ?? t("getStarted"),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The site's contact email in place of a magnet the hub cannot serve (D8).
|
|
146
|
+
* With a `title`, the page's own `<h1>` sits above it (#9219).
|
|
147
|
+
*/
|
|
148
|
+
export async function magnetUnavailable(locale: string, title?: string): Promise<ReactElement> {
|
|
149
|
+
const t = await getTranslations({ locale, namespace: "flow" });
|
|
150
|
+
return (
|
|
151
|
+
<Container className="py-section">
|
|
152
|
+
<div className="mx-auto max-w-container-sm">
|
|
153
|
+
{title !== undefined ? (
|
|
154
|
+
<Heading level={1} className="mb-block-tight">
|
|
155
|
+
{title}
|
|
156
|
+
</Heading>
|
|
157
|
+
) : null}
|
|
158
|
+
<ContactFallback
|
|
159
|
+
email={readSiteContactEmail()}
|
|
160
|
+
text={{
|
|
161
|
+
heading: t("unavailableHeading"),
|
|
162
|
+
body: t("unavailableBody"),
|
|
163
|
+
noEmail: t("unavailableNoEmail"),
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
</Container>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface MagnetLandingInput {
|
|
172
|
+
slug: string;
|
|
173
|
+
/** The landing's own path, e.g. `/diagnostic`; the CTA goes to `<path>/start`. */
|
|
174
|
+
path: string;
|
|
175
|
+
locale: string;
|
|
176
|
+
/** #9287 DD10 — the walk's path when it is not `<path>/start` (`/trial/request`). */
|
|
177
|
+
startPath?: string;
|
|
178
|
+
/**
|
|
179
|
+
* #9423 — a landing that exists only to sell its hub's flow (`/trial`) is a
|
|
180
|
+
* 404 on a site with no hub to ask (`not-configured`), as `/reports/<slug>`
|
|
181
|
+
* is (#9219). A configured hub that cannot answer still shows the contact
|
|
182
|
+
* email.
|
|
183
|
+
*/
|
|
184
|
+
notFoundWhenNotConfigured?: boolean;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The indexable landing of a magnet pair (D8). */
|
|
188
|
+
export async function renderMagnetLanding({
|
|
189
|
+
slug,
|
|
190
|
+
path,
|
|
191
|
+
locale,
|
|
192
|
+
startPath = `${path}/start`,
|
|
193
|
+
notFoundWhenNotConfigured = false,
|
|
194
|
+
}: MagnetLandingInput): Promise<ReactElement> {
|
|
195
|
+
const read = await readMagnet(slug);
|
|
196
|
+
if (read.kind === "absent") notFound();
|
|
197
|
+
if (read.kind === "not-configured" && notFoundWhenNotConfigured) notFound();
|
|
198
|
+
if (read.kind !== "ok") return magnetUnavailable(locale);
|
|
199
|
+
// #8848 D7 — a deliverable's one route.
|
|
200
|
+
if (read.config.deliverable !== undefined) redirect(localeHref(locale, `/reports/${slug}`));
|
|
201
|
+
|
|
202
|
+
const copy = await landingCopy(read.config, locale);
|
|
203
|
+
const sections: PageSection[] = [
|
|
204
|
+
{
|
|
205
|
+
id: "flow-landing-hero",
|
|
206
|
+
type: "hero",
|
|
207
|
+
data: {
|
|
208
|
+
variant: "centered",
|
|
209
|
+
headline: copy.title,
|
|
210
|
+
subheadline: copy.description,
|
|
211
|
+
...(copy.meta !== undefined ? { body: copy.meta } : {}),
|
|
212
|
+
primaryCta: { label: copy.cta, href: localeHref(locale, startPath) },
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<>
|
|
219
|
+
<JsonLd
|
|
220
|
+
schema={buildJsonLd({
|
|
221
|
+
type: "Assessment",
|
|
222
|
+
slug: read.config.slug,
|
|
223
|
+
title: copy.title,
|
|
224
|
+
description: copy.description,
|
|
225
|
+
locale,
|
|
226
|
+
})}
|
|
227
|
+
/>
|
|
228
|
+
<main>
|
|
229
|
+
<SectionRenderer sections={sections} locale={locale} />
|
|
230
|
+
</main>
|
|
231
|
+
</>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The landing's metadata: indexable under the site's own rule; a fallback never is. */
|
|
236
|
+
export async function renderMagnetLandingMetadata({
|
|
237
|
+
slug,
|
|
238
|
+
locale,
|
|
239
|
+
notFoundWhenNotConfigured = false,
|
|
240
|
+
}: Pick<MagnetLandingInput, "slug" | "locale" | "notFoundWhenNotConfigured">): Promise<Metadata> {
|
|
241
|
+
const read = await readMagnet(slug);
|
|
242
|
+
if (read.kind === "absent") return {};
|
|
243
|
+
if (read.kind === "not-configured" && notFoundWhenNotConfigured) return {};
|
|
244
|
+
if (read.kind !== "ok") return flowRouteMetadata();
|
|
245
|
+
const copy = await landingCopy(read.config, locale);
|
|
246
|
+
return { title: copy.title, description: copy.description };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface MagnetStartInput {
|
|
250
|
+
slug: string;
|
|
251
|
+
locale: string;
|
|
252
|
+
/** Where the back arrow goes, unlocalized (the landing, or the posting). */
|
|
253
|
+
backHref: string;
|
|
254
|
+
/** Page-supplied `hidden` field values (a posting's slug as `job`). */
|
|
255
|
+
hiddenValues?: Readonly<Record<string, string>>;
|
|
256
|
+
/** Replaces step 1's title at every locale it holds (a posting's title). */
|
|
257
|
+
firstStepTitle?: string;
|
|
258
|
+
/** #8848 D7 — the deliverable's own walk (`/reports/<slug>/start`): no redirect. */
|
|
259
|
+
asDeliverable?: boolean;
|
|
260
|
+
/**
|
|
261
|
+
* #9219 — a walk that exists apart from its flow (`/careers/apply`): the
|
|
262
|
+
* `<h1>` of its fallback, which an absent flow renders too, never a 404.
|
|
263
|
+
*/
|
|
264
|
+
title?: string;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function withFirstStepTitle(config: FlowConfig, title: string): FlowConfig {
|
|
268
|
+
const [first, ...rest] = config.steps;
|
|
269
|
+
if (!first) return config;
|
|
270
|
+
const titled = Object.fromEntries(Object.keys(first.copy.title).map((key) => [key, title]));
|
|
271
|
+
return { ...config, steps: [{ ...first, copy: { ...first.copy, title: titled } }, ...rest] };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The chrome-free, noindex walk of a magnet pair (D8). */
|
|
275
|
+
export async function renderMagnetStart({
|
|
276
|
+
slug,
|
|
277
|
+
locale,
|
|
278
|
+
backHref,
|
|
279
|
+
hiddenValues,
|
|
280
|
+
firstStepTitle,
|
|
281
|
+
asDeliverable = false,
|
|
282
|
+
title,
|
|
283
|
+
}: MagnetStartInput): Promise<ReactElement> {
|
|
284
|
+
const read = await readMagnet(slug);
|
|
285
|
+
if (read.kind === "absent" && title === undefined) notFound();
|
|
286
|
+
if (read.kind === "ok" && read.config.deliverable !== undefined && !asDeliverable) {
|
|
287
|
+
redirect(localeHref(locale, `/reports/${slug}/start`));
|
|
288
|
+
}
|
|
289
|
+
const t = await getTranslations({ locale, namespace: "flow" });
|
|
290
|
+
const back = <NavBackLink href={localeHref(locale, backHref)} label={t("back")} />;
|
|
291
|
+
if (read.kind !== "ok") {
|
|
292
|
+
return (
|
|
293
|
+
<>
|
|
294
|
+
<div className="px-control-x pt-control-y">{back}</div>
|
|
295
|
+
{await magnetUnavailable(locale, title)}
|
|
296
|
+
</>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const config =
|
|
301
|
+
firstStepTitle !== undefined ? withFirstStepTitle(read.config, firstStepTitle) : read.config;
|
|
302
|
+
return (
|
|
303
|
+
<FlowStepperClient
|
|
304
|
+
config={config}
|
|
305
|
+
locale={locale}
|
|
306
|
+
slug={read.config.slug}
|
|
307
|
+
header={back}
|
|
308
|
+
{...(hiddenValues !== undefined ? { hiddenValues } : {})}
|
|
309
|
+
/>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The walk's metadata: always `noindex, follow` (`flowRouteMetadata`). */
|
|
314
|
+
export async function renderMagnetStartMetadata({
|
|
315
|
+
slug,
|
|
316
|
+
locale,
|
|
317
|
+
firstStepTitle,
|
|
318
|
+
title,
|
|
319
|
+
}: Pick<MagnetStartInput, "slug" | "locale" | "firstStepTitle" | "title">): Promise<Metadata> {
|
|
320
|
+
const read = await readMagnet(slug);
|
|
321
|
+
if (read.kind !== "ok") return flowRouteMetadata(title !== undefined ? { title } : {});
|
|
322
|
+
const firstStep = read.config.steps[0];
|
|
323
|
+
if (!firstStep) return flowRouteMetadata();
|
|
324
|
+
return flowRouteMetadata({
|
|
325
|
+
title:
|
|
326
|
+
firstStepTitle ??
|
|
327
|
+
resolveLocalizedString(firstStep.copy.title, locale, read.config.defaultLocale),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Factories for a pair at a path (D8, D12) ──────────────────────────────
|
|
332
|
+
|
|
333
|
+
/** A magnet pair: the flow it walks, and the landing's own path. */
|
|
334
|
+
export interface MagnetRef {
|
|
335
|
+
slug: string;
|
|
336
|
+
path: string;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
interface MagnetPageProps {
|
|
340
|
+
params: Promise<{ locale: string }>;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function magnetLanding(ref: MagnetRef) {
|
|
344
|
+
return async function MagnetLandingPage({ params }: MagnetPageProps): Promise<ReactElement> {
|
|
345
|
+
const { locale } = await params;
|
|
346
|
+
return renderMagnetLanding({ slug: ref.slug, path: ref.path, locale });
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function magnetLandingMetadata(ref: MagnetRef) {
|
|
351
|
+
return async function generateMetadata({ params }: MagnetPageProps): Promise<Metadata> {
|
|
352
|
+
const { locale } = await params;
|
|
353
|
+
return renderMagnetLandingMetadata({ slug: ref.slug, locale });
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function magnetStart(ref: MagnetRef) {
|
|
358
|
+
return async function MagnetStartPage({ params }: MagnetPageProps): Promise<ReactElement> {
|
|
359
|
+
const { locale } = await params;
|
|
360
|
+
return renderMagnetStart({ slug: ref.slug, locale, backHref: ref.path });
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function magnetStartMetadata(ref: MagnetRef) {
|
|
365
|
+
return async function generateMetadata({ params }: MagnetPageProps): Promise<Metadata> {
|
|
366
|
+
const { locale } = await params;
|
|
367
|
+
return renderMagnetStartMetadata({ slug: ref.slug, locale });
|
|
368
|
+
};
|
|
369
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// oss: false
|
|
2
2
|
/**
|
|
3
|
-
* /[locale]/flows/[slug] — the tool's LANDING route (#5053 D3/D5).
|
|
3
|
+
* /[locale]/flows/[slug] — the tool's LANDING route (#5053 D3/D5). Standard
|
|
4
|
+
* magnet page (#8840): rendered by `magnet-page.tsx`'s `renderMagnetLanding`
|
|
5
|
+
* with the pair's path `/flows/<slug>`.
|
|
4
6
|
*
|
|
5
7
|
* An interactive tool is TWO routes. This is the indexed half: the document
|
|
6
8
|
* that describes the tool, carries its structured data, and links to the
|
|
@@ -18,92 +20,29 @@
|
|
|
18
20
|
* resting on #5019, which names `assessment` and `calculator` among the
|
|
19
21
|
* builders that keep that shape byte for byte).
|
|
20
22
|
*
|
|
21
|
-
* Copy:
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* Same `flows.get` fetch and same 404-on-`!res.ok` as the flow route: a flow
|
|
28
|
-
* that is absent or unconfigured has no landing either. Dynamic rendering
|
|
29
|
-
* for the same reason — the config is DB-backed live data.
|
|
23
|
+
* Copy (#8840 D8): the flow record's `landing` block — its title, line, meta
|
|
24
|
+
* line and button — else the flow's own first-step title and the site's
|
|
25
|
+
* `flow` messages. No consuming site's wording ships here. A flow that is
|
|
26
|
+
* absent has no landing either; a hub that cannot be reached shows the site's
|
|
27
|
+
* contact email. Dynamic rendering — the config is DB-backed live data.
|
|
30
28
|
*/
|
|
31
|
-
import { flows } from "@working-theory/api-client";
|
|
32
|
-
import { JsonLd } from "@working-theory/jsonld";
|
|
33
|
-
import { buildJsonLd } from "@working-theory/schema";
|
|
34
|
-
import { SectionRenderer } from "@working-theory/ui/sections";
|
|
35
|
-
import type { FlowConfig, PageSection } from "@working-theory/validation";
|
|
36
|
-
import { resolveLocalizedString } from "@working-theory/validation";
|
|
37
29
|
import type { Metadata } from "next";
|
|
38
|
-
import { notFound } from "next/navigation";
|
|
39
|
-
import { getTranslations } from "next-intl/server";
|
|
40
30
|
import type { ReactElement } from "react";
|
|
41
31
|
|
|
32
|
+
import { renderMagnetLanding, renderMagnetLandingMetadata } from "./magnet-page";
|
|
33
|
+
|
|
42
34
|
export const dynamic = "force-dynamic";
|
|
43
35
|
|
|
44
36
|
interface Props {
|
|
45
37
|
params: Promise<{ locale: string; slug: string }>;
|
|
46
38
|
}
|
|
47
39
|
|
|
48
|
-
/** The tool's name — the flow's own first-step title in the active locale. */
|
|
49
|
-
function toolName(config: FlowConfig, locale: string): string {
|
|
50
|
-
return resolveLocalizedString(
|
|
51
|
-
config.steps[0]!.copy.title,
|
|
52
|
-
locale,
|
|
53
|
-
config.defaultLocale,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
40
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
58
41
|
const { locale, slug } = await params;
|
|
59
|
-
|
|
60
|
-
if (!res.ok) return {};
|
|
61
|
-
|
|
62
|
-
const t = await getTranslations({ locale, namespace: "flow" });
|
|
63
|
-
return {
|
|
64
|
-
title: toolName(res.body, locale),
|
|
65
|
-
description: t("landingIntro"),
|
|
66
|
-
};
|
|
42
|
+
return renderMagnetLandingMetadata({ slug, locale });
|
|
67
43
|
}
|
|
68
44
|
|
|
69
45
|
export default async function Page({ params }: Props): Promise<ReactElement> {
|
|
70
46
|
const { locale, slug } = await params;
|
|
71
|
-
|
|
72
|
-
if (!res.ok) {
|
|
73
|
-
notFound();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const t = await getTranslations({ locale, namespace: "flow" });
|
|
77
|
-
const name = toolName(res.body, locale);
|
|
78
|
-
const intro = t("landingIntro");
|
|
79
|
-
const startHref = `/flows/${res.body.slug}/start`;
|
|
80
|
-
|
|
81
|
-
const sections: PageSection[] = [
|
|
82
|
-
{
|
|
83
|
-
id: "flow-landing-hero",
|
|
84
|
-
type: "hero",
|
|
85
|
-
data: {
|
|
86
|
-
headline: name,
|
|
87
|
-
subheadline: intro,
|
|
88
|
-
primaryCta: { label: t("getStarted"), href: startHref },
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
];
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<>
|
|
95
|
-
<JsonLd
|
|
96
|
-
schema={buildJsonLd({
|
|
97
|
-
type: "Assessment",
|
|
98
|
-
slug: res.body.slug,
|
|
99
|
-
title: name,
|
|
100
|
-
description: intro,
|
|
101
|
-
locale,
|
|
102
|
-
})}
|
|
103
|
-
/>
|
|
104
|
-
<main>
|
|
105
|
-
<SectionRenderer sections={sections} locale={locale} />
|
|
106
|
-
</main>
|
|
107
|
-
</>
|
|
108
|
-
);
|
|
47
|
+
return renderMagnetLanding({ slug, path: `/flows/${slug}`, locale });
|
|
109
48
|
}
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
// oss: false
|
|
2
2
|
/**
|
|
3
3
|
* /[locale]/flows/[slug]/start — the flow half of the two-route tool
|
|
4
|
-
* (#5053 D3/D5).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* (#5053 D3/D5). Standard magnet page (#8840): rendered by
|
|
5
|
+
* `magnet-page.tsx`'s `renderMagnetStart`, chrome-free with the back arrow to
|
|
6
|
+
* `/flows/<slug>` (`lib/magnet-routes.ts`).
|
|
7
7
|
*
|
|
8
|
-
* `flowRouteMetadata` (`@working-theory/metadata
|
|
9
|
-
* follow` — the whole point of D4 is that this route does
|
|
10
|
-
* robots value, it gets it from what it mounts. `follow`, not
|
|
11
|
-
* the links out of a flow still carry.
|
|
8
|
+
* `flowRouteMetadata` (`@working-theory/metadata`, mounted by the renderer)
|
|
9
|
+
* supplies `noindex, follow` — the whole point of D4 is that this route does
|
|
10
|
+
* NOT write the robots value, it gets it from what it mounts. `follow`, not
|
|
11
|
+
* `nofollow`: the links out of a flow still carry.
|
|
12
12
|
*/
|
|
13
|
-
import { flows } from "@working-theory/api-client";
|
|
14
|
-
import { flowRouteMetadata } from "@working-theory/metadata";
|
|
15
|
-
import { resolveLocalizedString } from "@working-theory/validation";
|
|
16
13
|
import type { Metadata } from "next";
|
|
17
|
-
import { notFound } from "next/navigation";
|
|
18
14
|
import type { ReactElement } from "react";
|
|
19
15
|
|
|
20
|
-
import {
|
|
16
|
+
import { renderMagnetStart, renderMagnetStartMetadata } from "../magnet-page";
|
|
21
17
|
|
|
22
18
|
export const dynamic = "force-dynamic";
|
|
23
19
|
|
|
@@ -27,27 +23,10 @@ interface Props {
|
|
|
27
23
|
|
|
28
24
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
29
25
|
const { locale, slug } = await params;
|
|
30
|
-
|
|
31
|
-
if (!res.ok) return flowRouteMetadata();
|
|
32
|
-
|
|
33
|
-
const firstStep = res.body.steps[0];
|
|
34
|
-
if (!firstStep) return flowRouteMetadata();
|
|
35
|
-
|
|
36
|
-
return flowRouteMetadata({
|
|
37
|
-
title: resolveLocalizedString(
|
|
38
|
-
firstStep.copy.title,
|
|
39
|
-
locale,
|
|
40
|
-
res.body.defaultLocale,
|
|
41
|
-
),
|
|
42
|
-
});
|
|
26
|
+
return renderMagnetStartMetadata({ slug, locale });
|
|
43
27
|
}
|
|
44
28
|
|
|
45
29
|
export default async function Page({ params }: Props): Promise<ReactElement> {
|
|
46
30
|
const { locale, slug } = await params;
|
|
47
|
-
|
|
48
|
-
if (!res.ok) {
|
|
49
|
-
notFound();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return <FlowStepperClient config={res.body} locale={locale} slug={slug} />;
|
|
31
|
+
return renderMagnetStart({ slug, locale, backHref: `/flows/${slug}` });
|
|
53
32
|
}
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
} from "@working-theory/ui/chrome";
|
|
40
40
|
import {
|
|
41
41
|
cookiePurpose,
|
|
42
|
+
NEWSLETTER_FLOW_SLUG,
|
|
42
43
|
resolveConsentStatement,
|
|
43
44
|
SITE_CONSENT_CONFIG_DEFAULTS,
|
|
44
45
|
} from "@working-theory/validation";
|
|
@@ -58,13 +59,21 @@ import {
|
|
|
58
59
|
SignedInNav,
|
|
59
60
|
type SignedInNavProps,
|
|
60
61
|
} from "../../components/signed-in-nav";
|
|
61
|
-
import { isAccountPath, isAuthPath } from "../../lib/auth-routes";
|
|
62
|
+
import { isAccountPath, isAuthPath, isBookingPath } from "../../lib/auth-routes";
|
|
62
63
|
import { isClosingFooterPath } from "../../lib/closing-footer-path";
|
|
64
|
+
import { pageOptsIntoDeck } from "../../lib/deck-page";
|
|
65
|
+
import {
|
|
66
|
+
inPageView,
|
|
67
|
+
newsletterSignup,
|
|
68
|
+
readCaptureStatus,
|
|
69
|
+
readServedMagnet,
|
|
70
|
+
} from "../../lib/in-page-magnet";
|
|
63
71
|
// #8687 D5 — the per-page deck opt-in. Importing this module registers the
|
|
64
72
|
// deck-page seam with page-renderer at module load (D3), the same
|
|
65
73
|
// precedent as `../../lib/site-identity` and `../../lib/listings` below.
|
|
66
|
-
import { pageOptsIntoDeck } from "../../lib/deck-page";
|
|
67
74
|
import { assertEnabledLocale } from "../../lib/locale-guard";
|
|
75
|
+
import { isMagnetWalkPath } from "../../lib/magnet-routes";
|
|
76
|
+
import { SCHEDULING_PAGES } from "../../lib/modules.generated";
|
|
68
77
|
import { footerColumns, navItems } from "../../lib/nav-config";
|
|
69
78
|
import { getOgImageUrl } from "../../lib/og";
|
|
70
79
|
import { backLinkHref } from "../../lib/profile-back-link";
|
|
@@ -268,6 +277,27 @@ interface Props {
|
|
|
268
277
|
params: Promise<{ locale: string }>;
|
|
269
278
|
}
|
|
270
279
|
|
|
280
|
+
/**
|
|
281
|
+
* #8847 D13 — the footer's newsletter signup: the newsletter magnet's compact
|
|
282
|
+
* `NewsletterFormInline`, only when the magnet is served (live), maps as a
|
|
283
|
+
* one-field signup, and the API accepts signups. The seeded built-in config
|
|
284
|
+
* never fills the footer. Both reads are shared for 15 seconds per instance.
|
|
285
|
+
*/
|
|
286
|
+
async function footerNewsletter(locale: string): Promise<ReactElement | undefined> {
|
|
287
|
+
const [magnet, status] = await Promise.all([
|
|
288
|
+
readServedMagnet(NEWSLETTER_FLOW_SLUG),
|
|
289
|
+
readCaptureStatus(),
|
|
290
|
+
]);
|
|
291
|
+
if (magnet === null || status?.newsletter !== true) return undefined;
|
|
292
|
+
const view = inPageView(magnet, locale, "newsletter");
|
|
293
|
+
if (view === null || !("kind" in view.form)) return undefined;
|
|
294
|
+
return (
|
|
295
|
+
<div data-testid="footer-newsletter">
|
|
296
|
+
{newsletterSignup({ ...view, form: view.form }, { locale, compact: true })}
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
271
301
|
export default async function LocaleLayout({
|
|
272
302
|
children,
|
|
273
303
|
params,
|
|
@@ -291,6 +321,8 @@ export default async function LocaleLayout({
|
|
|
291
321
|
// Null when signed out or auth is unset.
|
|
292
322
|
const session = await readSession(await headers());
|
|
293
323
|
|
|
324
|
+
const footerSignup = await footerNewsletter(locale);
|
|
325
|
+
|
|
294
326
|
// Resolve nav labels server-side so Nav + Footer components stay
|
|
295
327
|
// i18n-hook-free and are testable without a NextIntlClientProvider wrapper.
|
|
296
328
|
const tNav = await getTranslations({ locale, namespace: "nav" });
|
|
@@ -531,6 +563,22 @@ export default async function LocaleLayout({
|
|
|
531
563
|
// fail-safe direction: an absent `x-pathname` keeps the full frame.
|
|
532
564
|
const isAccountRoute = isAccountPath(pathname, enabledLocales);
|
|
533
565
|
|
|
566
|
+
// The booking branch (#8843 D10) — the scheduling module's /meet,
|
|
567
|
+
// /meet/<slug> and /private render chrome-free: the page draws its own
|
|
568
|
+
// back link (the index) or none (a meeting page). Only where the module is
|
|
569
|
+
// installed (`SCHEDULING_PAGES`), so a Free site's /meet placeholder keeps
|
|
570
|
+
// the marketing frame; /meet/confirmed keeps it too. The consent Banner
|
|
571
|
+
// stays, as on the auth branch. The same fail-safe direction: an absent
|
|
572
|
+
// `x-pathname` keeps the full frame.
|
|
573
|
+
const isBookingRoute = SCHEDULING_PAGES && isBookingPath(pathname, enabledLocales);
|
|
574
|
+
|
|
575
|
+
// #8840 D10 — a magnet's walk (`/<magnet>/start`, `/careers/apply`,
|
|
576
|
+
// `/careers/<posting>/apply`) renders full-screen: the auth branch's frame,
|
|
577
|
+
// no Nav and no Footer, and it KEEPS the cookie consent banner — a visitor
|
|
578
|
+
// can land straight on a walk, and analytics must not run on a page that
|
|
579
|
+
// never asked (SEC-8840-A). The same fail-safe direction.
|
|
580
|
+
const isMagnetWalkRoute = isMagnetWalkPath(pathname, enabledLocales);
|
|
581
|
+
|
|
534
582
|
// #8687 D5 — a page whose frontmatter says `layout: deck` presents as the
|
|
535
583
|
// deck home does, without the home's landing (no deckHero, no bare bar).
|
|
536
584
|
// Never the home itself, and never on the auth or account branches, which
|
|
@@ -539,6 +587,7 @@ export default async function LocaleLayout({
|
|
|
539
587
|
!isDeckHome &&
|
|
540
588
|
!isAuthRoute &&
|
|
541
589
|
!isAccountRoute &&
|
|
590
|
+
!isMagnetWalkRoute &&
|
|
542
591
|
(await pageOptsIntoDeck({ pathname, locale }));
|
|
543
592
|
|
|
544
593
|
// Every Nav prop that is safe to serialize — shared verbatim by the
|
|
@@ -717,7 +766,7 @@ export default async function LocaleLayout({
|
|
|
717
766
|
* emitted unconditionally in OSS forks; paying customers install
|
|
718
767
|
* @working-theory/branding and gate it in their own layout.
|
|
719
768
|
*/}
|
|
720
|
-
{isAuthRoute || isAccountRoute ? (
|
|
769
|
+
{isAuthRoute || isAccountRoute || isBookingRoute || isMagnetWalkRoute ? (
|
|
721
770
|
/*
|
|
722
771
|
* The (auth) branch (#5963): the five auth paths render their own
|
|
723
772
|
* `(auth)/layout.tsx` — AuthShell — with no Nav and no marketing
|
|
@@ -726,7 +775,8 @@ export default async function LocaleLayout({
|
|
|
726
775
|
* favicons, NextIntlClientProvider, the consent Banner and
|
|
727
776
|
* Telemetry) is shared by both branches and written once. The
|
|
728
777
|
* account paths (#7430) take this branch too, and also skip the
|
|
729
|
-
* consent Banner below.
|
|
778
|
+
* consent Banner below. The booking paths (#8843 D10) and a magnet
|
|
779
|
+
* walk (#8840 D10) take it and keep the Banner.
|
|
730
780
|
*/
|
|
731
781
|
children
|
|
732
782
|
) : (
|
|
@@ -869,6 +919,7 @@ export default async function LocaleLayout({
|
|
|
869
919
|
{...(isMinimalChrome ? { chromeStyle: "minimal" as const } : {})}
|
|
870
920
|
// #7742 D2 — no theme switch under `colorMode: light`.
|
|
871
921
|
showThemeToggle={!pinLight}
|
|
922
|
+
{...(footerSignup !== undefined && { subscribe: footerSignup })}
|
|
872
923
|
bottomBarAlign="content"
|
|
873
924
|
copyrightText={copyrightText}
|
|
874
925
|
legalLinks={legalLinks}
|