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,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal OG image route — /og?title=...&schemaType=...
|
|
3
|
+
*
|
|
4
|
+
* Design decisions (per spec § 3):
|
|
5
|
+
* - next/og ImageResponse at 1200×630 (Open Graph standard)
|
|
6
|
+
* - System sans-serif only — no .ttf fetches (the #1 source of OG bugs)
|
|
7
|
+
* - force-static: Vercel caches generated images at the edge after first request
|
|
8
|
+
* - Fallback strategy: on any ImageResponse throw, return a 302 redirect to
|
|
9
|
+
* /og-default.png (the static PNG in apps/web/public/). The redirect is
|
|
10
|
+
* edge-compatible (no fs access needed) and exposing the fallback URL is
|
|
11
|
+
* acceptable per spec § 3 ("redirecting is simpler but exposes the fallback URL;
|
|
12
|
+
* both acceptable"). Chosen over stream-bytes because edge runtime has no fs.
|
|
13
|
+
*
|
|
14
|
+
* Sanitization:
|
|
15
|
+
* - Strip control characters \x00–\x1F and \x7F from title and schemaType
|
|
16
|
+
* - Cap title at 200 chars, schemaType at 50 chars
|
|
17
|
+
* - If sanitization yields an empty title, fall back to static PNG redirect
|
|
18
|
+
*/
|
|
19
|
+
import { clientError } from "@working-theory/page-renderer/edge";
|
|
20
|
+
import { tokens } from "@working-theory/theme";
|
|
21
|
+
import { ImageResponse } from "next/og";
|
|
22
|
+
|
|
23
|
+
export const runtime = "edge";
|
|
24
|
+
export const dynamic = "force-static";
|
|
25
|
+
|
|
26
|
+
// Strips C0 control characters (U+0000–U+001F) and DEL (U+007F).
|
|
27
|
+
// eslint-disable-next-line no-control-regex
|
|
28
|
+
const CONTROL_CHAR_RE = new RegExp("[\u0000-\u001F\u007F]", "g");
|
|
29
|
+
|
|
30
|
+
const SYSTEM_FONT = "system-ui, -apple-system, sans-serif";
|
|
31
|
+
|
|
32
|
+
function sanitize(raw: string, cap: number): string {
|
|
33
|
+
return raw.replace(CONTROL_CHAR_RE, "").slice(0, cap);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function serveFallback(): Response {
|
|
37
|
+
// 302 redirect to the static PNG served by Next.js from /public.
|
|
38
|
+
// Edge-compatible: no fs access required.
|
|
39
|
+
// Using new Response + Location header instead of Response.redirect() because
|
|
40
|
+
// Response.redirect() requires an absolute URL in the Fetch API spec, but
|
|
41
|
+
// relative redirects work correctly in Next.js edge middleware at runtime.
|
|
42
|
+
return new Response(null, {
|
|
43
|
+
status: 302,
|
|
44
|
+
headers: { Location: "/og-default.png" },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function GET(request: Request): Promise<Response> {
|
|
49
|
+
const { searchParams } = new URL(request.url);
|
|
50
|
+
|
|
51
|
+
const rawTitle = searchParams.get("title") ?? "";
|
|
52
|
+
const rawSchemaType = searchParams.get("schemaType") ?? "";
|
|
53
|
+
|
|
54
|
+
const siteName = process.env.NEXT_PUBLIC_SITE_NAME ?? "AI Launch Kit";
|
|
55
|
+
const wordmark = process.env.NEXT_PUBLIC_SITE_WORDMARK ?? "ailaunchkit.ai";
|
|
56
|
+
|
|
57
|
+
const title = sanitize(rawTitle || siteName, 200);
|
|
58
|
+
const schemaType = rawSchemaType ? sanitize(rawSchemaType, 50) : "";
|
|
59
|
+
|
|
60
|
+
// Post-sanitization empty title → fallback
|
|
61
|
+
if (!title) {
|
|
62
|
+
return serveFallback();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
return new ImageResponse(
|
|
67
|
+
<div
|
|
68
|
+
style={{
|
|
69
|
+
display: "flex",
|
|
70
|
+
flexDirection: "column",
|
|
71
|
+
justifyContent: "center",
|
|
72
|
+
alignItems: "flex-start",
|
|
73
|
+
width: "100%",
|
|
74
|
+
height: "100%",
|
|
75
|
+
backgroundColor: tokens.colors.light.brand,
|
|
76
|
+
padding: "80px",
|
|
77
|
+
fontFamily: SYSTEM_FONT,
|
|
78
|
+
position: "relative",
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{schemaType ? (
|
|
82
|
+
<div
|
|
83
|
+
style={{
|
|
84
|
+
position: "absolute",
|
|
85
|
+
top: "48px",
|
|
86
|
+
right: "48px",
|
|
87
|
+
backgroundColor: "rgba(0,0,0,0.25)",
|
|
88
|
+
color: tokens.colors.light.fg,
|
|
89
|
+
borderRadius: "9999px",
|
|
90
|
+
padding: "6px 18px",
|
|
91
|
+
fontSize: "18px",
|
|
92
|
+
fontFamily: SYSTEM_FONT,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
{schemaType}
|
|
96
|
+
</div>
|
|
97
|
+
) : null}
|
|
98
|
+
|
|
99
|
+
<div
|
|
100
|
+
style={{
|
|
101
|
+
color: tokens.colors.light.fg,
|
|
102
|
+
fontSize: "56px",
|
|
103
|
+
fontWeight: 700,
|
|
104
|
+
lineHeight: 1.1,
|
|
105
|
+
maxWidth: "880px",
|
|
106
|
+
fontFamily: SYSTEM_FONT,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{title}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div
|
|
113
|
+
style={{
|
|
114
|
+
position: "absolute",
|
|
115
|
+
bottom: "48px",
|
|
116
|
+
right: "48px",
|
|
117
|
+
color: tokens.colors.light.fg,
|
|
118
|
+
fontSize: "24px",
|
|
119
|
+
opacity: 0.75,
|
|
120
|
+
fontFamily: SYSTEM_FONT,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{wordmark}
|
|
124
|
+
</div>
|
|
125
|
+
</div>,
|
|
126
|
+
{
|
|
127
|
+
width: 1200,
|
|
128
|
+
height: 630,
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
clientError(err);
|
|
133
|
+
return serveFallback();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /robots.txt
|
|
3
|
+
*
|
|
4
|
+
* Returns a robots.txt with sensible defaults for an AX-first product:
|
|
5
|
+
* - All crawlers allowed by default
|
|
6
|
+
* - Sitemap location declared
|
|
7
|
+
* - Optional path disallow via AILK_ROBOTS_DISALLOW env var
|
|
8
|
+
* (comma-separated paths, e.g. "/admin/,/private/")
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { resolveBaseUrl } from "../../src/lib/routes";
|
|
12
|
+
|
|
13
|
+
export function GET(): Response {
|
|
14
|
+
const baseUrl = resolveBaseUrl();
|
|
15
|
+
const disallowEnv = process.env["AILK_ROBOTS_DISALLOW"] ?? "";
|
|
16
|
+
|
|
17
|
+
const disallowPaths = disallowEnv
|
|
18
|
+
.split(",")
|
|
19
|
+
.map((p) => p.trim())
|
|
20
|
+
.filter((p) => p.length > 0);
|
|
21
|
+
|
|
22
|
+
const lines = [
|
|
23
|
+
"User-agent: *",
|
|
24
|
+
...disallowPaths.map((p) => `Disallow: ${p}`),
|
|
25
|
+
"",
|
|
26
|
+
`Sitemap: ${baseUrl}/sitemap.xml`,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
return new Response(lines.join("\n"), {
|
|
30
|
+
status: 200,
|
|
31
|
+
headers: {
|
|
32
|
+
"content-type": "text/plain; charset=utf-8",
|
|
33
|
+
"cache-control": "public, max-age=86400",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /sitemap.xml
|
|
3
|
+
*
|
|
4
|
+
* Returns a standard XML sitemap covering all content seeds in content/.
|
|
5
|
+
* Crawlers (Google, Bing) and AI agents use this to discover pages.
|
|
6
|
+
*
|
|
7
|
+
* Includes per-locale <xhtml:link rel="alternate"> entries per Google's
|
|
8
|
+
* sitemap protocol for international content. Locale list is read from
|
|
9
|
+
* NEXT_PUBLIC_ENABLED_LOCALES (baked by next.config.mjs from project.yaml
|
|
10
|
+
* at build time) — same pattern as middleware.ts. This avoids importing
|
|
11
|
+
* node:fs-dependent lib/locales into the prerender context where cwd is
|
|
12
|
+
* apps/web/ but project.yaml lives at the repo root.
|
|
13
|
+
*
|
|
14
|
+
* Phase 3 S3: pages are listed per enabled locale via the adapter's
|
|
15
|
+
* locale-scoped `listPages(type, locale)`. Lists are scoped — see the
|
|
16
|
+
* adapter contract in @working-theory/content-adapters/src/mdx/adapter.ts — so a
|
|
17
|
+
* page must exist in each locale's content directory to be enumerated for
|
|
18
|
+
* that locale. The default-locale list still picks up legacy non-prefixed
|
|
19
|
+
* content via the adapter's legacy fallback.
|
|
20
|
+
*
|
|
21
|
+
* URL composition:
|
|
22
|
+
* - Default locale: bare path /<path> (matches next-intl as-needed serving)
|
|
23
|
+
* - Non-default locales: /<locale>/<path>
|
|
24
|
+
*
|
|
25
|
+
* lastmod uses the page's publishedAt frontmatter field when present;
|
|
26
|
+
* falls back to the current date so the sitemap is always valid.
|
|
27
|
+
*
|
|
28
|
+
* DI note: `createSitemapHandler` accepts an optional `getContentAdapter` dep
|
|
29
|
+
* for testing — tests inject a stub adapter to avoid mocking the internal module.
|
|
30
|
+
* The exported `GET` uses the real implementation.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { SCHEMA_ORG_TYPES } from "@working-theory/content-adapters";
|
|
34
|
+
import type { ContentAdapter, SchemaOrgType } from "@working-theory/content-adapters";
|
|
35
|
+
import { getContentAdapter as defaultGetContentAdapter } from "@working-theory/page-renderer";
|
|
36
|
+
|
|
37
|
+
import { resolveBaseUrl, slugToPath } from "../../src/lib/routes";
|
|
38
|
+
|
|
39
|
+
export interface SitemapHandlerDeps {
|
|
40
|
+
/**
|
|
41
|
+
* Content adapter factory. Defaults to the real implementation.
|
|
42
|
+
* Override in tests to inject a stub adapter without mocking the internal module.
|
|
43
|
+
*/
|
|
44
|
+
getContentAdapter?: () => ContentAdapter;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function escapeXml(value: string): string {
|
|
48
|
+
return value
|
|
49
|
+
.replace(/&/g, "&")
|
|
50
|
+
.replace(/</g, "<")
|
|
51
|
+
.replace(/>/g, ">")
|
|
52
|
+
.replace(/"/g, """)
|
|
53
|
+
.replace(/'/g, "'");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Builds the locale-prefixed URL for a given locale and path.
|
|
58
|
+
* Default locale uses bare path; non-default uses /<locale>/<path>.
|
|
59
|
+
*/
|
|
60
|
+
function buildLocaleUrl(
|
|
61
|
+
baseUrl: string,
|
|
62
|
+
locale: string,
|
|
63
|
+
defaultLocale: string,
|
|
64
|
+
path: string,
|
|
65
|
+
): string {
|
|
66
|
+
if (locale === defaultLocale) {
|
|
67
|
+
return `${baseUrl}${path}`;
|
|
68
|
+
}
|
|
69
|
+
// Avoid double-slash: path starts with "/", locale prefix ends without one.
|
|
70
|
+
return `${baseUrl}/${locale}${path}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a sitemap GET handler parameterised by deps.
|
|
75
|
+
* Tests inject a stub `getContentAdapter` to avoid mocking the internal module.
|
|
76
|
+
*/
|
|
77
|
+
export function createSitemapHandler(deps: SitemapHandlerDeps = {}) {
|
|
78
|
+
const resolveAdapter = deps.getContentAdapter ?? defaultGetContentAdapter;
|
|
79
|
+
return async function sitemapGET(): Promise<Response> {
|
|
80
|
+
const adapter = resolveAdapter();
|
|
81
|
+
const baseUrl = resolveBaseUrl();
|
|
82
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
83
|
+
// Locale list and default locale are baked at build time by next.config.mjs
|
|
84
|
+
// from project.yaml. Reading env vars here mirrors middleware.ts exactly —
|
|
85
|
+
// avoids node:fs in prerender context where cwd != repo root.
|
|
86
|
+
const locales = (process.env.NEXT_PUBLIC_ENABLED_LOCALES ?? "en").split(",");
|
|
87
|
+
const defaultLocale = process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? "en";
|
|
88
|
+
|
|
89
|
+
const urlEntries: string[] = [];
|
|
90
|
+
|
|
91
|
+
const types = SCHEMA_ORG_TYPES as readonly SchemaOrgType[];
|
|
92
|
+
|
|
93
|
+
// Build (locale, type) tuples and fetch all lists in parallel. Adapter
|
|
94
|
+
// list calls are locale-scoped (no cross-locale fallback), so each locale
|
|
95
|
+
// gets its own enumeration — a page that exists only in `en/` is NOT
|
|
96
|
+
// auto-emitted for `es/` URLs. This honors AC-i18n-08 (lists scoped).
|
|
97
|
+
const tuples = locales.flatMap((locale) =>
|
|
98
|
+
types.map((type) => ({ locale, type })),
|
|
99
|
+
);
|
|
100
|
+
const lists = await Promise.all(
|
|
101
|
+
tuples.map(({ type, locale }) => adapter.listPages(type, locale)),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < tuples.length; i++) {
|
|
105
|
+
const { locale } = tuples[i]!;
|
|
106
|
+
for (const page of lists[i]!) {
|
|
107
|
+
const path = slugToPath(page.type, page.slug);
|
|
108
|
+
const lastmod = page.publishedAt ?? today;
|
|
109
|
+
|
|
110
|
+
const defaultUrl = `${baseUrl}${path}`;
|
|
111
|
+
const localeUrl = buildLocaleUrl(baseUrl, locale, defaultLocale, path);
|
|
112
|
+
const loc = escapeXml(localeUrl);
|
|
113
|
+
|
|
114
|
+
const alternates = locales
|
|
115
|
+
.map(
|
|
116
|
+
(alt) =>
|
|
117
|
+
` <xhtml:link rel="alternate" hreflang="${escapeXml(alt)}" href="${escapeXml(buildLocaleUrl(baseUrl, alt, defaultLocale, path))}"/>`,
|
|
118
|
+
)
|
|
119
|
+
.join("\n");
|
|
120
|
+
|
|
121
|
+
const xDefault = ` <xhtml:link rel="alternate" hreflang="x-default" href="${escapeXml(defaultUrl)}"/>`;
|
|
122
|
+
|
|
123
|
+
urlEntries.push(
|
|
124
|
+
` <url>\n <loc>${loc}</loc>\n <lastmod>${lastmod}</lastmod>\n${alternates}\n${xDefault}\n </url>`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const xml = [
|
|
130
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
131
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">',
|
|
132
|
+
...urlEntries,
|
|
133
|
+
"</urlset>",
|
|
134
|
+
].join("\n");
|
|
135
|
+
|
|
136
|
+
return new Response(xml, {
|
|
137
|
+
status: 200,
|
|
138
|
+
headers: {
|
|
139
|
+
"content-type": "application/xml; charset=utf-8",
|
|
140
|
+
"cache-control": "public, max-age=3600",
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const GET = createSitemapHandler();
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth initialisation — Node.js runtime only.
|
|
3
|
+
*
|
|
4
|
+
* Wires Better Auth's Prisma adapter (DB session strategy) against the
|
|
5
|
+
* `prismaForAuth` singleton in `./lib/prisma-for-auth.ts`. The adapter mediates
|
|
6
|
+
* all DB access so apps/web never imports @working-theory/database elsewhere.
|
|
7
|
+
*
|
|
8
|
+
* Exports:
|
|
9
|
+
* createAuth — factory that accepts optional `prisma` dep for testing.
|
|
10
|
+
* auth — lazy-proxy singleton using the real `prismaForAuth` client.
|
|
11
|
+
* `auth.handler` is the Web Fetch handler consumed by
|
|
12
|
+
* app/api/auth/[...all]/route.ts (via toNextJsHandler).
|
|
13
|
+
* `auth.api.getSession({ headers })` reads the active session
|
|
14
|
+
* server-side.
|
|
15
|
+
*
|
|
16
|
+
* Provider activation follows env-var presence (see lib/auth-env.ts) so the
|
|
17
|
+
* OSS standalone default — no env vars set — boots cleanly with zero providers.
|
|
18
|
+
*
|
|
19
|
+
* DI note: `createAuth` accepts an optional `prisma` client for testing —
|
|
20
|
+
* tests can pass a lightweight stub to avoid a real DB connection. The
|
|
21
|
+
* exported `auth` singleton uses the real `prismaForAuth` client.
|
|
22
|
+
* Pattern mirrors `apps-paid/dashboard-web/auth.ts`.
|
|
23
|
+
*
|
|
24
|
+
* Replaces the NextAuth v5-beta wiring per spec
|
|
25
|
+
* docs/internal/specs/ailk-pre-oss-security-phase-0.5-s0.5-better-auth-migration-spec.md.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { PrismaClient } from "@working-theory/database";
|
|
29
|
+
import { betterAuth } from "better-auth";
|
|
30
|
+
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
31
|
+
import { nextCookies } from "better-auth/next-js";
|
|
32
|
+
|
|
33
|
+
import { getActiveProviderIds } from "./lib/auth-env";
|
|
34
|
+
|
|
35
|
+
// Lazy-load prisma-for-auth: import is deferred until createAuth() is called
|
|
36
|
+
// without an explicit `prisma` dep. This prevents the PrismaClient singleton
|
|
37
|
+
// from initialising (and calling @working-theory/database) at module-load time, which
|
|
38
|
+
// lets tests import `createAuth` without triggering a DB connection.
|
|
39
|
+
function getDefaultPrisma(): PrismaClient {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
const { prismaForAuth } = require("./lib/prisma-for-auth") as {
|
|
42
|
+
prismaForAuth: PrismaClient;
|
|
43
|
+
};
|
|
44
|
+
return prismaForAuth;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type SocialProviders = {
|
|
48
|
+
google?: { clientId: string; clientSecret: string };
|
|
49
|
+
github?: { clientId: string; clientSecret: string };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function buildSocialProviders(): SocialProviders {
|
|
53
|
+
const activeIds = getActiveProviderIds();
|
|
54
|
+
const providers: SocialProviders = {};
|
|
55
|
+
|
|
56
|
+
if (activeIds.includes("google")) {
|
|
57
|
+
providers.google = {
|
|
58
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
59
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (activeIds.includes("github")) {
|
|
64
|
+
providers.github = {
|
|
65
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
66
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// "email" is reported by getActiveProviderIds when AUTH_EMAIL_SERVER +
|
|
71
|
+
// AUTH_EMAIL_FROM are set, but Better Auth core does not ship a built-in
|
|
72
|
+
// magic-link provider — that wiring is deferred to a follow-up
|
|
73
|
+
// (see spec §7 "Out of Scope").
|
|
74
|
+
|
|
75
|
+
return providers;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the secret used to sign session cookies. Better Auth requires a
|
|
80
|
+
* secret in production. In dev / test the bare default keeps the app bootable
|
|
81
|
+
* without configuration; production deployments MUST set BETTER_AUTH_SECRET.
|
|
82
|
+
*/
|
|
83
|
+
function resolveSecret(): string {
|
|
84
|
+
return (
|
|
85
|
+
process.env.BETTER_AUTH_SECRET ??
|
|
86
|
+
"ailk-dev-secret-do-not-use-in-production"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CreateAuthDeps {
|
|
91
|
+
/**
|
|
92
|
+
* Prisma client for the Better Auth adapter.
|
|
93
|
+
* Defaults to the singleton from `lib/prisma-for-auth.ts`.
|
|
94
|
+
* Override in tests to pass a lightweight stub and avoid a real DB
|
|
95
|
+
* connection at module-load time.
|
|
96
|
+
*/
|
|
97
|
+
prisma?: PrismaClient;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build a Better Auth instance parameterised by deps.
|
|
102
|
+
*
|
|
103
|
+
* Convention mirrors `apps-paid/marketing-api/src/routes/v1/account/license.ts`:
|
|
104
|
+
* the factory accepts optional deps with defaults to real implementations so
|
|
105
|
+
* existing callers (`import { auth }`) are unchanged.
|
|
106
|
+
*/
|
|
107
|
+
export function createAuth(deps: CreateAuthDeps = {}) {
|
|
108
|
+
const prisma = deps.prisma ?? getDefaultPrisma();
|
|
109
|
+
return betterAuth({
|
|
110
|
+
database: prismaAdapter(prisma, {
|
|
111
|
+
provider: "postgresql",
|
|
112
|
+
}),
|
|
113
|
+
secret: resolveSecret(),
|
|
114
|
+
baseURL: process.env.BETTER_AUTH_URL,
|
|
115
|
+
socialProviders: buildSocialProviders(),
|
|
116
|
+
// Persist sessions in the database so admin revocation by row delete remains
|
|
117
|
+
// the model the dual-mode preHandler relies on.
|
|
118
|
+
session: {
|
|
119
|
+
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
|
120
|
+
updateAge: 60 * 60 * 24, // refresh in DB once per day
|
|
121
|
+
},
|
|
122
|
+
plugins: [nextCookies()],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Lazy singleton: resolved on first access, not at module-load time.
|
|
127
|
+
// Defers the `require("./lib/prisma-for-auth")` call until auth is actually
|
|
128
|
+
// used, so tests can `import { createAuth }` without triggering a DB connection.
|
|
129
|
+
let _auth: ReturnType<typeof createAuth> | undefined;
|
|
130
|
+
|
|
131
|
+
export const auth: ReturnType<typeof createAuth> = new Proxy(
|
|
132
|
+
{} as ReturnType<typeof createAuth>,
|
|
133
|
+
{
|
|
134
|
+
get(_, prop: string | symbol) {
|
|
135
|
+
if (!_auth) _auth = createAuth();
|
|
136
|
+
return _auth[prop as keyof ReturnType<typeof createAuth>];
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
export type Auth = typeof auth;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ThemeToggle — narrow client component for theme switching (#1322)
|
|
4
|
+
//
|
|
5
|
+
// OSS template mirror of apps-paid/marketing-web/components/ThemeToggle.tsx.
|
|
6
|
+
// Reads current theme from document.documentElement.dataset.theme, toggles
|
|
7
|
+
// between "light" and "dark", persists to the ailk-theme cookie, and updates
|
|
8
|
+
// the DOM attribute synchronously.
|
|
9
|
+
//
|
|
10
|
+
// This is the ONLY "use client" component added to the layout tree.
|
|
11
|
+
|
|
12
|
+
import type { ReactElement } from "react";
|
|
13
|
+
import { useCallback, useEffect, useState } from "react";
|
|
14
|
+
|
|
15
|
+
const COOKIE_NAME = process.env.NEXT_PUBLIC_THEME_COOKIE_NAME ?? "ailk-theme";
|
|
16
|
+
|
|
17
|
+
function readCurrentTheme(): "light" | "dark" {
|
|
18
|
+
const attr = document.documentElement.getAttribute("data-theme");
|
|
19
|
+
return attr === "dark" ? "dark" : "light";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function setCookieTheme(theme: "light" | "dark"): void {
|
|
23
|
+
document.cookie = `${COOKIE_NAME}=${theme}; path=/; max-age=31536000; SameSite=Lax`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
"data-testid"?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function ThemeToggle({
|
|
31
|
+
"data-testid": testId,
|
|
32
|
+
}: Props): ReactElement {
|
|
33
|
+
const [theme, setTheme] = useState<"light" | "dark">("light");
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const current = readCurrentTheme();
|
|
37
|
+
setTheme(current);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const toggle = useCallback(() => {
|
|
41
|
+
const next = theme === "light" ? "dark" : "light";
|
|
42
|
+
document.documentElement.setAttribute("data-theme", next);
|
|
43
|
+
setCookieTheme(next);
|
|
44
|
+
setTheme(next);
|
|
45
|
+
}, [theme]);
|
|
46
|
+
|
|
47
|
+
const isDark = theme === "dark";
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
aria-pressed={isDark}
|
|
53
|
+
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
|
54
|
+
data-testid={testId}
|
|
55
|
+
onClick={toggle}
|
|
56
|
+
>
|
|
57
|
+
{isDark ? "Light" : "Dark"}
|
|
58
|
+
</button>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Google Analytics gtag wiring (#886). When NEXT_PUBLIC_GOOGLE_TAG_ID is set,
|
|
2
|
+
// every page emits the canonical gtag.js loader script + inline init script in
|
|
3
|
+
// <head>. When unset, renders null — no GA traffic at all.
|
|
4
|
+
//
|
|
5
|
+
// Sam's call (#886): fires whenever NEXT_PUBLIC_GOOGLE_TAG_ID is set, regardless
|
|
6
|
+
// of STEALTH. Pre-launch internal traffic will hit GA; filter in GA if needed.
|
|
7
|
+
//
|
|
8
|
+
// Mirrored by sibling components in apps-paid/marketing-web/ and
|
|
9
|
+
// apps-paid/dashboard-web/. Three-way duplication is deliberate — keeps each
|
|
10
|
+
// app self-contained for the OSS fork model (apps/web ships in the public
|
|
11
|
+
// template).
|
|
12
|
+
import Script from "next/script";
|
|
13
|
+
import type { ReactElement } from "react";
|
|
14
|
+
|
|
15
|
+
export function GoogleAnalytics(): ReactElement | null {
|
|
16
|
+
const tagId = process.env.NEXT_PUBLIC_GOOGLE_TAG_ID;
|
|
17
|
+
if (!tagId) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<Script
|
|
23
|
+
src={`https://www.googletagmanager.com/gtag/js?id=${tagId}`}
|
|
24
|
+
strategy="afterInteractive"
|
|
25
|
+
/>
|
|
26
|
+
<Script id="ga-init" strategy="afterInteractive">
|
|
27
|
+
{`window.dataLayer = window.dataLayer || [];
|
|
28
|
+
function gtag(){dataLayer.push(arguments);}
|
|
29
|
+
gtag('js', new Date());
|
|
30
|
+
gtag('config', '${tagId}');`}
|
|
31
|
+
</Script>
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Blog
|
|
3
|
+
description: Writing on AEO, answer-engine citation readiness, and what it means to ship software with three peers — UX, API, and MCP.
|
|
4
|
+
author:
|
|
5
|
+
name: The AI Launch Kit Team
|
|
6
|
+
tags:
|
|
7
|
+
- AEO
|
|
8
|
+
- AX
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Blog
|
|
12
|
+
|
|
13
|
+
This is where we think out loud. The topics stay close to the work: schema.org implementation details, how LLMs decide what to cite, and what AX-first looks like in practice.
|
|
14
|
+
|
|
15
|
+
The posts are opinionated because vague advice isn't useful. If something here turns out to be wrong, we'll update it and say so.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Launching AI Launch Kit
|
|
3
|
+
description: A short note on why we built an AX-first OSS template and what ships in v1.
|
|
4
|
+
date: "2026-05-06"
|
|
5
|
+
author:
|
|
6
|
+
name: The AI Launch Kit Team
|
|
7
|
+
tags:
|
|
8
|
+
- Announcements
|
|
9
|
+
- AX
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Launching AI Launch Kit
|
|
13
|
+
|
|
14
|
+
We're shipping the first public version today. The template gives you three
|
|
15
|
+
peer apps (web, api, mcp), a content-adapter contract with MDX as the default
|
|
16
|
+
backend, and a JSON-LD pipeline for SEO that doesn't require manual schema.org
|
|
17
|
+
authoring.
|
|
18
|
+
|
|
19
|
+
The goal is clone-to-shipping in under an hour. If you hit friction, the
|
|
20
|
+
README's standalone test should catch it before you do.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Getting Started
|
|
3
|
+
description: Everything you need to know to get your AI Launch Kit project running.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Getting Started
|
|
7
|
+
|
|
8
|
+
Welcome to AI Launch Kit. This guide walks you through the first steps after cloning the template.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
- Node.js 20+
|
|
13
|
+
- pnpm 9+
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm install
|
|
19
|
+
pnpm dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The dev server starts three apps in parallel:
|
|
23
|
+
|
|
24
|
+
- **web** (`localhost:3000`) — the Next.js UX surface
|
|
25
|
+
- **api** (`localhost:3001`) — the Fastify REST API
|
|
26
|
+
- **mcp** — the MCP server (stdio transport)
|
|
27
|
+
|
|
28
|
+
## Next steps
|
|
29
|
+
|
|
30
|
+
Replace the seed content in `apps/web/content/en/` with your own pages, blog posts, and docs. Each MDX file maps to a route automatically via Fumadocs.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// i18n S2 — next-intl getRequestConfig entry point (#949)
|
|
2
|
+
//
|
|
3
|
+
// Reads the message catalog for the requested locale and falls back to
|
|
4
|
+
// English when no catalog exists (e.g. a locale is enabled in project.yaml
|
|
5
|
+
// but its messages/<locale>.json has not been translated yet).
|
|
6
|
+
//
|
|
7
|
+
// Spec: docs/internal/specs/i18n-phase1-s2-next-intl-foundation-spec.md
|
|
8
|
+
|
|
9
|
+
import type { AbstractIntlMessages } from "next-intl";
|
|
10
|
+
import { getRequestConfig } from "next-intl/server";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_LOCALE = "en";
|
|
13
|
+
|
|
14
|
+
export default getRequestConfig(async ({ requestLocale }) => {
|
|
15
|
+
// requestLocale is the modern API (next-intl >= 3.22). It resolves the
|
|
16
|
+
// active locale from the request, validated by middleware.
|
|
17
|
+
const locale = (await requestLocale) ?? DEFAULT_LOCALE;
|
|
18
|
+
|
|
19
|
+
// Attempt to load the locale catalog; fall back to English on any error.
|
|
20
|
+
let messages: AbstractIntlMessages;
|
|
21
|
+
try {
|
|
22
|
+
// Dynamic import — bundler can tree-shake per-locale at build time.
|
|
23
|
+
const mod = await import(`../messages/${locale}.json`).catch(() => null);
|
|
24
|
+
if (mod) {
|
|
25
|
+
messages = (mod.default ?? mod) as AbstractIntlMessages;
|
|
26
|
+
} else {
|
|
27
|
+
const fallback = await import(`../messages/${DEFAULT_LOCALE}.json`);
|
|
28
|
+
messages = (fallback.default ?? fallback) as AbstractIntlMessages;
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
const fallback = await import(`../messages/${DEFAULT_LOCALE}.json`);
|
|
32
|
+
messages = (fallback.default ?? fallback) as AbstractIntlMessages;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { locale, messages };
|
|
36
|
+
});
|