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
|
@@ -1,101 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* newsletter/page.tsx —
|
|
3
|
-
* (#6730 D9).
|
|
2
|
+
* newsletter/page.tsx — the newsletter magnet is the only source (#8847 D11,
|
|
3
|
+
* AC-6), and a signup only when there is somewhere to keep it (#6730 D9).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Renders the REAL `NewsletterFormInline` and binds the REAL
|
|
6
|
+
* `subscribeNewsletterAction`. Mocked boundaries: `@working-theory/api-client`
|
|
7
|
+
* (`flows.get`, `flows.submitStep`, `leads.captureStatus`, `leads.subscribe`),
|
|
8
|
+
* `next/headers`, `next-intl/server` (over the real English catalog), the
|
|
9
|
+
* `_site.mdx` contact-email read, and the page-renderer's content loaders
|
|
10
|
+
* (injected).
|
|
9
11
|
*/
|
|
10
12
|
import React from "react";
|
|
11
|
-
import { render, screen } from "@testing-library/react";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
data: { action?: string; primaryCta?: { href: string } };
|
|
26
|
-
}>;
|
|
27
|
-
}) =>
|
|
28
|
-
React.createElement(
|
|
29
|
-
"div",
|
|
30
|
-
{ "data-testid": "section-renderer" },
|
|
31
|
-
sections.map((s) =>
|
|
32
|
-
s.type === "newsletter"
|
|
33
|
-
? React.createElement(
|
|
34
|
-
"form",
|
|
35
|
-
{ key: s.id, "data-section-id": s.id, action: s.data.action },
|
|
36
|
-
"signup",
|
|
37
|
-
)
|
|
38
|
-
: React.createElement(
|
|
39
|
-
"div",
|
|
40
|
-
{ key: s.id, "data-section-id": s.id },
|
|
41
|
-
s.data.primaryCta
|
|
42
|
-
? React.createElement("a", { href: s.data.primaryCta.href }, "cta")
|
|
43
|
-
: null,
|
|
44
|
-
),
|
|
45
|
-
),
|
|
46
|
-
),
|
|
47
|
-
localeHref: (locale: string, path: string) => `/${locale}${path}`,
|
|
48
|
-
LeadCaptureForm: (props: { fields?: Array<{ name: string; type: string }> }) =>
|
|
49
|
-
React.createElement("div", {
|
|
50
|
-
"data-testid": "lead-capture-form",
|
|
51
|
-
"data-fields": (props.fields ?? []).map((f) => `${f.name}:${f.type}`).join(","),
|
|
52
|
-
}),
|
|
13
|
+
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
14
|
+
import { newsletterFlowConfig, type FlowConfig } from "@working-theory/validation";
|
|
15
|
+
|
|
16
|
+
jest.mock("next/navigation", () => ({
|
|
17
|
+
__esModule: true,
|
|
18
|
+
notFound: jest.fn(() => {
|
|
19
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
jest.mock("next-mdx-remote/rsc", () => ({
|
|
24
|
+
__esModule: true,
|
|
25
|
+
MDXRemote: ({ source }: { source: string }) =>
|
|
26
|
+
React.createElement("div", { "data-testid": "mdx-rendered" }, source),
|
|
53
27
|
}));
|
|
54
28
|
|
|
55
29
|
jest.mock("@working-theory/api-client", () => ({
|
|
56
|
-
|
|
30
|
+
flows: { get: jest.fn(), submitStep: jest.fn() },
|
|
31
|
+
leads: { captureStatus: jest.fn(), submit: jest.fn(), subscribe: jest.fn() },
|
|
32
|
+
visitorForwardHeaders: () => undefined,
|
|
57
33
|
}));
|
|
58
34
|
|
|
59
|
-
jest.mock("next
|
|
60
|
-
|
|
35
|
+
jest.mock("next/headers", () => ({
|
|
36
|
+
__esModule: true,
|
|
37
|
+
headers: jest.fn().mockResolvedValue({ get: () => null }),
|
|
38
|
+
cookies: jest.fn().mockResolvedValue({ get: () => undefined }),
|
|
61
39
|
}));
|
|
62
40
|
|
|
63
|
-
//
|
|
64
|
-
jest.mock("
|
|
65
|
-
|
|
41
|
+
// The real English catalog, so the page's own strings render as they ship.
|
|
42
|
+
jest.mock("next-intl/server", () => {
|
|
43
|
+
const messages = jest.requireActual("../../../../messages/en.json") as Record<
|
|
44
|
+
string,
|
|
45
|
+
Record<string, unknown>
|
|
46
|
+
>;
|
|
47
|
+
return {
|
|
48
|
+
getLocale: async () => "en",
|
|
49
|
+
getTranslations: async (arg: string | { namespace: string }) => {
|
|
50
|
+
const ns = messages[typeof arg === "string" ? arg : arg.namespace] ?? {};
|
|
51
|
+
return (key: string, values?: Record<string, string>) =>
|
|
52
|
+
String(ns[key] ?? key).replace(/\{(\w+)\}/g, (m, name: string) => values?.[name] ?? m);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 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
|
|
58
|
+
jest.mock("../../contact/site-contact-email", () => ({
|
|
59
|
+
readSiteContactEmail: jest.fn().mockReturnValue("hi@x.co"),
|
|
66
60
|
}));
|
|
67
61
|
|
|
68
|
-
import { leads } from "@working-theory/api-client";
|
|
62
|
+
import { flows, leads } from "@working-theory/api-client";
|
|
69
63
|
|
|
64
|
+
import * as pageModule from "../page";
|
|
70
65
|
import NewsletterPage from "../page";
|
|
71
66
|
|
|
67
|
+
const mockFlowsGet = flows.get as jest.Mock;
|
|
68
|
+
const mockSubmitStep = flows.submitStep as jest.Mock;
|
|
72
69
|
const mockCaptureStatus = leads.captureStatus as jest.Mock;
|
|
70
|
+
|
|
73
71
|
const status = (newsletter: boolean) => ({
|
|
74
72
|
ok: true,
|
|
75
73
|
status: 200,
|
|
76
74
|
body: { ok: true, data: { contact: true, newsletter } },
|
|
77
75
|
});
|
|
78
76
|
|
|
77
|
+
/** The served magnet: the stored config minus its completion email. */
|
|
78
|
+
function served(config: FlowConfig) {
|
|
79
|
+
const { completionEmail: _omitted, ...body } = config;
|
|
80
|
+
return { ok: true, status: 200, body };
|
|
81
|
+
}
|
|
82
|
+
|
|
79
83
|
const source = {
|
|
80
84
|
frontmatter: {
|
|
81
85
|
type: "NewsletterSignup" as const,
|
|
82
86
|
slug: "newsletter",
|
|
83
|
-
title: "Newsletter",
|
|
87
|
+
title: "{Newsletter}",
|
|
84
88
|
description: "What subscribers get.",
|
|
85
89
|
},
|
|
86
90
|
body: "",
|
|
87
91
|
};
|
|
88
92
|
|
|
89
|
-
/**
|
|
90
|
-
const
|
|
91
|
-
{ id: "
|
|
92
|
-
{ id: "signup", type: "newsletter", data: { action: "/v1/leads/subscribe", source: "newsletter" } },
|
|
93
|
-
{ id: "faq", type: "faq", data: { items: [] } },
|
|
93
|
+
/** The starter's seed (#8847 D14): one column contact-form section. */
|
|
94
|
+
const STARTER_SECTIONS = [
|
|
95
|
+
{ id: "signup", type: "contact-form", data: { variant: "column", fields: [] } },
|
|
94
96
|
];
|
|
95
97
|
|
|
96
|
-
/**
|
|
97
|
-
const
|
|
98
|
-
{ id: "hero", type: "hero", data: { headline: "News" } },
|
|
98
|
+
/** `_base`'s pre-#8847 content: a hero whose CTA points at `#signup`, the signup section, an FAQ. */
|
|
99
|
+
const LEGACY_SECTIONS = [
|
|
100
|
+
{ id: "hero", type: "hero", data: { headline: "News", primaryCta: { label: "Subscribe", href: "#signup" } } },
|
|
101
|
+
{ id: "signup", type: "newsletter", data: { action: "/v1/leads/subscribe", source: "newsletter" } },
|
|
99
102
|
{ id: "faq", type: "faq", data: { items: [] } },
|
|
100
103
|
];
|
|
101
104
|
|
|
@@ -108,59 +111,126 @@ async function renderWith(sections: unknown[]) {
|
|
|
108
111
|
return render(element);
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
|
|
114
|
+
// The page shares its hub reads for a few seconds, per module instance. Each
|
|
115
|
+
// test starts an hour later so none sees another's read.
|
|
116
|
+
let clock = 1_900_000_000_000;
|
|
117
|
+
jest.spyOn(Date, "now").mockImplementation(() => clock);
|
|
112
118
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
jest.clearAllMocks();
|
|
121
|
+
clock += 60 * 60 * 1000;
|
|
122
|
+
mockCaptureStatus.mockResolvedValue(status(true));
|
|
123
|
+
mockFlowsGet.mockResolvedValue(served(newsletterFlowConfig));
|
|
124
|
+
});
|
|
116
125
|
|
|
117
|
-
|
|
126
|
+
describe("NewsletterPage — the newsletter magnet (#8847 AC-6)", () => {
|
|
127
|
+
it("is rendered on every request, never ISR", () => {
|
|
128
|
+
expect(pageModule.dynamic).toBe("force-dynamic");
|
|
129
|
+
expect("revalidate" in pageModule).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("the H1, label, consent and help line are the magnet's; no upsell", async () => {
|
|
133
|
+
await renderWith(STARTER_SECTIONS);
|
|
134
|
+
|
|
135
|
+
expect(mockFlowsGet).toHaveBeenCalledWith("newsletter");
|
|
136
|
+
const headings = screen.getAllByRole("heading", { level: 1 });
|
|
137
|
+
expect(headings).toHaveLength(1);
|
|
138
|
+
expect(headings[0]).toHaveTextContent("Get the newsletter");
|
|
139
|
+
expect(document.body).toHaveTextContent("One email when there is something new.");
|
|
140
|
+
expect(screen.getByText("Email")).not.toHaveClass("sr-only");
|
|
141
|
+
const statement = screen.getByTestId("newsletter-consent-statement");
|
|
142
|
+
expect(statement).toHaveTextContent("I accept the Terms and Privacy Policy.");
|
|
143
|
+
expect(statement.querySelector("a[href='/terms']")).not.toBeNull();
|
|
144
|
+
expect(statement.querySelector("a[href='/privacy']")).not.toBeNull();
|
|
145
|
+
expect(screen.getByTestId("newsletter-help")).toHaveTextContent("No spam. Unsubscribe any time.");
|
|
146
|
+
expect(screen.getByTestId("newsletter-submit")).toHaveTextContent("Subscribe");
|
|
147
|
+
for (const upsell of ["Substack", "Pro", "ailaunchkit.ai"]) {
|
|
148
|
+
expect(document.body).not.toHaveTextContent(upsell);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
118
151
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"
|
|
122
|
-
|
|
152
|
+
it("generateMetadata's title is the magnet's step title", async () => {
|
|
153
|
+
const metadata = await pageModule.generateMetadata({
|
|
154
|
+
params: Promise.resolve({ locale: "en" }),
|
|
155
|
+
getMdxSource: jest.fn().mockResolvedValue(source),
|
|
156
|
+
getPageSections: jest.fn().mockResolvedValue(STARTER_SECTIONS),
|
|
157
|
+
});
|
|
158
|
+
expect(String((metadata.title as { absolute?: string })?.absolute ?? metadata.title)).toContain(
|
|
159
|
+
"Get the newsletter",
|
|
123
160
|
);
|
|
124
161
|
});
|
|
125
162
|
|
|
126
|
-
it("
|
|
163
|
+
it("a signup goes through the magnet and the ending replaces the form", async () => {
|
|
164
|
+
mockSubmitStep.mockResolvedValue({
|
|
165
|
+
ok: true,
|
|
166
|
+
status: 200,
|
|
167
|
+
body: { sessionId: "s1", nextStepKey: null, completed: true },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await renderWith(STARTER_SECTIONS);
|
|
171
|
+
fireEvent.change(screen.getByTestId("newsletter-email-input"), { target: { value: "a@b.co" } });
|
|
172
|
+
fireEvent.click(screen.getByTestId("newsletter-consent-checkbox"));
|
|
173
|
+
await act(async () => {
|
|
174
|
+
fireEvent.submit(screen.getByTestId("newsletter-form"));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await waitFor(() =>
|
|
178
|
+
expect(screen.getByTestId("newsletter-success-headline")).toHaveTextContent("Check your inbox"),
|
|
179
|
+
);
|
|
180
|
+
expect(mockSubmitStep).toHaveBeenCalledWith("newsletter", {
|
|
181
|
+
stepKey: "subscribe",
|
|
182
|
+
values: { email: "a@b.co", "terms.newsletter": true },
|
|
183
|
+
locale: "en",
|
|
184
|
+
});
|
|
185
|
+
expect(leads.subscribe).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("with the hub unreadable, the seeded config renders", async () => {
|
|
189
|
+
mockFlowsGet.mockRejectedValue(new Error("hub down"));
|
|
190
|
+
|
|
191
|
+
await renderWith(STARTER_SECTIONS);
|
|
192
|
+
|
|
193
|
+
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Get the newsletter");
|
|
194
|
+
expect(screen.getByTestId("newsletter-form")).toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("with capture off, ContactFallback with the _site.mdx email renders and no newsletter form", async () => {
|
|
127
198
|
mockCaptureStatus.mockResolvedValue(status(false));
|
|
128
199
|
|
|
129
|
-
|
|
200
|
+
await renderWith(STARTER_SECTIONS);
|
|
130
201
|
|
|
131
|
-
expect(screen.queryByTestId("
|
|
132
|
-
expect(
|
|
202
|
+
expect(screen.queryByTestId("newsletter-form")).toBeNull();
|
|
203
|
+
expect(document.querySelector("a[href='mailto:hi@x.co']")).not.toBeNull();
|
|
133
204
|
});
|
|
134
205
|
|
|
135
|
-
it("
|
|
136
|
-
|
|
206
|
+
it("an authored newsletter section never renders beside the slot (one signup form)", async () => {
|
|
207
|
+
await renderWith([...STARTER_SECTIONS, LEGACY_SECTIONS[1]]);
|
|
208
|
+
|
|
209
|
+
expect(screen.getAllByTestId("newsletter-form")).toHaveLength(1);
|
|
210
|
+
expect(document.querySelector("form[action='/v1/leads/subscribe']")).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
137
213
|
|
|
138
|
-
|
|
214
|
+
describe("NewsletterPage — a page with no contact-form section keeps its composition (#6730 D9)", () => {
|
|
215
|
+
it("newsletter:true + its own newsletter section → that section renders, no second form", async () => {
|
|
216
|
+
await renderWith(LEGACY_SECTIONS);
|
|
139
217
|
|
|
140
|
-
expect(
|
|
141
|
-
expect(
|
|
142
|
-
expect(container.querySelector("a[href='#signup']")).not.toBeNull();
|
|
218
|
+
expect(screen.getAllByTestId("newsletter-form")).toHaveLength(1);
|
|
219
|
+
expect(document.querySelector("a[href='#signup']")).not.toBeNull();
|
|
143
220
|
});
|
|
144
221
|
|
|
145
|
-
it
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
expect(container.querySelector("a[href='#signup']")).toBeNull();
|
|
160
|
-
expect(screen.queryByTestId("lead-capture-form")).toBeNull();
|
|
161
|
-
// The rest of the page still renders.
|
|
162
|
-
expect(container.querySelector("[data-section-id='hero']")).not.toBeNull();
|
|
163
|
-
expect(container.querySelector("[data-section-id='faq']")).not.toBeNull();
|
|
164
|
-
},
|
|
165
|
-
);
|
|
222
|
+
it("newsletter:false → the signup section and the hero CTA to it are dropped", async () => {
|
|
223
|
+
mockCaptureStatus.mockResolvedValue(status(false));
|
|
224
|
+
|
|
225
|
+
await renderWith(LEGACY_SECTIONS);
|
|
226
|
+
|
|
227
|
+
expect(screen.queryByTestId("newsletter-form")).toBeNull();
|
|
228
|
+
expect(document.querySelector("a[href='#signup']")).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("newsletter:true + no signup section → the magnet's form is appended", async () => {
|
|
232
|
+
await renderWith([{ id: "faq", type: "faq", data: { items: [] } }]);
|
|
233
|
+
|
|
234
|
+
expect(screen.getAllByTestId("newsletter-form")).toHaveLength(1);
|
|
235
|
+
});
|
|
166
236
|
});
|
|
@@ -3,21 +3,26 @@
|
|
|
3
3
|
// content the route 404s. Mirrors for/page.tsx: a sections-composed
|
|
4
4
|
// page renders its sections, a prose-only page its MDX body.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// The newsletter magnet is the only source (#8847 D11): the page reads the
|
|
7
|
+
// workspace's `newsletter` magnet (`flows.get("newsletter")`, shared for 15
|
|
8
|
+
// seconds per instance like /contact's read), else the seeded
|
|
9
|
+
// `newsletterFlowConfig` when the hub cannot be read. Its step title is the
|
|
10
|
+
// page's H1 and `<title>`, its body the line under it (`framedSources`), and
|
|
11
|
+
// the standard `NewsletterFormInline` — label, consent, help line, button and
|
|
12
|
+
// ending all the magnet's — goes into the page's first `contact-form`
|
|
13
|
+
// section, bound to `subscribeNewsletterAction`. Rendered on every request.
|
|
9
14
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
15
|
+
// A signup only when there is somewhere to keep it (#6730 D9): when the API
|
|
16
|
+
// does not accept a subscription (`GET /v1/leads/capture` — a database is set
|
|
17
|
+
// up), the slot holds `ContactFallback` with the site's contact email.
|
|
18
|
+
//
|
|
19
|
+
// An authored `newsletter` section never renders beside the slot. A page with
|
|
20
|
+
// no `contact-form` section keeps the composition it had before
|
|
21
|
+
// #8847: a content `newsletter` section renders as authored when accepting
|
|
22
|
+
// (its own form posts to `/v1/leads/subscribe`), a page with no such section
|
|
23
|
+
// gets the signup appended, and when not accepting every `newsletter` section
|
|
24
|
+
// is dropped before render, with any leading-hero CTA that pointed at a
|
|
25
|
+
// dropped section's anchor.
|
|
21
26
|
import type {
|
|
22
27
|
getMdxSource as defaultGetMdxSource,
|
|
23
28
|
getPageSections as defaultGetPageSections,
|
|
@@ -28,14 +33,25 @@ import {
|
|
|
28
33
|
renderPage,
|
|
29
34
|
renderPageMetadata,
|
|
30
35
|
} from "@working-theory/page-renderer";
|
|
31
|
-
import {
|
|
36
|
+
import { ContactFallback } from "@working-theory/ui";
|
|
37
|
+
import { NEWSLETTER_FLOW_SLUG, newsletterFlowConfig } from "@working-theory/validation";
|
|
32
38
|
import type { Metadata } from "next";
|
|
33
39
|
import { getTranslations } from "next-intl/server";
|
|
34
40
|
import type { ReactElement } from "react";
|
|
35
41
|
|
|
36
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
framedSources,
|
|
44
|
+
inPageView,
|
|
45
|
+
type InPageView,
|
|
46
|
+
newsletterSignup,
|
|
47
|
+
type NewsletterFormView,
|
|
48
|
+
readCaptureStatus,
|
|
49
|
+
readServedMagnet,
|
|
50
|
+
} from "../../../lib/in-page-magnet";
|
|
51
|
+
import { readSiteContactEmail } from "../contact/site-contact-email";
|
|
37
52
|
|
|
38
|
-
|
|
53
|
+
// #8847 D11 — rendered on every request; the magnet read behind it is shared for 15 seconds.
|
|
54
|
+
export const dynamic = "force-dynamic";
|
|
39
55
|
|
|
40
56
|
interface Props {
|
|
41
57
|
params: Promise<{ locale: string }>;
|
|
@@ -44,15 +60,16 @@ interface Props {
|
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
type Section = Awaited<ReturnType<typeof defaultGetPageSections>>[number];
|
|
63
|
+
type NewsletterView = InPageView & { form: NewsletterFormView };
|
|
47
64
|
|
|
48
|
-
/**
|
|
49
|
-
async function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
/** The served newsletter magnet's view when it maps, else the seeded config's. */
|
|
66
|
+
async function newsletterView(locale: string): Promise<NewsletterView> {
|
|
67
|
+
const magnet = await readServedMagnet(NEWSLETTER_FLOW_SLUG);
|
|
68
|
+
const view =
|
|
69
|
+
(magnet ? inPageView(magnet, locale, "newsletter") : null) ??
|
|
70
|
+
// The seeded config always maps: one step, its email and consent fields.
|
|
71
|
+
inPageView(newsletterFlowConfig, locale, "newsletter")!;
|
|
72
|
+
return view as NewsletterView;
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
/**
|
|
@@ -77,12 +94,22 @@ function withoutSignup(sections: Section[]): Section[] {
|
|
|
77
94
|
|
|
78
95
|
export async function generateMetadata({
|
|
79
96
|
params,
|
|
80
|
-
|
|
97
|
+
getMdxSource,
|
|
98
|
+
getPageSections,
|
|
99
|
+
}: Props): Promise<Metadata> {
|
|
81
100
|
const { locale } = await params;
|
|
101
|
+
const view = await newsletterView(locale);
|
|
102
|
+
// #8847 D11 — the `<title>` is the magnet's step title, like the H1.
|
|
103
|
+
const framed = framedSources({
|
|
104
|
+
getMdxSource: getMdxSource ?? loadMdxSource,
|
|
105
|
+
getPageSections: getPageSections ?? loadPageSections,
|
|
106
|
+
view,
|
|
107
|
+
});
|
|
82
108
|
return renderPageMetadata({
|
|
83
109
|
type: "NewsletterSignup",
|
|
84
110
|
slug: "newsletter",
|
|
85
111
|
locale,
|
|
112
|
+
getMdxSource: framed.getMdxSource,
|
|
86
113
|
});
|
|
87
114
|
}
|
|
88
115
|
|
|
@@ -92,51 +119,61 @@ export default async function Page({
|
|
|
92
119
|
getPageSections,
|
|
93
120
|
}: Props): Promise<ReactElement> {
|
|
94
121
|
const { locale } = await params;
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
122
|
+
const [status, view] = await Promise.all([readCaptureStatus(), newsletterView(locale)]);
|
|
123
|
+
const accepting = status?.newsletter === true;
|
|
124
|
+
const framed = framedSources({
|
|
125
|
+
getMdxSource: getMdxSource ?? loadMdxSource,
|
|
126
|
+
getPageSections: getPageSections ?? loadPageSections,
|
|
127
|
+
view,
|
|
128
|
+
});
|
|
129
|
+
const source = await framed.getMdxSource("NewsletterSignup", "newsletter", locale);
|
|
130
|
+
const declared = source ? await framed.getPageSections(source.frontmatter) : [];
|
|
102
131
|
const composed = declared.length > 0;
|
|
132
|
+
const slot = declared.find((s) => s.type === "contact-form")?.id;
|
|
103
133
|
const hasSignupSection = declared.some((s) => s.type === "newsletter");
|
|
134
|
+
const contactEmail = readSiteContactEmail();
|
|
135
|
+
|
|
136
|
+
let surface: ReactElement;
|
|
137
|
+
if (accepting) {
|
|
138
|
+
surface = newsletterSignup(view, {
|
|
139
|
+
locale,
|
|
140
|
+
className: "mx-auto",
|
|
141
|
+
...(contactEmail !== undefined && { fallbackEmail: contactEmail }),
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
const tc = await getTranslations({ locale, namespace: "contact" });
|
|
145
|
+
surface = (
|
|
146
|
+
<ContactFallback
|
|
147
|
+
email={contactEmail}
|
|
148
|
+
text={{
|
|
149
|
+
heading: tc("unavailableHeading"),
|
|
150
|
+
body: tc("unavailableBody", { email: "{email}" }),
|
|
151
|
+
noEmail: tc("unavailableNoEmail"),
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
104
156
|
|
|
105
157
|
const rendered = await renderPage({
|
|
106
158
|
type: "NewsletterSignup",
|
|
107
159
|
slug: "newsletter",
|
|
108
160
|
locale,
|
|
109
161
|
...(composed ? {} : { bodyMode: "mdx-body" as const }),
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
162
|
+
getMdxSource: framed.getMdxSource,
|
|
163
|
+
// With a slot the magnet's form is the page's one signup, so an authored
|
|
164
|
+
// `newsletter` section (posting around the magnet) never renders beside it.
|
|
165
|
+
getPageSections:
|
|
166
|
+
accepting && slot === undefined
|
|
167
|
+
? framed.getPageSections
|
|
168
|
+
: async (frontmatter: unknown) => withoutSignup(await framed.getPageSections(frontmatter)),
|
|
169
|
+
...(slot !== undefined && { sectionSlots: { [slot]: surface } }),
|
|
114
170
|
});
|
|
115
171
|
|
|
116
|
-
if (!accepting || hasSignupSection || source === null) return rendered;
|
|
117
|
-
|
|
118
|
-
const [t, tc] = await Promise.all([
|
|
119
|
-
getTranslations({ locale, namespace: "newsletter" }),
|
|
120
|
-
getTranslations({ locale, namespace: "contact" }),
|
|
121
|
-
]);
|
|
172
|
+
if (slot !== undefined || !accepting || hasSignupSection || source === null) return rendered;
|
|
122
173
|
return (
|
|
123
174
|
<>
|
|
124
175
|
{rendered}
|
|
125
|
-
|
|
126
|
-
action={subscribeNewsletterAction}
|
|
127
|
-
fields={[{ name: "email", label: t("emailLabel"), type: "email", required: true }]}
|
|
128
|
-
termsHref={localeHref(locale, "/terms")}
|
|
129
|
-
text={{
|
|
130
|
-
submit: t("submit"),
|
|
131
|
-
sending: t("sending"),
|
|
132
|
-
success: t("success"),
|
|
133
|
-
consentStatement: t("consentStatement"),
|
|
134
|
-
genericError: tc("genericError"),
|
|
135
|
-
unavailableHeading: tc("unavailableHeading"),
|
|
136
|
-
unavailableBody: tc("unavailableBody", { email: "{email}" }),
|
|
137
|
-
unavailableNoEmail: tc("unavailableNoEmail"),
|
|
138
|
-
}}
|
|
139
|
-
/>
|
|
176
|
+
{surface}
|
|
140
177
|
</>
|
|
141
178
|
);
|
|
142
179
|
}
|
package/templates/apps/web/app/[locale]/pricing/__tests__/__snapshots__/unconnected.test.tsx.snap
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
|
2
|
+
|
|
3
|
+
exports[`/pricing on an unconnected site is today's page (#8927 AC-4) renders today's DI page for ?plan=BASIC 1`] = `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Pricing","description":"Plans for every stage."},{"@context":"https://schema.org","@type":"Offer","name":"Basic","price":"29","priceCurrency":"USD"},{"@context":"https://schema.org","@type":"Offer","name":"Business","price":"79","priceCurrency":"USD"},{"@context":"https://schema.org","@type":"Offer","name":"Agency","price":"199","priceCurrency":"USD"},{"@context":"https://schema.org","@type":"AggregateOffer","priceCurrency":"USD","lowPrice":"29","highPrice":"199","offerCount":3}]}</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">Pricing</h1><p data-testid="pricing-description" class="mt-block-tight text-lead">Plans for every stage.</p></div><div class="ailk-section-renderer" data-testid="section-renderer"><span id="section-plans" class="ailk-section-anchor" aria-hidden="true" data-testid="section-anchor"></span><section class="py-section bg-(--color-bg)" data-testid="pricing" data-section="pricing"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8"><span hidden="" data-testid="pricing-recommended-anchor"></span><div class="grid gap-grid md:grid-cols-2 lg:grid-cols-3 mt-10 items-stretch pricing-card-row-grid" data-testid="pricing-grid" data-separation="gapped"><div class="ailk-pricing-tier-card pricing-card-subgrid" data-testid="pricing-tier-card" aria-current="true"><div class="pricing-card-subgrid-body"><div class="h-full rounded-card p-card pricing-card-subgrid border-thin border-(--color-brand) bg-(--color-surface-raised)" data-testid="card"><div class="row-start-1 flex flex-wrap items-center gap-2 justify-between"><h3 class="text-heading-3 font-display text-fg text-2xl font-normal">Basic</h3><div class="group inline-flex items-center border-hairline rounded-full transition-colors focus-visible:outline-none bg-(--color-brand) border-transparent text-(--color-fg-on-brand) hover:bg-(--color-brand)/90 focus-visible:bg-(--color-brand)/90 ps-2 pe-2 py-0.5 text-body-sm" data-testid="tier-badge">Recommended for you</div></div><div class="row-start-3 mt-6"><h4 class="text-heading-4 font-display text-fg text-4xl font-medium tracking-tight">$29</h4><span class="text-body font-body leading-normal text-fg mt-1 text-body-sm text-(--color-text-muted)">/month</span></div><div class="row-start-5 mt-6"><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) w-full" href="/checkout/basic" data-testid="tier-cta">Choose Basic</a></div><ul class="row-start-6 mt-block-loose space-y-3" role="list"><li class="flex items-start gap-2"><span data-testid="feature-check-circle" class="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full check-badge"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check size-[var(--icon-size-xs)] check-glyph" aria-label="Included" role="img"><path d="M20 6 9 17l-5-5"></path></svg></span><span class="text-body font-body leading-normal text-fg text-body-sm text-(--color-fg)">One site</span></li></ul></div></div></div><div class="ailk-pricing-tier-card pricing-card-subgrid" data-testid="pricing-tier-card"><div class="pricing-card-subgrid-body"><div class="h-full rounded-card p-card pricing-card-subgrid border-hairline border-(--color-border) bg-(--color-surface-raised)" data-testid="card"><div class="row-start-1 flex flex-wrap items-center gap-2 justify-between"><h3 class="text-heading-3 font-display text-fg text-2xl font-normal">Business</h3></div><div class="row-start-3 mt-6"><h4 class="text-heading-4 font-display text-fg text-4xl font-medium tracking-tight">$79</h4><span class="text-body font-body leading-normal text-fg mt-1 text-body-sm text-(--color-text-muted)">/month</span></div><div class="row-start-5 mt-6"><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 border-hairline border-(--color-border) text-(--color-fg) bg-transparent 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 min-h-(--control-height-big) w-full" href="/checkout/business" data-testid="tier-cta">Choose Business</a></div><ul class="row-start-6 mt-block-loose space-y-3" role="list"><li class="flex items-start gap-2"><span data-testid="feature-check-circle" class="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full check-badge"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check size-[var(--icon-size-xs)] check-glyph" aria-label="Included" role="img"><path d="M20 6 9 17l-5-5"></path></svg></span><span class="text-body font-body leading-normal text-fg text-body-sm text-(--color-fg)">Three sites</span></li></ul></div></div></div><div class="ailk-pricing-tier-card pricing-card-subgrid" data-testid="pricing-tier-card"><div class="pricing-card-subgrid-body"><div class="h-full rounded-card p-card pricing-card-subgrid border-hairline border-(--color-border) bg-(--color-surface-raised)" data-testid="card"><div class="row-start-1 flex flex-wrap items-center gap-2 justify-between"><h3 class="text-heading-3 font-display text-fg text-2xl font-normal">Agency</h3></div><div class="row-start-3 mt-6"><h4 class="text-heading-4 font-display text-fg text-4xl font-medium tracking-tight">$199</h4><span class="text-body font-body leading-normal text-fg mt-1 text-body-sm text-(--color-text-muted)">/month</span></div><div class="row-start-5 mt-6"><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 border-hairline border-(--color-border) text-(--color-fg) bg-transparent 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 min-h-(--control-height-big) w-full" href="/checkout/agency" data-testid="tier-cta">Choose Agency</a></div><ul class="row-start-6 mt-block-loose space-y-3" role="list"><li class="flex items-start gap-2"><span data-testid="feature-check-circle" class="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full check-badge"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check size-[var(--icon-size-xs)] check-glyph" aria-label="Included" role="img"><path d="M20 6 9 17l-5-5"></path></svg></span><span class="text-body font-body leading-normal text-fg text-body-sm text-(--color-fg)">Unlimited sites</span></li></ul></div></div></div></div></div></section></div></main>"`;
|
|
4
|
+
|
|
5
|
+
exports[`/pricing on an unconnected site is today's page (#8927 AC-4) renders today's DI page for ?plan=agency 1`] = `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Pricing","description":"Plans for every stage."},{"@context":"https://schema.org","@type":"Offer","name":"Basic","price":"29","priceCurrency":"USD"},{"@context":"https://schema.org","@type":"Offer","name":"Business","price":"79","priceCurrency":"USD"},{"@context":"https://schema.org","@type":"Offer","name":"Agency","price":"199","priceCurrency":"USD"},{"@context":"https://schema.org","@type":"AggregateOffer","priceCurrency":"USD","lowPrice":"29","highPrice":"199","offerCount":3}]}</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">Pricing</h1><p data-testid="pricing-description" class="mt-block-tight text-lead">Plans for every stage.</p></div><div class="ailk-section-renderer" data-testid="section-renderer"><span id="section-plans" class="ailk-section-anchor" aria-hidden="true" data-testid="section-anchor"></span><section class="py-section bg-(--color-bg)" data-testid="pricing" data-section="pricing"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8"><span hidden="" data-testid="pricing-recommended-anchor"></span><div class="grid gap-grid md:grid-cols-2 lg:grid-cols-3 mt-10 items-stretch pricing-card-row-grid" data-testid="pricing-grid" data-separation="gapped"><div class="ailk-pricing-tier-card pricing-card-subgrid" data-testid="pricing-tier-card"><div class="pricing-card-subgrid-body"><div class="h-full rounded-card p-card pricing-card-subgrid border-hairline border-(--color-border) bg-(--color-surface-raised)" data-testid="card"><div class="row-start-1 flex flex-wrap items-center gap-2 justify-between"><h3 class="text-heading-3 font-display text-fg text-2xl font-normal">Basic</h3></div><div class="row-start-3 mt-6"><h4 class="text-heading-4 font-display text-fg text-4xl font-medium tracking-tight">$29</h4><span class="text-body font-body leading-normal text-fg mt-1 text-body-sm text-(--color-text-muted)">/month</span></div><div class="row-start-5 mt-6"><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 border-hairline border-(--color-border) text-(--color-fg) bg-transparent 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 min-h-(--control-height-big) w-full" href="/checkout/basic" data-testid="tier-cta">Choose Basic</a></div><ul class="row-start-6 mt-block-loose space-y-3" role="list"><li class="flex items-start gap-2"><span data-testid="feature-check-circle" class="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full check-badge"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check size-[var(--icon-size-xs)] check-glyph" aria-label="Included" role="img"><path d="M20 6 9 17l-5-5"></path></svg></span><span class="text-body font-body leading-normal text-fg text-body-sm text-(--color-fg)">One site</span></li></ul></div></div></div><div class="ailk-pricing-tier-card pricing-card-subgrid" data-testid="pricing-tier-card"><div class="pricing-card-subgrid-body"><div class="h-full rounded-card p-card pricing-card-subgrid border-hairline border-(--color-border) bg-(--color-surface-raised)" data-testid="card"><div class="row-start-1 flex flex-wrap items-center gap-2 justify-between"><h3 class="text-heading-3 font-display text-fg text-2xl font-normal">Business</h3></div><div class="row-start-3 mt-6"><h4 class="text-heading-4 font-display text-fg text-4xl font-medium tracking-tight">$79</h4><span class="text-body font-body leading-normal text-fg mt-1 text-body-sm text-(--color-text-muted)">/month</span></div><div class="row-start-5 mt-6"><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 border-hairline border-(--color-border) text-(--color-fg) bg-transparent 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 min-h-(--control-height-big) w-full" href="/checkout/business" data-testid="tier-cta">Choose Business</a></div><ul class="row-start-6 mt-block-loose space-y-3" role="list"><li class="flex items-start gap-2"><span data-testid="feature-check-circle" class="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full check-badge"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check size-[var(--icon-size-xs)] check-glyph" aria-label="Included" role="img"><path d="M20 6 9 17l-5-5"></path></svg></span><span class="text-body font-body leading-normal text-fg text-body-sm text-(--color-fg)">Three sites</span></li></ul></div></div></div><div class="ailk-pricing-tier-card pricing-card-subgrid" data-testid="pricing-tier-card" aria-current="true"><div class="pricing-card-subgrid-body"><div class="h-full rounded-card p-card pricing-card-subgrid border-thin border-(--color-brand) bg-(--color-surface-raised)" data-testid="card"><div class="row-start-1 flex flex-wrap items-center gap-2 justify-between"><h3 class="text-heading-3 font-display text-fg text-2xl font-normal">Agency</h3><div class="group inline-flex items-center border-hairline rounded-full transition-colors focus-visible:outline-none bg-(--color-brand) border-transparent text-(--color-fg-on-brand) hover:bg-(--color-brand)/90 focus-visible:bg-(--color-brand)/90 ps-2 pe-2 py-0.5 text-body-sm" data-testid="tier-badge">Recommended for you</div></div><div class="row-start-3 mt-6"><h4 class="text-heading-4 font-display text-fg text-4xl font-medium tracking-tight">$199</h4><span class="text-body font-body leading-normal text-fg mt-1 text-body-sm text-(--color-text-muted)">/month</span></div><div class="row-start-5 mt-6"><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) w-full" href="/checkout/agency" data-testid="tier-cta">Choose Agency</a></div><ul class="row-start-6 mt-block-loose space-y-3" role="list"><li class="flex items-start gap-2"><span data-testid="feature-check-circle" class="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full check-badge"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check size-[var(--icon-size-xs)] check-glyph" aria-label="Included" role="img"><path d="M20 6 9 17l-5-5"></path></svg></span><span class="text-body font-body leading-normal text-fg text-body-sm text-(--color-fg)">Unlimited sites</span></li></ul></div></div></div></div></div></section></div></main>"`;
|
|
6
|
+
|
|
7
|
+
exports[`/pricing on an unconnected site is today's page (#8927 AC-4) renders today's DI page for no plan 1`] = `"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Pricing","description":"Plans for every stage."},{"@context":"https://schema.org","@type":"Offer","name":"Basic","price":"29","priceCurrency":"USD"},{"@context":"https://schema.org","@type":"Offer","name":"Business","price":"79","priceCurrency":"USD"},{"@context":"https://schema.org","@type":"Offer","name":"Agency","price":"199","priceCurrency":"USD"},{"@context":"https://schema.org","@type":"AggregateOffer","priceCurrency":"USD","lowPrice":"29","highPrice":"199","offerCount":3}]}</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">Pricing</h1><p data-testid="pricing-description" class="mt-block-tight text-lead">Plans for every stage.</p></div><div class="ailk-section-renderer" data-testid="section-renderer"><span id="section-plans" class="ailk-section-anchor" aria-hidden="true" data-testid="section-anchor"></span><section class="py-section bg-(--color-bg)" data-testid="pricing" data-section="pricing"><div class="mx-auto w-full max-w-container-xl px-4 md:px-6 lg:px-8"><div class="grid gap-grid md:grid-cols-2 lg:grid-cols-3 mt-10 items-stretch pricing-card-row-grid" data-testid="pricing-grid" data-separation="gapped"><div class="ailk-pricing-tier-card pricing-card-subgrid" data-testid="pricing-tier-card"><div class="pricing-card-subgrid-body"><div class="h-full rounded-card p-card pricing-card-subgrid border-hairline border-(--color-border) bg-(--color-surface-raised)" data-testid="card"><div class="row-start-1 flex flex-wrap items-center gap-2 justify-between"><h3 class="text-heading-3 font-display text-fg text-2xl font-normal">Basic</h3></div><div class="row-start-3 mt-6"><h4 class="text-heading-4 font-display text-fg text-4xl font-medium tracking-tight">$29</h4><span class="text-body font-body leading-normal text-fg mt-1 text-body-sm text-(--color-text-muted)">/month</span></div><div class="row-start-5 mt-6"><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 border-hairline border-(--color-border) text-(--color-fg) bg-transparent 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 min-h-(--control-height-big) w-full" href="/checkout/basic" data-testid="tier-cta">Choose Basic</a></div><ul class="row-start-6 mt-block-loose space-y-3" role="list"><li class="flex items-start gap-2"><span data-testid="feature-check-circle" class="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full check-badge"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check size-[var(--icon-size-xs)] check-glyph" aria-label="Included" role="img"><path d="M20 6 9 17l-5-5"></path></svg></span><span class="text-body font-body leading-normal text-fg text-body-sm text-(--color-fg)">One site</span></li></ul></div></div></div><div class="ailk-pricing-tier-card pricing-card-subgrid" data-testid="pricing-tier-card" aria-current="true"><div class="pricing-card-subgrid-body"><div class="h-full rounded-card p-card pricing-card-subgrid border-thin border-(--color-brand) bg-(--color-surface-raised)" data-testid="card"><div class="row-start-1 flex flex-wrap items-center gap-2 justify-between"><h3 class="text-heading-3 font-display text-fg text-2xl font-normal">Business</h3><div class="group inline-flex items-center border-hairline rounded-full transition-colors focus-visible:outline-none bg-(--color-brand) border-transparent text-(--color-fg-on-brand) hover:bg-(--color-brand)/90 focus-visible:bg-(--color-brand)/90 ps-2 pe-2 py-0.5 text-body-sm" data-testid="tier-badge">Recommended</div></div><div class="row-start-3 mt-6"><h4 class="text-heading-4 font-display text-fg text-4xl font-medium tracking-tight">$79</h4><span class="text-body font-body leading-normal text-fg mt-1 text-body-sm text-(--color-text-muted)">/month</span></div><div class="row-start-5 mt-6"><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) w-full" href="/checkout/business" data-testid="tier-cta">Choose Business</a></div><ul class="row-start-6 mt-block-loose space-y-3" role="list"><li class="flex items-start gap-2"><span data-testid="feature-check-circle" class="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full check-badge"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check size-[var(--icon-size-xs)] check-glyph" aria-label="Included" role="img"><path d="M20 6 9 17l-5-5"></path></svg></span><span class="text-body font-body leading-normal text-fg text-body-sm text-(--color-fg)">Three sites</span></li></ul></div></div></div><div class="ailk-pricing-tier-card pricing-card-subgrid" data-testid="pricing-tier-card"><div class="pricing-card-subgrid-body"><div class="h-full rounded-card p-card pricing-card-subgrid border-hairline border-(--color-border) bg-(--color-surface-raised)" data-testid="card"><div class="row-start-1 flex flex-wrap items-center gap-2 justify-between"><h3 class="text-heading-3 font-display text-fg text-2xl font-normal">Agency</h3></div><div class="row-start-3 mt-6"><h4 class="text-heading-4 font-display text-fg text-4xl font-medium tracking-tight">$199</h4><span class="text-body font-body leading-normal text-fg mt-1 text-body-sm text-(--color-text-muted)">/month</span></div><div class="row-start-5 mt-6"><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 border-hairline border-(--color-border) text-(--color-fg) bg-transparent 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 min-h-(--control-height-big) w-full" href="/checkout/agency" data-testid="tier-cta">Choose Agency</a></div><ul class="row-start-6 mt-block-loose space-y-3" role="list"><li class="flex items-start gap-2"><span data-testid="feature-check-circle" class="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full check-badge"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check size-[var(--icon-size-xs)] check-glyph" aria-label="Included" role="img"><path d="M20 6 9 17l-5-5"></path></svg></span><span class="text-body font-body leading-normal text-fg text-body-sm text-(--color-fg)">Unlimited sites</span></li></ul></div></div></div></div></div></section></div></main>"`;
|