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,82 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* /[locale]/schedule — the scheduling module's booking page (#4633).
|
|
4
|
+
*
|
|
5
|
+
* Exists only because the `scheduling` module is installed: `create-ailk
|
|
6
|
+
* --modules marketing,scheduling` copies this directory, and a scaffold
|
|
7
|
+
* without the module never receives it (packages/create-ailk/src/surfaces.ts,
|
|
8
|
+
* the `schedule` gateable surface).
|
|
9
|
+
*
|
|
10
|
+
* API-backed rather than MDX-backed, the same shape as the `flows` surface:
|
|
11
|
+
* everything the page shows about the event — its title, its duration, its
|
|
12
|
+
* meeting type, its open slots — is DERIVED by the `booker` section from
|
|
13
|
+
* apps/api's `/v1/schedule/availability`, so there is no content file that
|
|
14
|
+
* could disagree with the calendar. The one thing the scaffolder cannot know
|
|
15
|
+
* is WHICH event to book, so that arrives as configuration.
|
|
16
|
+
*
|
|
17
|
+
* CREDENTIALS. `SCHEDULING_CAL_LINK` is read here, on the server, and it is
|
|
18
|
+
* not the API key — the key lives in apps/api's scheduling service and is
|
|
19
|
+
* never read by this app at all. `calLink` is a public identifier (it is the
|
|
20
|
+
* provider's own public booking-page path) and reaching the browser is its
|
|
21
|
+
* job: the booker sends it back to `/v1/schedule/*` to name the event. It is
|
|
22
|
+
* deliberately NOT a `NEXT_PUBLIC_` variable so that the whole scheduling
|
|
23
|
+
* config surface stays server-read, which is the rule architecture.yaml's
|
|
24
|
+
* scheduling block enforces.
|
|
25
|
+
*
|
|
26
|
+
* Unconfigured is a 404, not an error page — the flows precedent: to a
|
|
27
|
+
* visitor, a booking page for an event the operator never named is
|
|
28
|
+
* indistinguishable from a page that does not exist.
|
|
29
|
+
*/
|
|
30
|
+
import { SectionRenderer } from "@working-theory/ui/sections";
|
|
31
|
+
import { registerSchedulingSections } from "@working-theory/ui/slices/scheduling";
|
|
32
|
+
import type { PageSection } from "@working-theory/validation";
|
|
33
|
+
import type { Metadata } from "next";
|
|
34
|
+
import { notFound } from "next/navigation";
|
|
35
|
+
import { getTranslations } from "next-intl/server";
|
|
36
|
+
import type { ReactElement } from "react";
|
|
37
|
+
|
|
38
|
+
// `SectionRenderer`'s oss dispatch maps `booker` to a placeholder, because the
|
|
39
|
+
// real component strips with this slice; this call contributes the real one and
|
|
40
|
+
// must stay an explicit statement, not a bare side-effect import (#5214 D4).
|
|
41
|
+
registerSchedulingSections();
|
|
42
|
+
|
|
43
|
+
/** Site-relative path of the confirmation page this module also owns. */
|
|
44
|
+
const BOOKED_REDIRECT = "/schedule/confirmed";
|
|
45
|
+
|
|
46
|
+
interface Props {
|
|
47
|
+
params: Promise<{ locale: string }>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
51
|
+
const { locale } = await params;
|
|
52
|
+
const t = await getTranslations({ locale, namespace: "schedule" });
|
|
53
|
+
return { title: t("title"), description: t("description") };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default async function Page({ params }: Props): Promise<ReactElement> {
|
|
57
|
+
const { locale } = await params;
|
|
58
|
+
const calLink = process.env["SCHEDULING_CAL_LINK"];
|
|
59
|
+
if (!calLink) {
|
|
60
|
+
notFound();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const t = await getTranslations({ locale, namespace: "schedule" });
|
|
64
|
+
const sections: PageSection[] = [
|
|
65
|
+
{
|
|
66
|
+
id: "schedule-booker",
|
|
67
|
+
type: "booker",
|
|
68
|
+
data: {
|
|
69
|
+
calLink,
|
|
70
|
+
eyebrow: t("eyebrow"),
|
|
71
|
+
headline: t("headline"),
|
|
72
|
+
bookedRedirect: BOOKED_REDIRECT,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<main>
|
|
79
|
+
<SectionRenderer sections={sections} locale={locale} />
|
|
80
|
+
</main>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// next-mdx-remote/rsc, and emits a JSON-LD <script>. Unknown slugs trigger 404.
|
|
3
3
|
import {
|
|
4
4
|
renderPage,
|
|
5
|
+
renderPageMetadata,
|
|
5
6
|
getMdxSource as defaultGetMdxSource,
|
|
6
7
|
getMdxSlugs,
|
|
7
8
|
} from "@working-theory/page-renderer";
|
|
9
|
+
import type { Metadata } from "next";
|
|
8
10
|
import { notFound } from "next/navigation";
|
|
9
11
|
import type { ReactElement } from "react";
|
|
10
12
|
|
|
@@ -13,6 +15,13 @@ interface Props {
|
|
|
13
15
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
export async function generateMetadata({
|
|
19
|
+
params,
|
|
20
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
21
|
+
const { locale, slug } = await params;
|
|
22
|
+
return renderPageMetadata({ type: "Service", slug, locale });
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
export async function generateStaticParams({
|
|
17
26
|
params,
|
|
18
27
|
}: {
|
|
@@ -73,7 +73,7 @@ describe("ServicesPage — snapshot regression", () => {
|
|
|
73
73
|
});
|
|
74
74
|
const { container } = render(element);
|
|
75
75
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
76
|
-
`"<main><h1>Services</h1><p data-testid="section-empty-state">Services listing coming soon.</p></main>"`,
|
|
76
|
+
`"<main><h1 class="text-heading-1 font-display font-normal tracking-tighter text-fg hero-type-display">Services</h1><p data-testid="section-empty-state">Services listing coming soon.</p></main>"`,
|
|
77
77
|
);
|
|
78
78
|
});
|
|
79
79
|
|
|
@@ -86,7 +86,7 @@ describe("ServicesPage — snapshot regression", () => {
|
|
|
86
86
|
});
|
|
87
87
|
const { container } = render(element);
|
|
88
88
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
89
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Services","description":"All services.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Service"}]}}</script><main><h1>Services</h1><p data-testid="services-description">All services.</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="# Services
|
|
89
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Services","description":"All services.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Service"}]}}</script><main><h1>Services</h1><p data-testid="services-description">All services.</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="# Services
|
|
90
90
|
|
|
91
91
|
Body."></div></article></div></div></main>"
|
|
92
92
|
`);
|
|
@@ -110,7 +110,7 @@ describe("ServiceSlugPage — snapshot regression", () => {
|
|
|
110
110
|
});
|
|
111
111
|
const { container } = render(element);
|
|
112
112
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
113
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Implementation consulting","description":"Help adapting AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Service","name":"Implementation consulting","description":"Help adapting AI Launch Kit."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Services","item":"http://localhost:3000/en/services"},{"@type":"ListItem","position":3,"name":"Implementation consulting","item":"http://localhost:3000/en/services/consulting"}]}]}</script><main><h1>Implementation consulting</h1><p data-testid="service-description">Help adapting AI Launch Kit.</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="# Implementation consulting
|
|
113
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Implementation consulting","description":"Help adapting AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Service","name":"Implementation consulting","description":"Help adapting AI Launch Kit."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Services","item":"http://localhost:3000/en/services"},{"@type":"ListItem","position":3,"name":"Implementation consulting","item":"http://localhost:3000/en/services/consulting"}]}]}</script><main><h1>Implementation consulting</h1><p data-testid="service-description">Help adapting AI Launch Kit.</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="# Implementation consulting
|
|
114
114
|
|
|
115
115
|
Body."></div></article></div></div></main>"
|
|
116
116
|
`);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Services list page. Reads ServicesList MDX via the content adapter and emits
|
|
2
2
|
// a JSON-LD CollectionPage via the dispatcher.
|
|
3
3
|
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
4
|
-
import { renderPage } from "@working-theory/page-renderer";
|
|
4
|
+
import { renderPage, renderPageMetadata } from "@working-theory/page-renderer";
|
|
5
|
+
import type { Metadata } from "next";
|
|
5
6
|
import type { ReactElement } from "react";
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
@@ -9,6 +10,13 @@ interface Props {
|
|
|
9
10
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
export async function generateMetadata({
|
|
14
|
+
params,
|
|
15
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
16
|
+
const { locale } = await params;
|
|
17
|
+
return renderPageMetadata({ type: "ServicesList", slug: "services", locale });
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
export default async function Page({
|
|
13
21
|
params,
|
|
14
22
|
getMdxSource,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `resolveNextPath` — the /sign-in `?next=` sanitizer (#4984).
|
|
3
|
+
* Foreign, protocol-relative, backslash, control-char, and non-string inputs
|
|
4
|
+
* all fall back; plain in-app paths pass through unchanged.
|
|
5
|
+
*/
|
|
6
|
+
import { resolveNextPath } from "../next-path";
|
|
7
|
+
|
|
8
|
+
const FALLBACK = "/account";
|
|
9
|
+
|
|
10
|
+
describe("resolveNextPath()", () => {
|
|
11
|
+
it.each(["/account", "/en/account?tab=x", "/waitlist#a"])(
|
|
12
|
+
"accepts the in-app path %p",
|
|
13
|
+
(raw) => {
|
|
14
|
+
expect(resolveNextPath(raw, FALLBACK)).toBe(raw);
|
|
15
|
+
},
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
it.each([
|
|
19
|
+
["protocol-relative", "//evil.com"],
|
|
20
|
+
["backslash host trick", "/\\evil.com"],
|
|
21
|
+
["absolute URL", "https://evil.com"],
|
|
22
|
+
["bare host", "evil.com"],
|
|
23
|
+
["embedded space", "/acc ount"],
|
|
24
|
+
["CRLF", "/x\r\ny"],
|
|
25
|
+
["empty string", ""],
|
|
26
|
+
["undefined", undefined],
|
|
27
|
+
["string[] (repeated param)", ["/a"]],
|
|
28
|
+
])("rejects %s → fallback", (_label, raw) => {
|
|
29
|
+
expect(resolveNextPath(raw, FALLBACK)).toBe(FALLBACK);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `/[locale]/sign-in` page (#4984).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* (a) OAuth-only (google) → sso body, exactly that provider, no email
|
|
6
|
+
* field, no sign-up footer.
|
|
7
|
+
* (b) google + github + email → password body with both social buttons,
|
|
8
|
+
* still no sign-up footer.
|
|
9
|
+
* (c) zero providers → inert card (no buttons, no form, not-configured
|
|
10
|
+
* subhead) — never a fabricated session.
|
|
11
|
+
* (d) provider click → `signIn.social({ provider, callbackURL: next })`;
|
|
12
|
+
* a failed launch renders the providerFailed message.
|
|
13
|
+
* (e) a foreign / protocol-relative `?next=` falls back to /account.
|
|
14
|
+
* (f) password submit → `signIn.email` with the typed credentials; an
|
|
15
|
+
* error result renders the signInFailed message.
|
|
16
|
+
*
|
|
17
|
+
* `next-intl/server` is mocked globally (apps/web/__tests__/setup.ts) against
|
|
18
|
+
* the real en.json, so `signIn` copy is asserted by its real strings.
|
|
19
|
+
*/
|
|
20
|
+
import { render, screen } from "@testing-library/react";
|
|
21
|
+
import userEvent from "@testing-library/user-event";
|
|
22
|
+
|
|
23
|
+
const mockGetActiveProviderIds = jest.fn();
|
|
24
|
+
// eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; auth-env.ts is a pure env-introspection module with no external boundary to mock instead.
|
|
25
|
+
jest.mock("../../../../lib/auth-env", () => ({
|
|
26
|
+
__esModule: true,
|
|
27
|
+
getActiveProviderIds: () => mockGetActiveProviderIds(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const mockSocial = jest.fn();
|
|
31
|
+
const mockEmail = jest.fn();
|
|
32
|
+
jest.mock("better-auth/react", () => ({
|
|
33
|
+
__esModule: true,
|
|
34
|
+
createAuthClient: () => ({
|
|
35
|
+
signIn: {
|
|
36
|
+
social: (...args: unknown[]) => mockSocial(...args),
|
|
37
|
+
email: (...args: unknown[]) => mockEmail(...args),
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
import en from "../../../../messages/en.json";
|
|
43
|
+
import Page, { generateMetadata } from "../page";
|
|
44
|
+
|
|
45
|
+
async function renderPage(next?: string): Promise<void> {
|
|
46
|
+
const el = await Page({
|
|
47
|
+
params: Promise.resolve({ locale: "en" }),
|
|
48
|
+
searchParams: Promise.resolve(next === undefined ? {} : { next }),
|
|
49
|
+
});
|
|
50
|
+
render(el);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
mockGetActiveProviderIds.mockReset().mockReturnValue(["google"]);
|
|
55
|
+
mockSocial.mockReset().mockResolvedValue({ data: {}, error: null });
|
|
56
|
+
mockEmail.mockReset().mockResolvedValue({ data: {}, error: null });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("generateMetadata()", () => {
|
|
60
|
+
it("uses the signIn namespace title + description", async () => {
|
|
61
|
+
const meta = await generateMetadata({
|
|
62
|
+
params: Promise.resolve({ locale: "en" }),
|
|
63
|
+
searchParams: Promise.resolve({}),
|
|
64
|
+
});
|
|
65
|
+
expect(meta.title).toBe(en.signIn.title);
|
|
66
|
+
expect(meta.description).toBe(en.signIn.description);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("/sign-in page", () => {
|
|
71
|
+
it("(a) OAuth-only: sso body with exactly the reported provider, no sign-up footer", async () => {
|
|
72
|
+
await renderPage("/account");
|
|
73
|
+
expect(screen.getByTestId("sign-in-page")).toBeInTheDocument();
|
|
74
|
+
expect(screen.getByTestId("auth-card")).toBeInTheDocument();
|
|
75
|
+
expect(screen.getByTestId("auth-sso-google")).toBeInTheDocument();
|
|
76
|
+
expect(screen.queryByTestId("auth-sso-github")).toBeNull();
|
|
77
|
+
expect(screen.queryByTestId("auth-email")).toBeNull();
|
|
78
|
+
expect(screen.queryByTestId("auth-submit")).toBeNull();
|
|
79
|
+
expect(screen.queryByTestId("auth-footer-link")).toBeNull();
|
|
80
|
+
expect(screen.queryByText(/create an account/i)).toBeNull();
|
|
81
|
+
expect(screen.getByText(en.signIn.heading)).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("(b) email + OAuth: password body with both social buttons, no sign-up footer", async () => {
|
|
85
|
+
mockGetActiveProviderIds.mockReturnValue(["google", "github", "email"]);
|
|
86
|
+
await renderPage("/account");
|
|
87
|
+
expect(screen.getByTestId("auth-email")).toBeInTheDocument();
|
|
88
|
+
expect(screen.getByTestId("auth-password")).toBeInTheDocument();
|
|
89
|
+
expect(screen.getByTestId("auth-social-row")).toBeInTheDocument();
|
|
90
|
+
expect(screen.getByTestId("auth-social-google")).toBeInTheDocument();
|
|
91
|
+
expect(screen.getByTestId("auth-social-github")).toBeInTheDocument();
|
|
92
|
+
expect(screen.queryByTestId("auth-sso-stack")).toBeNull();
|
|
93
|
+
expect(screen.queryByTestId("auth-footer-link")).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("(c) zero providers: inert card with the not-configured subhead, no buttons, no form", async () => {
|
|
97
|
+
mockGetActiveProviderIds.mockReturnValue([]);
|
|
98
|
+
await renderPage();
|
|
99
|
+
expect(screen.getByTestId("auth-card")).toBeInTheDocument();
|
|
100
|
+
expect(screen.queryByTestId("auth-sso-google")).toBeNull();
|
|
101
|
+
expect(screen.queryByTestId("auth-sso-github")).toBeNull();
|
|
102
|
+
expect(screen.queryByTestId("auth-submit")).toBeNull();
|
|
103
|
+
expect(screen.queryByTestId("auth-email")).toBeNull();
|
|
104
|
+
expect(screen.getByText(en.signIn.notConfiguredSubhead)).toBeInTheDocument();
|
|
105
|
+
expect(mockSocial).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("(d) provider click launches signIn.social with the sanitised next path", async () => {
|
|
109
|
+
const user = userEvent.setup();
|
|
110
|
+
await renderPage("/account");
|
|
111
|
+
await user.click(screen.getByTestId("auth-sso-google"));
|
|
112
|
+
expect(mockSocial).toHaveBeenCalledTimes(1);
|
|
113
|
+
expect(mockSocial).toHaveBeenCalledWith({
|
|
114
|
+
provider: "google",
|
|
115
|
+
callbackURL: "/account",
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("(d) a failed provider launch renders the providerFailed message", async () => {
|
|
120
|
+
const user = userEvent.setup();
|
|
121
|
+
mockSocial.mockResolvedValue({ data: null, error: { message: "nope" } });
|
|
122
|
+
await renderPage("/account");
|
|
123
|
+
await user.click(screen.getByTestId("auth-sso-google"));
|
|
124
|
+
expect(await screen.findByText(en.signIn.providerFailed)).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("(e) a protocol-relative ?next= falls back to /account", async () => {
|
|
128
|
+
const user = userEvent.setup();
|
|
129
|
+
await renderPage("//evil.com");
|
|
130
|
+
await user.click(screen.getByTestId("auth-sso-google"));
|
|
131
|
+
expect(mockSocial).toHaveBeenCalledWith({
|
|
132
|
+
provider: "google",
|
|
133
|
+
callbackURL: "/account",
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("(f) password submit calls signIn.email and surfaces signInFailed on error", async () => {
|
|
138
|
+
const user = userEvent.setup();
|
|
139
|
+
mockGetActiveProviderIds.mockReturnValue(["google", "email"]);
|
|
140
|
+
mockEmail.mockResolvedValue({ data: null, error: { message: "bad" } });
|
|
141
|
+
await renderPage("/en/account?tab=x");
|
|
142
|
+
await user.type(screen.getByTestId("auth-email"), "ada@example.com");
|
|
143
|
+
await user.type(screen.getByTestId("auth-password"), "s3cr3t");
|
|
144
|
+
await user.click(screen.getByTestId("auth-submit"));
|
|
145
|
+
expect(mockEmail).toHaveBeenCalledWith({
|
|
146
|
+
email: "ada@example.com",
|
|
147
|
+
password: "s3cr3t",
|
|
148
|
+
callbackURL: "/en/account?tab=x",
|
|
149
|
+
});
|
|
150
|
+
expect(await screen.findByText(en.signIn.signInFailed)).toBeInTheDocument();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
Binary file
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/[locale]/sign-in` — the site's generic sign-in page (#4984).
|
|
3
|
+
*
|
|
4
|
+
* RSC: resolves the active providers from auth-env, the sign-in method
|
|
5
|
+
* (password when the email provider is active, else sso), the sanitised
|
|
6
|
+
* `?next=` return path, and the translated `signIn` labels — then renders the
|
|
7
|
+
* `SignInCard` client island. The locale layout above owns the Nav/Footer +
|
|
8
|
+
* MarketingShell frame; this page contributes only the centered card region
|
|
9
|
+
* (no nested shell).
|
|
10
|
+
*
|
|
11
|
+
* Signed-in visitors are not redirected here — the Nav's session-aware action
|
|
12
|
+
* cluster (Account / Sign out) is the affordance for them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { localeHref } from "@working-theory/ui";
|
|
16
|
+
import type { AuthCardLabels } from "@working-theory/ui/blocks";
|
|
17
|
+
import type { Metadata } from "next";
|
|
18
|
+
import { getTranslations } from "next-intl/server";
|
|
19
|
+
import type { ReactElement } from "react";
|
|
20
|
+
|
|
21
|
+
import { getActiveProviderIds } from "../../../lib/auth-env";
|
|
22
|
+
import { siteLogo } from "../../../lib/site-brand";
|
|
23
|
+
|
|
24
|
+
import { resolveNextPath } from "./next-path";
|
|
25
|
+
import { SignInCard, type SignInProvider } from "./sign-in-card";
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
params: Promise<{ locale: string }>;
|
|
29
|
+
searchParams: Promise<{ next?: string | string[] }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
33
|
+
const { locale } = await params;
|
|
34
|
+
const t = await getTranslations({ locale, namespace: "signIn" });
|
|
35
|
+
return { title: t("title"), description: t("description") };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default async function SignInPage({
|
|
39
|
+
params,
|
|
40
|
+
searchParams,
|
|
41
|
+
}: Props): Promise<ReactElement> {
|
|
42
|
+
const { locale } = await params;
|
|
43
|
+
const t = await getTranslations({ locale, namespace: "signIn" });
|
|
44
|
+
|
|
45
|
+
const active = getActiveProviderIds();
|
|
46
|
+
// Exactly the OAuth providers auth-env reports — never a default set.
|
|
47
|
+
const providers = active.filter(
|
|
48
|
+
(id): id is SignInProvider => id === "google" || id === "github",
|
|
49
|
+
);
|
|
50
|
+
const method = active.includes("email") ? "password" : "sso";
|
|
51
|
+
const next = resolveNextPath(
|
|
52
|
+
(await searchParams).next,
|
|
53
|
+
localeHref(locale, "/account"),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const labels: AuthCardLabels = {
|
|
57
|
+
signInHeading: t("heading"),
|
|
58
|
+
signInSubhead: t("subhead"),
|
|
59
|
+
ssoPrefix: t("ssoPrefix"),
|
|
60
|
+
google: t("google"),
|
|
61
|
+
github: t("github"),
|
|
62
|
+
email: t("email"),
|
|
63
|
+
password: t("password"),
|
|
64
|
+
forgot: t("forgot"),
|
|
65
|
+
signInSubmit: t("submit"),
|
|
66
|
+
orContinue: t("orContinue"),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<SignInCard
|
|
71
|
+
providers={providers}
|
|
72
|
+
method={method}
|
|
73
|
+
next={next}
|
|
74
|
+
logo={siteLogo()}
|
|
75
|
+
labels={labels}
|
|
76
|
+
notConfiguredSubhead={t("notConfiguredSubhead")}
|
|
77
|
+
errorLabels={{
|
|
78
|
+
signInFailed: t("signInFailed"),
|
|
79
|
+
providerFailed: t("providerFailed"),
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client"; // Better Auth's client calls (signIn.social / signIn.email) + error state
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SignInCard — the /sign-in page's client island (#4984).
|
|
5
|
+
*
|
|
6
|
+
* Composes the shipped `AuthCard` block (`intent="sign-in"`, `footer="none"`)
|
|
7
|
+
* and wires its provider buttons / password form to the Better Auth client,
|
|
8
|
+
* using the SAME `createAuthClient()` idiom the `(authed)` group's
|
|
9
|
+
* `SignInGate` uses. The page (RSC) decides providers + method from auth-env
|
|
10
|
+
* and hands them in; this component never reads env itself.
|
|
11
|
+
*
|
|
12
|
+
* No sign-up affordance anywhere: `footer="none"` drops the "Not a member?
|
|
13
|
+
* Create an account" cross-link (Better Auth `emailAndPassword.disableSignUp`
|
|
14
|
+
* posture).
|
|
15
|
+
*
|
|
16
|
+
* Password path: `apps/web/auth.ts` does not enable `emailAndPassword` today,
|
|
17
|
+
* so `signIn.email` is wired but inert (Better Auth answers with an error)
|
|
18
|
+
* until an operator enables it server-side — the page only selects
|
|
19
|
+
* `method="password"` when auth-env reports the "email" provider.
|
|
20
|
+
*
|
|
21
|
+
* Zero providers (method sso, providers []): the card renders in its inert
|
|
22
|
+
* state — heading + `notConfiguredSubhead`, no buttons, no form. Never a
|
|
23
|
+
* fabricated session.
|
|
24
|
+
*
|
|
25
|
+
* Error surface: AuthCard's `errors` prop is per-field. A failed provider
|
|
26
|
+
* launch lands on `errors.email` — the social row sits inside the password
|
|
27
|
+
* form beside the email field, so it renders there. The SSO body has no
|
|
28
|
+
* fields at all, so for `method="sso"` the same message is rendered as an
|
|
29
|
+
* `Alert` above the card instead.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
|
|
33
|
+
import { Alert, AlertDescription } from "@working-theory/ui/primitives";
|
|
34
|
+
import { createAuthClient } from "better-auth/react";
|
|
35
|
+
import type { ReactElement, ReactNode } from "react";
|
|
36
|
+
import { useState } from "react";
|
|
37
|
+
|
|
38
|
+
const authClient = createAuthClient();
|
|
39
|
+
|
|
40
|
+
/** The OAuth providers this page can launch — a subset of AuthCard's. */
|
|
41
|
+
export type SignInProvider = "google" | "github";
|
|
42
|
+
|
|
43
|
+
export interface SignInCardProps {
|
|
44
|
+
/** Active OAuth providers, exactly as auth-env reports them. */
|
|
45
|
+
providers: SignInProvider[];
|
|
46
|
+
/** "password" when the email provider is active; otherwise "sso". */
|
|
47
|
+
method: "sso" | "password";
|
|
48
|
+
/** Sanitised post-sign-in path (see `next-path.ts`). */
|
|
49
|
+
next: string;
|
|
50
|
+
/** Brand mark rendered top-center inside the card. */
|
|
51
|
+
logo?: ReactNode;
|
|
52
|
+
/** Translated AuthCard labels (resolved server-side by the page). */
|
|
53
|
+
labels: AuthCardLabels;
|
|
54
|
+
/** Subhead shown when no provider is configured. */
|
|
55
|
+
notConfiguredSubhead: string;
|
|
56
|
+
/** Translated error copy. */
|
|
57
|
+
errorLabels: { signInFailed: string; providerFailed: string };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type FieldErrors = { email?: string; password?: string };
|
|
61
|
+
|
|
62
|
+
export function SignInCard({
|
|
63
|
+
providers,
|
|
64
|
+
method,
|
|
65
|
+
next,
|
|
66
|
+
logo,
|
|
67
|
+
labels,
|
|
68
|
+
notConfiguredSubhead,
|
|
69
|
+
errorLabels,
|
|
70
|
+
}: SignInCardProps): ReactElement {
|
|
71
|
+
const [errors, setErrors] = useState<FieldErrors>({});
|
|
72
|
+
const [providerError, setProviderError] = useState<string | null>(null);
|
|
73
|
+
|
|
74
|
+
const notConfigured = method === "sso" && providers.length === 0;
|
|
75
|
+
|
|
76
|
+
const launchProvider = async (provider: SignInProvider): Promise<void> => {
|
|
77
|
+
setProviderError(null);
|
|
78
|
+
// Better Auth reports a failed launch as `{ error }`; a network/runtime
|
|
79
|
+
// failure rejects. Both land on the same providerFailed message.
|
|
80
|
+
const failed = await authClient.signIn
|
|
81
|
+
.social({ provider, callbackURL: next })
|
|
82
|
+
.then((res) => Boolean(res?.error), () => true);
|
|
83
|
+
if (failed) {
|
|
84
|
+
setProviderError(errorLabels.providerFailed);
|
|
85
|
+
setErrors((prev) => ({ ...prev, email: errorLabels.providerFailed }));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const submitPassword = async (email: string, password: string): Promise<void> => {
|
|
90
|
+
setErrors({});
|
|
91
|
+
const res = await authClient.signIn.email({ email, password, callbackURL: next });
|
|
92
|
+
if (res?.error) setErrors({ password: errorLabels.signInFailed });
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
className="flex min-h-screen items-center justify-center py-section"
|
|
98
|
+
data-testid="sign-in-page"
|
|
99
|
+
>
|
|
100
|
+
<div className="flex w-full max-w-md flex-col gap-4">
|
|
101
|
+
{method === "sso" && providerError ? (
|
|
102
|
+
<Alert variant="error" data-testid="sign-in-provider-error">
|
|
103
|
+
<AlertDescription>{providerError}</AlertDescription>
|
|
104
|
+
</Alert>
|
|
105
|
+
) : null}
|
|
106
|
+
<AuthCard
|
|
107
|
+
intent="sign-in"
|
|
108
|
+
method={method}
|
|
109
|
+
footer="none"
|
|
110
|
+
{...(logo !== undefined ? { logo } : {})}
|
|
111
|
+
{...(notConfigured ? { subhead: notConfiguredSubhead } : {})}
|
|
112
|
+
providers={providers.map((provider) => ({
|
|
113
|
+
provider,
|
|
114
|
+
onClick: () => void launchProvider(provider),
|
|
115
|
+
}))}
|
|
116
|
+
onSubmit={(data) => void submitPassword(data.email, data.password ?? "")}
|
|
117
|
+
errors={errors}
|
|
118
|
+
labels={labels}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// SoftwareProduct detail page. Reads the SoftwareProduct MDX for the given slug,
|
|
2
2
|
// renders via @working-theory/page-renderer, and emits a JSON-LD <script>. Unknown slugs 404.
|
|
3
3
|
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getMdxSlugs,
|
|
6
|
+
renderPage,
|
|
7
|
+
renderPageMetadata,
|
|
8
|
+
} from "@working-theory/page-renderer";
|
|
9
|
+
import type { Metadata } from "next";
|
|
5
10
|
import type { ReactElement } from "react";
|
|
6
11
|
|
|
7
12
|
interface Props {
|
|
@@ -9,6 +14,13 @@ interface Props {
|
|
|
9
14
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
10
15
|
}
|
|
11
16
|
|
|
17
|
+
export async function generateMetadata({
|
|
18
|
+
params,
|
|
19
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
20
|
+
const { locale, slug } = await params;
|
|
21
|
+
return renderPageMetadata({ type: "SoftwareProduct", slug, locale });
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
export async function generateStaticParams({
|
|
13
25
|
params,
|
|
14
26
|
}: {
|
|
@@ -61,7 +61,7 @@ describe("software/[slug] — snapshot regression", () => {
|
|
|
61
61
|
});
|
|
62
62
|
const { container } = render(element);
|
|
63
63
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
64
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"@working-theory/page-renderer","description":"Single rendering primitive for MDX-wired routes.","mainEntity":{"@type":"SoftwareApplication","name":"@working-theory/page-renderer"}}</script><main><h1>@working-theory/page-renderer</h1><p data-testid="software-description">Single rendering primitive for MDX-wired routes.</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-rendered">## Overview
|
|
64
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"@working-theory/page-renderer","description":"Single rendering primitive for MDX-wired routes.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"@working-theory/page-renderer","description":"Single rendering primitive for MDX-wired routes."}}</script><main><h1>@working-theory/page-renderer</h1><p data-testid="software-description">Single rendering primitive for MDX-wired routes.</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-rendered">## Overview
|
|
65
65
|
|
|
66
66
|
Body content.</div></article></div></div></main>"
|
|
67
67
|
`);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// TechArticle detail page. Reads the TechArticle MDX for the given slug, renders via
|
|
2
2
|
// @working-theory/page-renderer, and emits a JSON-LD <script>. Unknown slugs trigger 404.
|
|
3
3
|
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getMdxSlugs,
|
|
6
|
+
renderPage,
|
|
7
|
+
renderPageMetadata,
|
|
8
|
+
} from "@working-theory/page-renderer";
|
|
9
|
+
import type { Metadata } from "next";
|
|
5
10
|
import type { ReactElement } from "react";
|
|
6
11
|
|
|
7
12
|
interface Props {
|
|
@@ -9,6 +14,13 @@ interface Props {
|
|
|
9
14
|
getMdxSource?: typeof defaultGetMdxSource;
|
|
10
15
|
}
|
|
11
16
|
|
|
17
|
+
export async function generateMetadata({
|
|
18
|
+
params,
|
|
19
|
+
}: Pick<Props, "params">): Promise<Metadata> {
|
|
20
|
+
const { locale, slug } = await params;
|
|
21
|
+
return renderPageMetadata({ type: "TechArticle", slug, locale });
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
export async function generateStaticParams({
|
|
13
25
|
params,
|
|
14
26
|
}: {
|