create-ailk 0.1.0 → 0.2.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 +124 -3
- package/component-catalog.md +456 -0
- package/dist/cli.js +92 -16
- package/dist/component-catalog.d.ts +11 -0
- package/dist/component-catalog.js +39 -0
- package/dist/copy.d.ts +7 -0
- package/dist/copy.js +24 -0
- package/dist/derive-architecture.d.ts +13 -0
- package/dist/derive-architecture.js +238 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +19 -2
- package/dist/lib/extract-module-bundle.d.ts +16 -0
- package/dist/lib/extract-module-bundle.js +184 -0
- package/dist/lib/fetch-module.d.ts +55 -0
- package/dist/lib/fetch-module.js +125 -0
- package/dist/lib/module-license-gate.d.ts +48 -0
- package/dist/lib/module-license-gate.js +67 -0
- package/dist/lib/module-tier.d.ts +35 -0
- package/dist/lib/module-tier.js +37 -0
- package/dist/module-architecture.d.ts +175 -0
- package/dist/module-architecture.js +663 -0
- package/dist/parse-args.d.ts +47 -1
- package/dist/parse-args.js +143 -2
- package/dist/programmatic.d.ts +160 -3
- package/dist/programmatic.js +196 -7
- package/dist/surfaces.d.ts +211 -0
- package/dist/surfaces.js +532 -0
- package/dist/sync-routes.d.ts +92 -0
- package/dist/sync-routes.js +350 -0
- package/package.json +12 -5
- package/templates/.claude/rules/architecture.md +6 -2
- package/templates/.claude/skills/README.md +9 -9
- package/templates/.claude/skills/add-schema/SKILL.md +14 -7
- package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
- package/templates/.claude/skills/provision-config/SKILL.md +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
- package/templates/.env.example +126 -2
- package/templates/CLAUDE.md +11 -5
- package/templates/README.md +1 -1
- package/templates/apps/api/.env.example +38 -0
- package/templates/apps/api/CLAUDE.md +78 -1
- package/templates/apps/api/LICENSE +201 -0
- package/templates/apps/api/api/index.ts +6 -141
- package/templates/apps/api/jest.config.cjs +16 -0
- package/templates/apps/api/package.json +22 -6
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
- package/templates/apps/api/src/__tests__/server.test.ts +46 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
- package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
- package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
- package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
- package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
- package/templates/apps/api/src/bin/seed-presets.ts +58 -0
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
- package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
- package/templates/apps/api/src/cli.ts +5 -11
- package/templates/apps/api/src/config/index.ts +35 -0
- package/templates/apps/api/src/config/modules.ts +43 -0
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -0
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
- package/templates/apps/api/src/lib/site.ts +16 -0
- package/templates/apps/api/src/lib/stripe.ts +22 -0
- package/templates/apps/api/src/lib/tenant-db.ts +225 -0
- package/templates/apps/api/src/lib/ws-token.ts +91 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
- package/templates/apps/api/src/middleware/auth.ts +43 -8
- package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
- package/templates/apps/api/src/middleware/tenant.ts +99 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
- package/templates/apps/api/src/openapi/spec.ts +312 -3
- package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
- package/templates/apps/api/src/routes/aeo/index.ts +107 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
- package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
- package/templates/apps/api/src/routes/billing/index.ts +36 -0
- package/templates/apps/api/src/routes/billing/portal.ts +182 -0
- package/templates/apps/api/src/routes/billing/read.ts +75 -0
- package/templates/apps/api/src/routes/billing/usage.ts +153 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
- package/templates/apps/api/src/routes/content/create.ts +1 -0
- package/templates/apps/api/src/routes/content/delete.ts +1 -0
- package/templates/apps/api/src/routes/content/index.ts +42 -6
- package/templates/apps/api/src/routes/content/update.ts +1 -0
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
- package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
- package/templates/apps/api/src/routes/flows/README.md +147 -0
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
- package/templates/apps/api/src/routes/flows/index.ts +202 -0
- package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
- package/templates/apps/api/src/routes/leads/index.ts +43 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +558 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +869 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +221 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +468 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +707 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +375 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +467 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
- package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +120 -0
- package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
- package/templates/apps/api/src/routes/project-listings/me.ts +76 -0
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
- package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +106 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
- package/templates/apps/api/src/routes/schedule/index.ts +249 -0
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
- package/templates/apps/api/src/routes/slack/actions.ts +177 -0
- package/templates/apps/api/src/routes/slack/index.ts +38 -0
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
- package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
- package/templates/apps/api/src/routes/webhooks/README.md +35 -12
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
- package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
- package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
- package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
- package/templates/apps/api/src/server.ts +307 -7
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
- package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
- package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
- package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +684 -0
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
- package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
- package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
- package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
- package/templates/apps/api/src/services/abandon-email.ts +307 -0
- package/templates/apps/api/src/services/aeo-score.ts +288 -0
- package/templates/apps/api/src/services/answer-distributions.ts +138 -0
- package/templates/apps/api/src/services/consent.ts +236 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
- package/templates/apps/api/src/services/entitlements.ts +102 -0
- package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
- package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
- package/templates/apps/api/src/services/flow-engine.ts +1296 -0
- package/templates/apps/api/src/services/lead-promotion.ts +176 -0
- package/templates/apps/api/src/services/lead-routing.ts +3 -3
- package/templates/apps/api/src/services/listing-config.ts +102 -0
- package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
- package/templates/apps/api/src/services/listing-promotion.ts +342 -0
- package/templates/apps/api/src/services/playbook-compile.ts +398 -0
- package/templates/apps/api/src/services/playbook-render.ts +263 -0
- package/templates/apps/api/src/services/project-listing-decision.ts +510 -0
- package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
- package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
- package/templates/apps/api/src/services/project-listings.ts +1472 -0
- package/templates/apps/api/src/services/recommender-capture.ts +835 -0
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
- package/templates/apps/api/src/services/scheduling/index.ts +63 -0
- package/templates/apps/api/src/services/scheduling/types.ts +88 -0
- package/templates/apps/api/src/services/slack-notify.ts +240 -0
- package/templates/apps/api/src/services/tenant-context.ts +451 -0
- package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
- package/templates/apps/api/src/services/usage-credits.ts +340 -0
- package/templates/apps/api/src/services/usage-metering.ts +699 -0
- package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
- package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
- package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
- package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
- package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
- package/templates/apps/api/src/vercel-handler.ts +165 -0
- package/templates/apps/api/tsconfig.json +0 -12
- package/templates/apps/mcp/CLAUDE.md +19 -9
- package/templates/apps/mcp/LICENSE +201 -0
- package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
- package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
- package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
- package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
- package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
- package/templates/apps/mcp/package.json +6 -4
- package/templates/apps/mcp/src/config/modules.ts +37 -0
- package/templates/apps/mcp/src/server.ts +109 -18
- package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
- package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
- package/templates/apps/mcp/src/tools/create_page.ts +1 -0
- package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
- package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
- package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
- package/templates/apps/mcp/src/tools/index.ts +25 -0
- package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
- package/templates/apps/mcp/src/tools/purchase.ts +3 -0
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
- package/templates/apps/mcp/src/tools/update_page.ts +1 -0
- package/templates/apps/mcp/tsconfig.json +1 -9
- package/templates/apps/web/.env.example +44 -0
- package/templates/apps/web/CLAUDE.md +28 -0
- package/templates/apps/web/LICENSE +201 -0
- package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
- package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
- package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
- package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
- package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
- package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
- package/templates/apps/web/__tests__/setup.ts +22 -32
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
- package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/layout.tsx +127 -16
- package/templates/apps/web/app/[locale]/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
- package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
- package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
- package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
- package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/globals.css +35 -0
- package/templates/apps/web/app/llms.txt/route.ts +2 -1
- package/templates/apps/web/app/og/route.tsx +10 -2
- package/templates/apps/web/auth.ts +8 -0
- package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
- package/templates/apps/web/content/en/blog/README.txt +25 -0
- package/templates/apps/web/content/en/docs/index.mdx +28 -0
- package/templates/apps/web/jest.config.cjs +90 -27
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
- package/templates/apps/web/lib/account-config.ts +13 -0
- package/templates/apps/web/lib/server-api.ts +85 -0
- package/templates/apps/web/lib/site-brand.tsx +121 -0
- package/templates/apps/web/lib/site-theme.ts +74 -0
- package/templates/apps/web/messages/en.json +275 -7
- package/templates/apps/web/middleware.ts +11 -3
- package/templates/apps/web/next.config.mjs +8 -2
- package/templates/apps/web/package.json +17 -7
- package/templates/apps/web/postcss.config.mjs +23 -0
- package/templates/apps/web/public/.well-known/security.txt +3 -3
- package/templates/apps/web/src/lib/routes.ts +15 -2
- package/templates/apps/web/tsconfig.json +1 -16
- package/templates/content/_site.mdx +36 -25
- package/templates/content/en/AboutPage/about.mdx +24 -14
- package/templates/content/en/ContactPage/contact.mdx +23 -14
- package/templates/content/en/FAQPage/faq.mdx +10 -392
- package/templates/content/en/HomePage/home.mdx +14 -285
- package/templates/database/CHANGELOG.md +443 -0
- package/templates/database/LICENSE +201 -0
- package/templates/database/README.md +3 -1
- package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
- package/templates/database/__tests__/flows-presets.test.ts +173 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
- package/templates/database/__tests__/seed.test.ts +88 -0
- package/templates/database/__tests__/user-cascade.test.ts +19 -4
- package/templates/database/auth/schema.prisma +521 -0
- package/templates/database/config/schema.prisma +234 -0
- package/templates/database/content/schema.prisma +263 -0
- package/templates/database/flows/listing-diagnostic.ts +182 -0
- package/templates/database/flows/presets.ts +315 -0
- package/templates/database/inbox/__tests__/schema.test.ts +4 -0
- package/templates/database/inbox/schema.prisma +574 -3
- package/templates/database/jest.config.cjs +8 -0
- package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
- package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
- package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
- package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
- package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
- package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
- package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
- package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
- package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
- package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
- package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
- package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
- package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
- package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
- package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
- package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
- package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
- package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
- package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
- package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
- package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
- package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
- package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
- package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
- package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
- package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
- package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
- package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
- package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
- package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
- package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
- package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
- package/templates/database/ops/schema.prisma +27 -0
- package/templates/database/package.json +42 -4
- package/templates/database/prisma.config.ts +1 -1
- package/templates/database/schema.prisma +19 -435
- package/templates/database/seed-test.ts +14 -4
- package/templates/database/seed.ts +148 -0
- package/templates/database/tenancy.ts +45 -0
- package/templates/database/tsconfig.flows.json +15 -0
- package/templates/database/tsconfig.json +7 -1
- package/templates/database/tsconfig.tenancy.json +14 -0
- package/templates/package.json +30 -55
- package/templates/pnpm-workspace.yaml +0 -3
- package/templates/project.yaml +1 -1
- package/templates/tsconfig.json +1 -37
- package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
- package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
- package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
- package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
- package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
- package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
- package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
- package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
- package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
- package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
- package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
- package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
- package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
- package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
- package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
- package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
- package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
- package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
- package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
- package/templates/apps/web/__tests__/page.test.tsx +0 -230
- package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
- package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
- package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
- package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
- package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
- package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
- package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
- package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
- package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
- package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
- package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
- package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
- package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
- package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
- package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
- package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
- package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
- package/templates/apps/web/content/en/blog/index.mdx +0 -15
- package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
- package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
- package/templates/content/.gitkeep +0 -0
- package/templates/content/CLAUDE.md +0 -85
- package/templates/content/ar/HomePage/home.mdx +0 -123
- package/templates/content/ar/_site.mdx +0 -35
- package/templates/content/de/_site.mdx +0 -29
- package/templates/content/en/.gitkeep +0 -0
- package/templates/content/en/AboutPage/about.schema.json +0 -82
- package/templates/content/en/BookCall/book-call.mdx +0 -19
- package/templates/content/en/BookCall/book-call.schema.json +0 -121
- package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
- package/templates/content/en/CareersList/careers.mdx +0 -24
- package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
- package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
- package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
- package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
- package/templates/content/en/ClientsList/clients.mdx +0 -38
- package/templates/content/en/ContactPage/contact.schema.json +0 -100
- package/templates/content/en/ContactPoint/primary.mdx +0 -14
- package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
- package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
- package/templates/content/en/CoursesList/courses.mdx +0 -22
- package/templates/content/en/DownloadsList/downloads.mdx +0 -22
- package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
- package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
- package/templates/content/en/EventsList/events.mdx +0 -26
- package/templates/content/en/FAQPage/faq.schema.json +0 -231
- package/templates/content/en/GlossaryList/glossary.mdx +0 -19
- package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
- package/templates/content/en/HomePage/home.schema.json +0 -88
- package/templates/content/en/HomePage/launch-service.mdx +0 -475
- package/templates/content/en/HomePage/launch-service.schema.json +0 -138
- package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
- package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
- package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
- package/templates/content/en/Integration/github-actions.mdx +0 -31
- package/templates/content/en/Integration/openai-api.mdx +0 -26
- package/templates/content/en/Integration/vercel.mdx +0 -32
- package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
- package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
- package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
- package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
- package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
- package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
- package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
- package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
- package/templates/content/en/LandingPage/developers.mdx +0 -19
- package/templates/content/en/LandingPage/developers.schema.json +0 -109
- package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
- package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
- package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
- package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
- package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
- package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
- package/templates/content/en/LandingPage/platform.mdx +0 -369
- package/templates/content/en/LandingPage/platform.schema.json +0 -166
- package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
- package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
- package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
- package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
- package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
- package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
- package/templates/content/en/Organization/site-owner.mdx +0 -16
- package/templates/content/en/Partner/prisma.mdx +0 -31
- package/templates/content/en/Partner/supabase.mdx +0 -30
- package/templates/content/en/Partner/vercel.mdx +0 -28
- package/templates/content/en/PartnersList/partners.mdx +0 -22
- package/templates/content/en/Person/founder.mdx +0 -18
- package/templates/content/en/Pricing/pricing.mdx +0 -538
- package/templates/content/en/Pricing/pricing.schema.json +0 -209
- package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
- package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
- package/templates/content/en/Product/ailk.mdx +0 -19
- package/templates/content/en/Product/ailk.schema.json +0 -94
- package/templates/content/en/Product/api-launch-kit.mdx +0 -26
- package/templates/content/en/Product/launch-kit.mdx +0 -28
- package/templates/content/en/ProductsList/products.mdx +0 -26
- package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
- package/templates/content/en/ProjectsList/projects.mdx +0 -25
- package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
- package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
- package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
- package/templates/content/en/ResourcesHub/resources.mdx +0 -22
- package/templates/content/en/Service/consulting.mdx +0 -22
- package/templates/content/en/Service/implementation.mdx +0 -30
- package/templates/content/en/Service/launch-service.mdx +0 -232
- package/templates/content/en/Service/launch.mdx +0 -228
- package/templates/content/en/ServicesList/services.mdx +0 -84
- package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
- package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
- package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
- package/templates/content/en/TeamList/team.mdx +0 -63
- package/templates/content/en/TeamMember/founder.mdx +0 -72
- package/templates/content/en/TermsPage/terms.mdx +0 -19
- package/templates/content/en/TermsPage/terms.schema.json +0 -99
- package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
- package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
- package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
- package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
- package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
- package/templates/content/en-XA/AboutPage/about.mdx +0 -8
- package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
- package/templates/content/en-XA/HomePage/home.mdx +0 -8
- package/templates/content/es/_site.mdx +0 -30
- package/templates/content/fr/_site.mdx +0 -30
- package/templates/content/ja/_site.mdx +0 -30
- package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
- package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
- package/templates/content/pt-BR/HomePage/home.mdx +0 -23
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ListingApplicationClient — the REFERENCE MOUNT for the founder application
|
|
5
|
+
* (#5048).
|
|
6
|
+
*
|
|
7
|
+
* This file is the whole per-site cost of the consumer surface, and it is
|
|
8
|
+
* deliberately this short. Before it, a site had to hand-write three proxy
|
|
9
|
+
* routes, a form that saved on every advance, and a decision about where a
|
|
10
|
+
* bearer token lives. What is left is: bind `projectListings` to the section,
|
|
11
|
+
* and decide what to do with the token when `start` mints one.
|
|
12
|
+
*
|
|
13
|
+
* IT IS NOT A BFF, and a site must not copy it into one (the placement
|
|
14
|
+
* decision's D4). `apps/api` is called DIRECTLY: an agent-mediated caller
|
|
15
|
+
* (#5039) cannot use any site's proxy, so the direct path has to be safe on
|
|
16
|
+
* its own terms regardless — and it is, because `siteId` is resolved
|
|
17
|
+
* server-side from the public site key rather than taken from the body. A
|
|
18
|
+
* proxy would be convenience, not a security boundary.
|
|
19
|
+
*
|
|
20
|
+
* TOKEN CUSTODY (#5048 (a)). The token is caller-held. This mount reads a
|
|
21
|
+
* resume link's `id` and `token` query parameters and passes them straight to
|
|
22
|
+
* the section; when `start` mints a new pair it replaces the URL's query
|
|
23
|
+
* parameters with them, so a founder who reloads or navigates back keeps their
|
|
24
|
+
* draft. That is a deliberate choice with a cost: a token in a URL lands in
|
|
25
|
+
* browser history and in any `Referer` the page sends. It is the accepted cost
|
|
26
|
+
* of a resume link that has to survive being EMAILED and opened in a cold
|
|
27
|
+
* browser (samhenry #316 D6b), which is the constraint that rules out an
|
|
28
|
+
* httpOnly cookie entirely. A site with different constraints replaces this
|
|
29
|
+
* one function; nothing else here changes.
|
|
30
|
+
*
|
|
31
|
+
* NO ANTI-AUTOMATION CONTROLS. No honeypot, no timing check, deliberately —
|
|
32
|
+
* see #316 D6b and #5039.
|
|
33
|
+
*
|
|
34
|
+
* SHIPS NO COPY, AND NO ROUTE. Every string comes from the `config` and
|
|
35
|
+
* `messages` a caller passes in. There is deliberately no `page.tsx` beside
|
|
36
|
+
* this file: a route would need a question set, and a question set is the
|
|
37
|
+
* SITE's content — shipping a placeholder one here would put founder-readable
|
|
38
|
+
* copy in the template, which is exactly what this unit exists to stop. A site
|
|
39
|
+
* adds its own route, imports this wrapper, and passes its own config.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { projectListings } from "@working-theory/api-client";
|
|
43
|
+
import { ListingApplicationFlow } from "@working-theory/ui";
|
|
44
|
+
import type {
|
|
45
|
+
FlowStepperMessages,
|
|
46
|
+
ListingApplicationDraftClient,
|
|
47
|
+
ListingApplicationMessages,
|
|
48
|
+
} from "@working-theory/ui";
|
|
49
|
+
import type { ListingApplicationConfig } from "@working-theory/validation";
|
|
50
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
51
|
+
import type { ReactElement, ReactNode } from "react";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `projectListings` satisfies the section's structural client contract as-is —
|
|
55
|
+
* `start`, `patchDraft` and `submit` already have the right shapes, and the
|
|
56
|
+
* section defines the interface rather than importing this package so that
|
|
57
|
+
* `packages/ui` stays transport-agnostic (#3957 D2).
|
|
58
|
+
*/
|
|
59
|
+
const client: ListingApplicationDraftClient = projectListings;
|
|
60
|
+
|
|
61
|
+
/** The query parameters an emailed resume link carries. */
|
|
62
|
+
const DRAFT_ID_PARAM = "draft";
|
|
63
|
+
const RESUME_TOKEN_PARAM = "token";
|
|
64
|
+
|
|
65
|
+
interface Props {
|
|
66
|
+
config: ListingApplicationConfig;
|
|
67
|
+
locale: string;
|
|
68
|
+
messages: FlowStepperMessages & ListingApplicationMessages;
|
|
69
|
+
/** Rendered under the live tile — the per-listing privacy notice goes here. */
|
|
70
|
+
tileFooter?: ReactNode;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function ListingApplicationClient({
|
|
74
|
+
config,
|
|
75
|
+
locale,
|
|
76
|
+
messages,
|
|
77
|
+
tileFooter,
|
|
78
|
+
}: Props): ReactElement {
|
|
79
|
+
const router = useRouter();
|
|
80
|
+
const params = useSearchParams();
|
|
81
|
+
|
|
82
|
+
const draftId = params.get(DRAFT_ID_PARAM);
|
|
83
|
+
const resumeToken = params.get(RESUME_TOKEN_PARAM);
|
|
84
|
+
const resume =
|
|
85
|
+
draftId && resumeToken ? { id: draftId, resumeToken } : undefined;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<ListingApplicationFlow
|
|
89
|
+
config={config}
|
|
90
|
+
locale={locale}
|
|
91
|
+
messages={messages}
|
|
92
|
+
client={client}
|
|
93
|
+
{...(resume ? { resume } : {})}
|
|
94
|
+
{...(tileFooter !== undefined ? { tileFooter } : {})}
|
|
95
|
+
onDraftStarted={({ id, resumeToken: token }) => {
|
|
96
|
+
// Put the pair in the URL so a reload or a back-navigation resumes
|
|
97
|
+
// rather than starting a second draft. `replace`, not `push`: the
|
|
98
|
+
// founder should not have to walk back through one history entry per
|
|
99
|
+
// step. The site's own email send goes here too — the same two values
|
|
100
|
+
// are what its resume link carries.
|
|
101
|
+
const next = new URLSearchParams(params.toString());
|
|
102
|
+
next.set(DRAFT_ID_PARAM, id);
|
|
103
|
+
next.set(RESUME_TOKEN_PARAM, token);
|
|
104
|
+
router.replace(`?${next.toString()}`, { scroll: false });
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default ListingApplicationClient;
|
package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #5048 — the reference mount.
|
|
3
|
+
*
|
|
4
|
+
* Two things are worth pinning here and nothing else is: that an emailed
|
|
5
|
+
* resume link's query parameters reach the section as a resume, and that a
|
|
6
|
+
* freshly-minted draft's id and token are written back into the URL rather
|
|
7
|
+
* than into a cookie or browser storage. Everything else about the surface is
|
|
8
|
+
* tested where it lives, in `packages/ui`.
|
|
9
|
+
*/
|
|
10
|
+
import { render, screen } from "@testing-library/react";
|
|
11
|
+
|
|
12
|
+
const replace = jest.fn();
|
|
13
|
+
let searchParams = new URLSearchParams();
|
|
14
|
+
|
|
15
|
+
jest.mock("next/navigation", () => ({
|
|
16
|
+
useRouter: () => ({ replace }),
|
|
17
|
+
useSearchParams: () => searchParams,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// The section is the subject of its own tests; here it is a probe that records
|
|
21
|
+
// what the mount handed it.
|
|
22
|
+
const received: Record<string, unknown>[] = [];
|
|
23
|
+
jest.mock("@working-theory/ui", () => ({
|
|
24
|
+
ListingApplicationFlow: (props: Record<string, unknown>) => {
|
|
25
|
+
received.push(props);
|
|
26
|
+
return <div data-testid="listing-application-flow" />;
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
31
|
+
projectListings: { start: jest.fn(), patchDraft: jest.fn(), submit: jest.fn() },
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
// The import sits below the mocks deliberately: `jest.mock` calls are hoisted,
|
|
35
|
+
// but the module under test must not be evaluated before they are registered.
|
|
36
|
+
import { ListingApplicationClient } from "../ListingApplicationClient";
|
|
37
|
+
|
|
38
|
+
const config = { slug: "apply", defaultLocale: "en", siteKey: "pk_test", steps: [] };
|
|
39
|
+
const messages = {} as never;
|
|
40
|
+
|
|
41
|
+
function mount() {
|
|
42
|
+
return render(
|
|
43
|
+
<ListingApplicationClient
|
|
44
|
+
config={config as never}
|
|
45
|
+
locale="en"
|
|
46
|
+
messages={messages}
|
|
47
|
+
/>,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
received.length = 0;
|
|
53
|
+
replace.mockClear();
|
|
54
|
+
searchParams = new URLSearchParams();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("ListingApplicationClient", () => {
|
|
58
|
+
it("passes no resume when the URL carries no link parameters", () => {
|
|
59
|
+
mount();
|
|
60
|
+
expect(screen.getByTestId("listing-application-flow")).toBeInTheDocument();
|
|
61
|
+
expect(received[0]).not.toHaveProperty("resume");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("rehydrates a draft from an emailed resume link's query parameters", () => {
|
|
65
|
+
searchParams = new URLSearchParams("draft=listing_7&token=tok_from_the_email");
|
|
66
|
+
mount();
|
|
67
|
+
expect(received[0]!["resume"]).toEqual({
|
|
68
|
+
id: "listing_7",
|
|
69
|
+
resumeToken: "tok_from_the_email",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("needs BOTH parameters — a half link is not a resume", () => {
|
|
74
|
+
searchParams = new URLSearchParams("draft=listing_7");
|
|
75
|
+
mount();
|
|
76
|
+
expect(received[0]).not.toHaveProperty("resume");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("writes a newly minted draft back into the URL, not into a cookie", () => {
|
|
80
|
+
mount();
|
|
81
|
+
const onDraftStarted = received[0]!["onDraftStarted"] as (s: {
|
|
82
|
+
id: string;
|
|
83
|
+
resumeToken: string;
|
|
84
|
+
}) => void;
|
|
85
|
+
onDraftStarted({ id: "listing_9", resumeToken: "tok_new" });
|
|
86
|
+
|
|
87
|
+
expect(replace).toHaveBeenCalledWith(
|
|
88
|
+
"?draft=listing_9&token=tok_new",
|
|
89
|
+
{ scroll: false },
|
|
90
|
+
);
|
|
91
|
+
// The token is caller-held by design (#5048 (a)) — an httpOnly cookie
|
|
92
|
+
// cannot travel in an emailed link and a non-browser caller cannot present
|
|
93
|
+
// one at all. Nothing here may quietly set one.
|
|
94
|
+
expect(document.cookie).toBe("");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -4,7 +4,8 @@ import type {
|
|
|
4
4
|
getMdxSource as defaultGetMdxSource,
|
|
5
5
|
getPageSections as defaultGetPageSections,
|
|
6
6
|
} from "@working-theory/page-renderer";
|
|
7
|
-
import { renderPage } from "@working-theory/page-renderer";
|
|
7
|
+
import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
|
|
8
|
+
import type { Metadata } from "next";
|
|
8
9
|
import type { ReactElement } from "react";
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
@@ -13,6 +14,17 @@ interface Props {
|
|
|
13
14
|
getPageSections?: typeof defaultGetPageSections;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
export async function generateMetadata({
|
|
18
|
+
params,
|
|
19
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
20
|
+
const { locale } = await params;
|
|
21
|
+
return renderPageMetadata({
|
|
22
|
+
type: "ProjectsList",
|
|
23
|
+
slug: "projects",
|
|
24
|
+
locale,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
export default async function Page({
|
|
17
29
|
params,
|
|
18
30
|
getMdxSource,
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
getMdxSlugs,
|
|
8
8
|
getMdxSource as defaultGetMdxSourceImpl,
|
|
9
9
|
renderPage,
|
|
10
|
+
renderPageMetadata,
|
|
10
11
|
} from "@working-theory/page-renderer";
|
|
12
|
+
import type { Metadata } from "next";
|
|
11
13
|
import { notFound } from "next/navigation";
|
|
12
14
|
import type { ReactElement } from "react";
|
|
13
15
|
|
|
@@ -16,6 +18,13 @@ interface Props {
|
|
|
16
18
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
export async function generateMetadata({
|
|
22
|
+
params,
|
|
23
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
24
|
+
const { locale, slug } = await params;
|
|
25
|
+
return renderPageMetadata({ type: "ResourceDetail", slug, locale });
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
export async function generateStaticParams({
|
|
20
29
|
params,
|
|
21
30
|
}: {
|
package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx
CHANGED
|
@@ -19,6 +19,16 @@ import { render } from "@testing-library/react";
|
|
|
19
19
|
|
|
20
20
|
// ── @working-theory/ui mock ─────────────────────────────────────────────────────────────
|
|
21
21
|
jest.mock("@working-theory/ui", () => ({
|
|
22
|
+
// #4446/#4447: renderPage's own fallback/empty-state title now composes
|
|
23
|
+
// Heading (mobile type-budget classes, #4417) instead of a bare <h1>.
|
|
24
|
+
Heading: ({
|
|
25
|
+
children,
|
|
26
|
+
level,
|
|
27
|
+
}: {
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
level: number;
|
|
30
|
+
}): React.ReactElement =>
|
|
31
|
+
React.createElement(`h${level}`, null, children),
|
|
22
32
|
SectionRenderer: ({ sections }: { sections: unknown[] }) =>
|
|
23
33
|
React.createElement("div", {
|
|
24
34
|
"data-testid": "section-renderer",
|
|
@@ -71,7 +81,7 @@ describe("ResourcesPage — snapshot regression", () => {
|
|
|
71
81
|
});
|
|
72
82
|
const { container } = render(element);
|
|
73
83
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
74
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Resources","description":"Guides, templates, and tools.","mainEntity":{"@type":"ItemList"}}</script><main><h1>Resources</h1><p data-testid="resources-description">Guides, templates, and tools.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source=""></div></article></div></div></main>"`,
|
|
84
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Resources","description":"Guides, templates, and tools.","mainEntity":{"@type":"ItemList"}}</script><main><h1>Resources</h1><p data-testid="resources-description">Guides, templates, and tools.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source=""></div></article></div></div></main>"`,
|
|
75
85
|
);
|
|
76
86
|
});
|
|
77
87
|
});
|
|
@@ -101,7 +111,7 @@ describe("ResourceDetailPage — snapshot regression", () => {
|
|
|
101
111
|
});
|
|
102
112
|
const { container } = render(element);
|
|
103
113
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
104
|
-
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Getting Started","description":"A guide for new users.","mainEntity":{"@type":"Article","headline":"Getting Started"}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Resources","item":"http://localhost:3000/en/resources"},{"@type":"ListItem","position":3,"name":"Getting Started","item":"http://localhost:3000/en/resources/getting-started"}]}]}</script><main><h1>Getting Started</h1><p data-testid="resource-description">A guide for new users.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source=""></div></article></div></div></main>"`,
|
|
114
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Getting Started","description":"A guide for new users.","mainEntity":{"@context":"https://schema.org","@type":"Article","headline":"Getting Started","description":"A guide for new users."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Resources","item":"http://localhost:3000/en/resources"},{"@type":"ListItem","position":3,"name":"Getting Started","item":"http://localhost:3000/en/resources/getting-started"}]}]}</script><main><h1>Getting Started</h1><p data-testid="resource-description">A guide for new users.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source=""></div></article></div></div></main>"`,
|
|
105
115
|
);
|
|
106
116
|
});
|
|
107
117
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import type {
|
|
4
4
|
getMdxSource as defaultGetMdxSource,
|
|
5
5
|
} from "@working-theory/page-renderer";
|
|
6
|
-
import { renderPage } from "@working-theory/page-renderer";
|
|
6
|
+
import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
|
|
7
|
+
import type { Metadata } from "next";
|
|
7
8
|
import type { ReactElement } from "react";
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
@@ -11,6 +12,17 @@ interface Props {
|
|
|
11
12
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export async function generateMetadata({
|
|
16
|
+
params,
|
|
17
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
18
|
+
const { locale } = await params;
|
|
19
|
+
return renderPageMetadata({
|
|
20
|
+
type: "ResourcesHub",
|
|
21
|
+
slug: "resources",
|
|
22
|
+
locale,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
export default async function Page({
|
|
15
27
|
params,
|
|
16
28
|
getMdxSource,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the scheduling module's booking route (#4633).
|
|
4
|
+
*
|
|
5
|
+
* Two things matter here and nothing else does. The page must hand the booker
|
|
6
|
+
* the event identifier and the module's own confirmation path, so a successful
|
|
7
|
+
* booking lands on this site rather than on the provider's; and it must never
|
|
8
|
+
* put a credential into the section data, because section data is exactly what
|
|
9
|
+
* crosses into the client island.
|
|
10
|
+
*/
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { render } from "@testing-library/react";
|
|
13
|
+
|
|
14
|
+
jest.mock("next/navigation", () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
notFound: jest.fn(() => {
|
|
17
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
jest.mock("next-intl/server", () => ({
|
|
22
|
+
__esModule: true,
|
|
23
|
+
// Echo the key back so an assertion names the message key, not the copy —
|
|
24
|
+
// the copy is the consuming site's to change.
|
|
25
|
+
getTranslations: jest.fn().mockResolvedValue((key: string) => key),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Capture what the page hands the catalog rather than rendering the real
|
|
29
|
+
// booker: the island's own behaviour is covered in packages/ui, and mounting it
|
|
30
|
+
// here would put a network stub between this test and the thing it asserts.
|
|
31
|
+
const capturedSections: unknown[] = [];
|
|
32
|
+
jest.mock("@working-theory/ui/sections", () => ({
|
|
33
|
+
__esModule: true,
|
|
34
|
+
SectionRenderer: ({ sections }: { sections: unknown[] }) => {
|
|
35
|
+
capturedSections.splice(0, capturedSections.length, ...sections);
|
|
36
|
+
return React.createElement("div", {
|
|
37
|
+
"data-testid": "section-renderer",
|
|
38
|
+
"data-section-count": String(sections.length),
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
import { notFound } from "next/navigation";
|
|
44
|
+
import SchedulePage from "../page";
|
|
45
|
+
|
|
46
|
+
const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
|
|
47
|
+
|
|
48
|
+
const CAL_LINK = "acme/intro-call";
|
|
49
|
+
|
|
50
|
+
describe("SchedulePage — the module's booking route", () => {
|
|
51
|
+
const originalEnv = process.env["SCHEDULING_CAL_LINK"];
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
mockNotFound.mockClear();
|
|
55
|
+
capturedSections.length = 0;
|
|
56
|
+
process.env["SCHEDULING_CAL_LINK"] = CAL_LINK;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterAll(() => {
|
|
60
|
+
if (originalEnv === undefined) delete process.env["SCHEDULING_CAL_LINK"];
|
|
61
|
+
else process.env["SCHEDULING_CAL_LINK"] = originalEnv;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("renders one booker section from the catalog", async () => {
|
|
65
|
+
const element = await SchedulePage({
|
|
66
|
+
params: Promise.resolve({ locale: "en" }),
|
|
67
|
+
});
|
|
68
|
+
const { container } = render(element);
|
|
69
|
+
|
|
70
|
+
expect(
|
|
71
|
+
container.querySelector('[data-testid="section-renderer"]'),
|
|
72
|
+
).not.toBeNull();
|
|
73
|
+
expect(capturedSections).toHaveLength(1);
|
|
74
|
+
expect((capturedSections[0] as { type: string }).type).toBe("booker");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("sends a successful booking to the page this module owns", async () => {
|
|
78
|
+
render(await SchedulePage({ params: Promise.resolve({ locale: "en" }) }));
|
|
79
|
+
expect(capturedSections).toHaveLength(1);
|
|
80
|
+
|
|
81
|
+
const data = (capturedSections[0] as { data: Record<string, unknown> }).data;
|
|
82
|
+
expect(data["calLink"]).toBe(CAL_LINK);
|
|
83
|
+
expect(data["bookedRedirect"]).toBe("/schedule/confirmed");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("puts no credential into the section data the client island receives", async () => {
|
|
87
|
+
render(await SchedulePage({ params: Promise.resolve({ locale: "en" }) }));
|
|
88
|
+
// Fixture self-guard: an empty capture would make every assertion below
|
|
89
|
+
// pass vacuously.
|
|
90
|
+
expect(capturedSections).toHaveLength(1);
|
|
91
|
+
|
|
92
|
+
// Whatever the page decides to pass, the SHAPE is fixed: the event
|
|
93
|
+
// identifier, page framing, and the redirect. Anything else appearing here
|
|
94
|
+
// is a value crossing into the browser that nobody decided to send.
|
|
95
|
+
const data = (capturedSections[0] as { data: Record<string, unknown> }).data;
|
|
96
|
+
expect(Object.keys(data).sort()).toEqual([
|
|
97
|
+
"bookedRedirect",
|
|
98
|
+
"calLink",
|
|
99
|
+
"eyebrow",
|
|
100
|
+
"headline",
|
|
101
|
+
]);
|
|
102
|
+
// Belt and braces: no serialized value may contain a credential-shaped
|
|
103
|
+
// string, however the page came by it.
|
|
104
|
+
const serialized = JSON.stringify(data);
|
|
105
|
+
expect(serialized).not.toMatch(/api[_-]?key/i);
|
|
106
|
+
expect(serialized).not.toMatch(/\bcal_(live|test)_/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("404s when no event is configured", async () => {
|
|
110
|
+
delete process.env["SCHEDULING_CAL_LINK"];
|
|
111
|
+
|
|
112
|
+
await expect(
|
|
113
|
+
SchedulePage({ params: Promise.resolve({ locale: "en" }) }),
|
|
114
|
+
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
115
|
+
expect(mockNotFound).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the scheduling module's confirmation route (#4633).
|
|
4
|
+
*
|
|
5
|
+
* The page's whole claim is that it is finished but owns no design: it renders
|
|
6
|
+
* real content from the site's own message catalog, through the site's own
|
|
7
|
+
* catalog components, and contributes no styling of its own. Both halves are
|
|
8
|
+
* asserted — that it renders a full page rather than a placeholder, and that
|
|
9
|
+
* every string in it came from the `schedule` namespace rather than from this
|
|
10
|
+
* file.
|
|
11
|
+
*/
|
|
12
|
+
import React from "react";
|
|
13
|
+
import { render } from "@testing-library/react";
|
|
14
|
+
|
|
15
|
+
const requestedKeys: string[] = [];
|
|
16
|
+
jest.mock("next-intl/server", () => ({
|
|
17
|
+
__esModule: true,
|
|
18
|
+
getTranslations: jest.fn().mockResolvedValue((key: string) => {
|
|
19
|
+
requestedKeys.push(key);
|
|
20
|
+
return `copy:${key}`;
|
|
21
|
+
}),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const capturedSections: unknown[] = [];
|
|
25
|
+
jest.mock("@working-theory/ui/sections", () => ({
|
|
26
|
+
__esModule: true,
|
|
27
|
+
SectionRenderer: ({ sections }: { sections: unknown[] }) => {
|
|
28
|
+
capturedSections.splice(0, capturedSections.length, ...sections);
|
|
29
|
+
return React.createElement("div", {
|
|
30
|
+
"data-testid": "section-renderer",
|
|
31
|
+
"data-section-count": String(sections.length),
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
import ConfirmedPage, { generateMetadata } from "../page";
|
|
37
|
+
|
|
38
|
+
describe("ConfirmedPage — the module-owned confirmation page", () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
capturedSections.length = 0;
|
|
41
|
+
requestedKeys.length = 0;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("renders a finished page, not a placeholder", async () => {
|
|
45
|
+
const element = await ConfirmedPage({
|
|
46
|
+
params: Promise.resolve({ locale: "en" }),
|
|
47
|
+
});
|
|
48
|
+
const { container } = render(element);
|
|
49
|
+
|
|
50
|
+
expect(
|
|
51
|
+
container.querySelector('[data-testid="section-renderer"]'),
|
|
52
|
+
).not.toBeNull();
|
|
53
|
+
// Confirmation, what happens next, and a way onward — three sections, all
|
|
54
|
+
// populated. A stub would be one section or none.
|
|
55
|
+
expect(capturedSections.map((s) => (s as { type: string }).type)).toEqual([
|
|
56
|
+
"hero",
|
|
57
|
+
"content-block",
|
|
58
|
+
"cta",
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("takes every string from the site's own message catalog", async () => {
|
|
63
|
+
render(await ConfirmedPage({ params: Promise.resolve({ locale: "en" }) }));
|
|
64
|
+
expect(capturedSections).toHaveLength(3);
|
|
65
|
+
|
|
66
|
+
// Every rendered value resolved through getTranslations, so changing the
|
|
67
|
+
// copy is an edit to messages/<locale>.json — not a fork of this route.
|
|
68
|
+
const serialized = JSON.stringify(capturedSections);
|
|
69
|
+
for (const value of serialized.matchAll(/"(?:headline|subheadline|body|description|label)":"([^"]*)"/g)) {
|
|
70
|
+
expect(value[1]).toMatch(/^copy:confirmed\./);
|
|
71
|
+
}
|
|
72
|
+
expect(requestedKeys.length).toBeGreaterThan(0);
|
|
73
|
+
expect(requestedKeys.every((k) => k.startsWith("confirmed."))).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("declares no styling of its own — the site's design system decides", async () => {
|
|
77
|
+
render(await ConfirmedPage({ params: Promise.resolve({ locale: "en" }) }));
|
|
78
|
+
// Fixture self-guard: an empty capture would make the assertion vacuous.
|
|
79
|
+
expect(capturedSections).toHaveLength(3);
|
|
80
|
+
|
|
81
|
+
// No className, no color, no spacing anywhere in what the page contributes.
|
|
82
|
+
// This is what lets a site with its own design system adopt the page
|
|
83
|
+
// without fighting it.
|
|
84
|
+
const serialized = JSON.stringify(capturedSections);
|
|
85
|
+
expect(serialized).not.toMatch(/className|style|#[0-9a-fA-F]{3,8}\b/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("is not indexable", async () => {
|
|
89
|
+
// Reachable only by having just booked; indexing it would surface a
|
|
90
|
+
// confirmation page to people who never made a booking.
|
|
91
|
+
const metadata = await generateMetadata({
|
|
92
|
+
params: Promise.resolve({ locale: "en" }),
|
|
93
|
+
});
|
|
94
|
+
expect(metadata.robots).toEqual({ index: false, follow: true });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* /[locale]/schedule/confirmed — the scheduling module's confirmation page
|
|
4
|
+
* (#4633). The destination `BookerIsland` navigates to the moment the booking
|
|
5
|
+
* POST resolves (its `bookedRedirect` prop), so the visitor never leaves this
|
|
6
|
+
* site to find out the booking worked.
|
|
7
|
+
*
|
|
8
|
+
* The module OWNS this page. It is not a marketing page that happens to follow
|
|
9
|
+
* a booker: it ships with the module, it is gated with the module, and a
|
|
10
|
+
* scaffold without the module has no such route. That is why it lives under
|
|
11
|
+
* `schedule/` rather than at `/thanks/<slug>` — `/thanks` is a core marketing
|
|
12
|
+
* surface every scaffold carries, and a page in it would outlive the module
|
|
13
|
+
* that gave it a reason to exist.
|
|
14
|
+
*
|
|
15
|
+
* FINISHED, NOT A STUB — and it owns no design of its own. Every visual
|
|
16
|
+
* decision resolves through the consuming site's theme tokens and its
|
|
17
|
+
* `SectionRenderer` catalog; this file contributes a section list and nothing
|
|
18
|
+
* else. The copy is read from the site's own `schedule` message namespace,
|
|
19
|
+
* so customizing the page is an edit to `messages/<locale>.json` — the same
|
|
20
|
+
* surface the site already edits for every other page — not a fork of this
|
|
21
|
+
* route. A site whose design system differs has nothing here to fight.
|
|
22
|
+
*
|
|
23
|
+
* Carries no booking detail because it receives none: the redirect is a
|
|
24
|
+
* client-side `router.push` with no state, and re-reading the booking would
|
|
25
|
+
* mean a second provider call to tell the visitor something the confirmation
|
|
26
|
+
* email already carries. It confirms, and it says what happens next.
|
|
27
|
+
*
|
|
28
|
+
* `noindex`: a booking confirmation is reachable only by having just booked.
|
|
29
|
+
*/
|
|
30
|
+
import { SectionRenderer } from "@working-theory/ui/sections";
|
|
31
|
+
import type { PageSection } from "@working-theory/validation";
|
|
32
|
+
import type { Metadata } from "next";
|
|
33
|
+
import { getTranslations } from "next-intl/server";
|
|
34
|
+
import type { ReactElement } from "react";
|
|
35
|
+
|
|
36
|
+
interface Props {
|
|
37
|
+
params: Promise<{ locale: string }>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
41
|
+
const { locale } = await params;
|
|
42
|
+
const t = await getTranslations({ locale, namespace: "schedule" });
|
|
43
|
+
return {
|
|
44
|
+
title: t("confirmed.title"),
|
|
45
|
+
description: t("confirmed.description"),
|
|
46
|
+
robots: { index: false, follow: true },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default async function Page({ params }: Props): Promise<ReactElement> {
|
|
51
|
+
const { locale } = await params;
|
|
52
|
+
const t = await getTranslations({ locale, namespace: "schedule" });
|
|
53
|
+
|
|
54
|
+
const sections: PageSection[] = [
|
|
55
|
+
{
|
|
56
|
+
id: "schedule-confirmed-hero",
|
|
57
|
+
type: "hero",
|
|
58
|
+
data: {
|
|
59
|
+
headline: t("confirmed.headline"),
|
|
60
|
+
subheadline: t("confirmed.subheadline"),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "schedule-confirmed-prepare",
|
|
65
|
+
type: "content-block",
|
|
66
|
+
data: {
|
|
67
|
+
headline: t("confirmed.prepareHeadline"),
|
|
68
|
+
body: t("confirmed.prepareBody"),
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "schedule-confirmed-cta",
|
|
73
|
+
type: "cta",
|
|
74
|
+
data: {
|
|
75
|
+
headline: t("confirmed.ctaHeadline"),
|
|
76
|
+
description: t("confirmed.ctaDescription"),
|
|
77
|
+
primaryCta: { label: t("confirmed.ctaPrimaryLabel"), href: "/" },
|
|
78
|
+
secondaryCta: {
|
|
79
|
+
label: t("confirmed.ctaSecondaryLabel"),
|
|
80
|
+
href: "/contact",
|
|
81
|
+
variant: "ghost",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<main>
|
|
89
|
+
<SectionRenderer sections={sections} locale={locale} />
|
|
90
|
+
</main>
|
|
91
|
+
);
|
|
92
|
+
}
|