create-ailk 0.3.0 → 0.5.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.
Files changed (155) hide show
  1. package/component-catalog.json +11939 -0
  2. package/component-catalog.md +176 -64
  3. package/dist/cli.js +48 -10
  4. package/dist/derive-architecture.js +5 -2
  5. package/dist/lib/extract-module-bundle.d.ts +88 -1
  6. package/dist/lib/extract-module-bundle.js +375 -80
  7. package/dist/lib/fetch-guard.d.ts +28 -0
  8. package/dist/lib/fetch-guard.js +101 -0
  9. package/dist/lib/fetch-module.js +4 -0
  10. package/dist/lib/fetch-template.d.ts +20 -0
  11. package/dist/lib/fetch-template.js +133 -0
  12. package/dist/lib/module-license-gate.d.ts +8 -0
  13. package/dist/lib/module-license-gate.js +12 -4
  14. package/dist/module-architecture.d.ts +116 -7
  15. package/dist/module-architecture.js +465 -94
  16. package/dist/parse-args.d.ts +8 -1
  17. package/dist/parse-args.js +49 -2
  18. package/dist/programmatic.d.ts +36 -1
  19. package/dist/programmatic.js +216 -5
  20. package/dist/site-chrome.d.ts +80 -0
  21. package/dist/site-chrome.js +135 -0
  22. package/dist/surfaces.d.ts +21 -6
  23. package/dist/surfaces.js +140 -23
  24. package/dist/template-manifest.d.ts +144 -0
  25. package/dist/template-manifest.js +337 -0
  26. package/dist/template-names.d.ts +41 -0
  27. package/dist/template-names.js +45 -0
  28. package/package.json +10 -2
  29. package/site-templates/_base/ailk-template.json +6 -0
  30. package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
  31. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +79 -0
  32. package/site-templates/_base/content/en/TermsPage/terms.mdx +80 -0
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +331 -0
  34. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +393 -0
  35. package/site-templates/marketing-site/ailk-template.json +11 -0
  36. package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
  37. package/site-templates/marketing-site/provenance.json +4 -0
  38. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
  39. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +285 -0
  40. package/site-templates/marketing-site-free/ailk-template.json +9 -0
  41. package/site-templates/marketing-site-free/provenance.json +4 -0
  42. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +271 -0
  43. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +324 -0
  44. package/site-templates/minimalism-site/ailk-template.json +21 -0
  45. package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
  46. package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
  47. package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
  48. package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
  49. package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
  50. package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
  51. package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
  52. package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
  53. package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
  54. package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
  55. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
  56. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
  57. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
  58. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +214 -0
  59. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
  60. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
  61. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
  62. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
  63. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
  64. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
  65. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
  66. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
  67. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
  68. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
  69. package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
  70. package/site-templates/minimalism-site/provenance.json +4 -0
  71. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
  72. package/site-templates/saas-product/ailk-template.json +16 -0
  73. package/site-templates/saas-product/provenance.json +10 -0
  74. package/templates/.claude/skills/README.md +16 -25
  75. package/templates/.env.example +32 -7
  76. package/templates/.github/workflows/ci.yml +48 -0
  77. package/templates/README.md +3 -1
  78. package/templates/apps/api/CLAUDE.md +4 -1
  79. package/templates/apps/api/eslint.config.mjs +69 -0
  80. package/templates/apps/api/package.json +6 -2
  81. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +6 -2
  82. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
  83. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +173 -0
  84. package/templates/apps/api/src/config/modules.ts +1 -1
  85. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  86. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +339 -0
  87. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +108 -0
  88. package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
  89. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
  90. package/templates/apps/api/src/routes/leads/list.ts +104 -0
  91. package/templates/apps/api/src/server.ts +122 -3
  92. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
  93. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
  94. package/templates/apps/api/src/services/default-tenant.ts +261 -0
  95. package/templates/apps/api/src/services/lead-promotion.ts +161 -0
  96. package/templates/apps/api/src/services/leads-inbox.ts +263 -0
  97. package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
  98. package/templates/apps/mcp/CLAUDE.md +1 -1
  99. package/templates/apps/mcp/__tests__/parity.test.ts +2 -2
  100. package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
  101. package/templates/apps/mcp/eslint.config.mjs +69 -0
  102. package/templates/apps/mcp/package.json +6 -2
  103. package/templates/apps/mcp/src/server.ts +2 -2
  104. package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
  105. package/templates/apps/mcp/src/tools/index.ts +2 -2
  106. package/templates/apps/web/.env.example +3 -3
  107. package/templates/apps/web/CLAUDE.md +2 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +4 -6
  109. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +1 -1
  110. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
  111. package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
  112. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
  113. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
  114. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
  115. package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
  116. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
  117. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
  118. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
  119. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
  120. package/templates/apps/web/app/[locale]/layout.tsx +214 -17
  121. package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
  122. package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
  123. package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
  124. package/templates/apps/web/components/google-analytics.tsx +18 -1
  125. package/templates/apps/web/eslint.config.mjs +69 -0
  126. package/templates/apps/web/jest.config.cjs +12 -15
  127. package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
  128. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +32 -5
  129. package/templates/apps/web/lib/nav-config.ts +27 -7
  130. package/templates/apps/web/lib/server-api.ts +29 -13
  131. package/templates/apps/web/lib/site-chrome.ts +29 -0
  132. package/templates/apps/web/lib/telemetry-scrub.ts +46 -0
  133. package/templates/apps/web/messages/en.json +86 -1
  134. package/templates/apps/web/next.config.mjs +20 -0
  135. package/templates/apps/web/package.json +6 -2
  136. package/templates/apps/web/sentry.client.config.ts +131 -0
  137. package/templates/apps/web/sentry.edge.config.ts +72 -0
  138. package/templates/apps/web/sentry.server.config.ts +72 -0
  139. package/templates/apps/web/src/lib/routes.ts +13 -18
  140. package/templates/brand_assets/README.md +24 -0
  141. package/templates/content/en/AboutPage/about.mdx +160 -15
  142. package/templates/content/en/ContactPage/contact.mdx +90 -13
  143. package/templates/content/en/FAQPage/faq.mdx +62 -22
  144. package/templates/content/en/HomePage/home.mdx +244 -20
  145. package/templates/content/en/LandingPage/blog.mdx +83 -0
  146. package/templates/content/en/LandingPage/features.mdx +103 -0
  147. package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
  148. package/templates/database/CHANGELOG.md +21 -0
  149. package/templates/database/__tests__/seed.test.ts +1 -1
  150. package/templates/database/eslint.config.mjs +69 -0
  151. package/templates/database/package.json +7 -3
  152. package/templates/database/seed.ts +1 -1
  153. package/templates/package.json +1 -14
  154. package/versions.json +52 -0
  155. /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
@@ -25,8 +25,10 @@ import "../globals.css";
25
25
  import { Telemetry, VerificationMeta } from "@working-theory/analytics";
26
26
  import { MarketingShell } from "@working-theory/templates/shells";
27
27
  import { themeToCssVarsBothModes } from "@working-theory/theme";
28
- import { isRtl } from "@working-theory/ui";
28
+ import { Link, isRtl, localeHref } from "@working-theory/ui";
29
+ import { LocaleSwitcherNav } from "@working-theory/ui/blocks";
29
30
  import {
31
+ Banner,
30
32
  Footer,
31
33
  Nav,
32
34
  type NavAction,
@@ -49,6 +51,7 @@ import { SignOutTrigger } from "../../components/sign-out-trigger";
49
51
  import { navItems } from "../../lib/nav-config";
50
52
  import { getOgImageUrl } from "../../lib/og";
51
53
  import { SITE_NAME, siteLogo } from "../../lib/site-brand";
54
+ import { siteChrome } from "../../lib/site-chrome";
52
55
  import { loadSiteBrandTokens } from "../../lib/site-theme";
53
56
  import { resolveBaseUrl } from "../../src/lib/routes";
54
57
 
@@ -57,6 +60,12 @@ import { resolveBaseUrl } from "../../src/lib/routes";
57
60
  const DEFAULT_LOCALE = "en";
58
61
  const THEME_COOKIE_NAME =
59
62
  process.env.NEXT_PUBLIC_THEME_COOKIE_NAME ?? "ailk-theme";
63
+ /**
64
+ * The cookie the consent island persists the visitor's choice under (#5557).
65
+ * Passed to Banner explicitly so the server-side read below and the island's
66
+ * client-side write name the same cookie by construction.
67
+ */
68
+ const CONSENT_COOKIE_NAME = "ailk-consent";
60
69
 
61
70
  type ThemeMode = "light" | "dark";
62
71
 
@@ -73,7 +82,9 @@ type ThemeMode = "light" | "dark";
73
82
  * has to satisfy the type: one minimal fields-step and a dead `action`.
74
83
  */
75
84
  const NAV_CLIENT_ACTION_FLOW: NavActionFlow["flow"] = {
76
- steps: [{ id: "noop", fields: [{ name: "noop", label: "noop", type: "text" }] }],
85
+ steps: [
86
+ { id: "noop", fields: [{ name: "noop", label: "noop", type: "text" }] },
87
+ ],
77
88
  action: "#",
78
89
  };
79
90
 
@@ -100,6 +111,26 @@ async function resolveThemeMode(): Promise<ThemeMode> {
100
111
  }
101
112
  }
102
113
 
114
+ /**
115
+ * Whether this visitor has already accepted or declined cookies (#5557).
116
+ *
117
+ * Read on the server, not left to the island alone: `BannerConsentIsland`
118
+ * initialises its `consented` state from `document.cookie`, which makes the
119
+ * CLIENT render null while the SERVER (no `document`) has already streamed
120
+ * the banner — React then leaves that server markup in place, un-hydrated, so
121
+ * a returning visitor sees a dead banner whose buttons do nothing. Deciding
122
+ * here means a decided visitor gets no banner markup at all, and a fresh one
123
+ * gets the live island that hides itself on Accept / Reject.
124
+ */
125
+ async function hasConsentChoice(): Promise<boolean> {
126
+ try {
127
+ const cookieStore = await cookies();
128
+ return cookieStore.get(CONSENT_COOKIE_NAME)?.value !== undefined;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+
103
134
  /**
104
135
  * Blocking inline script that runs synchronously before any styled element.
105
136
  *
@@ -177,6 +208,7 @@ export default async function LocaleLayout({
177
208
  const { locale } = await params;
178
209
  const messages = await getMessages();
179
210
  const themeMode = await resolveThemeMode();
211
+ const consentDecided = await hasConsentChoice();
180
212
  // Server-side session read (#4984) — the same `auth.api.getSession` idiom
181
213
  // the `(authed)` segment guard uses. Null when signed out or auth is unset.
182
214
  const session = await auth.api.getSession({ headers: await headers() });
@@ -187,12 +219,26 @@ export default async function LocaleLayout({
187
219
  const navLabels = {
188
220
  items: {
189
221
  home: tNav("home"),
222
+ // The Free marketing-site chrome (#5469 D5): the Product flyout's three
223
+ // entries + descriptions, the top-level items, and the Get started CTA.
224
+ product: tNav("product"),
225
+ howItWorks: tNav("howItWorks"),
226
+ howItWorksDesc: tNav("howItWorksDesc"),
227
+ features: tNav("features"),
228
+ featuresDesc: tNav("featuresDesc"),
229
+ faq: tNav("faq"),
230
+ faqDesc: tNav("faqDesc"),
231
+ pricing: tNav("pricing"),
232
+ about: tNav("about"),
233
+ getStarted: tNav("getStarted"),
190
234
  docs: tNav("docs"),
191
235
  blog: tNav("blog"),
192
- // The scheduling module's booking page (#4633). Present in the map
193
- // unconditionally, like `docs` and `blog`: the map is a label lookup, and
194
- // an entry for a surface this site did not scaffold is never reached —
195
- // `nav-config.ts` only lists surfaces the declared architecture carries.
236
+ // The scheduling module's booking page, /meet (#4633; renamed from the
237
+ // schedule/ route by #5563 — the label key itself keeps its name, D1
238
+ // R2: it names copy, not a route). Present in the map unconditionally,
239
+ // like `docs` and `blog`: the map is a label lookup, and an entry for
240
+ // a surface this site did not scaffold is never reached — `nav-config.ts`
241
+ // only lists surfaces the declared architecture carries.
196
242
  schedule: tNav("schedule"),
197
243
  // Session-aware action labels (#4984). `account` carries the signed-in
198
244
  // user's display name (name, else email) so the Nav shows WHO is signed
@@ -207,8 +253,13 @@ export default async function LocaleLayout({
207
253
  closeMenu: tNav("closeMenu"),
208
254
  };
209
255
  const navOverflowLabel = tNav("moreLinks");
256
+ // Cookie-consent copy (#5557). The `consent` namespace ships in every
257
+ // scaffold's messages file; the Banner below is UI-only — OSS ships no
258
+ // enforcement, the callbacks are the hook for @working-theory/consent.
259
+ const tConsent = await getTranslations({ locale, namespace: "consent" });
210
260
 
211
- // Right-side action cluster (#4984). Signed out → a single outline-strong
261
+ // Right-side action cluster (#4984). Signed out → the Free template's pair
262
+ // (#5469 D5): a primary "Get started" → /contact beside the outline-strong
212
263
  // "Sign in" link (the operator's auth-pair idiom, #4370, MINUS Sign up —
213
264
  // this site offers no sign-up flow). Signed in → the user's name (a
214
265
  // non-navigating label) + an outline-strong "Sign out"; both authored as
@@ -224,7 +275,10 @@ export default async function LocaleLayout({
224
275
  variant: "outline-strong",
225
276
  },
226
277
  ]
227
- : [{ labelKey: "signIn", path: "/sign-in", variant: "outline-strong" }];
278
+ : [
279
+ { labelKey: "getStarted", path: "/contact", variant: "primary" },
280
+ { labelKey: "signIn", path: "/sign-in", variant: "outline-strong" },
281
+ ];
228
282
 
229
283
  // Only compose the flow seam when there is a flow action to compose. This
230
284
  // layout is a Server Component and `renderFlowTrigger` is a function, so
@@ -237,8 +291,64 @@ export default async function LocaleLayout({
237
291
  // this is the caller half of that contract.
238
292
  const hasFlowAction = navActions.some((action) => action.kind === "flow");
239
293
 
240
- // Shared brand mark injected into both Nav and Footer chrome slots.
294
+ // Shared brand mark injected into both Nav and Footer chrome slots. The
295
+ // mark comes from site-brand.tsx (the light/dark pair the /sign-in card
296
+ // also renders); the Nav adds the shipped wordmark beside it so BrandLogo
297
+ // composes mark + wordmark on desktop and mark-only below `md` (#5469 D5).
241
298
  const brandLogo = siteLogo();
299
+ const brandWordmark = {
300
+ src: "/wordmark.svg",
301
+ alt: SITE_NAME,
302
+ width: 140,
303
+ height: 16,
304
+ };
305
+
306
+ // The locales the footer's language switcher offers — the same list the
307
+ // middleware and hreflang alternates read (NEXT_PUBLIC_ENABLED_LOCALES).
308
+ const enabledLocales = readLocales();
309
+
310
+ // The announcement bar is HOME-ONLY (#5469 D5): its route gate matches the
311
+ // pathname literally, and under next-intl's `as-needed` prefixing the home
312
+ // path is `/` for the default locale and `/<locale>` for every other one.
313
+ const homePath = locale === DEFAULT_LOCALE ? "/" : `/${locale}`;
314
+
315
+ // Deck home (#5612 D3): `siteChrome.home === "deck"` only ever applies
316
+ // `snap="mandatory"` + `deckHero` on the locale-root route — every other
317
+ // path renders byte-identically regardless of the knob. `x-pathname` is the
318
+ // same header `generateMetadata` above reads (forwarded by middleware.ts);
319
+ // Next.js's `headers()` is request-scoped and cheap to call again here.
320
+ const pathname = (await headers()).get("x-pathname") ?? homePath;
321
+ const isDeckHome = siteChrome.home === "deck" && pathname === homePath;
322
+
323
+ // Placeholder social handles + copyright (#5469 D5): `{{COMPANY_HANDLE}}` /
324
+ // `{{COMPANY_NAME}}` are create-ailk markers substituted at scaffold time
325
+ // (packages/create-ailk/src/programmatic.ts), so a customer's site never
326
+ // carries the kit vendor's own identity here. The year is computed, never
327
+ // typed, so the placeholder does not go stale.
328
+ const socialLinks = [
329
+ { platform: "facebook", href: "https://facebook.com/{{COMPANY_HANDLE}}" },
330
+ { platform: "instagram", href: "https://instagram.com/{{COMPANY_HANDLE}}" },
331
+ { platform: "x", href: "https://x.com/{{COMPANY_HANDLE}}" },
332
+ {
333
+ platform: "linkedin",
334
+ href: "https://linkedin.com/company/{{COMPANY_HANDLE}}",
335
+ },
336
+ { platform: "youtube", href: "https://youtube.com/@{{COMPANY_HANDLE}}" },
337
+ ] as const;
338
+ const copyrightText = `© ${new Date().getFullYear()} {{COMPANY_NAME}}`;
339
+
340
+ // Footer legal links (#5612 D3). The centered variant folds in FAQ — the
341
+ // roster's third always-scaffolded core route (`/faq`, alongside `/privacy`
342
+ // and `/terms`) — since a mark-only nav (its usual pairing, #5589) has
343
+ // nowhere else on the page linking it. The default `columns` footer keeps
344
+ // exactly today's two-entry list, byte-identical.
345
+ const legalLinks = [
346
+ { label: "Privacy Policy", href: "/privacy" },
347
+ { label: "Terms of Service", href: "/terms" },
348
+ ...(siteChrome.footer === "centered"
349
+ ? [{ label: navLabels.items.faq, href: "/faq" }]
350
+ : []),
351
+ ];
242
352
 
243
353
  // The site's own declared colours, read from `content/_site.mdx`'s `theme:`
244
354
  // block and translated from that customer-facing vocabulary onto a token
@@ -257,7 +367,11 @@ export default async function LocaleLayout({
257
367
  );
258
368
 
259
369
  return (
260
- <html lang={locale} dir={isRtl(locale) ? "rtl" : "ltr"} data-theme={themeMode}>
370
+ <html
371
+ lang={locale}
372
+ dir={isRtl(locale) ? "rtl" : "ltr"}
373
+ data-theme={themeMode}
374
+ >
261
375
  <head>
262
376
  {/* Flash-prevention: runs synchronously before any styled element */}
263
377
  <script
@@ -271,10 +385,24 @@ export default async function LocaleLayout({
271
385
  />
272
386
  {/* Favicons — single brand mark (theme-independent), PNG fallbacks, ICO legacy */}
273
387
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
274
- <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
275
- <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
388
+ <link
389
+ rel="icon"
390
+ type="image/png"
391
+ sizes="32x32"
392
+ href="/favicon-32x32.png"
393
+ />
394
+ <link
395
+ rel="icon"
396
+ type="image/png"
397
+ sizes="16x16"
398
+ href="/favicon-16x16.png"
399
+ />
276
400
  <link rel="shortcut icon" href="/favicon.ico" />
277
- <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
401
+ <link
402
+ rel="apple-touch-icon"
403
+ sizes="180x180"
404
+ href="/apple-touch-icon.png"
405
+ />
278
406
  <link rel="manifest" href="/site.webmanifest" />
279
407
  <GoogleAnalytics />
280
408
  <VerificationMeta />
@@ -294,13 +422,35 @@ export default async function LocaleLayout({
294
422
  */}
295
423
  <MarketingShell
296
424
  contentOwnsMain
425
+ gutters="plain"
426
+ bannerPlacement="above-nav"
427
+ {...(siteChrome.announcementBanner
428
+ ? {
429
+ banner: (
430
+ <Banner
431
+ variant="contrast"
432
+ placement="above-nav"
433
+ message="Medium length banner heading with link goes here"
434
+ action={{ label: "Link", href: "/pricing", kind: "link" }}
435
+ routes={{ include: [homePath] }}
436
+ dismissible={false}
437
+ locale={locale}
438
+ />
439
+ ),
440
+ }
441
+ : {})}
442
+ {...(isDeckHome
443
+ ? { snap: "mandatory" as const, deckHero: true }
444
+ : {})}
297
445
  nav={
298
446
  <Nav
299
447
  locale={locale}
300
448
  labels={navLabels}
301
- navItems={navItems}
449
+ navItems={siteChrome.nav === "mark-only" ? [] : navItems}
302
450
  siteName={SITE_NAME}
451
+ showThemeToggle={false}
303
452
  logo={brandLogo}
453
+ wordmark={brandWordmark}
304
454
  overflowLabel={navOverflowLabel}
305
455
  actions={navActions}
306
456
  {...(hasFlowAction
@@ -327,8 +477,22 @@ export default async function LocaleLayout({
327
477
  navItems={navItems}
328
478
  siteName={SITE_NAME}
329
479
  logo={brandLogo}
330
- // #4157 — deliberately left as a literal "Built with AI Launch
331
- // Kit" / https://ailaunchkit.ai, not SITE_NAME: this credits
480
+ social={[...socialLinks]}
481
+ {...(siteChrome.footer === "centered"
482
+ ? { layout: "centered" as const }
483
+ : {})}
484
+ showThemeToggle
485
+ copyrightText={copyrightText}
486
+ legalLinks={legalLinks}
487
+ languageSwitcher={
488
+ <LocaleSwitcherNav
489
+ current={locale}
490
+ locales={enabledLocales}
491
+ pathname={pathname}
492
+ />
493
+ }
494
+ // #4157 — deliberately left as a literal "Built by Working
495
+ // Theory Labs" / https://ailaunchkit.ai, not SITE_NAME: this credits
332
496
  // the OSS TEMPLATE this site is built with (a "powered by"
333
497
  // link, unconditional in OSS forks per the comment above),
334
498
  // never the customer's own identity, so it isn't the vendor-
@@ -336,7 +500,7 @@ export default async function LocaleLayout({
336
500
  // docs/specs/4157-scaffold-brand-defaults-spec.md § Design
337
501
  // Decisions for the fuller reasoning.
338
502
  attribution={{
339
- label: "Built with AI Launch Kit",
503
+ label: "Built by Working Theory Labs",
340
504
  href: "https://ailaunchkit.ai",
341
505
  }}
342
506
  />
@@ -344,6 +508,39 @@ export default async function LocaleLayout({
344
508
  >
345
509
  {children}
346
510
  </MarketingShell>
511
+ {/*
512
+ * Cookie consent (#5557). Bottom-pinned `info` Banner in consent
513
+ * mode: `secondaryAction` makes Banner compose BannerConsentIsland,
514
+ * which reads/writes the `ailk-consent` cookie itself — a fresh
515
+ * visitor sees it, an accepted/declined one does not, on every
516
+ * page. Out of the shell's flow (placement `bottom` is a fixed
517
+ * overlay), so accepting causes no layout shift.
518
+ */}
519
+ {consentDecided ? null : (
520
+ <Banner
521
+ variant="info"
522
+ placement="bottom"
523
+ consentCookieName={CONSENT_COOKIE_NAME}
524
+ title={tConsent("title")}
525
+ message={
526
+ <>
527
+ {tConsent("body")}{" "}
528
+ <Link
529
+ href={localeHref(locale, "/privacy")}
530
+ variant="unstyled"
531
+ className="font-semibold text-current underline underline-offset-4"
532
+ >
533
+ {tConsent("policyLinkLabel")}
534
+ </Link>
535
+ </>
536
+ }
537
+ dismissible={false}
538
+ acceptLabel={tConsent("acceptLabel")}
539
+ secondaryAction={{ label: tConsent("rejectLabel") }}
540
+ rejectLabel={tConsent("rejectLabel")}
541
+ locale={locale}
542
+ />
543
+ )}
347
544
  <Telemetry />
348
545
  </NextIntlClientProvider>
349
546
  </body>
@@ -0,0 +1,40 @@
1
+ // Pricing page. Reads the Pricing MDX frontmatter, renders sections via
2
+ // SectionRenderer, and emits a JSON-LD <script>.
3
+ import type {
4
+ getMdxSource as defaultGetMdxSource,
5
+ getPageSections as defaultGetPageSections} from "@working-theory/page-renderer";
6
+ import {
7
+ renderPage,
8
+ renderPageMetadata
9
+ } from "@working-theory/page-renderer";
10
+ import type { Metadata } from "next";
11
+ import type { ReactElement } from "react";
12
+
13
+ interface Props {
14
+ params: Promise<{ locale: string }>;
15
+ getMdxSource?: typeof defaultGetMdxSource;
16
+ getPageSections?: typeof defaultGetPageSections;
17
+ }
18
+
19
+ export async function generateMetadata({
20
+ params,
21
+ }: Pick<Props, "params">): Promise<Metadata> {
22
+ const { locale } = await params;
23
+ return renderPageMetadata({ type: "Pricing", slug: "pricing", locale });
24
+ }
25
+
26
+ export default async function Page({
27
+ params,
28
+ getMdxSource,
29
+ getPageSections,
30
+ }: Props): Promise<ReactElement> {
31
+ const { locale } = await params;
32
+ return renderPage({
33
+ type: "Pricing",
34
+ slug: "pricing",
35
+ locale,
36
+ translationsNamespace: "pricing",
37
+ ...(getMdxSource !== undefined && { getMdxSource }),
38
+ ...(getPageSections !== undefined && { getPageSections }),
39
+ });
40
+ }
@@ -0,0 +1,40 @@
1
+ // Privacy page. Reads the PrivacyPage MDX frontmatter, renders sections via
2
+ // SectionRenderer, and emits a JSON-LD <script>.
3
+ import type {
4
+ getMdxSource as defaultGetMdxSource,
5
+ getPageSections as defaultGetPageSections} from "@working-theory/page-renderer";
6
+ import {
7
+ renderPage,
8
+ renderPageMetadata
9
+ } from "@working-theory/page-renderer";
10
+ import type { Metadata } from "next";
11
+ import type { ReactElement } from "react";
12
+
13
+ interface Props {
14
+ params: Promise<{ locale: string }>;
15
+ getMdxSource?: typeof defaultGetMdxSource;
16
+ getPageSections?: typeof defaultGetPageSections;
17
+ }
18
+
19
+ export async function generateMetadata({
20
+ params,
21
+ }: Pick<Props, "params">): Promise<Metadata> {
22
+ const { locale } = await params;
23
+ return renderPageMetadata({ type: "PrivacyPage", slug: "privacy", locale });
24
+ }
25
+
26
+ export default async function Page({
27
+ params,
28
+ getMdxSource,
29
+ getPageSections,
30
+ }: Props): Promise<ReactElement> {
31
+ const { locale } = await params;
32
+ return renderPage({
33
+ type: "PrivacyPage",
34
+ slug: "privacy",
35
+ locale,
36
+ translationsNamespace: "privacy",
37
+ ...(getMdxSource !== undefined && { getMdxSource }),
38
+ ...(getPageSections !== undefined && { getPageSections }),
39
+ });
40
+ }
@@ -0,0 +1,40 @@
1
+ // Terms page. Reads the TermsPage MDX frontmatter, renders sections via
2
+ // SectionRenderer, and emits a JSON-LD <script>.
3
+ import type {
4
+ getMdxSource as defaultGetMdxSource,
5
+ getPageSections as defaultGetPageSections} from "@working-theory/page-renderer";
6
+ import {
7
+ renderPage,
8
+ renderPageMetadata
9
+ } from "@working-theory/page-renderer";
10
+ import type { Metadata } from "next";
11
+ import type { ReactElement } from "react";
12
+
13
+ interface Props {
14
+ params: Promise<{ locale: string }>;
15
+ getMdxSource?: typeof defaultGetMdxSource;
16
+ getPageSections?: typeof defaultGetPageSections;
17
+ }
18
+
19
+ export async function generateMetadata({
20
+ params,
21
+ }: Pick<Props, "params">): Promise<Metadata> {
22
+ const { locale } = await params;
23
+ return renderPageMetadata({ type: "TermsPage", slug: "terms", locale });
24
+ }
25
+
26
+ export default async function Page({
27
+ params,
28
+ getMdxSource,
29
+ getPageSections,
30
+ }: Props): Promise<ReactElement> {
31
+ const { locale } = await params;
32
+ return renderPage({
33
+ type: "TermsPage",
34
+ slug: "terms",
35
+ locale,
36
+ translationsNamespace: "terms",
37
+ ...(getMdxSource !== undefined && { getMdxSource }),
38
+ ...(getPageSections !== undefined && { getPageSections }),
39
+ });
40
+ }
@@ -12,11 +12,19 @@
12
12
  import Script from "next/script";
13
13
  import type { ReactElement } from "react";
14
14
 
15
+ import { TOKEN_QUERY_PARAMS } from "../lib/telemetry-scrub";
16
+
15
17
  export function GoogleAnalytics(): ReactElement | null {
16
18
  const tagId = process.env.NEXT_PUBLIC_GOOGLE_TAG_ID;
17
19
  if (!tagId) {
18
20
  return null;
19
21
  }
22
+ // The init script below runs in the browser as raw inline JS, outside any
23
+ // module graph, so it can't `import` telemetry-scrub.ts — it gets its own
24
+ // copy of scrubTelemetryUrl, driven by the shared param list, so the
25
+ // report-grant token (?g=<id>, #5562) never reaches GA's page_location
26
+ // default (document.location.href) (#5613).
27
+ const scrubParamsJson = JSON.stringify(TOKEN_QUERY_PARAMS);
20
28
  return (
21
29
  <>
22
30
  <Script
@@ -27,7 +35,16 @@ export function GoogleAnalytics(): ReactElement | null {
27
35
  {`window.dataLayer = window.dataLayer || [];
28
36
  function gtag(){dataLayer.push(arguments);}
29
37
  gtag('js', new Date());
30
- gtag('config', '${tagId}');`}
38
+ function scrubTelemetryUrl(url) {
39
+ try {
40
+ var parsed = new URL(url);
41
+ ${scrubParamsJson}.forEach(function (param) { parsed.searchParams.delete(param); });
42
+ return parsed.toString();
43
+ } catch (e) {
44
+ return url;
45
+ }
46
+ }
47
+ gtag('config', '${tagId}', { page_location: scrubTelemetryUrl(window.location.href) });`}
31
48
  </Script>
32
49
  </>
33
50
  );
@@ -0,0 +1,69 @@
1
+ // Self-contained ESLint flat config for this scaffolded package.
2
+ //
3
+ // It deliberately does NOT depend on @working-theory/eslint-config — that
4
+ // package is internal to the AI Launch Kit monorepo and is not published, so
5
+ // it cannot be a dependency of a scaffolded starter. Everything below comes
6
+ // from public npm packages already pinned as devDependencies (see
7
+ // package.json): @eslint/js, globals and typescript-eslint.
8
+ //
9
+ // Source file name (#5556): this file is checked in as `_eslint.config.mjs`,
10
+ // not `eslint.config.mjs` — build-templates.mjs renames it at snapshot time
11
+ // (renameScaffoldLintConfigs()). A file literally named `eslint.config.mjs`
12
+ // anywhere under this monorepo (even one this repo's own root eslint.config.ts
13
+ // lists in its `ignores`) is auto-discovered by ESLint's flat-config nested
14
+ // resolution as a competing config root, which breaks the monorepo's own
15
+ // lint runs (verified live: typescript-eslint's "multiple candidate
16
+ // TSConfigRootDirs" parser error across unrelated packages/create-ailk
17
+ // files). The underscore prefix keeps it invisible to that discovery while
18
+ // it lives in this checkout; only the snapshot in templates/ — which ships
19
+ // to the customer, not this repo — gets the real filename.
20
+ import js from "@eslint/js";
21
+ import globals from "globals";
22
+ import tseslint from "typescript-eslint";
23
+
24
+ export default tseslint.config(
25
+ {
26
+ ignores: [
27
+ "**/.next/**",
28
+ "**/dist/**",
29
+ "**/generated/**",
30
+ "**/node_modules/**",
31
+ ],
32
+ },
33
+ js.configs.recommended,
34
+ ...tseslint.configs.recommended,
35
+ {
36
+ languageOptions: {
37
+ globals: {
38
+ ...globals.node,
39
+ ...globals.browser,
40
+ ...globals.jest,
41
+ },
42
+ // Explicit, self-contained root — this scaffold config is never meant
43
+ // to combine with any sibling scaffold package's config (each ships
44
+ // standalone), and leaving it unset makes typescript-eslint's
45
+ // candidate-root disambiguation ambiguous the moment more than one
46
+ // scaffold package's config resolves in the same ESLint process (e.g.
47
+ // this monorepo's own `templates/` build, where all four exist
48
+ // side by side; a real customer scaffold never has that problem,
49
+ // since it only ever ships and lints ONE of these four at a time).
50
+ parserOptions: {
51
+ tsconfigRootDir: import.meta.dirname,
52
+ },
53
+ },
54
+ rules: {
55
+ // Downgraded rather than off, so both stay visible without failing a
56
+ // fresh scaffold's lint:
57
+ // - unused-vars: most of the source follows the underscore-prefix
58
+ // convention for intentionally-unused bindings, which this rule's
59
+ // default options don't recognize; a handful of test fixtures also
60
+ // carry genuinely-unused locals this minimal config doesn't try to
61
+ // triage file-by-file.
62
+ // - require-imports: CommonJS require() is the correct form in Jest
63
+ // config/mock files and in a few test-only dynamic re-require
64
+ // patterns.
65
+ "@typescript-eslint/no-unused-vars": "warn",
66
+ "@typescript-eslint/no-require-imports": "warn",
67
+ },
68
+ },
69
+ );
@@ -58,9 +58,15 @@ module.exports = {
58
58
  "<rootDir>/node_modules/@working-theory/schema-validator/dist/index.js",
59
59
  "^@working-theory/sections-registry$":
60
60
  "<rootDir>/node_modules/@working-theory/sections-registry/dist/index.js",
61
-
62
61
  "^@working-theory/templates/shells$":
63
62
  "<rootDir>/node_modules/@working-theory/templates/dist/shells/index.js",
63
+ // Pure-data routes-map subpath (#5572 U2) — mapped directly to source so
64
+ // a consumer of the lightweight `./routes` export (routes.ts,
65
+ // next.config.mjs, this suite) never pulls in the templates package's
66
+ // shells (React/lucide-react) chain, mirroring the same entry in the
67
+ // root and create-ailk jest configs.
68
+ "^@working-theory/templates/routes$":
69
+ "<rootDir>/node_modules/@working-theory/templates/dist/routes-map.js",
64
70
  "^@working-theory/templates$": "<rootDir>/node_modules/@working-theory/templates/dist/index.js",
65
71
  "^@working-theory/ui$": "<rootDir>/node_modules/@working-theory/ui/dist/index.js",
66
72
  "^@working-theory/ui/blocks$": "<rootDir>/node_modules/@working-theory/ui/dist/blocks/index.js",
@@ -69,20 +75,6 @@ module.exports = {
69
75
  "<rootDir>/node_modules/@working-theory/ui/dist/primitives/index.js",
70
76
  "^@working-theory/ui/panels$": "<rootDir>/node_modules/@working-theory/ui/dist/panels/index.js",
71
77
  "^@working-theory/ui/sections$": "<rootDir>/node_modules/@working-theory/ui/dist/sections/index.js",
72
- // #5214: the scheduling slice's renderer-registration subpath. Mapped to
73
- // source like every sibling above — packages/ui/dist isn't built in this
74
- // workspace pre-`pnpm build`. The target strips with the slice on the public
75
- // tree, where nothing imports it, so the unused mapping is inert there.
76
- "^@working-theory/ui/slices/scheduling$":
77
- "<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/scheduling.js",
78
- // #5366: the signal-collectors slice's re-export subpath. Same reasoning
79
- // as the scheduling mapping above.
80
- "^@working-theory/ui/slices/signal-collectors$":
81
- "<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/signal-collectors.js",
82
- // #5364: the commerce slice's renderer-registration + CheckoutPriceCard
83
- // re-export subpath. Same reasoning as the scheduling mapping above.
84
- "^@working-theory/ui/slices/commerce$":
85
- "<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/commerce.js",
86
78
  "^@working-theory/theme$": "<rootDir>/node_modules/@working-theory/theme/dist/index.js",
87
79
  // site-config publishes ESM in dist/; map to source so ts-jest compiles it
88
80
  // through the same transformer as every sibling above. Only the
@@ -92,6 +84,11 @@ module.exports = {
92
84
  "<rootDir>/node_modules/@working-theory/site-config/dist/schema/index.js",
93
85
  "^@working-theory/validation$": "<rootDir>/node_modules/@working-theory/validation/dist/index.js",
94
86
  "^@working-theory/web-i18n$": "<rootDir>/node_modules/@working-theory/web-i18n/dist/index.js",
87
+ // The dependency-free tenancy-constants subpath (database/tenancy.ts —
88
+ // it imports only a type from @working-theory/ids). Mapped to source like
89
+ // every sibling above; `database/dist` is not built in this workspace
90
+ // pre-`pnpm build`.
91
+ "^@working-theory/database/tenancy$": "<rootDir>/node_modules/@working-theory/database/dist/tenancy.js",
95
92
  // #5025: pnpm keys next-intl's virtual-store path on its resolved peers,
96
93
  // so apps/web and packages/page-renderer can link two PHYSICALLY DISTINCT
97
94
  // copies of the same version — and a `jest.mock("next-intl/server")` in