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,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /blog catch-all route. With `content/blog/` empty, the index renders the
|
|
3
|
+
* BLOG_EMPTY_SENTINEL placeholder (testable contract).
|
|
4
|
+
*
|
|
5
|
+
* Q5 deferred post-MVP: no `/docs` companion ships in Phase 0 — only the
|
|
6
|
+
* `docsPageJsonLd` builder is exported from `@working-theory/fumadocs-schema`.
|
|
7
|
+
*/
|
|
8
|
+
import { blogListJsonLd, blogPostingJsonLd } from "@working-theory/fumadocs-schema";
|
|
9
|
+
import { JsonLdScript } from "@working-theory/page-renderer";
|
|
10
|
+
import { BlogList, BlogPost } from "@working-theory/ui";
|
|
11
|
+
import type { Metadata } from "next";
|
|
12
|
+
import { notFound } from "next/navigation";
|
|
13
|
+
import { getTranslations } from "next-intl/server";
|
|
14
|
+
import type { ComponentType, ReactElement } from "react";
|
|
15
|
+
|
|
16
|
+
import { toBlogListData } from "../../../../lib/blog-list-data";
|
|
17
|
+
import { toBlogPostData } from "../../../../lib/blog-post-data";
|
|
18
|
+
import { getOgImageUrl } from "../../../../lib/og";
|
|
19
|
+
import { blogSource as defaultBlogSource } from "../../../../lib/source";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sentinel string the empty-state placeholder must contain. Tests assert
|
|
23
|
+
* on this exact value to detect the empty-blog branch without coupling to
|
|
24
|
+
* copy. Exported so the integration test can import it directly.
|
|
25
|
+
*/
|
|
26
|
+
export const BLOG_EMPTY_SENTINEL = "ailk-blog-empty-placeholder";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Frontmatter contract — must match `source.config.ts` `defineDocs.docs.schema`.
|
|
30
|
+
*/
|
|
31
|
+
interface BlogFrontmatter {
|
|
32
|
+
title: string;
|
|
33
|
+
description: string;
|
|
34
|
+
date?: string;
|
|
35
|
+
updated?: string;
|
|
36
|
+
image?: string;
|
|
37
|
+
author?: { name: string; avatar?: string };
|
|
38
|
+
tags?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Post `page.data`: frontmatter plus the Fumadocs-generated `body` (compiled
|
|
43
|
+
* MDX component) and `toc` (table of contents). The body renders through the
|
|
44
|
+
* `BlogPost` section's `children` slot (issue #2714).
|
|
45
|
+
*/
|
|
46
|
+
interface BlogPostPageData extends BlogFrontmatter {
|
|
47
|
+
body?: ComponentType;
|
|
48
|
+
toc?: Array<{ title: unknown; url: string; depth: number }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Minimal blog-source interface covering the subset consumed by this route.
|
|
53
|
+
* Tests inject a stub without mocking lib/source at the module level.
|
|
54
|
+
*/
|
|
55
|
+
interface BlogSource {
|
|
56
|
+
getPages: () => Array<{ url: string; data: unknown }>;
|
|
57
|
+
getPage: (slug: string[]) => { data: unknown } | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface BlogRouteProps {
|
|
61
|
+
params: Promise<{ locale: string; slug?: string[] }>;
|
|
62
|
+
/**
|
|
63
|
+
* Blog source override for testing. Defaults to the real Fumadocs blogSource.
|
|
64
|
+
* Inject a stub to avoid mocking lib/source at the module level.
|
|
65
|
+
*/
|
|
66
|
+
blogSource?: BlogSource;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function generateMetadata(
|
|
70
|
+
props: BlogRouteProps,
|
|
71
|
+
): Promise<Metadata> {
|
|
72
|
+
const { slug } = await props.params;
|
|
73
|
+
const source = props.blogSource ?? defaultBlogSource;
|
|
74
|
+
|
|
75
|
+
if (slug && slug.length > 0) {
|
|
76
|
+
const page = source.getPage(slug);
|
|
77
|
+
if (page) {
|
|
78
|
+
const data = page.data as unknown as BlogFrontmatter;
|
|
79
|
+
const ogImageUrl = getOgImageUrl({
|
|
80
|
+
title: data.title,
|
|
81
|
+
schemaType: "BlogPosting",
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
openGraph: { images: [ogImageUrl] },
|
|
85
|
+
twitter: { images: [ogImageUrl] },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ogImageUrl = getOgImageUrl({ title: "Blog", schemaType: "BlogPosting" });
|
|
91
|
+
return {
|
|
92
|
+
openGraph: { images: [ogImageUrl] },
|
|
93
|
+
twitter: { images: [ogImageUrl] },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default async function BlogRoute(
|
|
98
|
+
props: BlogRouteProps,
|
|
99
|
+
): Promise<ReactElement> {
|
|
100
|
+
const { locale, slug } = await props.params;
|
|
101
|
+
const t = await getTranslations("blog");
|
|
102
|
+
const source = props.blogSource ?? defaultBlogSource;
|
|
103
|
+
|
|
104
|
+
if (slug && slug.length > 0) {
|
|
105
|
+
const page = source.getPage(slug);
|
|
106
|
+
if (!page) notFound();
|
|
107
|
+
|
|
108
|
+
// Fumadocs' `PageData` is opaque; the frontmatter contract is enforced
|
|
109
|
+
// by the `defineDocs` schema in source.config.ts at build time.
|
|
110
|
+
const data = page.data as unknown as BlogPostPageData;
|
|
111
|
+
const jsonLd = blogPostingJsonLd({
|
|
112
|
+
slug: slug.join("/"),
|
|
113
|
+
title: data.title,
|
|
114
|
+
description: data.description,
|
|
115
|
+
locale,
|
|
116
|
+
...(data.date !== undefined && { publishedAt: data.date }),
|
|
117
|
+
...(data.updated !== undefined && { updatedAt: data.updated }),
|
|
118
|
+
...(data.image !== undefined && { image: data.image }),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// The compiled MDX body renders through the section's `children` slot so it
|
|
122
|
+
// keeps full formatting; the section maps the rest of the post (#2714).
|
|
123
|
+
const MDXBody = data.body;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<>
|
|
127
|
+
<JsonLdScript data={jsonLd} />
|
|
128
|
+
<main>
|
|
129
|
+
<BlogPost sectionName="blog-post" data={toBlogPostData(data)}>
|
|
130
|
+
{MDXBody ? <MDXBody /> : null}
|
|
131
|
+
</BlogPost>
|
|
132
|
+
</main>
|
|
133
|
+
</>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const pages = source.getPages();
|
|
138
|
+
|
|
139
|
+
const indexJsonLd = blogListJsonLd({
|
|
140
|
+
slug: "blog",
|
|
141
|
+
title: "Blog",
|
|
142
|
+
description: "Writing on AEO and AX-first software.",
|
|
143
|
+
locale,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (pages.length === 0) {
|
|
147
|
+
return (
|
|
148
|
+
<>
|
|
149
|
+
<JsonLdScript data={indexJsonLd} />
|
|
150
|
+
<main>
|
|
151
|
+
<h1>{t("heading")}</h1>
|
|
152
|
+
<p
|
|
153
|
+
data-testid="blog-empty-placeholder"
|
|
154
|
+
data-sentinel={BLOG_EMPTY_SENTINEL}
|
|
155
|
+
>
|
|
156
|
+
{t("emptyState")} <code>{"content/blog/"}</code>{" "}
|
|
157
|
+
{t("emptyStateTrailing")}
|
|
158
|
+
</p>
|
|
159
|
+
</main>
|
|
160
|
+
</>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<>
|
|
166
|
+
<JsonLdScript data={indexJsonLd} />
|
|
167
|
+
<main>
|
|
168
|
+
<h1>{t("heading")}</h1>
|
|
169
|
+
<BlogList sectionName="blog-list" data={toBlogListData(pages)} />
|
|
170
|
+
</main>
|
|
171
|
+
</>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// JobPosting detail page. Reads the JobPosting MDX for the given slug, renders via
|
|
2
|
+
// next-mdx-remote/rsc, and emits a JSON-LD <script>. Unknown slugs trigger 404.
|
|
3
|
+
// Careers list at /careers links here; each job posting lives at /careers/<slug>.
|
|
4
|
+
import {
|
|
5
|
+
getMdxSource as defaultGetMdxSource,
|
|
6
|
+
getMdxSlugs,
|
|
7
|
+
renderPage,
|
|
8
|
+
} from "@working-theory/page-renderer";
|
|
9
|
+
import { notFound } from "next/navigation";
|
|
10
|
+
import type { ReactElement } from "react";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
params: Promise<{ locale: string; slug: string }>;
|
|
14
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function generateStaticParams({
|
|
18
|
+
params,
|
|
19
|
+
}: {
|
|
20
|
+
params: { locale: string };
|
|
21
|
+
}): Promise<{ slug: string }[]> {
|
|
22
|
+
const slugs = await getMdxSlugs("JobPosting", params.locale);
|
|
23
|
+
return slugs.map((slug: string) => ({ slug }));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default async function Page({
|
|
27
|
+
params,
|
|
28
|
+
getMdxSource = defaultGetMdxSource,
|
|
29
|
+
}: Props): Promise<ReactElement> {
|
|
30
|
+
const { locale, slug } = await params;
|
|
31
|
+
const source = await getMdxSource("JobPosting", slug, locale);
|
|
32
|
+
|
|
33
|
+
if (!source) {
|
|
34
|
+
notFound();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return renderPage({
|
|
38
|
+
type: "JobPosting",
|
|
39
|
+
slug,
|
|
40
|
+
locale,
|
|
41
|
+
translationsNamespace: "careers",
|
|
42
|
+
bodyMode: "mdx-body",
|
|
43
|
+
getMdxSource: async () => source,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for the careers 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
|
+
* Each route is tested in two states:
|
|
12
|
+
* - empty: getMdxSource returns null → empty-state branch
|
|
13
|
+
* - populated: getMdxSource returns a minimal MdxPageSource → full render
|
|
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
|
+
// ── next/navigation mock ───────────────────────────────────────────────────────
|
|
22
|
+
jest.mock("next/navigation", () => ({
|
|
23
|
+
__esModule: true,
|
|
24
|
+
notFound: jest.fn(() => {
|
|
25
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// ── next-mdx-remote/rsc mock ───────────────────────────────────────────────────
|
|
30
|
+
jest.mock("next-mdx-remote/rsc", () => ({
|
|
31
|
+
__esModule: true,
|
|
32
|
+
MDXRemote: ({ source }: { source: string }) =>
|
|
33
|
+
React.createElement("div", { "data-testid": "mdx-rendered" }, source),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// ── Import page components ─────────────────────────────────────────────────────
|
|
37
|
+
import CareersPage from "../page";
|
|
38
|
+
import CareersSlugPage from "../[slug]/page";
|
|
39
|
+
|
|
40
|
+
// ── Shared stubs ───────────────────────────────────────────────────────────────
|
|
41
|
+
const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
|
|
42
|
+
|
|
43
|
+
// ── careers/page (index) ───────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
describe("careers/page — snapshot regression", () => {
|
|
46
|
+
it("empty state renders deterministic HTML", async () => {
|
|
47
|
+
const element = await CareersPage({
|
|
48
|
+
params: Promise.resolve({ locale: "en" }),
|
|
49
|
+
getMdxSource: getMdxSourceEmpty,
|
|
50
|
+
});
|
|
51
|
+
const { container } = render(element);
|
|
52
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
53
|
+
`"<main><h1>Careers</h1><p data-testid="section-empty-state">Career listings coming soon.</p></main>"`,
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("populated state renders deterministic HTML", async () => {
|
|
58
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue({
|
|
59
|
+
frontmatter: {
|
|
60
|
+
type: "CareersList",
|
|
61
|
+
slug: "careers",
|
|
62
|
+
title: "Careers at AILK",
|
|
63
|
+
description: "Join the team building the AX-first platform.",
|
|
64
|
+
},
|
|
65
|
+
body: "mdx-body-content",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const element = await CareersPage({
|
|
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":"Careers at AILK","description":"Join the team building the AX-first platform.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"JobPosting"}]}}</script><main><h1>Careers at AILK</h1><p data-testid="careers-description">Join the team building the AX-first platform.</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>"`,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ── careers/[slug]/page (detail) ───────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
describe("careers/[slug]/page — snapshot regression", () => {
|
|
82
|
+
it("populated state renders deterministic HTML", async () => {
|
|
83
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue({
|
|
84
|
+
frontmatter: {
|
|
85
|
+
type: "JobPosting",
|
|
86
|
+
slug: "senior-engineer",
|
|
87
|
+
title: "Senior Engineer",
|
|
88
|
+
description: "Build the AILK platform.",
|
|
89
|
+
},
|
|
90
|
+
body: "mdx-body-content",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const element = await CareersSlugPage({
|
|
94
|
+
params: Promise.resolve({ locale: "en", slug: "senior-engineer" }),
|
|
95
|
+
getMdxSource: getMdxSourcePopulated,
|
|
96
|
+
});
|
|
97
|
+
const { container } = render(element);
|
|
98
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
99
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"Senior Engineer","description":"Build the AILK platform.","mainEntity":{"@context":"https://schema.org","@type":"JobPosting","title":"Senior Engineer","description":"Build the AILK platform."}},{"@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":"Careers","item":"http://localhost:3000/en/careers"},{"@type":"ListItem","position":3,"name":"Senior Engineer","item":"http://localhost:3000/en/careers/senior-engineer"}]}]}</script><main><h1>Senior Engineer</h1><p data-testid="senior-engineer-description">Build the AILK platform.</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>"`,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("null source triggers notFound()", async () => {
|
|
104
|
+
const { notFound } = jest.requireMock("next/navigation") as {
|
|
105
|
+
notFound: jest.MockedFunction<() => never>;
|
|
106
|
+
};
|
|
107
|
+
notFound.mockClear();
|
|
108
|
+
|
|
109
|
+
await expect(
|
|
110
|
+
CareersSlugPage({
|
|
111
|
+
params: Promise.resolve({ locale: "en", slug: "unknown" }),
|
|
112
|
+
getMdxSource: getMdxSourceEmpty,
|
|
113
|
+
}),
|
|
114
|
+
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
115
|
+
expect(notFound).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Careers list page. Reads CareersList MDX via the content adapter and emits
|
|
2
|
+
// a JSON-LD CollectionPage. Links to JobPosting detail pages at /careers/<slug>.
|
|
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: "CareersList",
|
|
19
|
+
slug: "careers",
|
|
20
|
+
locale,
|
|
21
|
+
translationsNamespace: "careers",
|
|
22
|
+
bodyMode: "mdx-body",
|
|
23
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Case Study detail page. Reads the CaseStudy 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("CaseStudy", 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("CaseStudy", slug, locale);
|
|
31
|
+
|
|
32
|
+
if (!source) {
|
|
33
|
+
notFound();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return renderPage({
|
|
37
|
+
type: "CaseStudy",
|
|
38
|
+
slug,
|
|
39
|
+
locale,
|
|
40
|
+
translationsNamespace: "case-studies",
|
|
41
|
+
bodyMode: "mdx-body",
|
|
42
|
+
getMdxSource: async () => source,
|
|
43
|
+
});
|
|
44
|
+
}
|
package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for the case-studies 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 CaseStudiesPage from "../page";
|
|
34
|
+
import CaseStudySlugPage from "../[slug]/page";
|
|
35
|
+
|
|
36
|
+
// ── Shared stubs ───────────────────────────────────────────────────────────────
|
|
37
|
+
const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
|
|
38
|
+
|
|
39
|
+
// ── case-studies/page (index) ─────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe("case-studies/page — snapshot regression", () => {
|
|
42
|
+
it("empty state renders deterministic HTML", async () => {
|
|
43
|
+
const element = await CaseStudiesPage({
|
|
44
|
+
params: Promise.resolve({ locale: "en" }),
|
|
45
|
+
getMdxSource: getMdxSourceEmpty,
|
|
46
|
+
});
|
|
47
|
+
const { container } = render(element);
|
|
48
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
49
|
+
`"<main><h1>Case Studies</h1><p data-testid="section-empty-state">Case studies 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: "CaseStudiesList",
|
|
57
|
+
slug: "case-studies",
|
|
58
|
+
title: "Case Studies",
|
|
59
|
+
description: "Real-world accounts of teams using AI Launch Kit.",
|
|
60
|
+
},
|
|
61
|
+
body: "mdx-body-content",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const element = await CaseStudiesPage({
|
|
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":"Case Studies","description":"Real-world accounts of teams using AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</script><main><h1>Case Studies</h1><p data-testid="case-studies-description">Real-world accounts of teams using 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
|
+
// ── case-studies/[slug]/page (detail) ────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
describe("case-studies/[slug]/page — snapshot regression", () => {
|
|
78
|
+
it("populated state renders deterministic HTML", async () => {
|
|
79
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue({
|
|
80
|
+
frontmatter: {
|
|
81
|
+
type: "CaseStudy",
|
|
82
|
+
slug: "ailk-aeo-rollout",
|
|
83
|
+
title: "AILK AEO Rollout",
|
|
84
|
+
description: "How a team shipped AEO with AI Launch Kit.",
|
|
85
|
+
},
|
|
86
|
+
body: "mdx-body-content",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const element = await CaseStudySlugPage({
|
|
90
|
+
params: Promise.resolve({ locale: "en", slug: "ailk-aeo-rollout" }),
|
|
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":"AILK AEO Rollout","description":"How a team shipped AEO with AI Launch Kit.","mainEntity":{"@type":"Article","headline":"AILK AEO Rollout"}},{"@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":"Case Studies","item":"http://localhost:3000/en/case-studies"},{"@type":"ListItem","position":3,"name":"AILK AEO Rollout","item":"http://localhost:3000/en/case-studies/ailk-aeo-rollout"}]}]}</script><main><h1>AILK AEO Rollout</h1><p data-testid="ailk-aeo-rollout-description">How a team shipped AEO 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>"`,
|
|
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
|
+
CaseStudySlugPage({
|
|
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
|
+
// Case Studies list page. Reads CaseStudiesList 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: "CaseStudiesList",
|
|
19
|
+
slug: "case-studies",
|
|
20
|
+
locale,
|
|
21
|
+
translationsNamespace: "case-studies",
|
|
22
|
+
bodyMode: "mdx-body",
|
|
23
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for clients/page.tsx → @working-theory/page-renderer migration.
|
|
3
|
+
*
|
|
4
|
+
* Two-commit strategy:
|
|
5
|
+
* Commit 1: this file added; snapshots captured against pre-migration route (RED-passing).
|
|
6
|
+
* Commit 2: route migrated; same snapshots verify diff=0 (GREEN).
|
|
7
|
+
*
|
|
8
|
+
* Two states tested:
|
|
9
|
+
* - empty: getMdxSource returns null → empty-state branch
|
|
10
|
+
* - populated: getMdxSource returns a minimal MdxPageSource → full render
|
|
11
|
+
*
|
|
12
|
+
* Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
|
|
13
|
+
*/
|
|
14
|
+
import React from "react";
|
|
15
|
+
import { render } from "@testing-library/react";
|
|
16
|
+
|
|
17
|
+
// ── @working-theory/ui mock ─────────────────────────────────────────────────────────────
|
|
18
|
+
jest.mock("@working-theory/ui", () => ({
|
|
19
|
+
SectionRenderer: ({ sections }: { sections: unknown[] }) =>
|
|
20
|
+
React.createElement("div", {
|
|
21
|
+
"data-testid": "section-renderer",
|
|
22
|
+
"data-section-count": String(sections.length),
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import ClientsPage from "../page";
|
|
27
|
+
|
|
28
|
+
const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
|
|
29
|
+
|
|
30
|
+
describe("ClientsPage — snapshot regression", () => {
|
|
31
|
+
it("empty state renders deterministic HTML", async () => {
|
|
32
|
+
const element = await ClientsPage({
|
|
33
|
+
params: Promise.resolve({ locale: "en" }),
|
|
34
|
+
getMdxSource: getMdxSourceEmpty,
|
|
35
|
+
});
|
|
36
|
+
const { container } = render(element);
|
|
37
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
38
|
+
`"<main><h1>Clients</h1><p data-testid="section-empty-state">Clients listing coming soon.</p></main>"`,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("populated state renders deterministic HTML", async () => {
|
|
43
|
+
const source = {
|
|
44
|
+
frontmatter: {
|
|
45
|
+
type: "ClientsList" as const,
|
|
46
|
+
slug: "clients",
|
|
47
|
+
title: "Our Clients",
|
|
48
|
+
description: "Companies that trust us.",
|
|
49
|
+
},
|
|
50
|
+
body: "",
|
|
51
|
+
};
|
|
52
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
|
|
53
|
+
|
|
54
|
+
const element = await ClientsPage({
|
|
55
|
+
params: Promise.resolve({ locale: "en" }),
|
|
56
|
+
getMdxSource: getMdxSourcePopulated,
|
|
57
|
+
});
|
|
58
|
+
const { container } = render(element);
|
|
59
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
60
|
+
`"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Our Clients","description":"Companies that trust us.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Organization"}]}}</script><main><h1>Our Clients</h1><p data-testid="clients-description">Companies that trust us.</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>"`,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Clients list page. Reads ClientsList 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: "ClientsList",
|
|
21
|
+
slug: "clients",
|
|
22
|
+
locale,
|
|
23
|
+
translationsNamespace: "clients",
|
|
24
|
+
bodyMode: "mdx-body",
|
|
25
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
26
|
+
});
|
|
27
|
+
}
|