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,25 @@
|
|
|
1
|
+
// Products list page. Reads ProductsList 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: "ProductsList",
|
|
19
|
+
slug: "products",
|
|
20
|
+
locale,
|
|
21
|
+
translationsNamespace: "products",
|
|
22
|
+
bodyMode: "mdx-body",
|
|
23
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Project detail page. Reads Project MDX for the given slug, renders sections
|
|
2
|
+
// via SectionRenderer, and emits a JSON-LD <script>. Unknown slugs 404 via notFound().
|
|
3
|
+
import type {
|
|
4
|
+
getMdxSource as defaultGetMdxSource,
|
|
5
|
+
getPageSections as defaultGetPageSections,
|
|
6
|
+
} from "@working-theory/page-renderer";
|
|
7
|
+
import { getContentAdapter, renderPage } from "@working-theory/page-renderer";
|
|
8
|
+
import type { ReactElement } from "react";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
params: Promise<{ locale: string; slug: string }>;
|
|
12
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
13
|
+
getPageSections?: typeof defaultGetPageSections;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function generateStaticParams({
|
|
17
|
+
params,
|
|
18
|
+
}: {
|
|
19
|
+
params: { locale: string };
|
|
20
|
+
}): Promise<{ slug: string }[]> {
|
|
21
|
+
const adapter = getContentAdapter();
|
|
22
|
+
const pages = await adapter.listDetail("Project", params.locale);
|
|
23
|
+
return pages.map((page) => ({ slug: page.slug }));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default async function Page({
|
|
27
|
+
params,
|
|
28
|
+
getMdxSource,
|
|
29
|
+
getPageSections,
|
|
30
|
+
}: Props): Promise<ReactElement> {
|
|
31
|
+
const { locale, slug } = await params;
|
|
32
|
+
return renderPage({
|
|
33
|
+
type: "Project",
|
|
34
|
+
slug,
|
|
35
|
+
locale,
|
|
36
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
37
|
+
...(getPageSections !== undefined && { getPageSections }),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for the projects 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
|
+
* Routes under test:
|
|
12
|
+
* - projects/page.tsx (index — empty-state + populated; sections mode)
|
|
13
|
+
* - projects/[slug]/page.tsx (detail — populated + null→notFound; sections mode)
|
|
14
|
+
*
|
|
15
|
+
* Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
|
|
16
|
+
* A non-zero diff indicates a behavior regression in the migration. Escalate.
|
|
17
|
+
*/
|
|
18
|
+
import React from "react";
|
|
19
|
+
import { render } from "@testing-library/react";
|
|
20
|
+
|
|
21
|
+
// ── @working-theory/ui mock ─────────────────────────────────────────────────────────────
|
|
22
|
+
jest.mock("@working-theory/ui", () => ({
|
|
23
|
+
SectionRenderer: ({ sections }: { sections: unknown[] }) =>
|
|
24
|
+
React.createElement("div", {
|
|
25
|
+
"data-testid": "section-renderer",
|
|
26
|
+
"data-section-count": String(sections.length),
|
|
27
|
+
}),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// ── next/navigation mock ───────────────────────────────────────────────────────
|
|
31
|
+
jest.mock("next/navigation", () => ({
|
|
32
|
+
__esModule: true,
|
|
33
|
+
notFound: jest.fn(() => {
|
|
34
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
35
|
+
}),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
import { notFound } from "next/navigation";
|
|
39
|
+
import ProjectsPage from "../page";
|
|
40
|
+
import ProjectSlugPage from "../[slug]/page";
|
|
41
|
+
|
|
42
|
+
const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
|
|
43
|
+
|
|
44
|
+
// ── Shared stub helpers ────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function makeProjectsListSource() {
|
|
47
|
+
return {
|
|
48
|
+
frontmatter: {
|
|
49
|
+
type: "ProjectsList" as const,
|
|
50
|
+
slug: "projects",
|
|
51
|
+
title: "Projects",
|
|
52
|
+
description: "Open-source projects built with AI Launch Kit.",
|
|
53
|
+
},
|
|
54
|
+
body: "# Projects\n\nBody.",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeProjectSource(slug: string) {
|
|
59
|
+
return {
|
|
60
|
+
frontmatter: {
|
|
61
|
+
type: "Project" as const,
|
|
62
|
+
slug,
|
|
63
|
+
title: "AI Launch Kit",
|
|
64
|
+
description: "The AX-first OSS template.",
|
|
65
|
+
},
|
|
66
|
+
body: "# AI Launch Kit\n\nBody.",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── projects/page.tsx — index ──────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
describe("ProjectsPage — snapshot regression", () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
mockNotFound.mockClear();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("empty state renders deterministic HTML", async () => {
|
|
78
|
+
const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
|
|
79
|
+
const element = await ProjectsPage({
|
|
80
|
+
params: Promise.resolve({ locale: "en" }),
|
|
81
|
+
getMdxSource: getMdxSourceEmpty,
|
|
82
|
+
});
|
|
83
|
+
const { container } = render(element);
|
|
84
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
85
|
+
`"<main><h1>Projects</h1><p data-testid="section-empty-state">Projects listing coming soon.</p></main>"`,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("populated state renders deterministic HTML", async () => {
|
|
90
|
+
const source = makeProjectsListSource();
|
|
91
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
|
|
92
|
+
const getPageSectionsStub = jest.fn().mockResolvedValue([]);
|
|
93
|
+
const element = await ProjectsPage({
|
|
94
|
+
params: Promise.resolve({ locale: "en" }),
|
|
95
|
+
getMdxSource: getMdxSourcePopulated,
|
|
96
|
+
getPageSections: getPageSectionsStub,
|
|
97
|
+
});
|
|
98
|
+
const { container } = render(element);
|
|
99
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
100
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Projects","description":"Open-source projects built with AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"CreativeWork"}]}}</script><main><h1>Projects</h1><p data-testid="projects-description">Open-source projects built with AI Launch Kit.</p><div data-testid="section-renderer" data-section-count="0"></div></main>"`,
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ── projects/[slug]/page.tsx — detail ─────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
describe("ProjectSlugPage — snapshot regression", () => {
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
mockNotFound.mockClear();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("populated state renders deterministic HTML", async () => {
|
|
113
|
+
const slug = "ailk";
|
|
114
|
+
const source = makeProjectSource(slug);
|
|
115
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
|
|
116
|
+
const getPageSectionsStub = jest.fn().mockResolvedValue([]);
|
|
117
|
+
const element = await ProjectSlugPage({
|
|
118
|
+
params: Promise.resolve({ locale: "en", slug }),
|
|
119
|
+
getMdxSource: getMdxSourcePopulated,
|
|
120
|
+
getPageSections: getPageSectionsStub,
|
|
121
|
+
});
|
|
122
|
+
const { container } = render(element);
|
|
123
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
124
|
+
`"<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":"CreativeWork","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":"Projects","item":"http://localhost:3000/en/projects"},{"@type":"ListItem","position":3,"name":"AI Launch Kit","item":"http://localhost:3000/en/projects/ailk"}]}]}</script><main><h1>AI Launch Kit</h1><p data-testid="ailk-description">The AX-first OSS template.</p><div data-testid="section-renderer" data-section-count="0"></div></main>"`,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("null source triggers notFound()", async () => {
|
|
129
|
+
const getMdxSourceNull = jest.fn().mockResolvedValue(null);
|
|
130
|
+
await expect(
|
|
131
|
+
ProjectSlugPage({
|
|
132
|
+
params: Promise.resolve({ locale: "en", slug: "unknown-slug" }),
|
|
133
|
+
getMdxSource: getMdxSourceNull,
|
|
134
|
+
}),
|
|
135
|
+
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
136
|
+
expect(mockNotFound).toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Projects listing page. Reads the ProjectsList MDX from the content backend,
|
|
2
|
+
// renders sections via SectionRenderer, and emits a JSON-LD <script>.
|
|
3
|
+
import type {
|
|
4
|
+
getMdxSource as defaultGetMdxSource,
|
|
5
|
+
getPageSections as defaultGetPageSections,
|
|
6
|
+
} from "@working-theory/page-renderer";
|
|
7
|
+
import { renderPage } from "@working-theory/page-renderer";
|
|
8
|
+
import type { ReactElement } from "react";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
params: Promise<{ locale: string }>;
|
|
12
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
13
|
+
getPageSections?: typeof defaultGetPageSections;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default async function Page({
|
|
17
|
+
params,
|
|
18
|
+
getMdxSource,
|
|
19
|
+
getPageSections,
|
|
20
|
+
}: Props): Promise<ReactElement> {
|
|
21
|
+
const { locale } = await params;
|
|
22
|
+
return renderPage({
|
|
23
|
+
type: "ProjectsList",
|
|
24
|
+
slug: "projects",
|
|
25
|
+
locale,
|
|
26
|
+
translationsNamespace: "projects",
|
|
27
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
28
|
+
...(getPageSections !== undefined && { getPageSections }),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Resource detail page. Reads the ResourceDetail MDX for the given slug, renders
|
|
2
|
+
// via renderPage (mdx-body mode), and emits a JSON-LD <script>. Unknown slugs trigger 404.
|
|
3
|
+
import type {
|
|
4
|
+
getMdxSource as defaultGetMdxSource,
|
|
5
|
+
} from "@working-theory/page-renderer";
|
|
6
|
+
import {
|
|
7
|
+
getMdxSlugs,
|
|
8
|
+
getMdxSource as defaultGetMdxSourceImpl,
|
|
9
|
+
renderPage,
|
|
10
|
+
} from "@working-theory/page-renderer";
|
|
11
|
+
import { notFound } from "next/navigation";
|
|
12
|
+
import type { ReactElement } from "react";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
params: Promise<{ locale: string; slug: string }>;
|
|
16
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function generateStaticParams({
|
|
20
|
+
params,
|
|
21
|
+
}: {
|
|
22
|
+
params: { locale: string };
|
|
23
|
+
}): Promise<{ slug: string }[]> {
|
|
24
|
+
const slugs = await getMdxSlugs("ResourceDetail", params.locale);
|
|
25
|
+
return slugs.map((slug) => ({ slug }));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default async function Page({
|
|
29
|
+
params,
|
|
30
|
+
getMdxSource = defaultGetMdxSourceImpl,
|
|
31
|
+
}: Props): Promise<ReactElement> {
|
|
32
|
+
const { locale, slug } = await params;
|
|
33
|
+
|
|
34
|
+
// Pre-check: verify slug exists; call notFound() on null (not empty-state JSX).
|
|
35
|
+
const source = await getMdxSource("ResourceDetail", slug, locale);
|
|
36
|
+
if (!source) {
|
|
37
|
+
notFound();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// translationsNamespace is required by RenderPageInput but unreachable here:
|
|
41
|
+
// notFound() above ensures renderPage's empty-state branch never executes.
|
|
42
|
+
return renderPage({
|
|
43
|
+
type: "ResourceDetail",
|
|
44
|
+
slug,
|
|
45
|
+
locale,
|
|
46
|
+
translationsNamespace: slug,
|
|
47
|
+
bodyMode: "mdx-body",
|
|
48
|
+
populatedDescriptionTestId: "resource-description",
|
|
49
|
+
getMdxSource,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for resources/page.tsx and resources/[slug]/page.tsx
|
|
3
|
+
* → @working-theory/page-renderer migration.
|
|
4
|
+
*
|
|
5
|
+
* Two-commit strategy:
|
|
6
|
+
* Commit 1: this file added; snapshots captured against pre-migration routes (RED-passing).
|
|
7
|
+
* Commit 2: routes migrated; same snapshots verify diff=0 (GREEN).
|
|
8
|
+
*
|
|
9
|
+
* Four states tested:
|
|
10
|
+
* - resources hub empty: getMdxSource returns null → empty-state branch
|
|
11
|
+
* - resources hub populated: getMdxSource returns a minimal MdxPageSource → full render
|
|
12
|
+
* - resource detail populated: getMdxSource returns populated source → full render
|
|
13
|
+
* - resource detail notFound: getMdxSource returns null → notFound() called
|
|
14
|
+
*
|
|
15
|
+
* Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
|
|
16
|
+
*/
|
|
17
|
+
import React from "react";
|
|
18
|
+
import { render } from "@testing-library/react";
|
|
19
|
+
|
|
20
|
+
// ── @working-theory/ui mock ─────────────────────────────────────────────────────────────
|
|
21
|
+
jest.mock("@working-theory/ui", () => ({
|
|
22
|
+
SectionRenderer: ({ sections }: { sections: unknown[] }) =>
|
|
23
|
+
React.createElement("div", {
|
|
24
|
+
"data-testid": "section-renderer",
|
|
25
|
+
"data-section-count": String(sections.length),
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// ── next/navigation mock ──────────────────────────────────────────────────────
|
|
30
|
+
const mockNotFound = jest.fn(() => {
|
|
31
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
32
|
+
});
|
|
33
|
+
jest.mock("next/navigation", () => ({
|
|
34
|
+
notFound: () => mockNotFound(),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
import ResourcesPage from "../page";
|
|
38
|
+
import ResourceDetailPage from "../[slug]/page";
|
|
39
|
+
|
|
40
|
+
const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
|
|
41
|
+
|
|
42
|
+
// ── Resources Hub ─────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe("ResourcesPage — snapshot regression", () => {
|
|
45
|
+
it("empty state renders deterministic HTML", async () => {
|
|
46
|
+
const element = await ResourcesPage({
|
|
47
|
+
params: Promise.resolve({ locale: "en" }),
|
|
48
|
+
getMdxSource: getMdxSourceEmpty,
|
|
49
|
+
});
|
|
50
|
+
const { container } = render(element);
|
|
51
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
52
|
+
`"<main><h1>Resources</h1><p data-testid="section-empty-state">Resources listing coming soon.</p></main>"`,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("populated state renders deterministic HTML", async () => {
|
|
57
|
+
const source = {
|
|
58
|
+
frontmatter: {
|
|
59
|
+
type: "ResourcesHub" as const,
|
|
60
|
+
slug: "resources",
|
|
61
|
+
title: "Resources",
|
|
62
|
+
description: "Guides, templates, and tools.",
|
|
63
|
+
},
|
|
64
|
+
body: "",
|
|
65
|
+
};
|
|
66
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
|
|
67
|
+
|
|
68
|
+
const element = await ResourcesPage({
|
|
69
|
+
params: Promise.resolve({ locale: "en" }),
|
|
70
|
+
getMdxSource: getMdxSourcePopulated,
|
|
71
|
+
});
|
|
72
|
+
const { container } = render(element);
|
|
73
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
74
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Resources","description":"Guides, templates, and tools.","mainEntity":{"@type":"ItemList"}}</script><main><h1>Resources</h1><p data-testid="resources-description">Guides, templates, and tools.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source=""></div></article></div></div></main>"`,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ── Resource Detail ───────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
describe("ResourceDetailPage — snapshot regression", () => {
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
mockNotFound.mockClear();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("populated state renders deterministic HTML", async () => {
|
|
87
|
+
const source = {
|
|
88
|
+
frontmatter: {
|
|
89
|
+
type: "ResourceDetail" as const,
|
|
90
|
+
slug: "getting-started",
|
|
91
|
+
title: "Getting Started",
|
|
92
|
+
description: "A guide for new users.",
|
|
93
|
+
},
|
|
94
|
+
body: "",
|
|
95
|
+
};
|
|
96
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
|
|
97
|
+
|
|
98
|
+
const element = await ResourceDetailPage({
|
|
99
|
+
params: Promise.resolve({ locale: "en", slug: "getting-started" }),
|
|
100
|
+
getMdxSource: getMdxSourcePopulated,
|
|
101
|
+
});
|
|
102
|
+
const { container } = render(element);
|
|
103
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
104
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Getting Started","description":"A guide for new users.","mainEntity":{"@type":"Article","headline":"Getting Started"}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Resources","item":"http://localhost:3000/en/resources"},{"@type":"ListItem","position":3,"name":"Getting Started","item":"http://localhost:3000/en/resources/getting-started"}]}]}</script><main><h1>Getting Started</h1><p data-testid="resource-description">A guide for new users.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source=""></div></article></div></div></main>"`,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("notFound state calls notFound() when source is null", async () => {
|
|
109
|
+
await expect(
|
|
110
|
+
ResourceDetailPage({
|
|
111
|
+
params: Promise.resolve({ locale: "en", slug: "missing" }),
|
|
112
|
+
getMdxSource: getMdxSourceEmpty,
|
|
113
|
+
}),
|
|
114
|
+
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
115
|
+
expect(mockNotFound).toHaveBeenCalledTimes(1);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Resources hub page. Reads ResourcesHub MDX via the content adapter and emits
|
|
2
|
+
// a JSON-LD CollectionPage via the dispatcher.
|
|
3
|
+
import type {
|
|
4
|
+
getMdxSource as defaultGetMdxSource,
|
|
5
|
+
} from "@working-theory/page-renderer";
|
|
6
|
+
import { renderPage } from "@working-theory/page-renderer";
|
|
7
|
+
import type { ReactElement } from "react";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
params: Promise<{ locale: string }>;
|
|
11
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default async function Page({
|
|
15
|
+
params,
|
|
16
|
+
getMdxSource,
|
|
17
|
+
}: Props): Promise<ReactElement> {
|
|
18
|
+
const { locale } = await params;
|
|
19
|
+
return renderPage({
|
|
20
|
+
type: "ResourcesHub",
|
|
21
|
+
slug: "resources",
|
|
22
|
+
locale,
|
|
23
|
+
translationsNamespace: "resources",
|
|
24
|
+
bodyMode: "mdx-body",
|
|
25
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Service detail page. Reads the Service 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("Service", 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("Service", slug, locale);
|
|
31
|
+
if (!source) {
|
|
32
|
+
notFound();
|
|
33
|
+
}
|
|
34
|
+
return renderPage({
|
|
35
|
+
type: "Service",
|
|
36
|
+
slug,
|
|
37
|
+
locale,
|
|
38
|
+
translationsNamespace: "services",
|
|
39
|
+
bodyMode: "mdx-body",
|
|
40
|
+
populatedDescriptionTestId: "service-description",
|
|
41
|
+
getMdxSource: async () => source,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for the services 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
|
+
* - services/page.tsx (index — empty-state + populated)
|
|
14
|
+
* - services/[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 ServicesPage from "../page";
|
|
31
|
+
import ServiceSlugPage from "../[slug]/page";
|
|
32
|
+
|
|
33
|
+
const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
|
|
34
|
+
|
|
35
|
+
// ── Shared stub helpers ────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function makeServicesListSource() {
|
|
38
|
+
return {
|
|
39
|
+
frontmatter: {
|
|
40
|
+
type: "ServicesList" as const,
|
|
41
|
+
slug: "services",
|
|
42
|
+
title: "Services",
|
|
43
|
+
description: "All services.",
|
|
44
|
+
},
|
|
45
|
+
body: "# Services\n\nBody.",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeServiceSource(slug: string) {
|
|
50
|
+
return {
|
|
51
|
+
frontmatter: {
|
|
52
|
+
type: "Service" as const,
|
|
53
|
+
slug,
|
|
54
|
+
title: "Implementation consulting",
|
|
55
|
+
description: "Help adapting AI Launch Kit.",
|
|
56
|
+
},
|
|
57
|
+
body: "# Implementation consulting\n\nBody.",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── services/page.tsx — index ──────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
describe("ServicesPage — 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 ServicesPage({
|
|
71
|
+
params: Promise.resolve({ locale: "en" }),
|
|
72
|
+
getMdxSource: getMdxSourceEmpty,
|
|
73
|
+
});
|
|
74
|
+
const { container } = render(element);
|
|
75
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
76
|
+
`"<main><h1>Services</h1><p data-testid="section-empty-state">Services listing coming soon.</p></main>"`,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("populated state renders deterministic HTML", async () => {
|
|
81
|
+
const source = makeServicesListSource();
|
|
82
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
|
|
83
|
+
const element = await ServicesPage({
|
|
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":"Services","description":"All services.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Service"}]}}</script><main><h1>Services</h1><p data-testid="services-description">All services.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source="# Services
|
|
90
|
+
|
|
91
|
+
Body."></div></article></div></div></main>"
|
|
92
|
+
`);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── services/[slug]/page.tsx — detail ─────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describe("ServiceSlugPage — snapshot regression", () => {
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
mockNotFound.mockClear();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("populated state renders deterministic HTML", async () => {
|
|
104
|
+
const slug = "consulting";
|
|
105
|
+
const source = makeServiceSource(slug);
|
|
106
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
|
|
107
|
+
const element = await ServiceSlugPage({
|
|
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":"Implementation consulting","description":"Help adapting AI Launch Kit.","mainEntity":{"@context":"https://schema.org","@type":"Service","name":"Implementation consulting","description":"Help adapting AI Launch Kit."}},{"@context":"https://schema.org","@type":"BreadcrumbList","inLanguage":"en","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http://localhost:3000/en/"},{"@type":"ListItem","position":2,"name":"Services","item":"http://localhost:3000/en/services"},{"@type":"ListItem","position":3,"name":"Implementation consulting","item":"http://localhost:3000/en/services/consulting"}]}]}</script><main><h1>Implementation consulting</h1><p data-testid="service-description">Help adapting AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-remote" data-source="# Implementation consulting
|
|
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
|
+
ServiceSlugPage({
|
|
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
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Services list page. Reads ServicesList 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: "ServicesList",
|
|
19
|
+
slug: "services",
|
|
20
|
+
locale,
|
|
21
|
+
translationsNamespace: "services",
|
|
22
|
+
bodyMode: "mdx-body",
|
|
23
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
24
|
+
});
|
|
25
|
+
}
|