create-ailk 0.6.1 → 0.8.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/README.md +87 -14
- package/component-catalog.json +735 -95
- package/component-catalog.md +13 -7
- package/dist/add-module-core.d.ts +22 -0
- package/dist/add-module-core.js +50 -0
- package/dist/add-module.d.ts +89 -0
- package/dist/add-module.js +334 -0
- package/dist/category-profile.d.ts +126 -0
- package/dist/category-profile.js +262 -0
- package/dist/cli.js +89 -29
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -1
- package/dist/lib/extract-module-bundle.d.ts +56 -1
- package/dist/lib/extract-module-bundle.js +170 -14
- package/dist/lib/license-key-files.d.ts +15 -0
- package/dist/lib/license-key-files.js +102 -0
- package/dist/lib/module-license-gate.d.ts +79 -0
- package/dist/lib/module-license-gate.js +160 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.d.ts +16 -10
- package/dist/lib/pro-callout.js +34 -3
- package/dist/license-set.d.ts +34 -0
- package/dist/license-set.js +194 -0
- package/dist/module-architecture.d.ts +20 -0
- package/dist/module-architecture.js +77 -19
- package/dist/modules/commerce/0.6.1.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
- package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
- package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
- package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
- package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
- package/dist/modules/workspace/0.6.1.tar.gz +0 -0
- package/dist/parse-args.d.ts +85 -10
- package/dist/parse-args.js +227 -15
- package/dist/programmatic.d.ts +81 -5
- package/dist/programmatic.js +291 -32
- package/dist/site-chrome.d.ts +18 -7
- package/dist/site-chrome.js +32 -14
- package/dist/site-roster.d.ts +127 -0
- package/dist/site-roster.js +499 -0
- package/dist/starter-grid.d.ts +216 -0
- package/dist/starter-grid.js +449 -0
- package/dist/surfaces.d.ts +20 -7
- package/dist/surfaces.js +204 -24
- package/dist/sync-routes.d.ts +9 -0
- package/dist/sync-routes.js +2 -2
- package/dist/template-manifest.d.ts +94 -19
- package/dist/template-manifest.js +170 -289
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +6 -4
- package/site-templates/_base/content/en/BookCall/book-call.mdx +52 -0
- package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
- package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +65 -0
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +67 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
- package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +17 -3
- package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
- package/site-templates/designs/minimalism/DESIGN.md +162 -0
- package/site-templates/designs/standard/DESIGN.md +167 -0
- package/site-templates/marketing-site/DESIGN.md +59 -40
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +376 -57
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +230 -90
- package/site-templates/marketing-site/ailk-template.json +1 -0
- package/site-templates/marketing-site-free/DESIGN.md +59 -40
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +323 -63
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +323 -113
- package/site-templates/marketing-site-free/ailk-template.json +3 -2
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
- package/site-templates/minimalism-site/DESIGN.md +54 -40
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +173 -51
- package/site-templates/minimalism-site/ailk-template.json +3 -2
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/saas-product/DESIGN.md +59 -40
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +369 -0
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
- package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
- package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/local-business-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
- package/site-templates/shapes/local-business-service/provenance.json +4 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
- package/site-templates/shapes/organization-service/ailk-template.json +10 -0
- package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
- package/site-templates/shapes/person-service/ailk-template.json +10 -0
- package/site-templates/shapes/person-service/content/_site.mdx +85 -0
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
- package/site-templates/shapes/person-service/provenance.json +4 -0
- package/templates/.claude/settings.json +15 -0
- package/templates/.claude/skills/README.md +10 -9
- package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
- package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
- package/templates/.claude/skills/add-page/references/page-types.md +115 -0
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
- package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
- package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
- package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
- package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
- package/templates/.claude/skills/fill-page/SKILL.md +180 -0
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
- package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
- package/templates/.claude/skills/setup-database/SKILL.md +156 -0
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
- package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +73 -41
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +21 -30
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +204 -0
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
- package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
- package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
- package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/CLAUDE.md +8 -2
- package/templates/DESIGN.md +56 -44
- package/templates/README.md +116 -46
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/CLAUDE.md +3 -2
- package/templates/apps/api/package.json +1 -1
- package/templates/apps/api/src/config/__tests__/index.test.ts +146 -10
- package/templates/apps/api/src/config/index.ts +46 -14
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
- package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
- package/templates/apps/api/src/lib/license-key-store.ts +45 -0
- package/templates/apps/api/src/lib/site.ts +5 -3
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
- package/templates/apps/api/src/middleware/auth.ts +9 -0
- package/templates/apps/api/src/openapi/spec.ts +63 -12
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
- package/templates/apps/api/src/routes/content/get-page.ts +86 -2
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
- package/templates/apps/api/src/routes/leads/index.ts +46 -1
- package/templates/apps/api/src/server.ts +13 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
- package/templates/apps/api/src/services/lead-routing.ts +140 -30
- package/templates/apps/api/src/services/leads-inbox.ts +48 -11
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/package.json +2 -1
- package/templates/apps/mcp/src/index.ts +7 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
- package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- package/templates/apps/web/__tests__/setup.ts +18 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
- package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +67 -29
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +94 -11
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +228 -86
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
- package/templates/apps/web/app/[locale]/contact/page.tsx +115 -7
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/layout.tsx +146 -52
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +142 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/{cancellation → purchase-policy}/page.tsx +15 -10
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +63 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
- package/templates/apps/web/app/error-fallback.tsx +17 -45
- package/templates/apps/web/app/not-found.tsx +14 -38
- package/templates/apps/web/app/og/route.tsx +6 -0
- package/templates/apps/web/eslint.config.mjs +3 -0
- package/templates/apps/web/instrumentation.ts +14 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
- package/templates/apps/web/lib/license-key-store.ts +52 -0
- package/templates/apps/web/lib/nav-config.ts +82 -18
- package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
- package/templates/apps/web/lib/site-brand.tsx +1 -1
- package/templates/apps/web/lib/site-chrome.ts +3 -3
- package/templates/apps/web/lib/site-identity.ts +148 -0
- package/templates/apps/web/lib/site-theme.ts +11 -0
- package/templates/apps/web/messages/en.json +78 -37
- 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 +3 -3
- package/templates/apps/web/public/logomark.svg +6 -3
- package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
- package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +4 -4
- package/templates/apps/web/public/wordmark-light.svg +4 -0
- package/templates/apps/web/public/wordmark.svg +3 -3
- package/templates/apps/web/security-headers.config.ts +5 -1
- package/templates/content/_site.mdx +25 -0
- package/templates/content/en/AboutPage/about.mdx +150 -144
- package/templates/content/en/BlogList/blog.mdx +126 -0
- package/templates/content/en/ContactPage/contact.mdx +21 -92
- package/templates/content/en/FAQPage/faq.mdx +86 -57
- package/templates/content/en/HomePage/home.mdx +268 -217
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +181 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +3 -1
- package/templates/tools/lint-architecture.ts +315 -0
- package/templates/tools/tsconfig.json +14 -0
- package/versions.json +33 -33
- package/site-templates/simple-service/DESIGN.md +0 -148
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +0 -240
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -278
- package/site-templates/simple-service/ailk-template.json +0 -10
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -67
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
- package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
- package/site-templates/simple-service/provenance.json +0 -4
- package/templates/.claude/skills/add-schema/SKILL.md +0 -277
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
- package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
- package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
- package/templates/.claude/skills/provision-config/references/runbook-template.md +0 -248
- package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -150
- package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
- package/templates/apps/web/app/[locale]/shipping/page.tsx +0 -44
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
- package/templates/content/en/LandingPage/blog.mdx +0 -83
- package/templates/content/en/LandingPage/features.mdx +0 -103
- package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx
CHANGED
|
@@ -73,7 +73,7 @@ describe("IntegrationsPage — snapshot regression", () => {
|
|
|
73
73
|
});
|
|
74
74
|
const { container } = render(element);
|
|
75
75
|
expect(container.innerHTML).toMatchInlineSnapshot(
|
|
76
|
-
`"<main><h1 class="text-heading-1
|
|
76
|
+
`"<main id="main"><h1 class="text-heading-1 lg:text-display-sm font-display text-fg hero-type-display">Integrations</h1><p data-testid="section-empty-state">Integrations listing coming soon.</p></main>"`,
|
|
77
77
|
);
|
|
78
78
|
});
|
|
79
79
|
|
|
@@ -86,7 +86,7 @@ describe("IntegrationsPage — snapshot regression", () => {
|
|
|
86
86
|
});
|
|
87
87
|
const { container } = render(element);
|
|
88
88
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
89
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Integrations","description":"First-party integrations for AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"SoftwareApplication"}]}}</script><main><h1>Integrations</h1><p data-testid="integrations-description">First-party integrations for AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) 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) min-h-(--control-height-big) 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="# Integrations
|
|
89
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@type":"CollectionPage","inLanguage":"en","name":"Integrations","description":"First-party integrations for AI Launch Kit.","mainEntity":{"@type":"ItemList","itemListElement":[{"@type":"SoftwareApplication"}]}}</script><main id="main"><h1>Integrations</h1><p data-testid="integrations-description">First-party integrations for AI Launch Kit.</p><div data-testid="markdown-view-toggle" class="ailk-markdown-view-toggle"><div class="flex justify-end mb-3"><div role="radiogroup" aria-label="Page view" data-testid="markdown-view-toggle" class="inline-flex items-center gap-0.5 rounded-lg p-1 bg-(--color-surface-secondary)"><button class="inline-flex items-center justify-center gap-2 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>svg]:shrink-0 text-(--color-fg) hover:bg-(--color-surface-secondary) active:bg-gray-200 rounded-control px-control-x h-(--control-height-thin) text-caption leading-none rounded-md px-3 text-body-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 min-w-(--control-height-thin) min-h-(--control-height-big) 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) min-h-(--control-height-big) 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="# Integrations
|
|
90
90
|
|
|
91
91
|
Body."></div></article></div></div></main>"
|
|
92
92
|
`);
|
|
@@ -110,7 +110,7 @@ describe("IntegrationSlugPage — snapshot regression", () => {
|
|
|
110
110
|
});
|
|
111
111
|
const { container } = render(element);
|
|
112
112
|
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
113
|
-
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"OpenAI API","description":"Connect AILK to OpenAI.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"OpenAI API","description":"Connect AILK to OpenAI."}},{"@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":"Integrations","item":"http://localhost:3000/en/integrations"},{"@type":"ListItem","position":3,"name":"OpenAI API","item":"http://localhost:3000/en/integrations/openai-api"}]}]}</script><main><h1>OpenAI API</h1><p data-testid="integration-description">Connect AILK to OpenAI.</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) min-h-(--control-height-big) 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) min-h-(--control-height-big) 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="# OpenAI API
|
|
113
|
+
"<script type="application/ld+json" data-testid="json-ld">{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"OpenAI API","description":"Connect AILK to OpenAI.","mainEntity":{"@context":"https://schema.org","@type":"SoftwareApplication","name":"OpenAI API","description":"Connect AILK to OpenAI."}},{"@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":"Integrations","item":"http://localhost:3000/en/integrations"},{"@type":"ListItem","position":3,"name":"OpenAI API","item":"http://localhost:3000/en/integrations/openai-api"}]}]}</script><main id="main"><h1>OpenAI API</h1><p data-testid="integration-description">Connect AILK to OpenAI.</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) min-h-(--control-height-big) 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) min-h-(--control-height-big) 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="# OpenAI API
|
|
114
114
|
|
|
115
115
|
Body."></div></article></div></div></main>"
|
|
116
116
|
`);
|
|
@@ -27,7 +27,7 @@ import { Telemetry, VerificationMeta } from "@working-theory/analytics";
|
|
|
27
27
|
import { MarketingShell } from "@working-theory/templates/shells";
|
|
28
28
|
import { themeToCssVarsBothModes } from "@working-theory/theme";
|
|
29
29
|
import { Link, isRtl, localeHref } from "@working-theory/ui";
|
|
30
|
-
import { LocaleSwitcherNav } from "@working-theory/ui/blocks";
|
|
30
|
+
import { BRAND_PATHS, LocaleSwitcherNav } from "@working-theory/ui/blocks";
|
|
31
31
|
import {
|
|
32
32
|
Banner,
|
|
33
33
|
Footer,
|
|
@@ -50,11 +50,14 @@ import { auth } from "../../auth";
|
|
|
50
50
|
import { GoogleAnalytics } from "../../components/google-analytics";
|
|
51
51
|
import { SignOutTrigger } from "../../components/sign-out-trigger";
|
|
52
52
|
import { isAuthPath } from "../../lib/auth-routes";
|
|
53
|
-
import { navItems } from "../../lib/nav-config";
|
|
53
|
+
import { footerColumns, navItems } from "../../lib/nav-config";
|
|
54
54
|
import { getOgImageUrl } from "../../lib/og";
|
|
55
55
|
import { SITE_NAME, siteLogo } from "../../lib/site-brand";
|
|
56
56
|
import { siteChrome } from "../../lib/site-chrome";
|
|
57
|
-
|
|
57
|
+
// Registers the `_site.mdx` identity with page-renderer at module load, so
|
|
58
|
+
// every page rendered under this layout carries the sitewide JSON-LD (#6597).
|
|
59
|
+
import "../../lib/site-identity";
|
|
60
|
+
import { loadSiteBrandTokens, loadSiteShapeCss } from "../../lib/site-theme";
|
|
58
61
|
import { resolveBaseUrl } from "../../src/lib/routes";
|
|
59
62
|
|
|
60
63
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
@@ -224,6 +227,25 @@ export default async function LocaleLayout({
|
|
|
224
227
|
// The Free marketing-site chrome (#5469 D5): the Product flyout's three
|
|
225
228
|
// entries + descriptions, the top-level items, and the Get started CTA.
|
|
226
229
|
product: tNav("product"),
|
|
230
|
+
useCases: tNav("useCases"),
|
|
231
|
+
useCasesDesc: tNav("useCasesDesc"),
|
|
232
|
+
footerSitePagesHeading: tNav("footerSitePagesHeading"),
|
|
233
|
+
footerFeaturesHeading: tNav("footerFeaturesHeading"),
|
|
234
|
+
footerResourcesHeading: tNav("footerResourcesHeading"),
|
|
235
|
+
feature1: tNav("feature1"),
|
|
236
|
+
feature2: tNav("feature2"),
|
|
237
|
+
feature3: tNav("feature3"),
|
|
238
|
+
feature4: tNav("feature4"),
|
|
239
|
+
feature5: tNav("feature5"),
|
|
240
|
+
allFeatures: tNav("allFeatures"),
|
|
241
|
+
// #6721 — the two-column Product flyout + the Use Cases footer column.
|
|
242
|
+
flyoutFeaturesHeading: tNav("flyoutFeaturesHeading"),
|
|
243
|
+
flyoutUseCasesHeading: tNav("flyoutUseCasesHeading"),
|
|
244
|
+
footerUseCasesHeading: tNav("footerUseCasesHeading"),
|
|
245
|
+
useCase1: tNav("useCase1"),
|
|
246
|
+
useCase2: tNav("useCase2"),
|
|
247
|
+
useCase3: tNav("useCase3"),
|
|
248
|
+
allUseCases: tNav("allUseCases"),
|
|
227
249
|
howItWorks: tNav("howItWorks"),
|
|
228
250
|
howItWorksDesc: tNav("howItWorksDesc"),
|
|
229
251
|
features: tNav("features"),
|
|
@@ -232,7 +254,17 @@ export default async function LocaleLayout({
|
|
|
232
254
|
faqDesc: tNav("faqDesc"),
|
|
233
255
|
pricing: tNav("pricing"),
|
|
234
256
|
about: tNav("about"),
|
|
257
|
+
// The footer's column groups (#6468): their two headings + Contact.
|
|
258
|
+
contact: tNav("contact"),
|
|
259
|
+
// The three pages every starter's roster names (#6658 D8), linked from
|
|
260
|
+
// the footer when their surfaces are on.
|
|
261
|
+
bookACall: tNav("bookACall"),
|
|
262
|
+
glossary: tNav("glossary"),
|
|
263
|
+
newsletter: tNav("newsletter"),
|
|
264
|
+
footerProductHeading: tNav("footerProductHeading"),
|
|
265
|
+
footerCompanyHeading: tNav("footerCompanyHeading"),
|
|
235
266
|
getStarted: tNav("getStarted"),
|
|
267
|
+
signUp: tNav("signUp"),
|
|
236
268
|
docs: tNav("docs"),
|
|
237
269
|
blog: tNav("blog"),
|
|
238
270
|
// The scheduling module's booking page, /meet (#4633; renamed from the
|
|
@@ -278,8 +310,9 @@ export default async function LocaleLayout({
|
|
|
278
310
|
},
|
|
279
311
|
]
|
|
280
312
|
: [
|
|
281
|
-
|
|
313
|
+
// NO_HAND_IMPLEMENT_OK: operator-directed live Free-template pass.
|
|
282
314
|
{ labelKey: "signIn", path: "/sign-in", variant: "outline-strong" },
|
|
315
|
+
{ labelKey: "signUp", path: "/sign-up", variant: "primary" },
|
|
283
316
|
];
|
|
284
317
|
|
|
285
318
|
// Only compose the flow seam when there is a flow action to compose. This
|
|
@@ -294,12 +327,14 @@ export default async function LocaleLayout({
|
|
|
294
327
|
const hasFlowAction = navActions.some((action) => action.kind === "flow");
|
|
295
328
|
|
|
296
329
|
// Shared brand mark injected into both Nav and Footer chrome slots. The
|
|
297
|
-
// mark comes from site-brand.tsx (the
|
|
298
|
-
//
|
|
299
|
-
//
|
|
330
|
+
// mark comes from site-brand.tsx (the mark the /sign-in card also renders);
|
|
331
|
+
// both add the shipped wordmark beside it — the Nav composes mark + wordmark
|
|
332
|
+
// on desktop and mark-only below `md` (#5469 D5), the columns Footer shows
|
|
333
|
+
// the full lockup at every width (#6507).
|
|
300
334
|
const brandLogo = siteLogo();
|
|
301
335
|
const brandWordmark = {
|
|
302
336
|
src: "/wordmark.svg",
|
|
337
|
+
darkSrc: "/wordmark-light.svg",
|
|
303
338
|
alt: SITE_NAME,
|
|
304
339
|
width: 140,
|
|
305
340
|
height: 16,
|
|
@@ -351,8 +386,22 @@ export default async function LocaleLayout({
|
|
|
351
386
|
href: "https://linkedin.com/company/{{COMPANY_HANDLE}}",
|
|
352
387
|
},
|
|
353
388
|
{ platform: "youtube", href: "https://youtube.com/@{{COMPANY_HANDLE}}" },
|
|
389
|
+
// #6739 — the kit's own repo, operator-directed: the Free template links
|
|
390
|
+
// back to where it came from.
|
|
391
|
+
{ platform: "github", href: "https://github.com/working-theory-labs/ai-launch-kit" },
|
|
354
392
|
] as const;
|
|
355
|
-
|
|
393
|
+
// Footer column groups (#6468), generated beside `navItems` as label keys;
|
|
394
|
+
// `Footer` takes translated strings, so resolve them through the same map.
|
|
395
|
+
const labelFor = (key: string): string =>
|
|
396
|
+
(navLabels.items as Record<string, string>)[key] ?? key;
|
|
397
|
+
const footerColumnDefs = footerColumns.map((col) => ({
|
|
398
|
+
heading: labelFor(col.headingKey),
|
|
399
|
+
headingKey: col.headingKey,
|
|
400
|
+
items: col.items.map((item) => ({ ...item, label: labelFor(item.labelKey) })),
|
|
401
|
+
}));
|
|
402
|
+
|
|
403
|
+
// NO_HAND_IMPLEMENT_OK: operator-directed live Free-template pass (2026-09-23).
|
|
404
|
+
const copyrightText = `© ${new Date().getFullYear()} SalesSmyth LLC. All rights reserved.`;
|
|
356
405
|
|
|
357
406
|
// Footer legal links (#5612 D3). The centered variant folds in FAQ — the
|
|
358
407
|
// roster's third always-scaffolded core route (`/faq`, alongside `/privacy`
|
|
@@ -362,6 +411,10 @@ export default async function LocaleLayout({
|
|
|
362
411
|
const legalLinks = [
|
|
363
412
|
{ label: "Privacy Policy", href: "/privacy" },
|
|
364
413
|
{ label: "Terms of Service", href: "/terms" },
|
|
414
|
+
// #6739 — a link, not the reopen button: the privacy page's
|
|
415
|
+
// #cookie-settings anchor explains the choice, and arriving there
|
|
416
|
+
// reopens the consent banner (BannerConsentIsland).
|
|
417
|
+
{ label: tConsent("settingsLabel"), href: "/privacy#cookie-settings" },
|
|
365
418
|
...(siteChrome.footer === "centered"
|
|
366
419
|
? [{ label: navLabels.items.faq, href: "/faq" }]
|
|
367
420
|
: []),
|
|
@@ -379,9 +432,11 @@ export default async function LocaleLayout({
|
|
|
379
432
|
// single-mode call inlined an unconditional :root block that never
|
|
380
433
|
// responded to a client-side data-theme toggle). themeMode still drives
|
|
381
434
|
// the initial data-theme attribute below for a no-flash first paint.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
435
|
+
// Site shape settings (`theme.corners`, `theme.eyebrowCase`) appended after
|
|
436
|
+
// the token block so they win; "" for a site that sets neither.
|
|
437
|
+
const inlineThemeStyle =
|
|
438
|
+
themeToCssVarsBothModes(siteBrandTokens ? { brand: siteBrandTokens } : {}) +
|
|
439
|
+
loadSiteShapeCss();
|
|
385
440
|
|
|
386
441
|
return (
|
|
387
442
|
<html
|
|
@@ -425,6 +480,17 @@ export default async function LocaleLayout({
|
|
|
425
480
|
<VerificationMeta />
|
|
426
481
|
</head>
|
|
427
482
|
<body>
|
|
483
|
+
{/* Skip link (operator ruling 2026-09-23): the first Tab stop on every
|
|
484
|
+
page jumps keyboard users past the banner and nav to <main id="main">,
|
|
485
|
+
which page-renderer (or the shell) owns. Visible only while focused. */}
|
|
486
|
+
<Link
|
|
487
|
+
href="#main"
|
|
488
|
+
variant="bare"
|
|
489
|
+
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:start-2 focus:z-50 focus:rounded-control focus:bg-(--color-bg) focus:px-control-x focus:py-control-y focus:text-(--color-fg) focus:ring-2 focus:ring-(--color-accent)"
|
|
490
|
+
data-testid="skip-to-content"
|
|
491
|
+
>
|
|
492
|
+
{tNav("skipToContent")}
|
|
493
|
+
</Link>
|
|
428
494
|
<NextIntlClientProvider messages={messages}>
|
|
429
495
|
{/*
|
|
430
496
|
* Shell + Chrome compose at the layout layer (decision: #2820, #2862).
|
|
@@ -450,7 +516,12 @@ export default async function LocaleLayout({
|
|
|
450
516
|
) : (
|
|
451
517
|
<MarketingShell
|
|
452
518
|
contentOwnsMain
|
|
453
|
-
|
|
519
|
+
// #6536 — no gutter frame: header, sections and footer share one
|
|
520
|
+
// left/right edge (the section Container's), matching the
|
|
521
|
+
// reference. Nav `frame="contained"` and Footer
|
|
522
|
+
// `bottomBarAlign="content"` below put the header row and the
|
|
523
|
+
// footer divider on that same edge.
|
|
524
|
+
gutters="none"
|
|
454
525
|
bannerPlacement="above-nav"
|
|
455
526
|
{...(siteChrome.announcementBanner
|
|
456
527
|
? {
|
|
@@ -458,10 +529,30 @@ export default async function LocaleLayout({
|
|
|
458
529
|
<Banner
|
|
459
530
|
variant="contrast"
|
|
460
531
|
placement="above-nav"
|
|
461
|
-
|
|
532
|
+
// NO_HAND_IMPLEMENT_OK: operator-directed live Free-template pass.
|
|
533
|
+
message={
|
|
534
|
+
<>
|
|
535
|
+
<a
|
|
536
|
+
href="https://github.com/working-theory-labs/ai-launch-kit"
|
|
537
|
+
className="inline-flex items-center gap-1 font-bold text-current underline underline-offset-2"
|
|
538
|
+
>
|
|
539
|
+
Get started for free
|
|
540
|
+
<svg
|
|
541
|
+
viewBox="0 0 24 24"
|
|
542
|
+
aria-hidden="true"
|
|
543
|
+
className="size-4 fill-current"
|
|
544
|
+
>
|
|
545
|
+
<path d={BRAND_PATHS.github} />
|
|
546
|
+
</svg>
|
|
547
|
+
</a>
|
|
548
|
+
{"\u00A0 "}
|
|
549
|
+
or
|
|
550
|
+
</>
|
|
551
|
+
}
|
|
552
|
+
messageWeight="regular"
|
|
462
553
|
action={{
|
|
463
|
-
label: "
|
|
464
|
-
href: "/pricing",
|
|
554
|
+
label: "upgrade to Pro",
|
|
555
|
+
href: "https://ailaunchkit.ai/pricing",
|
|
465
556
|
kind: "link",
|
|
466
557
|
}}
|
|
467
558
|
routes={{ include: [homePath] }}
|
|
@@ -493,6 +584,8 @@ export default async function LocaleLayout({
|
|
|
493
584
|
// render is byte-identical to before this branch existed.
|
|
494
585
|
{...(isDeckHome ? { sticky: true, deckMorph: true } : {})}
|
|
495
586
|
siteName={SITE_NAME}
|
|
587
|
+
frame="contained"
|
|
588
|
+
navAlignment="right"
|
|
496
589
|
showThemeToggle={false}
|
|
497
590
|
logo={brandLogo}
|
|
498
591
|
wordmark={brandWordmark}
|
|
@@ -520,13 +613,16 @@ export default async function LocaleLayout({
|
|
|
520
613
|
locale={locale}
|
|
521
614
|
itemLabels={navLabels.items}
|
|
522
615
|
navItems={navItems}
|
|
616
|
+
columns={footerColumnDefs}
|
|
523
617
|
siteName={SITE_NAME}
|
|
524
618
|
logo={brandLogo}
|
|
619
|
+
wordmark={brandWordmark}
|
|
525
620
|
social={[...socialLinks]}
|
|
526
621
|
{...(siteChrome.footer === "centered"
|
|
527
622
|
? { layout: "centered" as const }
|
|
528
623
|
: {})}
|
|
529
624
|
showThemeToggle
|
|
625
|
+
bottomBarAlign="content"
|
|
530
626
|
copyrightText={copyrightText}
|
|
531
627
|
legalLinks={legalLinks}
|
|
532
628
|
languageSwitcher={
|
|
@@ -534,20 +630,9 @@ export default async function LocaleLayout({
|
|
|
534
630
|
current={locale}
|
|
535
631
|
locales={enabledLocales}
|
|
536
632
|
pathname={pathname}
|
|
633
|
+
variant="quiet"
|
|
537
634
|
/>
|
|
538
635
|
}
|
|
539
|
-
// #4157 — deliberately left as a literal "Built by Working
|
|
540
|
-
// Theory Labs" / https://ailaunchkit.ai, not SITE_NAME: this credits
|
|
541
|
-
// the OSS TEMPLATE this site is built with (a "powered by"
|
|
542
|
-
// link, unconditional in OSS forks per the comment above),
|
|
543
|
-
// never the customer's own identity, so it isn't the vendor-
|
|
544
|
-
// brand-as-default defect #4157 fixes. See
|
|
545
|
-
// docs/specs/4157-scaffold-brand-defaults-spec.md § Design
|
|
546
|
-
// Decisions for the fuller reasoning.
|
|
547
|
-
attribution={{
|
|
548
|
-
label: "Built by Working Theory Labs",
|
|
549
|
-
href: "https://ailaunchkit.ai",
|
|
550
|
-
}}
|
|
551
636
|
/>
|
|
552
637
|
}
|
|
553
638
|
>
|
|
@@ -561,32 +646,41 @@ export default async function LocaleLayout({
|
|
|
561
646
|
* visitor sees it, an accepted/declined one does not, on every
|
|
562
647
|
* page. Out of the shell's flow (placement `bottom` is a fixed
|
|
563
648
|
* overlay), so accepting causes no layout shift.
|
|
649
|
+
*
|
|
650
|
+
* SOFT_LOCK_OK: #6510 (D3) — Banner is now ALWAYS mounted (the
|
|
651
|
+
* `consentDecided ? null :` gate is gone); `consentDecided` is
|
|
652
|
+
* passed straight through so the island's `initialConsented`
|
|
653
|
+
* starts from server truth instead of only its own client-side
|
|
654
|
+
* cookie read — same #5557 hydration-safety property, no gate
|
|
655
|
+
* needed. This also means the banner never blocks the page: it
|
|
656
|
+
* reserves its own space (D1) and carries a close-without-choosing
|
|
657
|
+
* control (D2) instead of a hard server-side hide.
|
|
564
658
|
*/}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
659
|
+
<Banner
|
|
660
|
+
variant="info"
|
|
661
|
+
placement="bottom"
|
|
662
|
+
consentCookieName={CONSENT_COOKIE_NAME}
|
|
663
|
+
consentDecided={consentDecided}
|
|
664
|
+
title={tConsent("title")}
|
|
665
|
+
message={
|
|
666
|
+
<>
|
|
667
|
+
{tConsent("body")}{" "}
|
|
668
|
+
<Link
|
|
669
|
+
href={localeHref(locale, "/privacy")}
|
|
670
|
+
variant="unstyled"
|
|
671
|
+
className="font-semibold text-current underline underline-offset-4"
|
|
672
|
+
>
|
|
673
|
+
{tConsent("policyLinkLabel")}
|
|
674
|
+
</Link>
|
|
675
|
+
</>
|
|
676
|
+
}
|
|
677
|
+
dismissible={false}
|
|
678
|
+
dismissLabel={tConsent("dismissLabel")}
|
|
679
|
+
acceptLabel={tConsent("acceptLabel")}
|
|
680
|
+
secondaryAction={{ label: tConsent("rejectLabel") }}
|
|
681
|
+
rejectLabel={tConsent("rejectLabel")}
|
|
682
|
+
locale={locale}
|
|
683
|
+
/>
|
|
590
684
|
<Telemetry />
|
|
591
685
|
</NextIntlClientProvider>
|
|
592
686
|
</body>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subscribeNewsletterAction (#6730 D9) — the Server Action twin of the
|
|
3
|
+
* `ailkSubscribeNewsletter` MCP tool.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { subscribeNewsletterAction } from "../newsletter.actions.js";
|
|
7
|
+
|
|
8
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
9
|
+
leads: { subscribe: jest.fn() },
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { leads } from "@working-theory/api-client";
|
|
13
|
+
const mockSubscribe = leads.subscribe as jest.Mock;
|
|
14
|
+
|
|
15
|
+
function formWith(email: string): FormData {
|
|
16
|
+
const fd = new FormData();
|
|
17
|
+
fd.append("email", email);
|
|
18
|
+
fd.append("consent", "true");
|
|
19
|
+
return fd;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEach(() => jest.clearAllMocks());
|
|
23
|
+
|
|
24
|
+
describe("subscribeNewsletterAction", () => {
|
|
25
|
+
it("calls leads.subscribe with source newsletter and the address", async () => {
|
|
26
|
+
mockSubscribe.mockResolvedValueOnce({ ok: true, body: { id: "subscr_1", persisted: "db" } });
|
|
27
|
+
|
|
28
|
+
const result = await subscribeNewsletterAction(null, formWith("ada@example.com"));
|
|
29
|
+
|
|
30
|
+
expect(result).toEqual({ kind: "success" });
|
|
31
|
+
expect(mockSubscribe).toHaveBeenCalledWith(
|
|
32
|
+
expect.objectContaining({ source: "newsletter", email: "ada@example.com" }),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("an invalid address → a field error, no API call", async () => {
|
|
37
|
+
const result = await subscribeNewsletterAction(null, formWith("not-an-email"));
|
|
38
|
+
|
|
39
|
+
expect(result).toMatchObject({ kind: "error", fieldErrors: { email: expect.any(String) } });
|
|
40
|
+
expect(mockSubscribe).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("503 lead_capture_not_configured → unavailable", async () => {
|
|
44
|
+
mockSubscribe.mockResolvedValueOnce({
|
|
45
|
+
ok: false,
|
|
46
|
+
status: 503,
|
|
47
|
+
body: { error: "lead_capture_not_configured" },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const result = await subscribeNewsletterAction(null, formWith("ada@example.com"));
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({ kind: "unavailable" });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("any other failure → the generic error, never success", async () => {
|
|
56
|
+
mockSubscribe.mockResolvedValueOnce({ ok: false, status: 503, body: { error: "persist_failed" } });
|
|
57
|
+
expect(await subscribeNewsletterAction(null, formWith("a@b.co"))).toMatchObject({
|
|
58
|
+
kind: "error",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
mockSubscribe.mockRejectedValueOnce(new Error("ECONNREFUSED"));
|
|
62
|
+
jest.spyOn(console, "error").mockImplementationOnce(() => undefined);
|
|
63
|
+
expect(await subscribeNewsletterAction(null, formWith("a@b.co"))).toMatchObject({
|
|
64
|
+
kind: "error",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("a filled honeypot → success, and nothing is sent", async () => {
|
|
69
|
+
const fd = formWith("ada@example.com");
|
|
70
|
+
fd.append("ailk_hp", "spam");
|
|
71
|
+
|
|
72
|
+
expect(await subscribeNewsletterAction(null, fd)).toEqual({ kind: "success" });
|
|
73
|
+
expect(mockSubscribe).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* newsletter/page.tsx — a signup only when there is somewhere to keep it
|
|
3
|
+
* (#6730 D9).
|
|
4
|
+
*
|
|
5
|
+
* Boundaries mocked: `@working-theory/ui` (the section tree is a stub that
|
|
6
|
+
* renders one element per section, with a `newsletter` section rendering the
|
|
7
|
+
* form its content declares), `@working-theory/api-client`
|
|
8
|
+
* (`leads.captureStatus`), `next-intl/server`, and the Server Action module.
|
|
9
|
+
*/
|
|
10
|
+
import React from "react";
|
|
11
|
+
import { render, screen } from "@testing-library/react";
|
|
12
|
+
|
|
13
|
+
jest.mock("@working-theory/ui", () => ({
|
|
14
|
+
Heading: ({ children, level }: { children: React.ReactNode; level: number }) =>
|
|
15
|
+
React.createElement(`h${level}`, null, children),
|
|
16
|
+
SectionRenderer: ({
|
|
17
|
+
sections,
|
|
18
|
+
}: {
|
|
19
|
+
sections: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
data: { action?: string; primaryCta?: { href: string } };
|
|
23
|
+
}>;
|
|
24
|
+
}) =>
|
|
25
|
+
React.createElement(
|
|
26
|
+
"div",
|
|
27
|
+
{ "data-testid": "section-renderer" },
|
|
28
|
+
sections.map((s) =>
|
|
29
|
+
s.type === "newsletter"
|
|
30
|
+
? React.createElement(
|
|
31
|
+
"form",
|
|
32
|
+
{ key: s.id, "data-section-id": s.id, action: s.data.action },
|
|
33
|
+
"signup",
|
|
34
|
+
)
|
|
35
|
+
: React.createElement(
|
|
36
|
+
"div",
|
|
37
|
+
{ key: s.id, "data-section-id": s.id },
|
|
38
|
+
s.data.primaryCta
|
|
39
|
+
? React.createElement("a", { href: s.data.primaryCta.href }, "cta")
|
|
40
|
+
: null,
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
localeHref: (locale: string, path: string) => `/${locale}${path}`,
|
|
45
|
+
LeadCaptureForm: (props: { fields?: Array<{ name: string; type: string }> }) =>
|
|
46
|
+
React.createElement("div", {
|
|
47
|
+
"data-testid": "lead-capture-form",
|
|
48
|
+
"data-fields": (props.fields ?? []).map((f) => `${f.name}:${f.type}`).join(","),
|
|
49
|
+
}),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
53
|
+
leads: { captureStatus: jest.fn() },
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
jest.mock("next-intl/server", () => ({
|
|
57
|
+
getTranslations: async () => (key: string) => key,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
// eslint-disable-next-line no-restricted-syntax -- test-seam mock: the Server Action module is a "use server" boundary; the page only passes it through
|
|
61
|
+
jest.mock("../newsletter.actions", () => ({
|
|
62
|
+
subscribeNewsletterAction: jest.fn(),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
import { leads } from "@working-theory/api-client";
|
|
66
|
+
|
|
67
|
+
import NewsletterPage from "../page";
|
|
68
|
+
|
|
69
|
+
const mockCaptureStatus = leads.captureStatus as jest.Mock;
|
|
70
|
+
const status = (newsletter: boolean) => ({
|
|
71
|
+
ok: true,
|
|
72
|
+
status: 200,
|
|
73
|
+
body: { ok: true, data: { contact: true, newsletter } },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const source = {
|
|
77
|
+
frontmatter: {
|
|
78
|
+
type: "NewsletterSignup" as const,
|
|
79
|
+
slug: "newsletter",
|
|
80
|
+
title: "Newsletter",
|
|
81
|
+
description: "What subscribers get.",
|
|
82
|
+
},
|
|
83
|
+
body: "",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** `_base`'s content: a hero whose CTA points at `#signup`, the signup section, an FAQ. */
|
|
87
|
+
const BASE_SECTIONS = [
|
|
88
|
+
{ id: "hero", type: "hero", data: { headline: "News", primaryCta: { label: "Subscribe", href: "#signup" } } },
|
|
89
|
+
{ id: "signup", type: "newsletter", data: { action: "/v1/leads/subscribe", source: "newsletter" } },
|
|
90
|
+
{ id: "faq", type: "faq", data: { items: [] } },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
/** #6738's Free content: no signup section. */
|
|
94
|
+
const FREE_SECTIONS = [
|
|
95
|
+
{ id: "hero", type: "hero", data: { headline: "News" } },
|
|
96
|
+
{ id: "faq", type: "faq", data: { items: [] } },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
async function renderWith(sections: unknown[]) {
|
|
100
|
+
const element = await NewsletterPage({
|
|
101
|
+
params: Promise.resolve({ locale: "en" }),
|
|
102
|
+
getMdxSource: jest.fn().mockResolvedValue(source),
|
|
103
|
+
getPageSections: jest.fn().mockResolvedValue(sections),
|
|
104
|
+
});
|
|
105
|
+
return render(element);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
beforeEach(() => jest.clearAllMocks());
|
|
109
|
+
|
|
110
|
+
describe("NewsletterPage — #6730 D9", () => {
|
|
111
|
+
it("newsletter:true + no newsletter section → one appended form with a single email field", async () => {
|
|
112
|
+
mockCaptureStatus.mockResolvedValue(status(true));
|
|
113
|
+
|
|
114
|
+
await renderWith(FREE_SECTIONS);
|
|
115
|
+
|
|
116
|
+
expect(screen.getAllByTestId("lead-capture-form")).toHaveLength(1);
|
|
117
|
+
expect(screen.getByTestId("lead-capture-form")).toHaveAttribute(
|
|
118
|
+
"data-fields",
|
|
119
|
+
"email:email",
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("newsletter:false + no newsletter section → no form at all", async () => {
|
|
124
|
+
mockCaptureStatus.mockResolvedValue(status(false));
|
|
125
|
+
|
|
126
|
+
const { container } = await renderWith(FREE_SECTIONS);
|
|
127
|
+
|
|
128
|
+
expect(screen.queryByTestId("lead-capture-form")).toBeNull();
|
|
129
|
+
expect(container.querySelector("form")).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("newsletter:true + _base's newsletter section → that section renders, no second form", async () => {
|
|
133
|
+
mockCaptureStatus.mockResolvedValue(status(true));
|
|
134
|
+
|
|
135
|
+
const { container } = await renderWith(BASE_SECTIONS);
|
|
136
|
+
|
|
137
|
+
expect(container.querySelector("form[action='/v1/leads/subscribe']")).not.toBeNull();
|
|
138
|
+
expect(screen.queryByTestId("lead-capture-form")).toBeNull();
|
|
139
|
+
expect(container.querySelector("a[href='#signup']")).not.toBeNull();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it.each([
|
|
143
|
+
["newsletter:false", () => mockCaptureStatus.mockResolvedValue(status(false))],
|
|
144
|
+
["the status read rejecting", () => mockCaptureStatus.mockRejectedValue(new Error("ECONNREFUSED"))],
|
|
145
|
+
["a 404 (older API)", () => mockCaptureStatus.mockResolvedValue({ ok: false, status: 404, body: {} })],
|
|
146
|
+
])(
|
|
147
|
+
"%s + _base's newsletter section → the section is absent, and so is the hero CTA to it",
|
|
148
|
+
async (_label, arrange) => {
|
|
149
|
+
arrange();
|
|
150
|
+
|
|
151
|
+
const { container } = await renderWith(BASE_SECTIONS);
|
|
152
|
+
|
|
153
|
+
expect(container.querySelector("[data-section-id='signup']")).toBeNull();
|
|
154
|
+
expect(container.querySelector("form")).toBeNull();
|
|
155
|
+
expect(container.innerHTML).not.toContain("/v1/leads/subscribe");
|
|
156
|
+
expect(container.querySelector("a[href='#signup']")).toBeNull();
|
|
157
|
+
expect(screen.queryByTestId("lead-capture-form")).toBeNull();
|
|
158
|
+
// The rest of the page still renders.
|
|
159
|
+
expect(container.querySelector("[data-section-id='hero']")).not.toBeNull();
|
|
160
|
+
expect(container.querySelector("[data-section-id='faq']")).not.toBeNull();
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
});
|