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,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Day-one structured-data check for the site this app ships (#5021).
|
|
3
|
+
*
|
|
4
|
+
* Two checks, both delegating to `@working-theory/schema-validator` so they
|
|
5
|
+
* get stricter as the validator does (the #5019 `mainEntity` rule included):
|
|
6
|
+
*
|
|
7
|
+
* 1. Content-driven — every content page under `content/<locale>/<Type>/`
|
|
8
|
+
* is built into JSON-LD through `pageToJsonLd`, the same path the app
|
|
9
|
+
* takes at render time (builders + pack + breadcrumb + section
|
|
10
|
+
* contributions), flattened, and validated for its type.
|
|
11
|
+
* 2. Route-emission — every `page.tsx` under `app/[locale]/` is rendered
|
|
12
|
+
* with the site's own content and must emit a
|
|
13
|
+
* `<script type="application/ld+json">` that validates for the route's
|
|
14
|
+
* type. A route that renders nothing, or that is not built on
|
|
15
|
+
* `renderPage`, fails unless it is listed in `NO_STRUCTURED_DATA_ROUTES`
|
|
16
|
+
* with a reason.
|
|
17
|
+
*
|
|
18
|
+
* Nothing here is a fixture list: the content walk reads whatever the site
|
|
19
|
+
* has under `content/`, and the route walk reads whatever the app has under
|
|
20
|
+
* `app/[locale]/`. A page written today is checked today.
|
|
21
|
+
*
|
|
22
|
+
* Lives in `lib/__tests__/` because `create-ailk`'s template snapshot strips
|
|
23
|
+
* `apps/web/__tests__` and `apps/web/app/[locale]/__tests__` (#4154) — a
|
|
24
|
+
* scaffolded site keeps this file. `packages/create-ailk/__tests__/
|
|
25
|
+
* structured-data-check.test.ts` pins that.
|
|
26
|
+
*
|
|
27
|
+
* Pages are enumerated through the content adapter rather than
|
|
28
|
+
* `content/.compiled/manifest.json`: the manifest is written by
|
|
29
|
+
* `tools/content-compile`, which the scaffold does not ship, and the adapter
|
|
30
|
+
* reads the same files with the same frontmatter schema the manifest is
|
|
31
|
+
* compiled from. See docs/internal/decisions/scaffold-structured-data-check.md.
|
|
32
|
+
*/
|
|
33
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
34
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
35
|
+
|
|
36
|
+
import { render } from "@testing-library/react";
|
|
37
|
+
import type {
|
|
38
|
+
ContentPage,
|
|
39
|
+
SchemaOrgType as PageType,
|
|
40
|
+
} from "@working-theory/content-adapters";
|
|
41
|
+
import {
|
|
42
|
+
SCHEMA_ORG_TYPES,
|
|
43
|
+
createMdxAdapter,
|
|
44
|
+
} from "@working-theory/content-adapters";
|
|
45
|
+
import {
|
|
46
|
+
getPageSections,
|
|
47
|
+
pageToJsonLd,
|
|
48
|
+
type JsonLdObject,
|
|
49
|
+
} from "@working-theory/page-renderer";
|
|
50
|
+
import {
|
|
51
|
+
knownSchemaOrgTypes,
|
|
52
|
+
validatePageGraph,
|
|
53
|
+
type SchemaOrgType,
|
|
54
|
+
type ValidationError,
|
|
55
|
+
} from "@working-theory/schema-validator";
|
|
56
|
+
import type { ReactElement } from "react";
|
|
57
|
+
import { parse as parseYaml } from "yaml";
|
|
58
|
+
|
|
59
|
+
// Detail routes call notFound() on a missing page; a thrown error is how the
|
|
60
|
+
// test sees it (the same shape the per-surface suites use).
|
|
61
|
+
jest.mock("next/navigation", () => ({
|
|
62
|
+
__esModule: true,
|
|
63
|
+
...jest.requireActual<Record<string, unknown>>("next/navigation"),
|
|
64
|
+
notFound: jest.fn(() => {
|
|
65
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
66
|
+
}),
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
// Telemetry is a side-effect boundary (the contact route's intake wrapper
|
|
70
|
+
// reports through it); the same mock the contact suites use.
|
|
71
|
+
jest.mock("@working-theory/analytics/server", () => ({
|
|
72
|
+
track: jest.fn().mockResolvedValue(undefined),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
// ── Where the site's content and routes live ────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const CONTENT_ROOT =
|
|
78
|
+
process.env.AILK_CONTENT_ROOT ?? resolve(__dirname, "../../../../content");
|
|
79
|
+
const ROUTE_ROOT = resolve(__dirname, "../../app/[locale]");
|
|
80
|
+
const SITE_ROOT = resolve(__dirname, "../../../..");
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The site's default locale, read the way `next.config.mjs` bakes it
|
|
84
|
+
* (`project.yaml` → `product.default_locale`, "en" when absent). The env var
|
|
85
|
+
* is the baked value at build time; under jest it is normally unset, so the
|
|
86
|
+
* file is the source and the env var only an override.
|
|
87
|
+
*/
|
|
88
|
+
function readDefaultLocale(): string {
|
|
89
|
+
const fromEnv = process.env.NEXT_PUBLIC_DEFAULT_LOCALE;
|
|
90
|
+
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv;
|
|
91
|
+
const file = join(SITE_ROOT, "project.yaml");
|
|
92
|
+
if (!existsSync(file)) return "en";
|
|
93
|
+
const doc = parseYaml(readFileSync(file, "utf8")) as {
|
|
94
|
+
product?: { default_locale?: unknown };
|
|
95
|
+
} | null;
|
|
96
|
+
const value = doc?.product?.default_locale;
|
|
97
|
+
return typeof value === "string" && value.length > 0 ? value : "en";
|
|
98
|
+
}
|
|
99
|
+
const DEFAULT_LOCALE = readDefaultLocale();
|
|
100
|
+
|
|
101
|
+
// `pageToJsonLd`'s content selector resolves `content/` from cwd; pin it to
|
|
102
|
+
// the same root this file walks so the route check renders the same pages.
|
|
103
|
+
if (process.env.AILK_CONTENT_ROOT === undefined) {
|
|
104
|
+
process.env.AILK_CONTENT_ROOT = CONTENT_ROOT;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Mirrors the locale-directory rule in tools/content-compile and the adapter.
|
|
108
|
+
const LOCALE_RE = /^[a-z]{2}(-[A-Z]{2})?$/;
|
|
109
|
+
|
|
110
|
+
function subdirs(dir: string): string[] {
|
|
111
|
+
if (!existsSync(dir)) return [];
|
|
112
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
113
|
+
.filter((e) => e.isDirectory())
|
|
114
|
+
.map((e) => e.name)
|
|
115
|
+
.sort();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `content/<locale>/<TypeDir>` → every (locale, type dir) pair on disk. */
|
|
119
|
+
function contentTypeDirs(): Array<{ locale: string; typeDir: string }> {
|
|
120
|
+
const out: Array<{ locale: string; typeDir: string }> = [];
|
|
121
|
+
for (const locale of subdirs(CONTENT_ROOT).filter((n) => LOCALE_RE.test(n))) {
|
|
122
|
+
for (const typeDir of subdirs(join(CONTENT_ROOT, locale))) {
|
|
123
|
+
// `_drafts/`-style hidden dirs are invisible to the adapter too.
|
|
124
|
+
if (typeDir.startsWith("_") || typeDir.startsWith(".")) continue;
|
|
125
|
+
out.push({ locale, typeDir });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const adapter = createMdxAdapter({
|
|
132
|
+
contentRoot: CONTENT_ROOT,
|
|
133
|
+
defaultLocale: DEFAULT_LOCALE,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── JSON-LD flatten + validate (the ten lines from tools/scrape-jsonld.ts) ──
|
|
137
|
+
|
|
138
|
+
function flattenBlocks(parsed: unknown): JsonLdObject[] {
|
|
139
|
+
if (parsed === null || typeof parsed !== "object") return [];
|
|
140
|
+
if (Array.isArray(parsed))
|
|
141
|
+
return parsed.flatMap((item) => flattenBlocks(item));
|
|
142
|
+
const obj = parsed as Record<string, unknown>;
|
|
143
|
+
if (Array.isArray(obj["@graph"])) {
|
|
144
|
+
const ctx = obj["@context"] as JsonLdObject["@context"] | undefined;
|
|
145
|
+
return (obj["@graph"] as unknown[]).flatMap((child) =>
|
|
146
|
+
flattenBlocks(child).map((b) =>
|
|
147
|
+
ctx === undefined || b["@context"] !== undefined
|
|
148
|
+
? b
|
|
149
|
+
: { ...b, "@context": ctx },
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return [obj as JsonLdObject];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatErrors(errors: ValidationError[]): string {
|
|
157
|
+
return errors
|
|
158
|
+
.map(
|
|
159
|
+
(e) => ` [${e.rule}] ${e.path}: expected ${e.expected}, got ${e.actual}`,
|
|
160
|
+
)
|
|
161
|
+
.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Validate flattened blocks for a page type; returns a readable failure or null. */
|
|
165
|
+
function validateFor(
|
|
166
|
+
blocks: JsonLdObject[],
|
|
167
|
+
type: string,
|
|
168
|
+
label: string,
|
|
169
|
+
): string | null {
|
|
170
|
+
if (!knownSchemaOrgTypes.has(type)) {
|
|
171
|
+
return `${label}: type "${type}" has no validator rule — it is not a registered page type (see packages/schema-validator/src/rules.ts)`;
|
|
172
|
+
}
|
|
173
|
+
const result = validatePageGraph(blocks, type as SchemaOrgType);
|
|
174
|
+
if (result.valid) return null;
|
|
175
|
+
return `${label} (${type}):\n${formatErrors(result.errors)}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Every `application/ld+json` block in rendered HTML, flattened. */
|
|
179
|
+
function extractJsonLdBlocks(container: HTMLElement): JsonLdObject[] {
|
|
180
|
+
const scripts = container.querySelectorAll<HTMLScriptElement>(
|
|
181
|
+
'script[type="application/ld+json"]',
|
|
182
|
+
);
|
|
183
|
+
const blocks: JsonLdObject[] = [];
|
|
184
|
+
for (const script of Array.from(scripts)) {
|
|
185
|
+
blocks.push(...flattenBlocks(JSON.parse(script.textContent ?? "")));
|
|
186
|
+
}
|
|
187
|
+
return blocks;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── 1. Content-driven: every content page builds valid JSON-LD ──────────────
|
|
191
|
+
|
|
192
|
+
describe("structured data — every content page builds valid JSON-LD", () => {
|
|
193
|
+
const typeDirs = contentTypeDirs();
|
|
194
|
+
|
|
195
|
+
it("finds content to check", () => {
|
|
196
|
+
// A site with no content directory at all is misconfigured, not clean.
|
|
197
|
+
expect(existsSync(CONTENT_ROOT)).toBe(true);
|
|
198
|
+
expect(typeDirs.length).toBeGreaterThan(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it.each(typeDirs)(
|
|
202
|
+
"content/$locale/$typeDir — every page validates as its type",
|
|
203
|
+
async ({ locale, typeDir }) => {
|
|
204
|
+
if (!(SCHEMA_ORG_TYPES as readonly string[]).includes(typeDir)) {
|
|
205
|
+
// Identity + sub-entity types (Organization, Person, BreadcrumbList,
|
|
206
|
+
// ContactPoint) hold sub-objects other pages' builders embed — never
|
|
207
|
+
// rendered as pages, and skipped by tools/content-compile the same
|
|
208
|
+
// way. Anything else is a directory nothing will ever render.
|
|
209
|
+
if (!knownSchemaOrgTypes.has(typeDir)) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`content/${locale}/${typeDir}: "${typeDir}" is not a registered page type — no route or builder will ever render it (see packages/content-adapters/src/types.ts SCHEMA_ORG_TYPES)`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
console.info(
|
|
215
|
+
`structured-data: content/${locale}/${typeDir} holds sub-objects, not pages — not checked`,
|
|
216
|
+
);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const type = typeDir as PageType;
|
|
220
|
+
|
|
221
|
+
const pages: ContentPage[] = await adapter.listDetail(type, locale);
|
|
222
|
+
const dirPath = join(CONTENT_ROOT, locale, typeDir);
|
|
223
|
+
const mdxCount = readdirSync(dirPath).filter(
|
|
224
|
+
(n) => n.endsWith(".mdx") && !n.startsWith("_"),
|
|
225
|
+
).length;
|
|
226
|
+
// The adapter drops a page whose frontmatter fails the schema; that
|
|
227
|
+
// page would render nothing, so count it here rather than lose it.
|
|
228
|
+
expect(pages.length).toBe(mdxCount);
|
|
229
|
+
|
|
230
|
+
const failures: string[] = [];
|
|
231
|
+
for (const page of pages) {
|
|
232
|
+
const sections = await getPageSections(page);
|
|
233
|
+
const jsonLd = await pageToJsonLd(page, locale, sections);
|
|
234
|
+
const failure = validateFor(
|
|
235
|
+
flattenBlocks(jsonLd),
|
|
236
|
+
page.type,
|
|
237
|
+
`${locale}/${page.type}/${page.slug}`,
|
|
238
|
+
);
|
|
239
|
+
if (failure !== null) failures.push(failure);
|
|
240
|
+
}
|
|
241
|
+
expect(failures).toEqual([]);
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ── 2. Route-emission: every route renders a valid JSON-LD script ───────────
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Routes (relative to `app/[locale]/`) that do not emit page-level JSON-LD
|
|
250
|
+
* through `renderPage`, each with the reason. An entry matches its own
|
|
251
|
+
* directory only; `subtree: true` extends it to every route below it. A route
|
|
252
|
+
* that is neither a `renderPage` route nor listed here fails — that is the
|
|
253
|
+
* "fourteen routes emitting nothing" failure class this check exists for.
|
|
254
|
+
*
|
|
255
|
+
* Listing a route here is a statement that its JSON-LD is someone else's job
|
|
256
|
+
* (or that it has none to emit) — not a way to make a red route green: an
|
|
257
|
+
* exempt route whose page.tsx does render through `renderPage` fails too,
|
|
258
|
+
* because the reason given for it is false.
|
|
259
|
+
*/
|
|
260
|
+
const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
|
|
261
|
+
route: string;
|
|
262
|
+
reason: string;
|
|
263
|
+
subtree?: true;
|
|
264
|
+
}> = [
|
|
265
|
+
// Fumadocs routes: JSON-LD is emitted by @working-theory/fumadocs-schema
|
|
266
|
+
// from the fumadocs `.source` collection, which is not built in this
|
|
267
|
+
// harness. Not checked here.
|
|
268
|
+
{
|
|
269
|
+
route: "blog",
|
|
270
|
+
reason: "fumadocs route — emits via @working-theory/fumadocs-schema",
|
|
271
|
+
subtree: true,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
route: "docs",
|
|
275
|
+
reason: "fumadocs route — emits via @working-theory/fumadocs-schema",
|
|
276
|
+
subtree: true,
|
|
277
|
+
},
|
|
278
|
+
// Hub routes that enumerate a detail type with no list page type of their
|
|
279
|
+
// own; they hand-compose a listing and emit no JSON-LD today. The hub only:
|
|
280
|
+
// each one's `[slug]` detail route renders through renderPage and is checked.
|
|
281
|
+
{
|
|
282
|
+
route: "how-to",
|
|
283
|
+
reason: "hand-composed listing; no HowTo list type exists",
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
route: "software",
|
|
287
|
+
reason: "hand-composed listing; no SoftwareProduct list type exists",
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
route: "tech-articles",
|
|
291
|
+
reason: "hand-composed listing; no TechArticle list type exists",
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
route: "whitepapers",
|
|
295
|
+
reason: "hand-composed listing; no Whitepaper list type exists",
|
|
296
|
+
},
|
|
297
|
+
// App-shaped routes with no content page behind them.
|
|
298
|
+
{
|
|
299
|
+
route: "(authed)",
|
|
300
|
+
reason: "authenticated app surface, not a content page",
|
|
301
|
+
subtree: true,
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
route: "dev",
|
|
305
|
+
reason: "development-only purchase harness",
|
|
306
|
+
subtree: true,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
route: "flows",
|
|
310
|
+
reason: "interactive flow engine, API-backed",
|
|
311
|
+
subtree: true,
|
|
312
|
+
},
|
|
313
|
+
{ route: "schedule", reason: "booking surface, API-backed", subtree: true },
|
|
314
|
+
{ route: "sign-in", reason: "auth surface, not a content page" },
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
interface RouteEntry {
|
|
318
|
+
/** Route directory relative to `app/[locale]/` ("" for the home page). */
|
|
319
|
+
route: string;
|
|
320
|
+
file: string;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function walkRoutes(dir: string, out: RouteEntry[] = []): RouteEntry[] {
|
|
324
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
|
|
325
|
+
a.name.localeCompare(b.name),
|
|
326
|
+
)) {
|
|
327
|
+
if (entry.name === "__tests__" || entry.name === "node_modules") continue;
|
|
328
|
+
const full = join(dir, entry.name);
|
|
329
|
+
if (entry.isDirectory()) walkRoutes(full, out);
|
|
330
|
+
else if (entry.name === "page.tsx") {
|
|
331
|
+
out.push({
|
|
332
|
+
route: relative(ROUTE_ROOT, dirname(full)).split(sep).join("/"),
|
|
333
|
+
file: full,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function exemption(route: string): string | undefined {
|
|
341
|
+
return NO_STRUCTURED_DATA_ROUTES.find(
|
|
342
|
+
(e) =>
|
|
343
|
+
route === e.route ||
|
|
344
|
+
(e.subtree === true && route.startsWith(e.route + "/")),
|
|
345
|
+
)?.reason;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// The page identity a route renders — the two `renderPage({ type, slug })`
|
|
349
|
+
// shapes tools/lint-check-content-renders.ts recognises (a literal slug, or a
|
|
350
|
+
// slug taken from params). A route that reads its identity another way
|
|
351
|
+
// (e.g. a page-type manifest) is not recognised and must be declared.
|
|
352
|
+
const RENDER_PAGE_EXACT_RE =
|
|
353
|
+
/renderPage\s*\(\s*\{[^}]*type\s*:\s*["']([A-Za-z]+)["'][^}]*slug\s*:\s*["']([^"']+)["']/s;
|
|
354
|
+
const RENDER_PAGE_DYNAMIC_RE =
|
|
355
|
+
/renderPage\s*\(\s*\{[^}]*type\s*:\s*["']([A-Za-z]+)["'][^}]*\bslug\s*(?!:\s*["'])/s;
|
|
356
|
+
|
|
357
|
+
function routeIdentity(
|
|
358
|
+
file: string,
|
|
359
|
+
): { type: string; slug: string | null } | null {
|
|
360
|
+
const src = readFileSync(file, "utf8");
|
|
361
|
+
const exact = RENDER_PAGE_EXACT_RE.exec(src);
|
|
362
|
+
if (exact) return { type: exact[1] as string, slug: exact[2] as string };
|
|
363
|
+
const dynamic = RENDER_PAGE_DYNAMIC_RE.exec(src);
|
|
364
|
+
if (dynamic) return { type: dynamic[1] as string, slug: null };
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
type PageModule = {
|
|
369
|
+
default: (props: {
|
|
370
|
+
params: Promise<{ locale: string; slug?: string }>;
|
|
371
|
+
}) => Promise<ReactElement>;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
async function renderRoute(
|
|
375
|
+
file: string,
|
|
376
|
+
slug: string | undefined,
|
|
377
|
+
): Promise<HTMLElement> {
|
|
378
|
+
// Resolved at test time from the walk above — a static import would dangle
|
|
379
|
+
// in a scaffold that gated the surface off.
|
|
380
|
+
const mod = require(file) as PageModule;
|
|
381
|
+
const params: { locale: string; slug?: string } = { locale: DEFAULT_LOCALE };
|
|
382
|
+
if (slug !== undefined) params.slug = slug;
|
|
383
|
+
const element = await mod.default({ params: Promise.resolve(params) });
|
|
384
|
+
return render(element).container;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
describe("structured data — every route emits a valid JSON-LD script", () => {
|
|
388
|
+
const routes = walkRoutes(ROUTE_ROOT);
|
|
389
|
+
|
|
390
|
+
it("finds routes to check", () => {
|
|
391
|
+
expect(routes.length).toBeGreaterThan(0);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it.each(routes)("$route", async ({ route, file }) => {
|
|
395
|
+
const label = `/${DEFAULT_LOCALE}${route === "" ? "" : "/" + route}`;
|
|
396
|
+
const exempt = exemption(route);
|
|
397
|
+
const identity = routeIdentity(file);
|
|
398
|
+
if (exempt !== undefined) {
|
|
399
|
+
// Declared, with a reason, in NO_STRUCTURED_DATA_ROUTES above. The
|
|
400
|
+
// reason is false for a route that does render through renderPage —
|
|
401
|
+
// that route is checkable, so declaring it would only hide it.
|
|
402
|
+
if (identity !== null) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`${label} is listed in NO_STRUCTURED_DATA_ROUTES but renders through renderPage (${identity.type}); remove it from the list so it is checked.`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
console.info(`structured-data: ${label} not checked — ${exempt}`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (identity === null) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`${label}: ${basename(dirname(file))}/page.tsx has no renderPage({ type, slug }) call this check can read, so nothing is known to emit its JSON-LD. ` +
|
|
414
|
+
`Render it through renderPage, or add it to NO_STRUCTURED_DATA_ROUTES in ${basename(__filename)} with the reason.`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
expect(knownSchemaOrgTypes.has(identity.type)).toBe(true);
|
|
418
|
+
const type = identity.type as PageType;
|
|
419
|
+
|
|
420
|
+
// The pages this route serves from the site's own content, in the
|
|
421
|
+
// default locale. A fixed route names one page: if that page is missing
|
|
422
|
+
// the route 404s in production, which for `/` or `/about` is a broken
|
|
423
|
+
// site, not a route with nothing to check. A `[slug]` route with no
|
|
424
|
+
// pages of its type is simply empty — say so, do not fail.
|
|
425
|
+
let slugs: string[];
|
|
426
|
+
if (identity.slug !== null) {
|
|
427
|
+
const page = await adapter.getPageSource(
|
|
428
|
+
type,
|
|
429
|
+
identity.slug,
|
|
430
|
+
DEFAULT_LOCALE,
|
|
431
|
+
);
|
|
432
|
+
if (page === null) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`${label}: renders ${type}/${identity.slug}, but content/${DEFAULT_LOCALE}/${type}/${identity.slug}.mdx does not exist — the route serves 404 in production.`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
slugs = [identity.slug];
|
|
438
|
+
} else {
|
|
439
|
+
slugs = (await adapter.listDetail(type, DEFAULT_LOCALE)).map(
|
|
440
|
+
(p: ContentPage) => p.slug,
|
|
441
|
+
);
|
|
442
|
+
if (slugs.length === 0) {
|
|
443
|
+
console.info(
|
|
444
|
+
`structured-data: ${label} has no ${type} content in ${DEFAULT_LOCALE}; every slug serves 404 in production — nothing to check`,
|
|
445
|
+
);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const failures: string[] = [];
|
|
451
|
+
for (const slug of slugs) {
|
|
452
|
+
const container = await renderRoute(
|
|
453
|
+
file,
|
|
454
|
+
identity.slug === null ? slug : undefined,
|
|
455
|
+
);
|
|
456
|
+
const blocks = extractJsonLdBlocks(container);
|
|
457
|
+
const pageLabel =
|
|
458
|
+
identity.slug === null
|
|
459
|
+
? `${label.replace(/\[slug\]$/, "")}${slug}`
|
|
460
|
+
: label;
|
|
461
|
+
if (blocks.length === 0) {
|
|
462
|
+
failures.push(
|
|
463
|
+
`${pageLabel}: rendered no <script type="application/ld+json">`,
|
|
464
|
+
);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const failure = validateFor(blocks, type, pageLabel);
|
|
468
|
+
if (failure !== null) failures.push(failure);
|
|
469
|
+
}
|
|
470
|
+
expect(failures).toEqual([]);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// ── 3. The two defect shapes that shipped, pinned ───────────────────────────
|
|
475
|
+
//
|
|
476
|
+
// These prove the checks above reach the validator rule that rejects each
|
|
477
|
+
// shape, so a change that quietly loosens the validator or the flatten step
|
|
478
|
+
// shows up here rather than in a customer's launch review.
|
|
479
|
+
|
|
480
|
+
describe("structured data — the check rejects the shapes that shipped", () => {
|
|
481
|
+
it("a rendered route with no JSON-LD script yields no blocks", () => {
|
|
482
|
+
const { container } = render(<main>{"no structured data here"}</main>);
|
|
483
|
+
expect(extractJsonLdBlocks(container)).toEqual([]);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("a mainEntity carrying only @type and headline fails validation (#5019)", () => {
|
|
487
|
+
const thin: JsonLdObject = {
|
|
488
|
+
"@context": "https://schema.org",
|
|
489
|
+
"@type": "WebPage",
|
|
490
|
+
inLanguage: "en",
|
|
491
|
+
name: "x",
|
|
492
|
+
description: "x",
|
|
493
|
+
mainEntity: { "@type": "BlogPosting", headline: "x" },
|
|
494
|
+
};
|
|
495
|
+
const failure = validateFor([thin], "BlogPosting", "fixture");
|
|
496
|
+
expect(failure).not.toBeNull();
|
|
497
|
+
expect(failure).toContain("mainEntity.description");
|
|
498
|
+
});
|
|
499
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// account-config.ts — Experiment registry for the /account Results section (#4984)
|
|
2
|
+
//
|
|
3
|
+
// Mirrors `nav-config.ts`: the OSS template declares NO experiments — nothing
|
|
4
|
+
// in `apps/web` runs a diagnostic today, so this list ships empty and the
|
|
5
|
+
// Results section renders its empty state. A consuming site lists the
|
|
6
|
+
// `experimentKey`s of the diagnostics it runs (the `key` of each
|
|
7
|
+
// `waitlistExperiment` it registers) so `/account` can read the signed-in
|
|
8
|
+
// respondent's own newest signup for each via `waitlistSignups.me(key)`
|
|
9
|
+
// (`GET /v1/waitlist-signups/me?experimentKey=…`, #4731 D5). Keys the caller
|
|
10
|
+
// has no signup for come back 404 and are skipped, so listing every
|
|
11
|
+
// experiment a site runs is safe.
|
|
12
|
+
|
|
13
|
+
export const accountExperimentKeys: string[] = [];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side (RSC) helper for reading the operator-dashboard read API
|
|
3
|
+
* (#4665 D9).
|
|
4
|
+
*
|
|
5
|
+
* Forwards the incoming request's cookies to `@working-theory/api-client`'s
|
|
6
|
+
* `waitlistExperiments` namespace (B0, #4671) so `apps/api`'s cookie-session
|
|
7
|
+
* auth preHandler resolves the SAME Better Auth session the browser holds —
|
|
8
|
+
* both apps share the session store. GETs only, every page is an RSC; there
|
|
9
|
+
* is no client fetch and no `NEXT_PUBLIC_` credential (D9).
|
|
10
|
+
*
|
|
11
|
+
* `apps/web` may not import `apps/api` directly (architecture.yaml) — every
|
|
12
|
+
* read goes through `@working-theory/api-client`'s typed namespace, never a
|
|
13
|
+
* hand-rolled `fetch`. This module supplies exactly the two things that
|
|
14
|
+
* namespace doesn't own: the cookie-forwarding `init` (B0's additive
|
|
15
|
+
* `init?: { headers }` on `http.get`) and the envelope→`ApiError` unwrap
|
|
16
|
+
* (mirrors `apps-paid/workspace/lib/api-error.ts`'s `unwrapEnvelope`, adapted to
|
|
17
|
+
* the `@working-theory/api-client` `ApiResult<T>` shape B0 already returns
|
|
18
|
+
* rather than re-parsing the wire envelope by hand).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { cookies } from "next/headers";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Structural mirror of `@working-theory/api-client`'s `ApiResult<T>`
|
|
25
|
+
* (`packages/api-client/src/http.ts`) — not imported, because that type is
|
|
26
|
+
* not re-exported from the package root and this module's scope (#4665 UI)
|
|
27
|
+
* does not touch `packages/api-client` (B0's). Every `waitlistExperiments.*`
|
|
28
|
+
* call already returns exactly this shape, so TypeScript's structural typing
|
|
29
|
+
* accepts it here without a cast.
|
|
30
|
+
*/
|
|
31
|
+
type ApiResult<T> =
|
|
32
|
+
| { ok: true; body: T }
|
|
33
|
+
| { ok: false; status: number; body: unknown };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ApiError — thrown when the API responds with `{ ok: false }` (or the
|
|
37
|
+
* response body doesn't parse as the expected envelope). Carries the HTTP
|
|
38
|
+
* status + the envelope's `error.code` so callers can branch (404 →
|
|
39
|
+
* `notFound()`, 401/403 → an `EmptyState`) without string-matching on
|
|
40
|
+
* messages.
|
|
41
|
+
*/
|
|
42
|
+
export class ApiError extends Error {
|
|
43
|
+
readonly status: number;
|
|
44
|
+
readonly code: string;
|
|
45
|
+
|
|
46
|
+
constructor(status: number, code: string, message: string) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "ApiError";
|
|
49
|
+
this.status = status;
|
|
50
|
+
this.code = code;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The subset of an error response body every route's envelope carries. */
|
|
55
|
+
interface ErrorBody {
|
|
56
|
+
error?: { code?: string; message?: string };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Forward the incoming request's cookies as the `init` every
|
|
61
|
+
* `waitlistExperiments.*` call accepts. Call once per request — `cookies()`
|
|
62
|
+
* reads the current RSC request context.
|
|
63
|
+
*/
|
|
64
|
+
export async function forwardedCookieInit(): Promise<{
|
|
65
|
+
headers: { cookie: string };
|
|
66
|
+
}> {
|
|
67
|
+
const cookieHeader = (await cookies()).toString();
|
|
68
|
+
return { headers: { cookie: cookieHeader } };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Unwrap an `ApiResult<T>` — the shape every `@working-theory/api-client`
|
|
73
|
+
* call returns — into `T`, throwing `ApiError` on `{ ok: false }`.
|
|
74
|
+
*/
|
|
75
|
+
export async function unwrapApiResult<T>(
|
|
76
|
+
result: Promise<ApiResult<T>>,
|
|
77
|
+
): Promise<T> {
|
|
78
|
+
const res = await result;
|
|
79
|
+
if (res.ok) return res.body;
|
|
80
|
+
|
|
81
|
+
const body = res.body as ErrorBody | null;
|
|
82
|
+
const code = body?.error?.code ?? "invalid_response";
|
|
83
|
+
const message = body?.error?.message ?? "Malformed API response";
|
|
84
|
+
throw new ApiError(res.status, code, message);
|
|
85
|
+
}
|