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,34 @@
|
|
|
1
|
+
// SoftwareProduct detail page. Reads the SoftwareProduct MDX for the given slug,
|
|
2
|
+
// renders via @working-theory/page-renderer, and emits a JSON-LD <script>. Unknown slugs 404.
|
|
3
|
+
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
4
|
+
import { getMdxSlugs, renderPage } from "@working-theory/page-renderer";
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
params: Promise<{ locale: string; slug: string }>;
|
|
9
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function generateStaticParams({
|
|
13
|
+
params,
|
|
14
|
+
}: {
|
|
15
|
+
params: { locale: string };
|
|
16
|
+
}): Promise<{ slug: string }[]> {
|
|
17
|
+
const slugs = await getMdxSlugs("SoftwareProduct", params.locale);
|
|
18
|
+
return slugs.map((slug) => ({ slug }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default async function Page({
|
|
22
|
+
params,
|
|
23
|
+
getMdxSource,
|
|
24
|
+
}: Props): Promise<ReactElement> {
|
|
25
|
+
const { locale, slug } = await params;
|
|
26
|
+
return renderPage({
|
|
27
|
+
type: "SoftwareProduct",
|
|
28
|
+
slug,
|
|
29
|
+
locale,
|
|
30
|
+
bodyMode: "mdx-body",
|
|
31
|
+
populatedDescriptionTestId: "software-description",
|
|
32
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for software/[slug] — @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.
|
|
6
|
+
*
|
|
7
|
+
* Two-commit strategy:
|
|
8
|
+
* Commit 1: this file added; snapshots captured against pre-migration routes.
|
|
9
|
+
* Commit 2: routes migrated; same snapshots verify diff=0.
|
|
10
|
+
*
|
|
11
|
+
* Routes covered:
|
|
12
|
+
* - software/[slug] (populated: populated state)
|
|
13
|
+
* - software/[slug] (notFound: unknown slug triggers notFound())
|
|
14
|
+
*
|
|
15
|
+
* Note: software/page.tsx is a slug-listing route (not MDX-body); only the
|
|
16
|
+
* import path changes. No snapshot tests needed for software/page.
|
|
17
|
+
*
|
|
18
|
+
* Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
|
|
19
|
+
* A non-zero diff indicates a behavior regression in the migration. Escalate.
|
|
20
|
+
*/
|
|
21
|
+
import React from "react";
|
|
22
|
+
import { render } from "@testing-library/react";
|
|
23
|
+
|
|
24
|
+
jest.mock("next-mdx-remote/rsc", () => ({
|
|
25
|
+
__esModule: true,
|
|
26
|
+
MDXRemote: ({ source }: { source: string }): React.ReactElement =>
|
|
27
|
+
React.createElement("div", { "data-testid": "mdx-rendered" }, source),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
jest.mock("next/navigation", () => ({
|
|
31
|
+
__esModule: true,
|
|
32
|
+
notFound: jest.fn(() => {
|
|
33
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
34
|
+
}),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
import SoftwareSlugPage from "../[slug]/page";
|
|
38
|
+
import { notFound } from "next/navigation";
|
|
39
|
+
|
|
40
|
+
const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
|
|
41
|
+
|
|
42
|
+
describe("software/[slug] — snapshot regression", () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
mockNotFound.mockClear();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("populated state renders deterministic HTML", async () => {
|
|
48
|
+
const getMdxSource = jest.fn().mockResolvedValue({
|
|
49
|
+
frontmatter: {
|
|
50
|
+
type: "SoftwareProduct" as const,
|
|
51
|
+
slug: "page-renderer",
|
|
52
|
+
title: "@working-theory/page-renderer",
|
|
53
|
+
description: "Single rendering primitive for MDX-wired routes.",
|
|
54
|
+
},
|
|
55
|
+
body: "## Overview\n\nBody content.",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const element = await SoftwareSlugPage({
|
|
59
|
+
params: Promise.resolve({ locale: "en", slug: "page-renderer" }),
|
|
60
|
+
getMdxSource,
|
|
61
|
+
});
|
|
62
|
+
const { container } = render(element);
|
|
63
|
+
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
64
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"@working-theory/page-renderer","description":"Single rendering primitive for MDX-wired routes.","mainEntity":{"@type":"SoftwareApplication","name":"@working-theory/page-renderer"}}</script><main><h1>@working-theory/page-renderer</h1><p data-testid="software-description">Single rendering primitive for MDX-wired routes.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">## Overview
|
|
65
|
+
|
|
66
|
+
Body content.</div></article></div></div></main>"
|
|
67
|
+
`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("unknown slug triggers notFound()", async () => {
|
|
71
|
+
const getMdxSource = jest.fn().mockResolvedValue(null);
|
|
72
|
+
|
|
73
|
+
await expect(
|
|
74
|
+
SoftwareSlugPage({
|
|
75
|
+
params: Promise.resolve({ locale: "en", slug: "unknown-slug" }),
|
|
76
|
+
getMdxSource,
|
|
77
|
+
}),
|
|
78
|
+
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
79
|
+
expect(mockNotFound).toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Software list page. Enumerates SoftwareProduct slugs via the content adapter
|
|
2
|
+
// and renders a directory listing. No dedicated list MDX type exists for SoftwareProduct.
|
|
3
|
+
import { getMdxSlugs as defaultGetMdxSlugs } from "@working-theory/page-renderer";
|
|
4
|
+
import { getTranslations } from "next-intl/server";
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
params: Promise<{ locale: string }>;
|
|
9
|
+
getMdxSlugs?: typeof defaultGetMdxSlugs;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default async function Page({
|
|
13
|
+
params,
|
|
14
|
+
getMdxSlugs = defaultGetMdxSlugs,
|
|
15
|
+
}: Props): Promise<ReactElement> {
|
|
16
|
+
const { locale } = await params;
|
|
17
|
+
const t = await getTranslations("software");
|
|
18
|
+
const slugs = await getMdxSlugs("SoftwareProduct", locale);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<main>
|
|
22
|
+
<h1>{t("heading")}</h1>
|
|
23
|
+
<p data-testid="software-description">
|
|
24
|
+
{t("description")}
|
|
25
|
+
</p>
|
|
26
|
+
{slugs.length === 0 ? (
|
|
27
|
+
<p data-testid="section-empty-state">{t("emptyState")}</p>
|
|
28
|
+
) : (
|
|
29
|
+
<ul data-testid="software-list">
|
|
30
|
+
{slugs.map((slug) => (
|
|
31
|
+
<li key={slug}>
|
|
32
|
+
<a href={`/${locale}/software/${slug}`}>{slug}</a>
|
|
33
|
+
</li>
|
|
34
|
+
))}
|
|
35
|
+
</ul>
|
|
36
|
+
)}
|
|
37
|
+
</main>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// TechArticle detail page. Reads the TechArticle MDX for the given slug, renders via
|
|
2
|
+
// @working-theory/page-renderer, and emits a JSON-LD <script>. Unknown slugs trigger 404.
|
|
3
|
+
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
4
|
+
import { getMdxSlugs, renderPage } from "@working-theory/page-renderer";
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
params: Promise<{ locale: string; slug: string }>;
|
|
9
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function generateStaticParams({
|
|
13
|
+
params,
|
|
14
|
+
}: {
|
|
15
|
+
params: { locale: string };
|
|
16
|
+
}): Promise<{ slug: string }[]> {
|
|
17
|
+
const slugs = await getMdxSlugs("TechArticle", params.locale);
|
|
18
|
+
return slugs.map((slug) => ({ slug }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default async function Page({
|
|
22
|
+
params,
|
|
23
|
+
getMdxSource,
|
|
24
|
+
}: Props): Promise<ReactElement> {
|
|
25
|
+
const { locale, slug } = await params;
|
|
26
|
+
return renderPage({
|
|
27
|
+
type: "TechArticle",
|
|
28
|
+
slug,
|
|
29
|
+
locale,
|
|
30
|
+
bodyMode: "mdx-body",
|
|
31
|
+
populatedDescriptionTestId: "tech-article-description",
|
|
32
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
33
|
+
});
|
|
34
|
+
}
|
package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for tech-articles/[slug] — @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.
|
|
6
|
+
*
|
|
7
|
+
* Two-commit strategy:
|
|
8
|
+
* Commit 1: this file added; snapshots captured against pre-migration routes.
|
|
9
|
+
* Commit 2: routes migrated; same snapshots verify diff=0.
|
|
10
|
+
*
|
|
11
|
+
* Routes covered:
|
|
12
|
+
* - tech-articles/[slug] (populated: populated state)
|
|
13
|
+
* - tech-articles/[slug] (notFound: unknown slug triggers notFound())
|
|
14
|
+
*
|
|
15
|
+
* Note: tech-articles/page.tsx is a slug-listing route (not MDX-body); only the
|
|
16
|
+
* import path changes. No snapshot tests needed for tech-articles/page.
|
|
17
|
+
*
|
|
18
|
+
* Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
|
|
19
|
+
* A non-zero diff indicates a behavior regression in the migration. Escalate.
|
|
20
|
+
*/
|
|
21
|
+
import React from "react";
|
|
22
|
+
import { render } from "@testing-library/react";
|
|
23
|
+
|
|
24
|
+
jest.mock("next-mdx-remote/rsc", () => ({
|
|
25
|
+
__esModule: true,
|
|
26
|
+
MDXRemote: ({ source }: { source: string }): React.ReactElement =>
|
|
27
|
+
React.createElement("div", { "data-testid": "mdx-rendered" }, source),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
jest.mock("next/navigation", () => ({
|
|
31
|
+
__esModule: true,
|
|
32
|
+
notFound: jest.fn(() => {
|
|
33
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
34
|
+
}),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
import TechArticleSlugPage from "../[slug]/page";
|
|
38
|
+
import { notFound } from "next/navigation";
|
|
39
|
+
|
|
40
|
+
const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
|
|
41
|
+
|
|
42
|
+
describe("tech-articles/[slug] — snapshot regression", () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
mockNotFound.mockClear();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("populated state renders deterministic HTML", async () => {
|
|
48
|
+
const getMdxSource = jest.fn().mockResolvedValue({
|
|
49
|
+
frontmatter: {
|
|
50
|
+
type: "TechArticle" as const,
|
|
51
|
+
slug: "aeo-layer1-scoring",
|
|
52
|
+
title: "AEO Layer 1 Scoring",
|
|
53
|
+
description:
|
|
54
|
+
"Deep dive into AEO Layer 1 scoring and AILK audit internals.",
|
|
55
|
+
},
|
|
56
|
+
body: "## AEO Layer 1\n\nBody content.",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const element = await TechArticleSlugPage({
|
|
60
|
+
params: Promise.resolve({ locale: "en", slug: "aeo-layer1-scoring" }),
|
|
61
|
+
getMdxSource,
|
|
62
|
+
});
|
|
63
|
+
const { container } = render(element);
|
|
64
|
+
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
65
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"AEO Layer 1 Scoring","description":"Deep dive into AEO Layer 1 scoring and AILK audit internals.","mainEntity":{"@type":"TechArticle","headline":"AEO Layer 1 Scoring"}},{"@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":"Tech Articles","item":"http://localhost:3000/en/tech-articles"},{"@type":"ListItem","position":3,"name":"AEO Layer 1 Scoring","item":"http://localhost:3000/en/tech-articles/aeo-layer1-scoring"}]}]}</script><main><h1>AEO Layer 1 Scoring</h1><p data-testid="tech-article-description">Deep dive into AEO Layer 1 scoring and AILK audit internals.</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">## AEO Layer 1
|
|
66
|
+
|
|
67
|
+
Body content.</div></article></div></div></main>"
|
|
68
|
+
`);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("unknown slug triggers notFound()", async () => {
|
|
72
|
+
const getMdxSource = jest.fn().mockResolvedValue(null);
|
|
73
|
+
|
|
74
|
+
await expect(
|
|
75
|
+
TechArticleSlugPage({
|
|
76
|
+
params: Promise.resolve({ locale: "en", slug: "unknown-slug" }),
|
|
77
|
+
getMdxSource,
|
|
78
|
+
}),
|
|
79
|
+
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
80
|
+
expect(mockNotFound).toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Tech Articles list page. Enumerates TechArticle slugs via the content adapter
|
|
2
|
+
// and renders a directory listing. No dedicated list MDX type exists for TechArticle.
|
|
3
|
+
import { getMdxSlugs as defaultGetMdxSlugs } from "@working-theory/page-renderer";
|
|
4
|
+
import { getTranslations } from "next-intl/server";
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
params: Promise<{ locale: string }>;
|
|
9
|
+
getMdxSlugs?: typeof defaultGetMdxSlugs;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default async function Page({
|
|
13
|
+
params,
|
|
14
|
+
getMdxSlugs = defaultGetMdxSlugs,
|
|
15
|
+
}: Props): Promise<ReactElement> {
|
|
16
|
+
const { locale } = await params;
|
|
17
|
+
const t = await getTranslations("tech-articles");
|
|
18
|
+
const slugs = await getMdxSlugs("TechArticle", locale);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<main>
|
|
22
|
+
<h1>{t("heading")}</h1>
|
|
23
|
+
<p data-testid="tech-articles-description">
|
|
24
|
+
{t("description")}
|
|
25
|
+
</p>
|
|
26
|
+
{slugs.length === 0 ? (
|
|
27
|
+
<p data-testid="section-empty-state">{t("emptyState")}</p>
|
|
28
|
+
) : (
|
|
29
|
+
<ul data-testid="tech-articles-list">
|
|
30
|
+
{slugs.map((slug) => (
|
|
31
|
+
<li key={slug}>
|
|
32
|
+
<a href={`/${locale}/tech-articles/${slug}`}>{slug}</a>
|
|
33
|
+
</li>
|
|
34
|
+
))}
|
|
35
|
+
</ul>
|
|
36
|
+
)}
|
|
37
|
+
</main>
|
|
38
|
+
);
|
|
39
|
+
}
|
package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for testimonials/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 TestimonialsPage from "../page";
|
|
27
|
+
|
|
28
|
+
const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
|
|
29
|
+
|
|
30
|
+
describe("TestimonialsPage — snapshot regression", () => {
|
|
31
|
+
it("empty state renders deterministic HTML", async () => {
|
|
32
|
+
const element = await TestimonialsPage({
|
|
33
|
+
params: Promise.resolve({ locale: "en" }),
|
|
34
|
+
getMdxSource: getMdxSourceEmpty,
|
|
35
|
+
});
|
|
36
|
+
const { container } = render(element);
|
|
37
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
38
|
+
`"<main><h1>Testimonials</h1><p data-testid="section-empty-state">Testimonials coming soon.</p></main>"`,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("populated state renders deterministic HTML", async () => {
|
|
43
|
+
const source = {
|
|
44
|
+
frontmatter: {
|
|
45
|
+
type: "TestimonialsList" as const,
|
|
46
|
+
slug: "testimonials",
|
|
47
|
+
title: "Customer Testimonials",
|
|
48
|
+
description: "What our customers say.",
|
|
49
|
+
},
|
|
50
|
+
body: "",
|
|
51
|
+
};
|
|
52
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
|
|
53
|
+
|
|
54
|
+
const element = await TestimonialsPage({
|
|
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":"Customer Testimonials","description":"What our customers say.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Review"}]},"aggregateRating":{"@context":"https://schema.org","@type":"AggregateRating","ratingValue":4.9,"reviewCount":0,"bestRating":5,"worstRating":1,"itemReviewed":{"@type":"SoftwareApplication"}}}</script><main><h1>Customer Testimonials</h1><p data-testid="testimonials-description">What our customers say.</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
|
+
// Testimonials list page. Reads TestimonialsList 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: "TestimonialsList",
|
|
21
|
+
slug: "testimonials",
|
|
22
|
+
locale,
|
|
23
|
+
translationsNamespace: "testimonials",
|
|
24
|
+
bodyMode: "mdx-body",
|
|
25
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for use-cases/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 UseCasesPage from "../page";
|
|
27
|
+
|
|
28
|
+
const getMdxSourceEmpty = jest.fn().mockResolvedValue(null);
|
|
29
|
+
|
|
30
|
+
describe("UseCasesPage — snapshot regression", () => {
|
|
31
|
+
it("empty state renders deterministic HTML", async () => {
|
|
32
|
+
const element = await UseCasesPage({
|
|
33
|
+
params: Promise.resolve({ locale: "en" }),
|
|
34
|
+
getMdxSource: getMdxSourceEmpty,
|
|
35
|
+
});
|
|
36
|
+
const { container } = render(element);
|
|
37
|
+
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
38
|
+
`"<main><h1>Use Cases</h1><p data-testid="section-empty-state">Use cases listing coming soon.</p></main>"`,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("populated state renders deterministic HTML", async () => {
|
|
43
|
+
const source = {
|
|
44
|
+
frontmatter: {
|
|
45
|
+
type: "UseCasesList" as const,
|
|
46
|
+
slug: "use-cases",
|
|
47
|
+
title: "Use Cases",
|
|
48
|
+
description: "How teams use our platform.",
|
|
49
|
+
},
|
|
50
|
+
body: "",
|
|
51
|
+
};
|
|
52
|
+
const getMdxSourcePopulated = jest.fn().mockResolvedValue(source);
|
|
53
|
+
|
|
54
|
+
const element = await UseCasesPage({
|
|
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":"Use Cases","description":"How teams use our platform.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"Article"}]}}</script><main><h1>Use Cases</h1><p data-testid="use-cases-description">How teams use our 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-remote" data-source=""></div></article></div></div></main>"`,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Use Cases list page. Reads UseCasesList MDX via the content adapter and emits
|
|
2
|
+
// a JSON-LD CollectionPage via the dispatcher. List-only — no detail route.
|
|
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: "UseCasesList",
|
|
21
|
+
slug: "use-cases",
|
|
22
|
+
locale,
|
|
23
|
+
translationsNamespace: "use-cases",
|
|
24
|
+
bodyMode: "mdx-body",
|
|
25
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Whitepaper detail page. Reads the Whitepaper MDX for the given slug, renders via
|
|
2
|
+
// @working-theory/page-renderer, and emits a JSON-LD <script>. Unknown slugs trigger 404.
|
|
3
|
+
import type { getMdxSource as defaultGetMdxSource } from "@working-theory/page-renderer";
|
|
4
|
+
import { getMdxSlugs, renderPage } from "@working-theory/page-renderer";
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
params: Promise<{ locale: string; slug: string }>;
|
|
9
|
+
getMdxSource?: typeof defaultGetMdxSource;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function generateStaticParams({
|
|
13
|
+
params,
|
|
14
|
+
}: {
|
|
15
|
+
params: { locale: string };
|
|
16
|
+
}): Promise<{ slug: string }[]> {
|
|
17
|
+
const slugs = await getMdxSlugs("Whitepaper", params.locale);
|
|
18
|
+
return slugs.map((slug) => ({ slug }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default async function Page({
|
|
22
|
+
params,
|
|
23
|
+
getMdxSource,
|
|
24
|
+
}: Props): Promise<ReactElement> {
|
|
25
|
+
const { locale, slug } = await params;
|
|
26
|
+
return renderPage({
|
|
27
|
+
type: "Whitepaper",
|
|
28
|
+
slug,
|
|
29
|
+
locale,
|
|
30
|
+
bodyMode: "mdx-body",
|
|
31
|
+
populatedDescriptionTestId: "whitepaper-description",
|
|
32
|
+
...(getMdxSource !== undefined && { getMdxSource }),
|
|
33
|
+
});
|
|
34
|
+
}
|
package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML snapshot regression tests for whitepapers/[slug] — @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.
|
|
6
|
+
*
|
|
7
|
+
* Two-commit strategy:
|
|
8
|
+
* Commit 1: this file added; snapshots captured against pre-migration routes.
|
|
9
|
+
* Commit 2: routes migrated; same snapshots verify diff=0.
|
|
10
|
+
*
|
|
11
|
+
* Routes covered:
|
|
12
|
+
* - whitepapers/[slug] (populated: populated state)
|
|
13
|
+
* - whitepapers/[slug] (notFound: unknown slug triggers notFound())
|
|
14
|
+
*
|
|
15
|
+
* Note: whitepapers/page.tsx is a slug-listing route (not MDX-body); only the
|
|
16
|
+
* import path changes. No snapshot tests needed for whitepapers/page.
|
|
17
|
+
*
|
|
18
|
+
* Failure = snapshot diff > 0 bytes. DO NOT regenerate snapshots to pass.
|
|
19
|
+
* A non-zero diff indicates a behavior regression in the migration. Escalate.
|
|
20
|
+
*/
|
|
21
|
+
import React from "react";
|
|
22
|
+
import { render } from "@testing-library/react";
|
|
23
|
+
|
|
24
|
+
jest.mock("next-mdx-remote/rsc", () => ({
|
|
25
|
+
__esModule: true,
|
|
26
|
+
MDXRemote: ({ source }: { source: string }): React.ReactElement =>
|
|
27
|
+
React.createElement("div", { "data-testid": "mdx-rendered" }, source),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
jest.mock("next/navigation", () => ({
|
|
31
|
+
__esModule: true,
|
|
32
|
+
notFound: jest.fn(() => {
|
|
33
|
+
throw new Error("NEXT_NOT_FOUND");
|
|
34
|
+
}),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
import WhitepaperSlugPage from "../[slug]/page";
|
|
38
|
+
import { notFound } from "next/navigation";
|
|
39
|
+
|
|
40
|
+
const mockNotFound = notFound as jest.MockedFunction<typeof notFound>;
|
|
41
|
+
|
|
42
|
+
describe("whitepapers/[slug] — snapshot regression", () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
mockNotFound.mockClear();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("populated state renders deterministic HTML", async () => {
|
|
48
|
+
const getMdxSource = jest.fn().mockResolvedValue({
|
|
49
|
+
frontmatter: {
|
|
50
|
+
type: "Whitepaper" as const,
|
|
51
|
+
slug: "ailk-aeo-strategy-2026",
|
|
52
|
+
title: "AILK AEO Strategy 2026",
|
|
53
|
+
description:
|
|
54
|
+
"In-depth research on answer engine optimization for AILK products.",
|
|
55
|
+
},
|
|
56
|
+
body: "## Executive Summary\n\nBody content.",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const element = await WhitepaperSlugPage({
|
|
60
|
+
params: Promise.resolve({ locale: "en", slug: "ailk-aeo-strategy-2026" }),
|
|
61
|
+
getMdxSource,
|
|
62
|
+
});
|
|
63
|
+
const { container } = render(element);
|
|
64
|
+
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
65
|
+
"<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 Strategy 2026","description":"In-depth research on answer engine optimization for AILK products.","mainEntity":{"@type":"Article","headline":"AILK AEO Strategy 2026"}},{"@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":"Whitepapers","item":"http://localhost:3000/en/whitepapers"},{"@type":"ListItem","position":3,"name":"AILK AEO Strategy 2026","item":"http://localhost:3000/en/whitepapers/ailk-aeo-strategy-2026"}]}]}</script><main><h1>AILK AEO Strategy 2026</h1><p data-testid="whitepaper-description">In-depth research on answer engine optimization for AILK products.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) bg-(--color-bg) text-(--color-fg) shadow-sm" type="button" role="radio" aria-checked="true" aria-label="Rendered" tabindex="0" data-testid="markdown-view-toggle-rendered">Rendered</button><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) text-(--color-text-muted) hover:text-(--color-fg)" type="button" role="radio" aria-checked="false" aria-label="Markdown" tabindex="-1" data-testid="markdown-view-toggle-markdown">Markdown</button></div></div><div role="status" aria-live="polite" aria-atomic="true" class="sr-only" data-testid="markdown-view-toggle-status">Showing Rendered view</div><div data-testid="markdown-view-rendered"><article data-testid="mdx-body"><div data-testid="mdx-rendered">## Executive Summary
|
|
66
|
+
|
|
67
|
+
Body content.</div></article></div></div></main>"
|
|
68
|
+
`);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("unknown slug triggers notFound()", async () => {
|
|
72
|
+
const getMdxSource = jest.fn().mockResolvedValue(null);
|
|
73
|
+
|
|
74
|
+
await expect(
|
|
75
|
+
WhitepaperSlugPage({
|
|
76
|
+
params: Promise.resolve({ locale: "en", slug: "unknown-slug" }),
|
|
77
|
+
getMdxSource,
|
|
78
|
+
}),
|
|
79
|
+
).rejects.toThrow("NEXT_NOT_FOUND");
|
|
80
|
+
expect(mockNotFound).toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
});
|