create-ailk 0.6.1 → 0.7.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 (232) hide show
  1. package/README.md +43 -4
  2. package/component-catalog.json +146 -21
  3. package/component-catalog.md +9 -6
  4. package/dist/add-module-core.d.ts +22 -0
  5. package/dist/add-module-core.js +50 -0
  6. package/dist/add-module.d.ts +89 -0
  7. package/dist/add-module.js +334 -0
  8. package/dist/category-profile.d.ts +126 -0
  9. package/dist/category-profile.js +262 -0
  10. package/dist/cli.js +73 -11
  11. package/dist/index.d.ts +4 -2
  12. package/dist/index.js +6 -1
  13. package/dist/lib/extract-module-bundle.d.ts +56 -1
  14. package/dist/lib/extract-module-bundle.js +165 -13
  15. package/dist/lib/license-key-files.d.ts +15 -0
  16. package/dist/lib/license-key-files.js +102 -0
  17. package/dist/lib/pro-callout.d.ts +16 -10
  18. package/dist/lib/pro-callout.js +26 -3
  19. package/dist/license-set.d.ts +34 -0
  20. package/dist/license-set.js +194 -0
  21. package/dist/module-architecture.d.ts +20 -0
  22. package/dist/module-architecture.js +77 -19
  23. package/dist/modules/commerce/0.6.1.tar.gz +0 -0
  24. package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
  25. package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
  26. package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
  27. package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
  28. package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
  29. package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
  30. package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
  31. package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
  32. package/dist/modules/workspace/0.6.1.tar.gz +0 -0
  33. package/dist/parse-args.d.ts +7 -0
  34. package/dist/parse-args.js +23 -1
  35. package/dist/programmatic.d.ts +46 -0
  36. package/dist/programmatic.js +130 -9
  37. package/dist/site-chrome.d.ts +18 -7
  38. package/dist/site-chrome.js +32 -14
  39. package/dist/site-roster.d.ts +127 -0
  40. package/dist/site-roster.js +494 -0
  41. package/dist/surfaces.d.ts +20 -7
  42. package/dist/surfaces.js +149 -28
  43. package/dist/sync-routes.d.ts +9 -0
  44. package/dist/sync-routes.js +2 -2
  45. package/dist/template-manifest.d.ts +58 -2
  46. package/dist/template-manifest.js +196 -69
  47. package/package.json +6 -4
  48. package/site-templates/_base/content/en/BookCall/book-call.mdx +53 -0
  49. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +66 -0
  50. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +68 -0
  51. package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
  52. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +2 -3
  53. package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
  54. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +23 -0
  55. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +23 -0
  56. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +23 -0
  57. package/site-templates/designs/minimalism/DESIGN.md +162 -0
  58. package/site-templates/designs/standard/DESIGN.md +162 -0
  59. package/site-templates/marketing-site/DESIGN.md +55 -41
  60. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +312 -57
  61. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +209 -90
  62. package/site-templates/marketing-site/ailk-template.json +1 -0
  63. package/site-templates/marketing-site-free/DESIGN.md +55 -41
  64. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +260 -66
  65. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +294 -114
  66. package/site-templates/marketing-site-free/ailk-template.json +3 -2
  67. package/site-templates/minimalism-site/DESIGN.md +54 -40
  68. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
  69. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +185 -51
  70. package/site-templates/minimalism-site/ailk-template.json +3 -2
  71. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +53 -0
  72. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
  73. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
  74. package/site-templates/saas-product/DESIGN.md +55 -41
  75. package/site-templates/simple-service/DESIGN.md +55 -41
  76. package/site-templates/simple-service/TEMPLATE-MANIFEST.json +135 -26
  77. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +114 -59
  78. package/site-templates/simple-service/ailk-template.json +4 -3
  79. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +53 -0
  80. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +1 -36
  81. package/site-templates/simple-service/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
  82. package/templates/.claude/settings.json +15 -0
  83. package/templates/.claude/skills/README.md +8 -8
  84. package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
  85. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
  86. package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
  87. package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
  88. package/templates/.claude/skills/add-page/references/page-types.md +111 -0
  89. package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
  90. package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
  91. package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
  92. package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
  93. package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
  94. package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
  95. package/templates/.claude/skills/fill-page/SKILL.md +180 -0
  96. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
  97. package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
  98. package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +8 -8
  99. package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +2 -2
  100. package/templates/.claude/skills/{provision-config → setup-deploy}/references/runbook-template.md +8 -8
  101. package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
  102. package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
  103. package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
  104. package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
  105. package/templates/.github/workflows/ci.yml +5 -0
  106. package/templates/CLAUDE.md +8 -2
  107. package/templates/DESIGN.md +52 -45
  108. package/templates/README.md +108 -46
  109. package/templates/apps/api/.env.example +2 -1
  110. package/templates/apps/api/package.json +1 -1
  111. package/templates/apps/api/src/config/__tests__/index.test.ts +37 -2
  112. package/templates/apps/api/src/config/index.ts +19 -6
  113. package/templates/apps/api/src/lib/license-key-store.ts +45 -0
  114. package/templates/apps/api/src/lib/site.ts +5 -3
  115. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
  116. package/templates/apps/api/src/middleware/auth.ts +9 -0
  117. package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
  118. package/templates/apps/api/src/routes/content/get-page.ts +86 -2
  119. package/templates/apps/api/src/server.ts +6 -0
  120. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
  121. package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
  122. package/templates/apps/mcp/package.json +2 -1
  123. package/templates/apps/mcp/src/index.ts +7 -0
  124. package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
  125. package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
  126. package/templates/apps/web/__tests__/setup.ts +18 -0
  127. package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
  128. package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
  129. package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
  130. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
  131. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
  132. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
  133. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
  134. package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
  135. package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
  136. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
  137. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
  138. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
  139. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +94 -33
  140. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +53 -23
  141. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +96 -0
  142. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +84 -2
  143. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -6
  144. package/templates/apps/web/app/[locale]/contact/page.tsx +25 -7
  145. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
  146. package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
  147. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
  148. package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
  149. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  150. package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
  151. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
  152. package/templates/apps/web/app/[locale]/layout.tsx +113 -51
  153. package/templates/apps/web/app/[locale]/newsletter/page.tsx +50 -0
  154. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
  155. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
  156. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
  157. package/templates/apps/web/app/[locale]/{shipping → purchase-policy}/page.tsx +15 -10
  158. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
  159. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
  160. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
  161. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  162. package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
  163. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  164. package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
  165. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
  166. package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +48 -0
  167. package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
  168. package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
  169. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  170. package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
  171. package/templates/apps/web/app/error-fallback.tsx +17 -45
  172. package/templates/apps/web/app/not-found.tsx +14 -38
  173. package/templates/apps/web/app/og/route.tsx +6 -0
  174. package/templates/apps/web/eslint.config.mjs +3 -0
  175. package/templates/apps/web/instrumentation.ts +14 -0
  176. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
  177. package/templates/apps/web/lib/license-key-store.ts +52 -0
  178. package/templates/apps/web/lib/nav-config.ts +64 -12
  179. package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
  180. package/templates/apps/web/lib/site-brand.tsx +1 -1
  181. package/templates/apps/web/lib/site-chrome.ts +3 -3
  182. package/templates/apps/web/lib/site-identity.ts +134 -0
  183. package/templates/apps/web/lib/site-theme.ts +11 -0
  184. package/templates/apps/web/messages/en.json +52 -37
  185. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  186. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  187. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  188. package/templates/apps/web/public/favicon-16x16.png +0 -0
  189. package/templates/apps/web/public/favicon-32x32.png +0 -0
  190. package/templates/apps/web/public/favicon.ico +0 -0
  191. package/templates/apps/web/public/favicon.svg +3 -3
  192. package/templates/apps/web/public/logomark.svg +6 -3
  193. package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
  194. package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
  195. package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
  196. package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
  197. package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
  198. package/templates/apps/web/public/logos/salessmyth.svg +4 -0
  199. package/templates/apps/web/public/site.webmanifest +4 -4
  200. package/templates/apps/web/public/wordmark-light.svg +4 -0
  201. package/templates/apps/web/public/wordmark.svg +3 -3
  202. package/templates/apps/web/security-headers.config.ts +5 -1
  203. package/templates/content/_site.mdx +23 -0
  204. package/templates/content/en/AboutPage/about.mdx +150 -144
  205. package/templates/content/en/BlogList/blog.mdx +105 -0
  206. package/templates/content/en/ContactPage/contact.mdx +44 -85
  207. package/templates/content/en/FAQPage/faq.mdx +86 -57
  208. package/templates/content/en/HomePage/home.mdx +276 -217
  209. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +182 -0
  210. package/templates/content/en/SoftwareProduct/product.mdx +175 -0
  211. package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
  212. package/templates/database/CHANGELOG.md +7 -0
  213. package/templates/database/package.json +1 -1
  214. package/templates/package.json +3 -1
  215. package/templates/tools/lint-architecture.ts +315 -0
  216. package/templates/tools/tsconfig.json +14 -0
  217. package/versions.json +33 -33
  218. package/templates/.claude/skills/add-schema/SKILL.md +0 -277
  219. package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
  220. package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
  221. package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
  222. package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
  223. package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
  224. package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
  225. package/templates/apps/web/app/[locale]/cancellation/page.tsx +0 -44
  226. package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
  227. package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
  228. package/templates/content/en/LandingPage/blog.mdx +0 -83
  229. package/templates/content/en/LandingPage/features.mdx +0 -103
  230. package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
  231. package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
  232. package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
@@ -1,48 +1,24 @@
1
1
  /* eslint-disable no-restricted-syntax -- i18n-allow: not-found boundary renders outside the [locale] intl provider; translations unavailable at this segment (ADR 0019) */
2
2
  // Next.js App Router not-found boundary. Renders for any route that calls
3
3
  // notFound() or when no route matches. Server Component by default.
4
- import { tokens } from "@working-theory/theme";
5
- import type { ReactElement } from "react";
4
+ //
5
+ // This boundary renders under the pass-through root layout, outside
6
+ // app/[locale]/layout.tsx — the only other importer of globals.css — so it
7
+ // imports the stylesheet itself; without it the primitives' token classes
8
+ // would render unstyled here (#6514).
9
+ import "./globals.css";
6
10
 
7
- const styles = {
8
- main: {
9
- background: tokens.colors.light.bg,
10
- color: tokens.colors.light.fg,
11
- minHeight: "100vh",
12
- display: "flex",
13
- flexDirection: "column" as const,
14
- alignItems: "center",
15
- justifyContent: "center",
16
- padding: tokens.radius.lg,
17
- gap: tokens.radius.md,
18
- },
19
- heading: {
20
- fontSize: "1.5rem",
21
- lineHeight: tokens.lineHeight.md,
22
- margin: 0,
23
- },
24
- body: {
25
- fontSize: "1rem",
26
- lineHeight: tokens.lineHeight.md,
27
- margin: 0,
28
- },
29
- link: {
30
- color: tokens.colors.light.brand,
31
- fontSize: "1rem",
32
- lineHeight: tokens.lineHeight.md,
33
- },
34
- } as const;
11
+ import { Heading, Link, Text } from "@working-theory/ui/primitives";
12
+ import type { ReactElement } from "react";
35
13
 
36
14
  export default function NotFound(): ReactElement {
37
15
  return (
38
- <main style={styles.main}>
39
- <h1 style={styles.heading}>We couldn&apos;t find that page</h1>
40
- <p style={styles.body}>
41
- The page you are looking for does not exist or may have moved.
42
- </p>
43
- <a href="/" style={styles.link}>
44
- Return home
45
- </a>
16
+ <main className="flex min-h-screen flex-col items-center justify-center gap-4 bg-(--color-bg) p-card text-(--color-fg)">
17
+ <Heading level={1} visual={3}>
18
+ We couldn&apos;t find that page
19
+ </Heading>
20
+ <Text>The page you are looking for does not exist or may have moved.</Text>
21
+ <Link href="/">Return home</Link>
46
22
  </main>
47
23
  );
48
24
  }
@@ -15,6 +15,12 @@
15
15
  * - Strip control characters \x00–\x1F and \x7F from title and schemaType
16
16
  * - Cap title at 200 chars, schemaType at 50 chars
17
17
  * - If sanitization yields an empty title, fall back to static PNG redirect
18
+ *
19
+ * Composition exception (#6514): this route renders raw elements with inline
20
+ * `style` objects, and is deliberately left out of the page-surface
21
+ * `no-raw-html-elements` scope in eslint.config.ts. Satori (next/og) rasterises
22
+ * a JSX tree to PNG — it reads only inline styles, supports no stylesheet or
23
+ * Tailwind classes, and cannot render @working-theory/ui components.
18
24
  */
19
25
  import { clientError } from "@working-theory/page-renderer/edge";
20
26
  import { tokens } from "@working-theory/theme";
@@ -28,6 +28,9 @@ export default tseslint.config(
28
28
  "**/dist/**",
29
29
  "**/generated/**",
30
30
  "**/node_modules/**",
31
+ // fumadocs-mdx's generated collection code (#6680) — present only with
32
+ // the docs-blog module, and never hand-edited.
33
+ "**/.source/**",
31
34
  ],
32
35
  },
33
36
  js.configs.recommended,
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Next.js instrumentation hook — runs once per server instance at boot.
3
+ *
4
+ * Registers the durable home of a renewed licence key (#6677 DD-6) before any
5
+ * request resolves the licence. Node runtime only: the store reaches the
6
+ * database through Prisma, which the Edge runtime cannot load.
7
+ */
8
+
9
+ export async function register(): Promise<void> {
10
+ if (process.env.NEXT_RUNTIME === "nodejs") {
11
+ const { registerLicenseKeyStore } = await import("./lib/license-key-store");
12
+ await registerLicenseKeyStore();
13
+ }
14
+ }
@@ -56,6 +56,10 @@ import {
56
56
  import type { ReactElement } from "react";
57
57
  import { parse as parseYaml } from "yaml";
58
58
 
59
+ // Registers the site's `_site.mdx` identity at module load, as the `[locale]`
60
+ // layout does in production (#6597).
61
+ import { readSiteIdentity } from "../site-identity";
62
+
59
63
  // Detail routes call notFound() on a missing page; a thrown error is how the
60
64
  // test sees it (the same shape the per-surface suites use).
61
65
  jest.mock("next/navigation", () => ({
@@ -187,6 +191,38 @@ function extractJsonLdBlocks(container: HTMLElement): JsonLdObject[] {
187
191
  return blocks;
188
192
  }
189
193
 
194
+ const SITE_IDENTITY = readSiteIdentity(CONTENT_ROOT);
195
+ const SITE_ORIGIN = (
196
+ process.env.NEXT_PUBLIC_SITE_URL ??
197
+ process.env.MARKETING_WEB_BASE_URL ??
198
+ "http://localhost:3000"
199
+ ).replace(/\/+$/, "");
200
+
201
+ /**
202
+ * The sitewide identity trio every rendered page carries when the site's own
203
+ * `_site.mdx` reads (#6597): exactly one identity node at its `@id`, exactly
204
+ * one `WebSite` published by it, and the page's primary linked `isPartOf`.
205
+ * `null` when the page carries all three.
206
+ */
207
+ function identityFailure(blocks: JsonLdObject[], label: string): string | null {
208
+ if (SITE_IDENTITY === undefined) return null;
209
+ const identityId = `${SITE_ORIGIN}/#${SITE_IDENTITY.identity.kind === "Person" ? "person" : "organization"}`;
210
+ const webSiteId = `${SITE_ORIGIN}/#website`;
211
+ const identity = blocks.filter((b) => b["@id"] === identityId);
212
+ const webSite = blocks.filter((b) => b["@id"] === webSiteId);
213
+ const problems: string[] = [];
214
+ if (identity.length !== 1 || identity[0]?.["@type"] !== SITE_IDENTITY.identity.kind)
215
+ problems.push(`expected one ${SITE_IDENTITY.identity.kind} at ${identityId}, got ${identity.length}`);
216
+ if (webSite.length !== 1 || webSite[0]?.["@type"] !== "WebSite")
217
+ problems.push(`expected one WebSite at ${webSiteId}, got ${webSite.length}`);
218
+ else if ((webSite[0]["publisher"] as { "@id"?: unknown } | undefined)?.["@id"] !== identityId)
219
+ problems.push(`WebSite publisher is not {"@id": "${identityId}"}`);
220
+ // A page's own `isPartOf` wins over the sitewide link (composeGraph), so the
221
+ // check is that the primary is linked, not what to.
222
+ if (blocks[0]?.["isPartOf"] === undefined) problems.push("the primary carries no isPartOf");
223
+ return problems.length === 0 ? null : `${label}: sitewide identity — ${problems.join("; ")}`;
224
+ }
225
+
190
226
  // ── 1. Content-driven: every content page builds valid JSON-LD ──────────────
191
227
 
192
228
  describe("structured data — every content page builds valid JSON-LD", () => {
@@ -510,6 +546,8 @@ describe("structured data — every route emits a valid JSON-LD script", () => {
510
546
  }
511
547
  const failure = validateFor(blocks, type, pageLabel);
512
548
  if (failure !== null) failures.push(failure);
549
+ const missingIdentity = identityFailure(blocks, pageLabel);
550
+ if (missingIdentity !== null) failures.push(missingIdentity);
513
551
  }
514
552
  expect(failures).toEqual([]);
515
553
  });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Registers the durable home of a renewed licence key (#6677 DD-6).
3
+ *
4
+ * `@working-theory/license` renews the deployment's key before it lapses and
5
+ * keeps the renewed key in a `LicenseKeyStore`. Its default store is a file,
6
+ * which a redeploy wipes — and this app's one real `checkLicense()` caller
7
+ * (`src/lib/commerce-claim.ts`, gating /purchase/* and /.well-known/mcp.json)
8
+ * would then run on the older `AILK_LICENSE_KEY` alone. With a database
9
+ * configured, this registers the store backed by ONE deployment-wide
10
+ * `SiteSetting` row, `(DEFAULT_WORKSPACE_ID, "license.renewed_key")`, shared
11
+ * with apps/api and apps/mcp.
12
+ *
13
+ * NARROW, NAMED EXCEPTION to "apps/web never touches the database directly"
14
+ * (spec 6677 DD-6 and § 7; architecture.yaml `web.description`; the
15
+ * `prisma-for-auth.ts` header): this file, and only this file, hands
16
+ * `prismaForAuth.siteSetting` to `createSiteSettingLicenseKeyStore`, which
17
+ * reads and writes the one `license.renewed_key` row and nothing else. The
18
+ * alternative — an apps/api route serving the key over HTTP — is a hard floor
19
+ * and a wider exposure of a bearer credential.
20
+ *
21
+ * Imported from `instrumentation.ts` `register()` on the Node runtime only.
22
+ * Both imports are dynamic and reached only when `DATABASE_URL` is set.
23
+ */
24
+
25
+ /**
26
+ * Register the SiteSetting-backed store when `DATABASE_URL` is set. An
27
+ * explicitly set `AILK_LICENSE_STORAGE_PATH` is the operator choosing the
28
+ * file store, and is honoured. Never throws: a failure logs and leaves the
29
+ * file store in place.
30
+ */
31
+ export async function registerLicenseKeyStore(): Promise<void> {
32
+ if (!process.env.DATABASE_URL || process.env.AILK_LICENSE_STORAGE_PATH) return;
33
+ try {
34
+ const [{ prismaForAuth: prisma }, { DEFAULT_WORKSPACE_ID }, license] =
35
+ await Promise.all([
36
+ import("./prisma-for-auth"),
37
+ import("@working-theory/database/tenancy"),
38
+ import("@working-theory/license"),
39
+ ]);
40
+ license.setLicenseKeyStore(
41
+ license.createSiteSettingLicenseKeyStore(
42
+ prisma.siteSetting,
43
+ DEFAULT_WORKSPACE_ID,
44
+ ),
45
+ );
46
+ } catch (err) {
47
+ console.error(
48
+ "[web] could not register the SiteSetting licence-key store — the file store stays in place:",
49
+ err instanceof Error ? err.name : "non-Error thrown",
50
+ );
51
+ }
52
+ }
@@ -1,33 +1,85 @@
1
- // nav-config.ts — Nav item registry for apps/web (#1451, re-shaped by #5469).
1
+ // nav-config.ts — Nav item registry for apps/web (#1451, re-shaped by #5469,
2
+ // #6508).
2
3
  //
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.
4
+ // The Free marketing-site template's nav (the approved chrome, #5469 D5,
5
+ // #6508 D5): a Product flyout (How it works · Features · FAQ) rendered as a
6
+ // fixed-640px two-column panel (the Standard Issue direction — #6508), then
7
+ // How it works, Pricing and About as top-level items. Paths are
8
+ // locale-agnostic; Nav prefixes them with the active locale at render.
9
+ // `footerColumns` carries the footer's column groups (#6468), keys only — the
10
+ // layout resolves the labels.
7
11
  //
8
12
  // In a SCAFFOLD this file is regenerated by create-ailk from the declared
9
13
  // route surfaces (`renderNavConfig` in packages/create-ailk/src/surfaces.ts)
10
14
  // so the nav never links a page the scaffold does not carry — keep the two in
11
15
  // step when the base item set changes.
16
+ //
17
+ // SOFT_LOCK_OK: #6508 — Product's flyout is `width: "panel"` (D4/D5): a fixed
18
+ // 640px, two-column grid, replacing the prior "list"/"trigger" one-column
19
+ // menu. `presentation` is omitted — it defaults to "grid" (D5). The `docs`
20
+ // action is dropped (it duplicated the first feature's /how-it-works
21
+ // destination, § 1.4). This note lives here, not inside the `navItems` array
22
+ // below, so this file's `navItems`/`footerColumns` blocks stay byte-identical
23
+ // to `renderNavConfig`'s generated output — the mirror test (#6468) diffs the
24
+ // arrays verbatim.
12
25
 
13
26
  import type { NavItem } from "@working-theory/ui";
14
- import { BookOpen, HelpCircle, Layers, Sparkles } from "lucide-react";
27
+ import { HelpCircle, Layers, Sparkles } from "lucide-react";
15
28
 
16
29
  export const navItems: NavItem[] = [
17
30
  {
18
31
  labelKey: "product",
19
32
  flyout: {
20
- presentation: "list",
21
- width: "trigger",
33
+ width: "panel",
22
34
  features: [
23
- { labelKey: "howItWorks", descKey: "howItWorksDesc", icon: Layers, path: "/how-it-works" },
24
- { labelKey: "features", descKey: "featuresDesc", icon: Sparkles, path: "/features" },
35
+ { labelKey: "useCases", descKey: "useCasesDesc", icon: Layers, path: "/use-cases" },
36
+ { labelKey: "features", descKey: "featuresDesc", icon: Sparkles, path: "/software/product" },
25
37
  { labelKey: "faq", descKey: "faqDesc", icon: HelpCircle, path: "/faq" },
26
38
  ],
27
- actions: [{ labelKey: "docs", icon: BookOpen, path: "/how-it-works" }],
39
+ actions: [],
28
40
  },
29
41
  },
30
- { labelKey: "howItWorks", path: "/how-it-works" },
42
+ { labelKey: "useCases", path: "/use-cases" },
43
+ { labelKey: "blog", path: "/blog" },
31
44
  { labelKey: "pricing", path: "/pricing" },
32
45
  { labelKey: "about", path: "/about" },
33
46
  ];
47
+
48
+ export interface FooterColumnConfig {
49
+ headingKey: string;
50
+ items: { labelKey: string; path: string }[];
51
+ }
52
+
53
+ export const footerColumns: FooterColumnConfig[] = [
54
+ {
55
+ headingKey: "footerSitePagesHeading",
56
+ items: [
57
+ { labelKey: "features", path: "/software/product" },
58
+ { labelKey: "pricing", path: "/pricing" },
59
+ { labelKey: "useCases", path: "/use-cases" },
60
+ { labelKey: "about", path: "/about" },
61
+ { labelKey: "contact", path: "/contact" },
62
+ { labelKey: "bookACall", path: "/book-a-call" },
63
+ ],
64
+ },
65
+ {
66
+ headingKey: "footerFeaturesHeading",
67
+ items: [
68
+ { labelKey: "feature1", path: "/software/product" },
69
+ { labelKey: "feature2", path: "/software/product" },
70
+ { labelKey: "feature3", path: "/software/product" },
71
+ { labelKey: "feature4", path: "/software/product" },
72
+ { labelKey: "feature5", path: "/software/product" },
73
+ { labelKey: "allFeatures", path: "/software/product" },
74
+ ],
75
+ },
76
+ {
77
+ headingKey: "footerResourcesHeading",
78
+ items: [
79
+ { labelKey: "blog", path: "/blog" },
80
+ { labelKey: "glossary", path: "/glossary" },
81
+ { labelKey: "newsletter", path: "/newsletter" },
82
+ { labelKey: "faq", path: "/faq" },
83
+ ],
84
+ },
85
+ ];
@@ -5,16 +5,19 @@
5
5
  * Better Auth migration (#587). Driver adapter wrapping landed in Phase B
6
6
  * of the Prisma 7 migration (#661) ahead of the v7 bump in Phase C.
7
7
  *
8
- * Only `apps/web/auth.ts` should import this — business-logic callers go
9
- * through apps/api, never the database directly (architecture.yaml
10
- * `apps/web` may_not_import_from `database`; the single-file singleton
11
- * here is the documented exception, matching the pre-migration carve-out
12
- * for NextAuth's adapter).
8
+ * Two files import this: `apps/web/auth.ts`, for Better Auth, and
9
+ * `lib/license-key-store.ts`, which hands `prismaForAuth.siteSetting` to the
10
+ * licence client's SiteSetting store for the ONE deployment-wide
11
+ * `license.renewed_key` row (#6677 DD-6 — a narrow, named exception).
12
+ * Every other business-logic caller goes through apps/api, never the
13
+ * database directly (architecture.yaml `apps/web` may_not_import_from
14
+ * `database`; the single-file singleton here is the documented exception,
15
+ * matching the pre-migration carve-out for NextAuth's adapter).
13
16
  */
14
17
 
15
18
 
16
- import { PrismaClient } from "@working-theory/database";
17
19
  import { PrismaPg } from "@prisma/adapter-pg";
20
+ import { PrismaClient } from "@working-theory/database";
18
21
 
19
22
  declare global {
20
23
  var __ailk_prisma: PrismaClient | undefined;
@@ -109,7 +109,7 @@ export const SITE_BASE_URL =
109
109
 
110
110
  /**
111
111
  * Shared brand mark — the square logomark (the central brand library's
112
- * `logomark/loop/icon.svg`). The Nav pairs it with `/wordmark.svg`.
112
+ * `logomark/loop/tile-black.svg`). The Nav pairs it with `/wordmark.svg`.
113
113
  * RSC-safe (no hooks, no client state).
114
114
  */
115
115
  export function siteLogo(): ReactElement {
@@ -2,8 +2,8 @@
2
2
  //
3
3
  // GENERATED by create-ailk from the declared template's `layout` block
4
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:
5
+ // block, gets the defaults below (the announcement banner is off unless a
6
+ // template turns it on, #6469). `apps/web/app/[locale]/layout.tsx` reads this to decide:
7
7
  // - nav: "mark-only" → Nav renders the brand mark + right-hand controls
8
8
  // only (no nav links).
9
9
  // - footer: "centered" → Footer renders its existing centered variant.
@@ -24,6 +24,6 @@ export interface SiteChrome {
24
24
  export const siteChrome: SiteChrome = {
25
25
  nav: "full",
26
26
  footer: "columns",
27
- announcementBanner: true,
27
+ announcementBanner: false,
28
28
  home: "default",
29
29
  };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Site identity — reads `siteIdentity` out of `content/_site.mdx` and
3
+ * registers it with `@working-theory/page-renderer`, so every page's JSON-LD
4
+ * carries the site's entity graph: the identity node, its optional secondary
5
+ * (the product), and the `WebSite` (#6597).
6
+ *
7
+ * A mirror of `lib/site-theme.ts`: a local frontmatter parse (the
8
+ * `@working-theory/site-config/server` subpath is forbidden in `apps/web`,
9
+ * #1567), validated against the canonical `SiteIdentitySchema` from the
10
+ * package's client-safe `/schema` entry. `page-renderer` may not import
11
+ * site-config (`architecture.yaml`), so the identity is read here, once, and
12
+ * handed over through `setSiteIdentity`.
13
+ *
14
+ * Registration happens at module load. `app/[locale]/layout.tsx` imports this
15
+ * module, and Next.js evaluates a layout module before any page under it
16
+ * renders, so every `renderPage` / `pageToJsonLd` call sees the identity
17
+ * without a per-route prop.
18
+ */
19
+
20
+ import { readdirSync, readFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+
23
+ import {
24
+ setSiteIdentity,
25
+ type SiteIdentityRegistration,
26
+ } from "@working-theory/page-renderer";
27
+ import { featureListFromSections } from "@working-theory/schema";
28
+ import { SiteIdentitySchema } from "@working-theory/site-config/schema";
29
+ import { parse as parseYaml } from "yaml";
30
+
31
+ import { resolveContentRoot } from "./site-brand";
32
+
33
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
34
+
35
+ /** The parsed frontmatter of one file, or `undefined` when it cannot be read. */
36
+ function readFrontmatter(path: string): Record<string, unknown> | undefined {
37
+ try {
38
+ const match = FRONTMATTER_RE.exec(readFileSync(path, "utf8"));
39
+ if (!match) return undefined;
40
+ const parsed: unknown = parseYaml(match[1] ?? "");
41
+ return parsed && typeof parsed === "object"
42
+ ? (parsed as Record<string, unknown>)
43
+ : undefined;
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * The product's `featureList`: the feature titles of the product's own page —
51
+ * the first `SoftwareProduct` seed in the default locale, in sorted order
52
+ * (#6658 D3) — projected by `featureListFromSections`. A site with no product
53
+ * page falls back to a page whose slug is `features` (any type directory,
54
+ * first match in sorted order). Empty when there is neither.
55
+ */
56
+ function readFeatureList(contentRoot: string, locale: string): string[] {
57
+ // A `feature` section in the `steps` variant is how the product is set up,
58
+ // not what it does (the product page carries both, #6658 D3), so its step
59
+ // titles never enter the featureList.
60
+ const featuresOf = (frontmatter: Record<string, unknown>): string[] =>
61
+ featureListFromSections(
62
+ (Array.isArray(frontmatter.sections)
63
+ ? (frontmatter.sections as Array<{ type: string; data?: unknown }>)
64
+ : []
65
+ ).filter(
66
+ (section) =>
67
+ !(
68
+ section.type === "feature" &&
69
+ (section.data as { variant?: unknown } | undefined)?.variant === "steps"
70
+ ),
71
+ ),
72
+ );
73
+ // #6658 D3: the product's own detail page (the first `SoftwareProduct`
74
+ // seed in sorted order) is where the feature sections live now.
75
+ const productDir = join(contentRoot, locale, "SoftwareProduct");
76
+ let productSeeds: string[] = [];
77
+ try {
78
+ productSeeds = readdirSync(productDir)
79
+ .filter((name) => name.endsWith(".mdx"))
80
+ .sort();
81
+ } catch {
82
+ // No SoftwareProduct directory — fall through to the legacy probe.
83
+ }
84
+ for (const name of productSeeds) {
85
+ const frontmatter = readFrontmatter(join(productDir, name));
86
+ if (frontmatter !== undefined) return featuresOf(frontmatter);
87
+ }
88
+ let typeDirs: string[];
89
+ try {
90
+ typeDirs = readdirSync(join(contentRoot, locale)).sort();
91
+ } catch {
92
+ return [];
93
+ }
94
+ for (const typeDir of typeDirs) {
95
+ const frontmatter = readFrontmatter(
96
+ join(contentRoot, locale, typeDir, "features.mdx"),
97
+ );
98
+ if (frontmatter !== undefined) return featuresOf(frontmatter);
99
+ }
100
+ return [];
101
+ }
102
+
103
+ /**
104
+ * The site's identity registration, or `undefined` when `_site.mdx` is absent,
105
+ * unreadable, or declares no `siteIdentity`. An invalid `siteIdentity` throws,
106
+ * deliberately: a customer whose identity silently vanished from every page
107
+ * would have been lied to (the stance `site-theme.ts` takes on colours).
108
+ */
109
+ export function readSiteIdentity(
110
+ contentRoot: string = resolveContentRoot(),
111
+ ): SiteIdentityRegistration | undefined {
112
+ const frontmatter = readFrontmatter(join(contentRoot, "_site.mdx"));
113
+ if (frontmatter?.siteIdentity === undefined) return undefined;
114
+
115
+ const identity = SiteIdentitySchema.parse(frontmatter.siteIdentity);
116
+ const { siteName, baseUrl, defaultLocale } = frontmatter;
117
+ const registration: SiteIdentityRegistration = {
118
+ siteName: typeof siteName === "string" ? siteName : identity.name,
119
+ baseUrl: typeof baseUrl === "string" ? baseUrl : identity.url,
120
+ identity,
121
+ };
122
+ if (
123
+ identity.kind === "Organization" &&
124
+ identity.secondary?.kind === "SoftwareApplication"
125
+ ) {
126
+ registration.featureList = readFeatureList(
127
+ contentRoot,
128
+ typeof defaultLocale === "string" ? defaultLocale : "en",
129
+ );
130
+ }
131
+ return registration;
132
+ }
133
+
134
+ setSiteIdentity(readSiteIdentity());
@@ -25,6 +25,7 @@ import { join } from "node:path";
25
25
 
26
26
  import { SiteThemeSchema } from "@working-theory/site-config/schema";
27
27
  import {
28
+ siteThemeShapeCss,
28
29
  siteThemeToBrandTokens,
29
30
  type BrandTokenOverrides,
30
31
  } from "@working-theory/theme";
@@ -72,3 +73,13 @@ export function loadSiteBrandTokens(): BrandTokenOverrides | undefined {
72
73
  const theme = SiteThemeSchema.parse(block);
73
74
  return siteThemeToBrandTokens(theme, SOURCE);
74
75
  }
76
+
77
+ /**
78
+ * The site's shape settings (`theme.corners`, `theme.eyebrowCase`) as a CSS
79
+ * override block for `:root`. "" when the site sets neither.
80
+ */
81
+ export function loadSiteShapeCss(): string {
82
+ const block = readThemeBlock();
83
+ if (block === undefined) return "";
84
+ return siteThemeShapeCss(SiteThemeSchema.parse(block));
85
+ }