create-ailk 0.4.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 (146) hide show
  1. package/component-catalog.json +11939 -0
  2. package/component-catalog.md +176 -64
  3. package/dist/cli.js +29 -1
  4. package/dist/derive-architecture.js +5 -2
  5. package/dist/lib/extract-module-bundle.d.ts +50 -2
  6. package/dist/lib/extract-module-bundle.js +223 -15
  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.js +4 -0
  11. package/dist/module-architecture.d.ts +59 -8
  12. package/dist/module-architecture.js +334 -115
  13. package/dist/parse-args.js +6 -2
  14. package/dist/programmatic.d.ts +10 -0
  15. package/dist/programmatic.js +134 -19
  16. package/dist/site-chrome.d.ts +80 -0
  17. package/dist/site-chrome.js +135 -0
  18. package/dist/surfaces.d.ts +21 -6
  19. package/dist/surfaces.js +107 -32
  20. package/dist/template-manifest.d.ts +55 -3
  21. package/dist/template-manifest.js +181 -12
  22. package/dist/template-names.d.ts +1 -1
  23. package/dist/template-names.js +1 -0
  24. package/package.json +9 -2
  25. package/site-templates/_base/ailk-template.json +6 -0
  26. package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
  27. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +331 -0
  28. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +393 -0
  29. package/site-templates/marketing-site/ailk-template.json +11 -0
  30. package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
  31. package/site-templates/marketing-site/provenance.json +4 -0
  32. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
  33. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +285 -0
  34. package/site-templates/marketing-site-free/ailk-template.json +2 -1
  35. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +271 -0
  36. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +324 -0
  37. package/site-templates/minimalism-site/ailk-template.json +21 -0
  38. package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
  39. package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
  40. package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
  41. package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
  42. package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
  43. package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
  44. package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
  45. package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
  46. package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
  47. package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
  48. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
  49. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
  50. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
  51. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +214 -0
  52. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
  53. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
  54. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
  55. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
  56. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
  57. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
  58. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
  59. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
  60. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
  61. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
  62. package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
  63. package/site-templates/minimalism-site/provenance.json +4 -0
  64. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
  65. package/site-templates/saas-product/ailk-template.json +16 -0
  66. package/site-templates/saas-product/provenance.json +10 -0
  67. package/templates/.claude/skills/README.md +16 -25
  68. package/templates/.env.example +32 -7
  69. package/templates/.github/workflows/ci.yml +48 -0
  70. package/templates/README.md +3 -1
  71. package/templates/apps/api/CLAUDE.md +4 -1
  72. package/templates/apps/api/eslint.config.mjs +69 -0
  73. package/templates/apps/api/package.json +6 -2
  74. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +6 -2
  75. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
  76. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +8 -1
  77. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  78. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +339 -0
  79. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +108 -0
  80. package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
  81. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
  82. package/templates/apps/api/src/routes/leads/list.ts +104 -0
  83. package/templates/apps/api/src/server.ts +82 -6
  84. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
  85. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
  86. package/templates/apps/api/src/services/default-tenant.ts +261 -0
  87. package/templates/apps/api/src/services/lead-promotion.ts +161 -0
  88. package/templates/apps/api/src/services/leads-inbox.ts +263 -0
  89. package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
  90. package/templates/apps/mcp/CLAUDE.md +1 -1
  91. package/templates/apps/mcp/__tests__/parity.test.ts +2 -2
  92. package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
  93. package/templates/apps/mcp/eslint.config.mjs +69 -0
  94. package/templates/apps/mcp/package.json +6 -2
  95. package/templates/apps/mcp/src/server.ts +2 -2
  96. package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
  97. package/templates/apps/mcp/src/tools/index.ts +2 -2
  98. package/templates/apps/web/.env.example +3 -3
  99. package/templates/apps/web/CLAUDE.md +2 -0
  100. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +4 -6
  101. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +1 -1
  102. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
  103. package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
  104. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
  105. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
  106. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
  107. package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
  108. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
  109. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
  110. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
  111. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
  112. package/templates/apps/web/app/[locale]/layout.tsx +214 -17
  113. package/templates/apps/web/components/google-analytics.tsx +18 -1
  114. package/templates/apps/web/eslint.config.mjs +69 -0
  115. package/templates/apps/web/jest.config.cjs +7 -15
  116. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +32 -5
  117. package/templates/apps/web/lib/nav-config.ts +27 -7
  118. package/templates/apps/web/lib/server-api.ts +1 -1
  119. package/templates/apps/web/lib/site-chrome.ts +29 -0
  120. package/templates/apps/web/lib/telemetry-scrub.ts +46 -0
  121. package/templates/apps/web/messages/en.json +29 -2
  122. package/templates/apps/web/next.config.mjs +20 -0
  123. package/templates/apps/web/package.json +6 -2
  124. package/templates/apps/web/sentry.client.config.ts +131 -0
  125. package/templates/apps/web/sentry.edge.config.ts +72 -0
  126. package/templates/apps/web/sentry.server.config.ts +72 -0
  127. package/templates/apps/web/src/lib/routes.ts +13 -18
  128. package/templates/brand_assets/README.md +24 -0
  129. package/templates/content/en/AboutPage/about.mdx +160 -15
  130. package/templates/content/en/ContactPage/contact.mdx +90 -13
  131. package/templates/content/en/FAQPage/faq.mdx +62 -22
  132. package/templates/content/en/HomePage/home.mdx +244 -20
  133. package/templates/content/en/LandingPage/blog.mdx +83 -0
  134. package/templates/content/en/LandingPage/features.mdx +103 -0
  135. package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
  136. package/templates/database/CHANGELOG.md +7 -0
  137. package/templates/database/__tests__/seed.test.ts +1 -1
  138. package/templates/database/eslint.config.mjs +69 -0
  139. package/templates/database/package.json +7 -3
  140. package/templates/database/seed.ts +1 -1
  141. package/templates/package.json +1 -14
  142. package/versions.json +52 -0
  143. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
  144. /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
  145. /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
  146. /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>
@@ -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
@@ -310,7 +310,7 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
310
310
  reason: "interactive flow engine, API-backed",
311
311
  subtree: true,
312
312
  },
313
- { route: "schedule", reason: "booking surface, API-backed", subtree: true },
313
+ { route: "meet", reason: "booking surface, API-backed", subtree: true },
314
314
  { route: "sign-in", reason: "auth surface, not a content page" },
315
315
  ];
316
316
 
@@ -345,14 +345,36 @@ function exemption(route: string): string | undefined {
345
345
  )?.reason;
346
346
  }
347
347
 
348
- // The page identity a route renders — the two `renderPage({ type, slug })`
349
- // shapes tools/lint-check-content-renders.ts recognises (a literal slug, or a
350
- // slug taken from params). A route that reads its identity another way
351
- // (e.g. a page-type manifest) is not recognised and must be declared.
348
+ // The page identity a route renders — the three shapes
349
+ // tools/lint-check-content-renders.ts recognises: `renderPage({ type, slug })`
350
+ // with a literal slug, the same with a slug taken from params, and a direct
351
+ // `getMdxSource("<Type>", slug, …)` for a route that composes its own body.
352
+ // This file previously mirrored only the first two and so drifted from the
353
+ // tool its comment names (whose own `GET_MDX_SOURCE_RE` has carried the third
354
+ // arm all along); #5562 restored the parity. A route that reads its identity
355
+ // some other way (e.g. a page-type manifest) is still unrecognised and must
356
+ // be declared.
352
357
  const RENDER_PAGE_EXACT_RE =
353
358
  /renderPage\s*\(\s*\{[^}]*type\s*:\s*["']([A-Za-z]+)["'][^}]*slug\s*:\s*["']([^"']+)["']/s;
354
359
  const RENDER_PAGE_DYNAMIC_RE =
355
360
  /renderPage\s*\(\s*\{[^}]*type\s*:\s*["']([A-Za-z]+)["'][^}]*\bslug\s*(?!:\s*["'])/s;
361
+ // A route that composes its own body instead of calling `renderPage` — it
362
+ // resolves content with `getMdxSource(<Type>, slug, locale)` and emits the
363
+ // graph itself via `pageToJsonLd` + `<JsonLdScript>`. That is not a route
364
+ // hiding from this check; it is the SAME declaration one level down, since
365
+ // `renderPage` resolves its own content with exactly this call. `/reports/
366
+ // [slug]` (#5562) is the first: it owns a full-bleed `<main>` and a header
367
+ // section that already renders the title, so `renderPage`'s own `<h1>`/`<p>`
368
+ // would duplicate it.
369
+ //
370
+ // Recognising the shape is deliberately better than exempting the route:
371
+ // an exemption declares "nothing is known to emit JSON-LD here" — false for
372
+ // these routes — and would silence the check for a page that DOES emit a
373
+ // graph. Recognised, the route is rendered and validated like any other.
374
+ // Matched only after both `renderPage` shapes above, so a route that uses
375
+ // `renderPage` is still read through that (richer, slug-bearing) form.
376
+ // Mirrors `GET_MDX_SOURCE_RE` in tools/lint-check-content-renders.ts.
377
+ const GET_MDX_SOURCE_RE = /getMdxSource\s*\(\s*["']([A-Za-z]+)["']/s;
356
378
 
357
379
  function routeIdentity(
358
380
  file: string,
@@ -362,6 +384,11 @@ function routeIdentity(
362
384
  if (exact) return { type: exact[1] as string, slug: exact[2] as string };
363
385
  const dynamic = RENDER_PAGE_DYNAMIC_RE.exec(src);
364
386
  if (dynamic) return { type: dynamic[1] as string, slug: null };
387
+ // Self-composing route (see GET_MDX_SOURCE_RE). Its slug always comes from
388
+ // the route params — a self-composing FIXED route would name its slug as a
389
+ // literal to `getMdxSource`, and none exists — so the dynamic path applies.
390
+ const ownBody = GET_MDX_SOURCE_RE.exec(src);
391
+ if (ownBody) return { type: ownBody[1] as string, slug: null };
365
392
  return null;
366
393
  }
367
394
 
@@ -1,13 +1,33 @@
1
- // nav-config.ts — Nav item registry for apps/web OSS template (#1451)
1
+ // nav-config.ts — Nav item registry for apps/web (#1451, re-shaped by #5469).
2
2
  //
3
- // The OSS template ships a focused navigation inventory — home, docs, and blog.
4
- // Marketing-site items (pricing, services, about) belong in the per-site fork.
5
- // Paths are locale-agnostic; Nav prefixes them with the active locale at render.
3
+ // The Free marketing-site template's nav (the approved chrome, #5469 D5):
4
+ // a Product flyout (How it works · Features · FAQ), then How it works,
5
+ // Pricing and About as top-level items. Paths are locale-agnostic; Nav
6
+ // prefixes them with the active locale at render.
7
+ //
8
+ // In a SCAFFOLD this file is regenerated by create-ailk from the declared
9
+ // route surfaces (`renderNavConfig` in packages/create-ailk/src/surfaces.ts)
10
+ // so the nav never links a page the scaffold does not carry — keep the two in
11
+ // step when the base item set changes.
6
12
 
7
13
  import type { NavItem } from "@working-theory/ui";
14
+ import { BookOpen, HelpCircle, Layers, Sparkles } from "lucide-react";
8
15
 
9
16
  export const navItems: NavItem[] = [
10
- { labelKey: "home", path: "/" },
11
- { labelKey: "docs", path: "/docs" },
12
- { labelKey: "blog", path: "/blog" },
17
+ {
18
+ labelKey: "product",
19
+ flyout: {
20
+ presentation: "list",
21
+ width: "trigger",
22
+ features: [
23
+ { labelKey: "howItWorks", descKey: "howItWorksDesc", icon: Layers, path: "/how-it-works" },
24
+ { labelKey: "features", descKey: "featuresDesc", icon: Sparkles, path: "/features" },
25
+ { labelKey: "faq", descKey: "faqDesc", icon: HelpCircle, path: "/faq" },
26
+ ],
27
+ actions: [{ labelKey: "docs", icon: BookOpen, path: "/how-it-works" }],
28
+ },
29
+ },
30
+ { labelKey: "howItWorks", path: "/how-it-works" },
31
+ { labelKey: "pricing", path: "/pricing" },
32
+ { labelKey: "about", path: "/about" },
13
33
  ];
@@ -17,7 +17,7 @@
17
17
  * hand-rolled `fetch`. This module supplies exactly the two things that
18
18
  * namespace doesn't own: the cookie/origin-forwarding `init` (B0's additive
19
19
  * `init?: { headers }` on `http.get`) and the envelope→`ApiError` unwrap
20
- * (mirrors `apps-paid/workspace/lib/api-error.ts`'s `unwrapEnvelope`, adapted to
20
+ * (mirrors `apps-paid/admin/lib/api-error.ts`'s `unwrapEnvelope`, adapted to
21
21
  * the `@working-theory/api-client` `ApiResult<T>` shape B0 already returns
22
22
  * rather than re-parsing the wire envelope by hand).
23
23
  */
@@ -0,0 +1,29 @@
1
+ // site-chrome.ts — resolved template layout knobs.
2
+ //
3
+ // GENERATED by create-ailk from the declared template's `layout` block
4
+ // (#5612). A scaffold with no template, or a template with no `layout`
5
+ // block, gets these defaults — byte-identical to every scaffold before this
6
+ // file existed. `apps/web/app/[locale]/layout.tsx` reads this to decide:
7
+ // - nav: "mark-only" → Nav renders the brand mark + right-hand controls
8
+ // only (no nav links).
9
+ // - footer: "centered" → Footer renders its existing centered variant.
10
+ // - announcementBanner: false → the home-only announcement Banner is not
11
+ // mounted (the cookie-consent Banner is unaffected — it is not an
12
+ // announcement).
13
+ // - home: "deck" → the locale-root route mounts MarketingShell with
14
+ // snap="mandatory" + deckHero (spec 5426 D7); every other route is
15
+ // unaffected.
16
+
17
+ export interface SiteChrome {
18
+ nav: "full" | "mark-only";
19
+ footer: "columns" | "centered";
20
+ announcementBanner: boolean;
21
+ home: "default" | "deck";
22
+ }
23
+
24
+ export const siteChrome: SiteChrome = {
25
+ nav: "full",
26
+ footer: "columns",
27
+ announcementBanner: true,
28
+ home: "default",
29
+ };