create-ailk 0.1.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/LICENSE +201 -0
- package/README.md +39 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +80 -0
- package/dist/copy.d.ts +8 -0
- package/dist/copy.js +109 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/lib/pro-callout.d.ts +12 -0
- package/dist/lib/pro-callout.js +20 -0
- package/dist/parse-args.d.ts +13 -0
- package/dist/parse-args.js +81 -0
- package/dist/programmatic.d.ts +49 -0
- package/dist/programmatic.js +89 -0
- package/dist/substitute.d.ts +31 -0
- package/dist/substitute.js +45 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +70 -0
- package/package.json +47 -0
- package/templates/.claude/.gitkeep +0 -0
- package/templates/.claude/agents/web.md +80 -0
- package/templates/.claude/rules/architecture.md +62 -0
- package/templates/.claude/rules/preserve-existing-style.md +51 -0
- package/templates/.claude/skills/README.md +43 -0
- package/templates/.claude/skills/add-schema/SKILL.md +270 -0
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +200 -0
- package/templates/.claude/skills/add-schema/references/file-finding.md +123 -0
- package/templates/.claude/skills/add-schema/references/schema-types.md +131 -0
- package/templates/.claude/skills/audit-site/SKILL.md +213 -0
- package/templates/.claude/skills/audit-site/references/common-errors.md +113 -0
- package/templates/.claude/skills/audit-site/references/findings-glossary.md +138 -0
- package/templates/.claude/skills/audit-site/references/severity-levels.md +114 -0
- package/templates/.claude/skills/provision-config/SKILL.md +243 -0
- package/templates/.claude/skills/provision-config/references/env-vars-template.md +184 -0
- package/templates/.claude/skills/provision-config/references/runbook-template.md +248 -0
- package/templates/.claude/skills/provision-config/references/vercel-json-template.md +98 -0
- package/templates/.claude/skills/scaffold-auth/SKILL.md +261 -0
- package/templates/.claude/skills/scaffold-auth/references/env-vars.md +91 -0
- package/templates/.claude/skills/scaffold-auth/references/oauth-app-setup.md +171 -0
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +310 -0
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +71 -0
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +83 -0
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +72 -0
- package/templates/.claude/skills/scaffold-page/SKILL.md +243 -0
- package/templates/.claude/skills/scaffold-page/references/page-types.md +169 -0
- package/templates/.claude/skills/scaffold-page/templates/content.mdx.tmpl +28 -0
- package/templates/.claude/skills/scaffold-page/templates/route.tsx.tmpl +28 -0
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +242 -0
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +287 -0
- package/templates/.env.example +205 -0
- package/templates/.gitkeep +2 -0
- package/templates/CLAUDE.md +142 -0
- package/templates/CONVENTIONS.md +222 -0
- package/templates/README.md +110 -0
- package/templates/apps/api/.env.example +40 -0
- package/templates/apps/api/CLAUDE.md +63 -0
- package/templates/apps/api/api/index.ts +144 -0
- package/templates/apps/api/jest.config.cjs +74 -0
- package/templates/apps/api/package.json +54 -0
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +138 -0
- package/templates/apps/api/src/__tests__/factories/index.ts +70 -0
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +82 -0
- package/templates/apps/api/src/__tests__/index.test.ts +118 -0
- package/templates/apps/api/src/__tests__/server.test.ts +207 -0
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +126 -0
- package/templates/apps/api/src/__tests__/setup.ts +24 -0
- package/templates/apps/api/src/cli.ts +30 -0
- package/templates/apps/api/src/config/__tests__/index.test.ts +97 -0
- package/templates/apps/api/src/config/index.ts +56 -0
- package/templates/apps/api/src/index.ts +32 -0
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +15 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +45 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +178 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +100 -0
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +94 -0
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +144 -0
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +8 -0
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +227 -0
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +165 -0
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +129 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +74 -0
- package/templates/apps/api/src/lib/content-adapter.ts +86 -0
- package/templates/apps/api/src/lib/error-serializer.ts +77 -0
- package/templates/apps/api/src/lib/errors.ts +95 -0
- package/templates/apps/api/src/lib/file-fallback.ts +87 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +110 -0
- package/templates/apps/api/src/lib/locales.ts +201 -0
- package/templates/apps/api/src/lib/outbound-http.ts +122 -0
- package/templates/apps/api/src/lib/prisma.ts +21 -0
- package/templates/apps/api/src/lib/require-paid-tier.ts +53 -0
- package/templates/apps/api/src/lib/stripe.ts +30 -0
- package/templates/apps/api/src/lib/webhook-idempotency.ts +92 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +111 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +172 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +119 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +138 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +149 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +139 -0
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +175 -0
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +76 -0
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +153 -0
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +72 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +292 -0
- package/templates/apps/api/src/middleware/auth.ts +246 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +132 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +129 -0
- package/templates/apps/api/src/openapi/index.ts +12 -0
- package/templates/apps/api/src/openapi/spec.ts +408 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +193 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +201 -0
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +294 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +486 -0
- package/templates/apps/api/src/routes/auth/tokens.ts +199 -0
- package/templates/apps/api/src/routes/auth/user.ts +244 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +588 -0
- package/templates/apps/api/src/routes/checkout/index.ts +13 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +185 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +239 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +201 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +141 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +122 -0
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +44 -0
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +118 -0
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +252 -0
- package/templates/apps/api/src/routes/content/create.ts +114 -0
- package/templates/apps/api/src/routes/content/delete.ts +81 -0
- package/templates/apps/api/src/routes/content/get-page.ts +98 -0
- package/templates/apps/api/src/routes/content/index.ts +32 -0
- package/templates/apps/api/src/routes/content/list-pages.ts +83 -0
- package/templates/apps/api/src/routes/content/list-types.ts +19 -0
- package/templates/apps/api/src/routes/content/rate-limit.ts +13 -0
- package/templates/apps/api/src/routes/content/slug.ts +9 -0
- package/templates/apps/api/src/routes/content/update.ts +111 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +415 -0
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +165 -0
- package/templates/apps/api/src/routes/leads/index.ts +130 -0
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +296 -0
- package/templates/apps/api/src/routes/test/auth/session.ts +141 -0
- package/templates/apps/api/src/routes/webhooks/README.md +57 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +247 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +198 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +633 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +41 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +253 -0
- package/templates/apps/api/src/server.ts +157 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +395 -0
- package/templates/apps/api/src/services/enrichment-shim.ts +53 -0
- package/templates/apps/api/src/services/lead-routing.ts +338 -0
- package/templates/apps/api/tsconfig.json +34 -0
- package/templates/apps/api/vercel.json +17 -0
- package/templates/apps/mcp/.env.example +52 -0
- package/templates/apps/mcp/CLAUDE.md +84 -0
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +52 -0
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +46 -0
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +36 -0
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +54 -0
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +110 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +213 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +217 -0
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +52 -0
- package/templates/apps/mcp/__tests__/create_page.test.ts +201 -0
- package/templates/apps/mcp/__tests__/delete_page.test.ts +176 -0
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +82 -0
- package/templates/apps/mcp/__tests__/get_faq.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_locations.test.ts +65 -0
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +90 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +81 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +323 -0
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +75 -0
- package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +75 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +112 -0
- package/templates/apps/mcp/__tests__/locale.test.ts +135 -0
- package/templates/apps/mcp/__tests__/parity.test.ts +205 -0
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +34 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/request_callback.test.ts +163 -0
- package/templates/apps/mcp/__tests__/search_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/server.test.ts +115 -0
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +165 -0
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +161 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +227 -0
- package/templates/apps/mcp/__tests__/tools/scaffold.test.ts +237 -0
- package/templates/apps/mcp/__tests__/update_page.test.ts +195 -0
- package/templates/apps/mcp/jest.config.cjs +82 -0
- package/templates/apps/mcp/package.json +37 -0
- package/templates/apps/mcp/src/abuse/daily-cap.ts +47 -0
- package/templates/apps/mcp/src/abuse/email-validator.ts +25 -0
- package/templates/apps/mcp/src/abuse/honeypot.ts +21 -0
- package/templates/apps/mcp/src/abuse/index.ts +111 -0
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +58 -0
- package/templates/apps/mcp/src/http-transport.ts +303 -0
- package/templates/apps/mcp/src/index.ts +67 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +194 -0
- package/templates/apps/mcp/src/lib/errors.ts +61 -0
- package/templates/apps/mcp/src/lib/locales.ts +171 -0
- package/templates/apps/mcp/src/lib/require-paid-tier.ts +44 -0
- package/templates/apps/mcp/src/lib/resolve-locale.ts +30 -0
- package/templates/apps/mcp/src/server.ts +139 -0
- package/templates/apps/mcp/src/tools/audit.ts +103 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +52 -0
- package/templates/apps/mcp/src/tools/create_page.ts +95 -0
- package/templates/apps/mcp/src/tools/delete_page.ts +73 -0
- package/templates/apps/mcp/src/tools/get_business_hours.ts +66 -0
- package/templates/apps/mcp/src/tools/get_faq.ts +69 -0
- package/templates/apps/mcp/src/tools/get_locations.ts +85 -0
- package/templates/apps/mcp/src/tools/get_offerings.ts +66 -0
- package/templates/apps/mcp/src/tools/get_pricing.ts +74 -0
- package/templates/apps/mcp/src/tools/get_site_info.ts +84 -0
- package/templates/apps/mcp/src/tools/index.ts +125 -0
- package/templates/apps/mcp/src/tools/list_pages.ts +72 -0
- package/templates/apps/mcp/src/tools/purchase.ts +89 -0
- package/templates/apps/mcp/src/tools/read_content.ts +61 -0
- package/templates/apps/mcp/src/tools/request_callback.ts +51 -0
- package/templates/apps/mcp/src/tools/scaffold.ts +119 -0
- package/templates/apps/mcp/src/tools/search_content.ts +59 -0
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +47 -0
- package/templates/apps/mcp/src/tools/update_page.ts +91 -0
- package/templates/apps/mcp/tsconfig.json +19 -0
- package/templates/apps/web/.env.example +47 -0
- package/templates/apps/web/CLAUDE.md +81 -0
- package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +94 -0
- package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +75 -0
- package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +82 -0
- package/templates/apps/web/__tests__/auth/env-helpers.test.ts +168 -0
- package/templates/apps/web/__tests__/authority-list-routes.test.tsx +208 -0
- package/templates/apps/web/__tests__/better-stack.test.ts +66 -0
- package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +87 -0
- package/templates/apps/web/__tests__/blog-source-populated.test.ts +130 -0
- package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +60 -0
- package/templates/apps/web/__tests__/google-analytics.test.tsx +104 -0
- package/templates/apps/web/__tests__/i18n/middleware.test.ts +119 -0
- package/templates/apps/web/__tests__/i18n/routing.test.ts +114 -0
- package/templates/apps/web/__tests__/i18n/translations.test.ts +191 -0
- package/templates/apps/web/__tests__/json-ld.test.ts +98 -0
- package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +580 -0
- package/templates/apps/web/__tests__/layout-stealth.test.ts +39 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +103 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +114 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +94 -0
- package/templates/apps/web/__tests__/page.test.tsx +230 -0
- package/templates/apps/web/__tests__/products-services-phase1.test.tsx +321 -0
- package/templates/apps/web/__tests__/projects-routes.test.tsx +392 -0
- package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +475 -0
- package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +53 -0
- package/templates/apps/web/__tests__/routes/blog-route.test.tsx +191 -0
- package/templates/apps/web/__tests__/routes/llms-txt.test.ts +123 -0
- package/templates/apps/web/__tests__/routes/robots-txt.test.ts +52 -0
- package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +78 -0
- package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +203 -0
- package/templates/apps/web/__tests__/section-routes.test.tsx +693 -0
- package/templates/apps/web/__tests__/security-headers.test.ts +201 -0
- package/templates/apps/web/__tests__/sentry-config.test.ts +116 -0
- package/templates/apps/web/__tests__/setup.ts +82 -0
- package/templates/apps/web/__tests__/sitemap-locales.test.ts +415 -0
- package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +844 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +81 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +110 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +58 -0
- package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +120 -0
- package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +202 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +173 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +45 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/careers/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/clients/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +130 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +152 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +92 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +79 -0
- package/templates/apps/web/app/[locale]/contact/error.tsx +24 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +38 -0
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +119 -0
- package/templates/apps/web/app/[locale]/courses/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +160 -0
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +117 -0
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +58 -0
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +118 -0
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +72 -0
- package/templates/apps/web/app/[locale]/downloads/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +47 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +130 -0
- package/templates/apps/web/app/[locale]/events/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/faq/page.tsx +36 -0
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +78 -0
- package/templates/apps/web/app/[locale]/how-to/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/integrations/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/launch-service/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/layout.tsx +241 -0
- package/templates/apps/web/app/[locale]/page.tsx +41 -0
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/partners/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/products/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +138 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +30 -0
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/resources/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +81 -0
- package/templates/apps/web/app/[locale]/software/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +39 -0
- package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +152 -0
- package/templates/apps/web/app/api/auth/[...all]/route.ts +16 -0
- package/templates/apps/web/app/error-fallback.tsx +75 -0
- package/templates/apps/web/app/error.tsx +23 -0
- package/templates/apps/web/app/layout.tsx +18 -0
- package/templates/apps/web/app/llms.txt/route.ts +101 -0
- package/templates/apps/web/app/not-found.tsx +48 -0
- package/templates/apps/web/app/og/__tests__/route.test.ts +164 -0
- package/templates/apps/web/app/og/route.tsx +135 -0
- package/templates/apps/web/app/robots.txt/route.ts +36 -0
- package/templates/apps/web/app/sitemap.xml/route.ts +146 -0
- package/templates/apps/web/auth.ts +141 -0
- package/templates/apps/web/components/ThemeToggle.tsx +60 -0
- package/templates/apps/web/components/google-analytics.tsx +34 -0
- package/templates/apps/web/content/en/blog/index.mdx +15 -0
- package/templates/apps/web/content/en/blog/launching-ailk.mdx +20 -0
- package/templates/apps/web/content/en/docs/getting-started.mdx +30 -0
- package/templates/apps/web/i18n/request.ts +36 -0
- package/templates/apps/web/jest.config.cjs +100 -0
- package/templates/apps/web/lib/__mocks__/next-mdx-remote-rsc.js +12 -0
- package/templates/apps/web/lib/__mocks__/source-server.js +31 -0
- package/templates/apps/web/lib/__tests__/og.test.ts +65 -0
- package/templates/apps/web/lib/auth-env.ts +66 -0
- package/templates/apps/web/lib/blog-list-data.ts +95 -0
- package/templates/apps/web/lib/blog-post-data.ts +79 -0
- package/templates/apps/web/lib/nav-config.ts +13 -0
- package/templates/apps/web/lib/observability/better-stack.ts +27 -0
- package/templates/apps/web/lib/og.ts +23 -0
- package/templates/apps/web/lib/prisma-for-auth.ts +34 -0
- package/templates/apps/web/lib/source.ts +34 -0
- package/templates/apps/web/messages/en.json +131 -0
- package/templates/apps/web/middleware.ts +80 -0
- package/templates/apps/web/next.config.mjs +167 -0
- package/templates/apps/web/package.json +62 -0
- package/templates/apps/web/public/.well-known/security.txt +17 -0
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +4 -0
- package/templates/apps/web/public/logo-dark.svg +21 -0
- package/templates/apps/web/public/logo-light.svg +21 -0
- package/templates/apps/web/public/logomark-dark.svg +4 -0
- package/templates/apps/web/public/logomark-light.svg +4 -0
- package/templates/apps/web/public/og-default.png +0 -0
- package/templates/apps/web/public/site.webmanifest +19 -0
- package/templates/apps/web/scripts/assert-content-bundled.ts +55 -0
- package/templates/apps/web/security-headers.config.ts +132 -0
- package/templates/apps/web/sentry.client.config.ts +27 -0
- package/templates/apps/web/sentry.edge.config.ts +15 -0
- package/templates/apps/web/sentry.server.config.ts +20 -0
- package/templates/apps/web/source.config.ts +78 -0
- package/templates/apps/web/src/components/SiteNav.tsx +21 -0
- package/templates/apps/web/src/lib/routes.ts +37 -0
- package/templates/apps/web/tsconfig.json +35 -0
- package/templates/apps/web/types/messages.d.ts +19 -0
- package/templates/content/.gitkeep +0 -0
- package/templates/content/CLAUDE.md +85 -0
- package/templates/content/_site.mdx +38 -0
- package/templates/content/ar/HomePage/home.mdx +123 -0
- package/templates/content/ar/_site.mdx +35 -0
- package/templates/content/de/_site.mdx +29 -0
- package/templates/content/en/.gitkeep +0 -0
- package/templates/content/en/AboutPage/about.mdx +19 -0
- package/templates/content/en/AboutPage/about.schema.json +82 -0
- package/templates/content/en/BookCall/book-call.mdx +19 -0
- package/templates/content/en/BookCall/book-call.schema.json +121 -0
- package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +20 -0
- package/templates/content/en/CareersList/careers.mdx +24 -0
- package/templates/content/en/CaseStudiesList/case-studies.mdx +22 -0
- package/templates/content/en/CaseStudiesList/showcase.mdx +8 -0
- package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +32 -0
- package/templates/content/en/CaseStudy/schema-builder-migration.mdx +41 -0
- package/templates/content/en/ClientsList/clients.mdx +38 -0
- package/templates/content/en/ContactPage/contact.mdx +19 -0
- package/templates/content/en/ContactPage/contact.schema.json +100 -0
- package/templates/content/en/ContactPoint/primary.mdx +14 -0
- package/templates/content/en/Course/aeo-fundamentals.mdx +37 -0
- package/templates/content/en/Course/schema-builder-workshop.mdx +62 -0
- package/templates/content/en/CoursesList/courses.mdx +22 -0
- package/templates/content/en/DownloadsList/downloads.mdx +22 -0
- package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +39 -0
- package/templates/content/en/Event/ailk-launch-day-2026.mdx +35 -0
- package/templates/content/en/EventsList/events.mdx +26 -0
- package/templates/content/en/FAQPage/faq.mdx +420 -0
- package/templates/content/en/FAQPage/faq.schema.json +231 -0
- package/templates/content/en/GlossaryList/glossary.mdx +19 -0
- package/templates/content/en/GlossaryList/glossary.schema.json +300 -0
- package/templates/content/en/HomePage/home.mdx +312 -0
- package/templates/content/en/HomePage/home.schema.json +88 -0
- package/templates/content/en/HomePage/launch-service.mdx +475 -0
- package/templates/content/en/HomePage/launch-service.schema.json +138 -0
- package/templates/content/en/HowTo/add-mdx-content.mdx +38 -0
- package/templates/content/en/HowTo/configure-aeo-scoring.mdx +35 -0
- package/templates/content/en/HowTo/set-up-json-ld.mdx +36 -0
- package/templates/content/en/Integration/github-actions.mdx +31 -0
- package/templates/content/en/Integration/openai-api.mdx +26 -0
- package/templates/content/en/Integration/vercel.mdx +32 -0
- package/templates/content/en/IntegrationsList/integrations.mdx +22 -0
- package/templates/content/en/JobPosting/content-strategist.mdx +46 -0
- package/templates/content/en/JobPosting/developer-advocate.mdx +47 -0
- package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +57 -0
- package/templates/content/en/LandingPage/aeo-audit.mdx +19 -0
- package/templates/content/en/LandingPage/aeo-audit.schema.json +92 -0
- package/templates/content/en/LandingPage/agency-waitlist.mdx +340 -0
- package/templates/content/en/LandingPage/agency-waitlist.schema.json +152 -0
- package/templates/content/en/LandingPage/developers.mdx +19 -0
- package/templates/content/en/LandingPage/developers.schema.json +109 -0
- package/templates/content/en/LandingPage/for-agencies.mdx +390 -0
- package/templates/content/en/LandingPage/for-agencies.schema.json +107 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +417 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +105 -0
- package/templates/content/en/LandingPage/for-businesses.mdx +19 -0
- package/templates/content/en/LandingPage/for-businesses.schema.json +96 -0
- package/templates/content/en/LandingPage/platform.mdx +369 -0
- package/templates/content/en/LandingPage/platform.schema.json +166 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +19 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +102 -0
- package/templates/content/en/LandingPage/vs-wordpress.mdx +19 -0
- package/templates/content/en/LandingPage/vs-wordpress.schema.json +53 -0
- package/templates/content/en/NewsletterSignup/newsletter.mdx +236 -0
- package/templates/content/en/NewsletterSignup/newsletter.schema.json +91 -0
- package/templates/content/en/Organization/site-owner.mdx +16 -0
- package/templates/content/en/Partner/prisma.mdx +31 -0
- package/templates/content/en/Partner/supabase.mdx +30 -0
- package/templates/content/en/Partner/vercel.mdx +28 -0
- package/templates/content/en/PartnersList/partners.mdx +22 -0
- package/templates/content/en/Person/founder.mdx +18 -0
- package/templates/content/en/Pricing/pricing.mdx +538 -0
- package/templates/content/en/Pricing/pricing.schema.json +209 -0
- package/templates/content/en/PrivacyPage/privacy.mdx +165 -0
- package/templates/content/en/PrivacyPage/privacy.schema.json +64 -0
- package/templates/content/en/Product/ailk.mdx +19 -0
- package/templates/content/en/Product/ailk.schema.json +94 -0
- package/templates/content/en/Product/api-launch-kit.mdx +26 -0
- package/templates/content/en/Product/launch-kit.mdx +28 -0
- package/templates/content/en/ProductsList/products.mdx +26 -0
- package/templates/content/en/Project/ailk-phase-2.mdx +54 -0
- package/templates/content/en/ProjectsList/projects.mdx +25 -0
- package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +27 -0
- package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +28 -0
- package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +39 -0
- package/templates/content/en/ResourcesHub/resources.mdx +22 -0
- package/templates/content/en/Service/consulting.mdx +22 -0
- package/templates/content/en/Service/implementation.mdx +30 -0
- package/templates/content/en/Service/launch-service.mdx +232 -0
- package/templates/content/en/Service/launch.mdx +228 -0
- package/templates/content/en/ServicesList/services.mdx +84 -0
- package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +44 -0
- package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +47 -0
- package/templates/content/en/SoftwareProduct/ailk-schema.mdx +49 -0
- package/templates/content/en/TeamList/team.mdx +63 -0
- package/templates/content/en/TeamMember/founder.mdx +72 -0
- package/templates/content/en/TermsPage/terms.mdx +19 -0
- package/templates/content/en/TermsPage/terms.schema.json +99 -0
- package/templates/content/en/TestimonialsList/testimonials.mdx +44 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +30 -0
- package/templates/content/en/Whitepaper/ax-first-product-development.mdx +27 -0
- package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +27 -0
- package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +31 -0
- package/templates/content/en-XA/AboutPage/about.mdx +8 -0
- package/templates/content/en-XA/FAQPage/faq.mdx +8 -0
- package/templates/content/en-XA/HomePage/home.mdx +8 -0
- package/templates/content/es/_site.mdx +30 -0
- package/templates/content/fr/_site.mdx +30 -0
- package/templates/content/ja/_site.mdx +30 -0
- package/templates/content/pt-BR/AboutPage/about.mdx +8 -0
- package/templates/content/pt-BR/FAQPage/faq.mdx +8 -0
- package/templates/content/pt-BR/HomePage/home.mdx +23 -0
- package/templates/database/CHANGELOG.md +5 -0
- package/templates/database/README.md +234 -0
- package/templates/database/__tests__/user-cascade.test.ts +125 -0
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +3 -0
- package/templates/database/inbox/__tests__/schema.test.ts +40 -0
- package/templates/database/inbox/schema.prisma +52 -0
- package/templates/database/jest.config.cjs +29 -0
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +328 -0
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +100 -0
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +28 -0
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +25 -0
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +42 -0
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +67 -0
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +21 -0
- package/templates/database/migrations/migration_lock.toml +3 -0
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/package.json +45 -0
- package/templates/database/prisma.config.ts +20 -0
- package/templates/database/schema.prisma +487 -0
- package/templates/database/scripts/db-generate-locked.sh +80 -0
- package/templates/database/seed-test.ts +85 -0
- package/templates/database/tsconfig.build.json +16 -0
- package/templates/database/tsconfig.json +9 -0
- package/templates/package.json +125 -0
- package/templates/pnpm-workspace.yaml +6 -0
- package/templates/project.yaml +59 -0
- package/templates/tsconfig.base.json +18 -0
- package/templates/tsconfig.json +45 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// i18n S2 — Locale-aware root layout (#949)
|
|
2
|
+
// i18n P2 S1 — hreflang Metadata API alternates.languages (#972)
|
|
3
|
+
// Theme 1.9 — SSR theme + flash-prevention + CSS vars + meta + JSON-LD (#1322)
|
|
4
|
+
// Chrome extract — Nav + Footer from @working-theory/ui (#1451)
|
|
5
|
+
// Shell/Chrome composition — Nav + Footer wrap via @working-theory/templates MarketingShell (#2820)
|
|
6
|
+
//
|
|
7
|
+
// Sits under app/[locale]/ and sets the <html lang> attribute to the
|
|
8
|
+
// active locale. next-intl's NextIntlClientProvider is wired here so
|
|
9
|
+
// Client Components can call useTranslations() inside this subtree.
|
|
10
|
+
//
|
|
11
|
+
// The outer app/layout.tsx remains as a root shell that renders children;
|
|
12
|
+
// this layout handles the locale-specific <html> / <body> tree.
|
|
13
|
+
//
|
|
14
|
+
// Spec: docs/internal/specs/i18n-phase1-s2-next-intl-foundation-spec.md §4
|
|
15
|
+
// Spec: docs/internal/specs/i18n-phase2-s1-hreflang-metadata-spec.md
|
|
16
|
+
// Spec: docs/internal/specs/theme-wiring-phase1-site-rendering-spec.md
|
|
17
|
+
|
|
18
|
+
import { Telemetry, VerificationMeta } from "@working-theory/analytics";
|
|
19
|
+
import { MarketingShell } from "@working-theory/templates/shells";
|
|
20
|
+
import { themeToCssVars } from "@working-theory/theme";
|
|
21
|
+
import { isRtl } from "@working-theory/ui";
|
|
22
|
+
import { Footer, Nav } from "@working-theory/ui/chrome";
|
|
23
|
+
import {
|
|
24
|
+
buildHreflangAlternates,
|
|
25
|
+
getStealthRobots,
|
|
26
|
+
readLocales,
|
|
27
|
+
} from "@working-theory/web-i18n";
|
|
28
|
+
import type { Metadata, Viewport } from "next";
|
|
29
|
+
import { cookies, headers } from "next/headers";
|
|
30
|
+
import { NextIntlClientProvider } from "next-intl";
|
|
31
|
+
import { getMessages, getTranslations } from "next-intl/server";
|
|
32
|
+
import type { ReactElement, ReactNode } from "react";
|
|
33
|
+
|
|
34
|
+
import { GoogleAnalytics } from "../../components/google-analytics";
|
|
35
|
+
import { navItems } from "../../lib/nav-config";
|
|
36
|
+
import { getOgImageUrl } from "../../lib/og";
|
|
37
|
+
import { resolveBaseUrl } from "../../src/lib/routes";
|
|
38
|
+
|
|
39
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME ?? "AI Launch Kit";
|
|
42
|
+
const DEFAULT_LOCALE = "en";
|
|
43
|
+
const THEME_COOKIE_NAME =
|
|
44
|
+
process.env.NEXT_PUBLIC_THEME_COOKIE_NAME ?? "ailk-theme";
|
|
45
|
+
|
|
46
|
+
type ThemeMode = "light" | "dark";
|
|
47
|
+
|
|
48
|
+
// ── Theme helpers ────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Server-side theme resolution.
|
|
52
|
+
*
|
|
53
|
+
* Cookie values: "light" | "dark" → pass through directly.
|
|
54
|
+
* Cookie value "system" or absent → cannot resolve OS preference server-side,
|
|
55
|
+
* so we fall back to "light" for the initial HTML. The blocking inline script
|
|
56
|
+
* below immediately corrects this before first paint using matchMedia.
|
|
57
|
+
*/
|
|
58
|
+
async function resolveThemeMode(): Promise<ThemeMode> {
|
|
59
|
+
try {
|
|
60
|
+
const cookieStore = await cookies();
|
|
61
|
+
const value = cookieStore.get(THEME_COOKIE_NAME)?.value;
|
|
62
|
+
if (value === "dark") return "dark";
|
|
63
|
+
if (value === "light") return "light";
|
|
64
|
+
// "system" or absent — use light as SSR default; inline script corrects before paint
|
|
65
|
+
return "light";
|
|
66
|
+
} catch {
|
|
67
|
+
return "light";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Blocking inline script that runs synchronously before any styled element.
|
|
73
|
+
*
|
|
74
|
+
* Resolves the effective theme from the cookie:
|
|
75
|
+
* - "light" or "dark" → apply directly.
|
|
76
|
+
* - "system" or absent → resolve via prefers-color-scheme.
|
|
77
|
+
*
|
|
78
|
+
* This ensures data-theme is never the literal "system" and there is no
|
|
79
|
+
* flash of unstyled/wrong-theme content on page load.
|
|
80
|
+
*/
|
|
81
|
+
const FLASH_PREVENTION_SCRIPT = `(function(){
|
|
82
|
+
var c=document.cookie.match(/(?:^|;\\s*)${THEME_COOKIE_NAME}=([^;]+)/);
|
|
83
|
+
var v=c?c[1]:null;
|
|
84
|
+
var t=(v==='light'||v==='dark')?v:window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';
|
|
85
|
+
document.documentElement.setAttribute('data-theme',t);
|
|
86
|
+
})();`;
|
|
87
|
+
|
|
88
|
+
// ── Metadata ─────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
91
|
+
const ogImageUrl = getOgImageUrl({ title: SITE_NAME, schemaType: "WebSite" });
|
|
92
|
+
|
|
93
|
+
const headerStore = await headers();
|
|
94
|
+
const pathname = headerStore.get("x-pathname") ?? "/";
|
|
95
|
+
|
|
96
|
+
const locales = readLocales();
|
|
97
|
+
const baseUrl = resolveBaseUrl();
|
|
98
|
+
const languages = buildHreflangAlternates(
|
|
99
|
+
pathname,
|
|
100
|
+
locales,
|
|
101
|
+
DEFAULT_LOCALE,
|
|
102
|
+
baseUrl,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
title: "AI Launch Kit",
|
|
107
|
+
description: "AX-first OSS template — API + MCP as canonical surfaces.",
|
|
108
|
+
openGraph: {
|
|
109
|
+
title: "AI Launch Kit",
|
|
110
|
+
description: "AX-first OSS template — API + MCP as canonical surfaces.",
|
|
111
|
+
type: "website",
|
|
112
|
+
url: baseUrl,
|
|
113
|
+
images: [ogImageUrl],
|
|
114
|
+
},
|
|
115
|
+
twitter: {
|
|
116
|
+
card: "summary_large_image",
|
|
117
|
+
title: "AI Launch Kit",
|
|
118
|
+
description: "AX-first OSS template — API + MCP as canonical surfaces.",
|
|
119
|
+
images: [ogImageUrl],
|
|
120
|
+
},
|
|
121
|
+
...getStealthRobots(),
|
|
122
|
+
alternates: {
|
|
123
|
+
canonical: baseUrl,
|
|
124
|
+
languages,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const viewport: Viewport = {
|
|
130
|
+
width: "device-width",
|
|
131
|
+
initialScale: 1,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// ── Layout ───────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
interface Props {
|
|
137
|
+
children: ReactNode;
|
|
138
|
+
params: Promise<{ locale: string }>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default async function LocaleLayout({
|
|
142
|
+
children,
|
|
143
|
+
params,
|
|
144
|
+
}: Props): Promise<ReactElement> {
|
|
145
|
+
const { locale } = await params;
|
|
146
|
+
const messages = await getMessages();
|
|
147
|
+
const themeMode = await resolveThemeMode();
|
|
148
|
+
|
|
149
|
+
// Resolve nav labels server-side so Nav + Footer components stay
|
|
150
|
+
// i18n-hook-free and are testable without a NextIntlClientProvider wrapper.
|
|
151
|
+
const tNav = await getTranslations({ locale, namespace: "nav" });
|
|
152
|
+
const navLabels = {
|
|
153
|
+
items: {
|
|
154
|
+
home: tNav("home"),
|
|
155
|
+
docs: tNav("docs"),
|
|
156
|
+
blog: tNav("blog"),
|
|
157
|
+
},
|
|
158
|
+
openMenu: tNav("openMenu"),
|
|
159
|
+
closeMenu: tNav("closeMenu"),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Shared brand mark injected into both Nav and Footer chrome slots.
|
|
163
|
+
const siteLogo = (
|
|
164
|
+
<>
|
|
165
|
+
<img src="/logo-light.svg" alt={SITE_NAME} height={28} className="block dark:hidden" />
|
|
166
|
+
<img src="/logo-dark.svg" alt={SITE_NAME} height={28} className="hidden dark:block" />
|
|
167
|
+
</>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const cssVars = themeToCssVars({ mode: themeMode });
|
|
171
|
+
const inlineThemeStyle = `:root { ${cssVars} }`;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<html lang={locale} dir={isRtl(locale) ? "rtl" : "ltr"} data-theme={themeMode}>
|
|
175
|
+
<head>
|
|
176
|
+
{/* Flash-prevention: runs synchronously before any styled element */}
|
|
177
|
+
<script
|
|
178
|
+
data-testid="theme-flash-prevention"
|
|
179
|
+
dangerouslySetInnerHTML={{ __html: FLASH_PREVENTION_SCRIPT }}
|
|
180
|
+
/>
|
|
181
|
+
{/* Inline CSS-variable declarations for the active theme mode */}
|
|
182
|
+
<style
|
|
183
|
+
data-ailk="theme-vars"
|
|
184
|
+
dangerouslySetInnerHTML={{ __html: inlineThemeStyle }}
|
|
185
|
+
/>
|
|
186
|
+
{/* Favicons — single brand mark (theme-independent), PNG fallbacks, ICO legacy */}
|
|
187
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
188
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
189
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
190
|
+
<link rel="shortcut icon" href="/favicon.ico" />
|
|
191
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
192
|
+
<link rel="manifest" href="/site.webmanifest" />
|
|
193
|
+
<GoogleAnalytics />
|
|
194
|
+
<VerificationMeta />
|
|
195
|
+
</head>
|
|
196
|
+
<body>
|
|
197
|
+
<NextIntlClientProvider messages={messages}>
|
|
198
|
+
{/*
|
|
199
|
+
* Shell + Chrome compose at the layout layer (decision: #2820, #2862).
|
|
200
|
+
* MarketingShell is a slot-frame (geometry only); the layer injects the
|
|
201
|
+
* real Nav + Footer into its top-/bottom-chrome slots — the per-app
|
|
202
|
+
* navItems, labels, siteName, logo, and attribution live here, not in
|
|
203
|
+
* the shell. renderPage owns the <main>/<h1>/<p> document landmarks
|
|
204
|
+
* (ADR-0027), so contentOwnsMain keeps the shell's content slot a
|
|
205
|
+
* non-landmark <div> — exactly one <main> per page. Attribution is
|
|
206
|
+
* emitted unconditionally in OSS forks; paying customers install
|
|
207
|
+
* @working-theory/branding and gate it in their own layout.
|
|
208
|
+
*/}
|
|
209
|
+
<MarketingShell
|
|
210
|
+
contentOwnsMain
|
|
211
|
+
nav={
|
|
212
|
+
<Nav
|
|
213
|
+
locale={locale}
|
|
214
|
+
labels={navLabels}
|
|
215
|
+
navItems={navItems}
|
|
216
|
+
siteName={SITE_NAME}
|
|
217
|
+
logo={siteLogo}
|
|
218
|
+
/>
|
|
219
|
+
}
|
|
220
|
+
footer={
|
|
221
|
+
<Footer
|
|
222
|
+
locale={locale}
|
|
223
|
+
itemLabels={navLabels.items}
|
|
224
|
+
navItems={navItems}
|
|
225
|
+
siteName={SITE_NAME}
|
|
226
|
+
logo={siteLogo}
|
|
227
|
+
attribution={{
|
|
228
|
+
label: "Built with AI Launch Kit",
|
|
229
|
+
href: "https://ailaunchkit.ai",
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
}
|
|
233
|
+
>
|
|
234
|
+
{children}
|
|
235
|
+
</MarketingShell>
|
|
236
|
+
<Telemetry />
|
|
237
|
+
</NextIntlClientProvider>
|
|
238
|
+
</body>
|
|
239
|
+
</html>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// i18n S2 — Home page under [locale] segment (#949)
|
|
2
|
+
//
|
|
3
|
+
// Migrated from app/page.tsx to app/[locale]/page.tsx.
|
|
4
|
+
// Reads the HomePage MDX frontmatter, renders sections via SectionRenderer,
|
|
5
|
+
// and emits a JSON-LD <script> for crawlers.
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
getMdxSource as defaultGetMdxSource,
|
|
9
|
+
getPageSections as defaultGetPageSections} from "@working-theory/page-renderer";
|
|
10
|
+
import {
|
|
11
|
+
renderPage
|
|
12
|
+
} from "@working-theory/page-renderer";
|
|
13
|
+
import type { ReactElement } from "react";
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
params: Promise<{ locale: string }>;
|
|
17
|
+
/**
|
|
18
|
+
* Overrides for testing — inject stubs to avoid mocking internal modules.
|
|
19
|
+
* Production code leaves these undefined and uses the real implementations.
|
|
20
|
+
*/
|
|
21
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
22
|
+
getPageSections?: typeof defaultGetPageSections;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default async function Page({
|
|
26
|
+
params,
|
|
27
|
+
getMdxSource,
|
|
28
|
+
getPageSections,
|
|
29
|
+
}: Props): Promise<ReactElement> {
|
|
30
|
+
const { locale } = await params;
|
|
31
|
+
return renderPage({
|
|
32
|
+
type: "HomePage",
|
|
33
|
+
slug: "home",
|
|
34
|
+
locale,
|
|
35
|
+
translationsNamespace: "home",
|
|
36
|
+
populatedDescriptionTestId: "home-description",
|
|
37
|
+
emptyStateTestId: "home-description",
|
|
38
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
39
|
+
...(getPageSections !== undefined && { getPageSections }),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Partner detail page. Reads the Partner MDX for the given slug, renders via
|
|
2
|
+
// next-mdx-remote/rsc, and emits a JSON-LD <script>. Unknown slugs trigger 404.
|
|
3
|
+
import {
|
|
4
|
+
getMdxSource as defaultGetMdxSource,
|
|
5
|
+
getMdxSlugs,
|
|
6
|
+
renderPage,
|
|
7
|
+
} from "@working-theory/page-renderer";
|
|
8
|
+
import { notFound } from "next/navigation";
|
|
9
|
+
import type { ReactElement } from "react";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
params: Promise<{ locale: string; slug: string }>;
|
|
13
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function generateStaticParams({
|
|
17
|
+
params,
|
|
18
|
+
}: {
|
|
19
|
+
params: { locale: string };
|
|
20
|
+
}): Promise<{ slug: string }[]> {
|
|
21
|
+
const slugs = await getMdxSlugs("Partner", params.locale);
|
|
22
|
+
return slugs.map((slug: string) => ({ slug }));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default async function Page({
|
|
26
|
+
params,
|
|
27
|
+
getMdxSource = defaultGetMdxSource,
|
|
28
|
+
}: Props): Promise<ReactElement> {
|
|
29
|
+
const { locale, slug } = await params;
|
|
30
|
+
const source = await getMdxSource("Partner", slug, locale);
|
|
31
|
+
|
|
32
|
+
if (!source) {
|
|
33
|
+
notFound();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return renderPage({
|
|
37
|
+
type: "Partner",
|
|
38
|
+
slug,
|
|
39
|
+
locale,
|
|
40
|
+
translationsNamespace: "partners",
|
|
41
|
+
bodyMode: "mdx-body",
|
|
42
|
+
getMdxSource: async () => source,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for the partners routes → @working-theory/page-renderer migration.
|
|
3
|
+
*
|
|
4
|
+
* Node version: captured against Node 22.x (same as CI).
|
|
5
|
+
* MDX seed content: DI-injected stub — no fs reads, deterministic across environments.
|
|
6
|
+
*
|
|
7
|
+
* Two-commit strategy:
|
|
8
|
+
* Commit 1: this file added; snapshots captured against pre-migration routes (RED-passing).
|
|
9
|
+
* Commit 2: routes migrated; same snapshots verify diff=0 (GREEN).
|
|
10
|
+
*
|
|
11
|
+
* Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
|
|
12
|
+
* A non-zero diff indicates a behavior regression in the migration. Escalate.
|
|
13
|
+
*/
|
|
14
|
+
import React from "react";
|
|
15
|
+
import { render } from "@testing-library/react";
|
|
16
|
+
|
|
17
|
+
// ── next/navigation mock ───────────────────────────────────────────────────────
|
|
18
|
+
jest.mock("next/navigation", () => ({
|
|
19
|
+
__esModule: true,
|
|
20
|
+
notFound: jest.fn(() => {
|
|
21
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// ── next-mdx-remote/rsc mock ───────────────────────────────────────────────────
|
|
26
|
+
jest.mock("next-mdx-remote/rsc", () => ({
|
|
27
|
+
__esModule: true,
|
|
28
|
+
MDXRemote: ({ source }: { source: string }) =>
|
|
29
|
+
React.createElement("div", { "data-testid": "mdx-rendered" }, source),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
// ── Import page components ─────────────────────────────────────────────────────
|
|
33
|
+
import PartnersPage from "../page";
|
|
34
|
+
import PartnerSlugPage from "../[slug]/page";
|
|
35
|
+
|
|
36
|
+
// ── Shared stubs ───────────────────────────────────────────────────────────────
|
|
37
|
+
const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
|
|
38
|
+
|
|
39
|
+
// ── partners/page (index) ─────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe("partners/page — snapshot regression", () => {
|
|
42
|
+
it("empty state renders deterministic HTML", async () => {
|
|
43
|
+
const element = await PartnersPage({
|
|
44
|
+
params: Promise.resolve({ locale: "en" }),
|
|
45
|
+
getMdxSource: getMdxSourceEmpty,
|
|
46
|
+
});
|
|
47
|
+
const { container } = render(element);
|
|
48
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
49
|
+
`"<main><h1>Partners</h1><p data-testid="section-empty-state">Partners listing coming soon.</p></main>"`,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("populated state renders deterministic HTML", async () => {
|
|
54
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue({
|
|
55
|
+
frontmatter: {
|
|
56
|
+
type: "PartnersList",
|
|
57
|
+
slug: "partners",
|
|
58
|
+
title: "Partners",
|
|
59
|
+
description: "Organisations partnering with AI Launch Kit.",
|
|
60
|
+
},
|
|
61
|
+
body: "mdx-body-content",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const element = await PartnersPage({
|
|
65
|
+
params: Promise.resolve({ locale: "en" }),
|
|
66
|
+
getMdxSource: getMdxSourcePopulated,
|
|
67
|
+
});
|
|
68
|
+
const { container } = render(element);
|
|
69
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
70
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Partners","description":"Organisations partnering with AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Organization"}]}}</script><main><h1>Partners</h1><p data-testid="partners-description">Organisations partnering with 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-rendered">mdx-body-content</div></article></div></div></main>"`,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── partners/[slug]/page (detail) ─────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
describe("partners/[slug]/page — snapshot regression", () => {
|
|
78
|
+
it("populated state renders deterministic HTML", async () => {
|
|
79
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue({
|
|
80
|
+
frontmatter: {
|
|
81
|
+
type: "Partner",
|
|
82
|
+
slug: "acme-corp",
|
|
83
|
+
title: "Acme Corp",
|
|
84
|
+
description: "Acme Corp is a strategic partner.",
|
|
85
|
+
},
|
|
86
|
+
body: "mdx-body-content",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const element = await PartnerSlugPage({
|
|
90
|
+
params: Promise.resolve({ locale: "en", slug: "acme-corp" }),
|
|
91
|
+
getMdxSource: getMdxSourcePopulated,
|
|
92
|
+
});
|
|
93
|
+
const { container } = render(element);
|
|
94
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
95
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Acme Corp","description":"Acme Corp is a strategic partner.","mainEntity":{"@context":"https://schema.org","@type":"Organization","name":"Acme Corp","description":"Acme Corp is a strategic partner."}},{"@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":"Partners","item":"http://localhost:3000/en/partners"},{"@type":"ListItem","position":3,"name":"Acme Corp","item":"http://localhost:3000/en/partners/acme-corp"}]}]}</script><main><h1>Acme Corp</h1><p data-testid="acme-corp-description">Acme Corp is a strategic partner.</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">mdx-body-content</div></article></div></div></main>"`,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("null source triggers notFound()", async () => {
|
|
100
|
+
const { notFound } = jest.requireMock("next/navigation") as {
|
|
101
|
+
notFound: jest.MockedFunction<() => never>;
|
|
102
|
+
};
|
|
103
|
+
notFound.mockClear();
|
|
104
|
+
|
|
105
|
+
await expect(
|
|
106
|
+
PartnerSlugPage({
|
|
107
|
+
params: Promise.resolve({ locale: "en", slug: "unknown" }),
|
|
108
|
+
getMdxSource: getMdxSourceEmpty,
|
|
109
|
+
}),
|
|
110
|
+
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
111
|
+
expect(notFound).toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Partners list page. Reads PartnersList MDX via the content adapter and emits
|
|
2
|
+
// a JSON-LD CollectionPage via the dispatcher.
|
|
3
|
+
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
4
|
+
import { renderPage } from "@working-theory/page-renderer";
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
params: Promise<{ locale: string }>;
|
|
9
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default async function Page({
|
|
13
|
+
params,
|
|
14
|
+
getMdxSource,
|
|
15
|
+
}: Props): Promise<ReactElement> {
|
|
16
|
+
const { locale } = await params;
|
|
17
|
+
return renderPage({
|
|
18
|
+
type: "PartnersList",
|
|
19
|
+
slug: "partners",
|
|
20
|
+
locale,
|
|
21
|
+
translationsNamespace: "partners",
|
|
22
|
+
bodyMode: "mdx-body",
|
|
23
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Product detail page. Reads the Product MDX for the given slug, renders via
|
|
2
|
+
// next-mdx-remote/rsc, and emits a JSON-LD <script>. Unknown slugs trigger 404.
|
|
3
|
+
import {
|
|
4
|
+
renderPage,
|
|
5
|
+
getMdxSource as defaultGetMdxSource,
|
|
6
|
+
getMdxSlugs,
|
|
7
|
+
} from "@working-theory/page-renderer";
|
|
8
|
+
import { notFound } from "next/navigation";
|
|
9
|
+
import type { ReactElement } from "react";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
params: Promise<{ locale: string; slug: string }>;
|
|
13
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function generateStaticParams({
|
|
17
|
+
params,
|
|
18
|
+
}: {
|
|
19
|
+
params: { locale: string };
|
|
20
|
+
}): Promise<{ slug: string }[]> {
|
|
21
|
+
const slugs = await getMdxSlugs("Product", params.locale);
|
|
22
|
+
return slugs.map((slug) => ({ slug }));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default async function Page({
|
|
26
|
+
params,
|
|
27
|
+
getMdxSource = defaultGetMdxSource,
|
|
28
|
+
}: Props): Promise<ReactElement> {
|
|
29
|
+
const { locale, slug } = await params;
|
|
30
|
+
const source = await getMdxSource("Product", slug, locale);
|
|
31
|
+
if (!source) {
|
|
32
|
+
notFound();
|
|
33
|
+
}
|
|
34
|
+
return renderPage({
|
|
35
|
+
type: "Product",
|
|
36
|
+
slug,
|
|
37
|
+
locale,
|
|
38
|
+
translationsNamespace: "products",
|
|
39
|
+
bodyMode: "mdx-body",
|
|
40
|
+
populatedDescriptionTestId: "product-description",
|
|
41
|
+
getMdxSource: async () => source,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for the products catalog routes
|
|
3
|
+
* apps/web → @working-theory/page-renderer migration (Spec 1.5.1).
|
|
4
|
+
*
|
|
5
|
+
* Node version: captured against Node 22.x (same as CI).
|
|
6
|
+
* MDX seed content: DI-injected stub — no fs reads, deterministic across environments.
|
|
7
|
+
*
|
|
8
|
+
* Two-commit strategy (per spec §6):
|
|
9
|
+
* Commit 1: this file added; snapshots captured against pre-migration routes.
|
|
10
|
+
* Commit 2: routes migrated; same snapshots verify diff=0 (GREEN).
|
|
11
|
+
*
|
|
12
|
+
* Routes under test:
|
|
13
|
+
* - products/page.tsx (index — empty-state + populated)
|
|
14
|
+
* - products/[slug]/page.tsx (detail — populated + null→notFound)
|
|
15
|
+
*
|
|
16
|
+
* Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
|
|
17
|
+
* A non-zero diff indicates a behavior regression in the migration. Escalate.
|
|
18
|
+
*/
|
|
19
|
+
import React from "react";
|
|
20
|
+
import { render } from "@testing-library/react";
|
|
21
|
+
|
|
22
|
+
jest.mock("next/navigation", () => ({
|
|
23
|
+
__esModule: true,
|
|
24
|
+
notFound: jest.fn(() => {
|
|
25
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { notFound } from "next/navigation";
|
|
30
|
+
import ProductsPage from "../page";
|
|
31
|
+
import ProductSlugPage from "../[slug]/page";
|
|
32
|
+
|
|
33
|
+
const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
|
|
34
|
+
|
|
35
|
+
// ── Shared stub helpers ────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function makeProductsListSource() {
|
|
38
|
+
return {
|
|
39
|
+
frontmatter: {
|
|
40
|
+
type: "ProductsList" as const,
|
|
41
|
+
slug: "products",
|
|
42
|
+
title: "Products",
|
|
43
|
+
description: "All products.",
|
|
44
|
+
},
|
|
45
|
+
body: "# Products\n\nBody.",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeProductSource(slug: string) {
|
|
50
|
+
return {
|
|
51
|
+
frontmatter: {
|
|
52
|
+
type: "Product" as const,
|
|
53
|
+
slug,
|
|
54
|
+
title: "AI Launch Kit",
|
|
55
|
+
description: "The AX-first OSS template.",
|
|
56
|
+
},
|
|
57
|
+
body: "# AI Launch Kit\n\nBody.",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── products/page.tsx — index ──────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
describe("ProductsPage — snapshot regression", () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
mockNotFound.mockClear();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("empty state renders deterministic HTML", async () => {
|
|
69
|
+
const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
|
|
70
|
+
const element = await ProductsPage({
|
|
71
|
+
params: Promise.resolve({ locale: "en" }),
|
|
72
|
+
getMdxSource: getMdxSourceEmpty,
|
|
73
|
+
});
|
|
74
|
+
const { container } = render(element);
|
|
75
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
76
|
+
`"<main><h1>Products</h1><p data-testid="section-empty-state">Products listing coming soon.</p></main>"`,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("populated state renders deterministic HTML", async () => {
|
|
81
|
+
const source = makeProductsListSource();
|
|
82
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
|
|
83
|
+
const element = await ProductsPage({
|
|
84
|
+
params: Promise.resolve({ locale: "en" }),
|
|
85
|
+
getMdxSource: getMdxSourcePopulated,
|
|
86
|
+
});
|
|
87
|
+
const { container } = render(element);
|
|
88
|
+
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
89
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Products","description":"All products.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"SoftwareApplication"}]}}</script><main><h1>Products</h1><p data-testid="products-description">All products.</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="# Products
|
|
90
|
+
|
|
91
|
+
Body."></div></article></div></div></main>"
|
|
92
|
+
`);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── products/[slug]/page.tsx — detail ─────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describe("ProductSlugPage — snapshot regression", () => {
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
mockNotFound.mockClear();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("populated state renders deterministic HTML", async () => {
|
|
104
|
+
const slug = "launch-kit";
|
|
105
|
+
const source = makeProductSource(slug);
|
|
106
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
|
|
107
|
+
const element = await ProductSlugPage({
|
|
108
|
+
params: Promise.resolve({ locale: "en", slug }),
|
|
109
|
+
getMdxSource: getMdxSourcePopulated,
|
|
110
|
+
});
|
|
111
|
+
const { container } = render(element);
|
|
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":"AI Launch Kit","description":"The AX-first OSS template.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"AI Launch Kit","description":"The AX-first OSS template."}},{"@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":"Products","item":"http://localhost:3000/en/products"},{"@type":"ListItem","position":3,"name":"AI Launch Kit","item":"http://localhost:3000/en/products/launch-kit"}]}]}</script><main><h1>AI Launch Kit</h1><p data-testid="product-description">The AX-first OSS template.</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="# AI Launch Kit
|
|
114
|
+
|
|
115
|
+
Body."></div></article></div></div></main>"
|
|
116
|
+
`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("null source calls notFound()", async () => {
|
|
120
|
+
const getMdxSourceNull = jest.fn().mockResolvedValue(null);
|
|
121
|
+
await expect(
|
|
122
|
+
ProductSlugPage({
|
|
123
|
+
params: Promise.resolve({ locale: "en", slug: "unknown-slug" }),
|
|
124
|
+
getMdxSource: getMdxSourceNull,
|
|
125
|
+
}),
|
|
126
|
+
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
127
|
+
expect(mockNotFound).toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
});
|