create-ailk 0.27.0 → 0.28.1

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 (219) hide show
  1. package/README.md +84 -7
  2. package/dist/add-module-core.js +2 -1
  3. package/dist/add-module.d.ts +2 -1
  4. package/dist/add-module.js +9 -4
  5. package/dist/add-route.js +46 -19
  6. package/dist/body-heading.d.ts +56 -0
  7. package/dist/body-heading.js +394 -0
  8. package/dist/cli.js +28 -1
  9. package/dist/content-payloads.js +2 -2
  10. package/dist/derive-architecture.js +8 -2
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.js +4 -1
  13. package/dist/lib/auth-secret-file.d.ts +6 -2
  14. package/dist/lib/auth-secret-file.js +46 -20
  15. package/dist/lib/chrome-readers.d.ts +21 -5
  16. package/dist/lib/chrome-readers.js +128 -20
  17. package/dist/lib/declared-env.d.ts +6 -0
  18. package/dist/lib/declared-env.js +2 -0
  19. package/dist/lib/env-catalog.js +20 -0
  20. package/dist/lib/import-graph.d.ts +16 -0
  21. package/dist/lib/import-graph.js +53 -0
  22. package/dist/lib/site-modules.d.ts +8 -0
  23. package/dist/lib/site-modules.js +34 -0
  24. package/dist/module-architecture.js +23 -14
  25. package/dist/paid-sections.d.ts +17 -0
  26. package/dist/paid-sections.js +39 -0
  27. package/dist/programmatic.d.ts +16 -0
  28. package/dist/programmatic.js +73 -4
  29. package/dist/remove-module.d.ts +98 -0
  30. package/dist/remove-module.js +620 -0
  31. package/dist/seed-flows.d.ts +2 -2
  32. package/dist/seed-flows.js +16 -9
  33. package/dist/site-roster.js +4 -1
  34. package/dist/starter-grid.d.ts +26 -0
  35. package/dist/starter-grid.js +34 -0
  36. package/dist/surfaces.d.ts +26 -0
  37. package/dist/surfaces.js +105 -9
  38. package/dist/sync-routes.d.ts +7 -0
  39. package/dist/sync-routes.js +53 -6
  40. package/dist/template-pages.d.ts +36 -0
  41. package/dist/template-pages.js +129 -0
  42. package/dist/template-record.d.ts +13 -4
  43. package/dist/template-record.js +8 -4
  44. package/dist/topology.d.ts +2 -1
  45. package/dist/topology.js +4 -2
  46. package/dist/update.d.ts +34 -2
  47. package/dist/update.js +395 -48
  48. package/package.json +3 -3
  49. package/release-scope.json +2 -1
  50. package/site-templates/aliases/hub.TEMPLATE-MANIFEST.json +115 -0
  51. package/templates/.env.example +5 -2
  52. package/templates/apps/api/.env.example +9 -5
  53. package/templates/apps/api/CLAUDE.md +14 -9
  54. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +37 -0
  55. package/templates/apps/api/src/vercel-handler.ts +8 -1
  56. package/templates/apps/mcp/CLAUDE.md +18 -10
  57. package/templates/apps/web/.env.example +25 -11
  58. package/templates/apps/web/CLAUDE.md +45 -4
  59. package/templates/apps/web/__tests__/setup.ts +7 -0
  60. package/templates/apps/web/app/.well-known/mcp.json/route.ts +7 -7
  61. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +61 -2
  62. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +81 -0
  63. package/templates/apps/web/app/[locale]/(authed)/account/connect/__tests__/page.test.tsx +197 -0
  64. package/templates/apps/web/app/[locale]/(authed)/account/connect/page.tsx +152 -0
  65. package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +2 -2
  66. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +76 -21
  67. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +46 -16
  68. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +20 -3
  69. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +180 -2
  70. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +6 -2
  71. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +122 -0
  72. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +1 -1
  73. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +20 -3
  74. package/templates/apps/web/app/[locale]/case-studies/__tests__/hub-children.test.tsx +65 -0
  75. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +6 -2
  76. package/templates/apps/web/app/[locale]/case-studies/page.tsx +23 -11
  77. package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +11 -1
  78. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
  79. package/templates/apps/web/app/[locale]/consent.actions.ts +44 -6
  80. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +4 -4
  81. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +269 -42
  82. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +88 -30
  83. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +20 -3
  84. package/templates/apps/web/app/[locale]/courses/__tests__/hub-children.test.tsx +32 -0
  85. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +6 -2
  86. package/templates/apps/web/app/[locale]/courses/page.tsx +22 -6
  87. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +15 -2
  88. package/templates/apps/web/app/[locale]/events/__tests__/hub-children.test.tsx +32 -0
  89. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +8 -2
  90. package/templates/apps/web/app/[locale]/events/page.tsx +23 -7
  91. package/templates/apps/web/app/[locale]/features/__tests__/hub-children.test.tsx +32 -0
  92. package/templates/apps/web/app/[locale]/features/page.tsx +23 -25
  93. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +95 -0
  94. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/save.actions.test.ts +193 -0
  95. package/templates/apps/web/app/[locale]/flows/[slug]/email-moment.ts +92 -0
  96. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +10 -0
  97. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +513 -0
  98. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +122 -0
  99. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.no-commerce.test.tsx +195 -0
  100. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/landing.test.tsx +293 -0
  101. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +397 -0
  102. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/checkout-seam.ts +74 -0
  103. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +264 -0
  104. package/templates/apps/web/app/[locale]/flows/[slug]/save.actions.ts +75 -0
  105. package/templates/apps/web/app/[locale]/for/__tests__/hub-children.test.tsx +32 -0
  106. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +5 -1
  107. package/templates/apps/web/app/[locale]/for/page.tsx +23 -25
  108. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +20 -3
  109. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  110. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +25 -4
  111. package/templates/apps/web/app/[locale]/integrations/__tests__/hub-children.test.tsx +38 -0
  112. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +8 -2
  113. package/templates/apps/web/app/[locale]/integrations/page.tsx +23 -11
  114. package/templates/apps/web/app/[locale]/layout.tsx +157 -139
  115. package/templates/apps/web/app/[locale]/page.tsx +51 -3
  116. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +16 -3
  117. package/templates/apps/web/app/[locale]/partners/__tests__/hub-children.test.tsx +32 -0
  118. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +6 -2
  119. package/templates/apps/web/app/[locale]/partners/page.tsx +22 -6
  120. package/templates/apps/web/app/[locale]/portfolio/__tests__/hub-children.test.tsx +38 -0
  121. package/templates/apps/web/app/[locale]/portfolio/page.tsx +25 -24
  122. package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +321 -0
  123. package/templates/apps/web/app/[locale]/pricing/[slug]/page.tsx +87 -0
  124. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
  125. package/templates/apps/web/app/[locale]/pricing/__tests__/pricing-pages.test.tsx +215 -0
  126. package/templates/apps/web/app/[locale]/pricing/page.tsx +56 -341
  127. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +21 -4
  128. package/templates/apps/web/app/[locale]/products/__tests__/hub-children.test.tsx +32 -0
  129. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +8 -2
  130. package/templates/apps/web/app/[locale]/products/page.tsx +22 -6
  131. package/templates/apps/web/app/[locale]/projects/__tests__/hub-children.test.tsx +32 -0
  132. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -0
  133. package/templates/apps/web/app/[locale]/projects/page.tsx +18 -11
  134. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +25 -4
  135. package/templates/apps/web/app/[locale]/resources/__tests__/hub-children.test.tsx +38 -0
  136. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +6 -2
  137. package/templates/apps/web/app/[locale]/resources/page.tsx +20 -10
  138. package/templates/apps/web/app/[locale]/services/[slug]/[child]/page.tsx +68 -0
  139. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +64 -9
  140. package/templates/apps/web/app/[locale]/services/__tests__/hub-children.test.tsx +158 -0
  141. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +12 -2
  142. package/templates/apps/web/app/[locale]/services/__tests__/service-detail-sections.test.tsx +224 -0
  143. package/templates/apps/web/app/[locale]/services/__tests__/service-families.test.tsx +368 -0
  144. package/templates/apps/web/app/[locale]/services/page.tsx +45 -19
  145. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  146. package/templates/apps/web/app/[locale]/team/__tests__/hub-children.test.tsx +32 -0
  147. package/templates/apps/web/app/[locale]/team/page.tsx +25 -24
  148. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +24 -3
  149. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  150. package/templates/apps/web/app/[locale]/templates/[slug]/__tests__/page.test.tsx +203 -0
  151. package/templates/apps/web/app/[locale]/templates/[slug]/page.tsx +81 -0
  152. package/templates/apps/web/app/[locale]/templates/__tests__/page.test.tsx +237 -0
  153. package/templates/apps/web/app/[locale]/templates/page.tsx +108 -0
  154. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
  155. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +24 -3
  156. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  157. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +204 -2
  158. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +151 -33
  159. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +233 -7
  160. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +92 -41
  161. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +77 -39
  162. package/templates/apps/web/app/llms.txt/route.ts +41 -3
  163. package/templates/apps/web/app/robots.txt/route.ts +5 -1
  164. package/templates/apps/web/app/sitemap.xml/route.ts +45 -3
  165. package/templates/apps/web/auth.ts +102 -50
  166. package/templates/apps/web/components/signed-in-nav.tsx +8 -1
  167. package/templates/apps/web/lib/__tests__/bot-check.test.ts +254 -0
  168. package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +212 -13
  169. package/templates/apps/web/lib/__tests__/deck-page.test.ts +13 -1
  170. package/templates/apps/web/lib/__tests__/docs-index.test.ts +93 -0
  171. package/templates/apps/web/lib/__tests__/docs-path.test.ts +168 -0
  172. package/templates/apps/web/lib/__tests__/hub-children.fixtures.ts +279 -0
  173. package/templates/apps/web/lib/__tests__/nav-label-keys.test.ts +138 -0
  174. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +178 -2
  175. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +183 -0
  176. package/templates/apps/web/lib/__tests__/page-chrome.test.ts +412 -0
  177. package/templates/apps/web/lib/__tests__/pricing-page.test.ts +142 -0
  178. package/templates/apps/web/lib/__tests__/relay.test.ts +202 -10
  179. package/templates/apps/web/lib/__tests__/route-family.test.ts +57 -0
  180. package/templates/apps/web/lib/__tests__/service-listing.test.ts +111 -0
  181. package/templates/apps/web/lib/__tests__/services-path.test.ts +96 -0
  182. package/templates/apps/web/lib/__tests__/site-header.test.ts +151 -0
  183. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +148 -6
  184. package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +84 -0
  185. package/templates/apps/web/lib/__tests__/template-listing.test.ts +158 -0
  186. package/templates/apps/web/lib/auth-secret.ts +57 -0
  187. package/templates/apps/web/lib/bot-check.ts +138 -0
  188. package/templates/apps/web/lib/consent-visitor.ts +81 -7
  189. package/templates/apps/web/lib/deck-page.ts +6 -3
  190. package/templates/apps/web/lib/docs-index.ts +100 -0
  191. package/templates/apps/web/lib/docs-path.ts +94 -0
  192. package/templates/apps/web/lib/hub-children.tsx +318 -0
  193. package/templates/apps/web/lib/modules.generated.ts +9 -0
  194. package/templates/apps/web/lib/nav-label-check.ts +61 -0
  195. package/templates/apps/web/lib/nav-label-keys.ts +78 -0
  196. package/templates/apps/web/lib/newsletter.actions.ts +31 -6
  197. package/templates/apps/web/lib/offerings-pricing.ts +135 -6
  198. package/templates/apps/web/lib/page-chrome.ts +367 -0
  199. package/templates/apps/web/lib/posting-title.ts +26 -4
  200. package/templates/apps/web/lib/pricing-page.ts +430 -0
  201. package/templates/apps/web/lib/relay.ts +31 -106
  202. package/templates/apps/web/lib/route-family.ts +69 -0
  203. package/templates/apps/web/lib/service-listing.ts +89 -0
  204. package/templates/apps/web/lib/services-path.ts +97 -0
  205. package/templates/apps/web/lib/site-header.ts +94 -0
  206. package/templates/apps/web/lib/telemetry-scrub.ts +68 -3
  207. package/templates/apps/web/lib/template-listing.ts +169 -0
  208. package/templates/apps/web/messages/en.json +86 -30
  209. package/templates/apps/web/middleware.ts +171 -11
  210. package/templates/apps/web/next.config.mjs +211 -2
  211. package/templates/apps/web/package.json +1 -1
  212. package/templates/apps/web/scripts/assert-content-bundled.ts +28 -6
  213. package/templates/apps/web/scripts/check-nav-labels.ts +39 -0
  214. package/templates/apps/web/sentry.server.config.ts +30 -2
  215. package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -9
  216. package/templates/apps/web/src/lib/rate-limit.ts +68 -27
  217. package/templates/apps/web/src/lib/routes.ts +15 -1
  218. package/templates/tools/declared-env.ts +7 -0
  219. package/versions.json +48 -48
package/README.md CHANGED
@@ -60,10 +60,20 @@ pnpm create ailk@latest my-site --shape organization-service --license <key>
60
60
  `--design minimalism` picks the other look and needs a Pro or Pro Plus key;
61
61
  `standard` (the default) needs none of its own (the run itself still needs a
62
62
  key while Free is held, above). `--template <alias>` scaffolds one of the
63
- three named aliases (`marketing-site-free`, `marketing-site`, `saas-product`) —
64
- each fixes a shape, a design and its own module list — and cannot be combined
65
- with `--shape` or `--design`; `marketing-site-free` with no key is refused the
66
- same way while Free is held.
63
+ four named aliases (`marketing-site-free`, `marketing-site`, `saas-product`,
64
+ `hub`) — each fixes a shape, a design and its own module list — and cannot be
65
+ combined with `--shape` or `--design`; `marketing-site-free` with no key is
66
+ refused the same way while Free is held.
67
+
68
+ `hub` is the starter for a hub (#10040): the `organization-service` shape with
69
+ every public row dropped and no module in its own list, so the scaffold is the
70
+ sign-in pages, the account area, `/privacy`, `/terms` and `/working-agreement`,
71
+ the API and the MCP server — the same tree `remove-module marketing` leaves —
72
+ and the Admin and the rest ride the key's `--modules`:
73
+
74
+ ```bash
75
+ pnpm create ailk@latest my-hub --template hub --modules multi-tenant --license <key>
76
+ ```
67
77
 
68
78
  ### Add a purchased module to an existing site
69
79
 
@@ -110,6 +120,58 @@ result of the roster check (`Page roster check: clean`). Running it again
110
120
  changes nothing and reports nothing to add.
111
121
  Run `pnpm install` afterwards.
112
122
 
123
+ ### Remove a module from an existing site
124
+
125
+ `remove-module` is the inverse of `add-module` for the modules whose
126
+ pages are public: `marketing` and `docs-blog` (#10040). A hub — the site
127
+ every spoke of an organization signs in through — needs sign-in, the account
128
+ area, the API and the Admin, and no second copy of the public site:
129
+
130
+ ```bash
131
+ # in an already-scaffolded repo (default target: cwd)
132
+ create-ailk remove-module marketing
133
+ # or point at the repo explicitly, and delete the site's own edits too
134
+ create-ailk remove-module marketing ./my-hub --force
135
+ ```
136
+
137
+ It deletes the module's route directories under `apps/web/app/[locale]/` —
138
+ everything but the `(auth)` and `(authed)` groups and the `/privacy`,
139
+ `/terms` and `/working-agreement` pages the sign-up form links — and the
140
+ seed content under `content/<locale>/` except those three pages' seeds; for
141
+ `docs-blog`, the module's own files and sample content. Then it:
142
+
143
+ - records every removed app file as **retired** in `.ailk-template-files.json`
144
+ (its row stays, with the template's hash), so `create-ailk update` reads it
145
+ as deleted locally and never writes it back, even across a version bump;
146
+ `sync-routes` and `add-module` stop copying the public tree on a site whose
147
+ `.ailk-site-manifest.json` lists no `marketing`;
148
+ - keeps any file in a removed directory that a kept file imports — the
149
+ locale layout reads `contact/site-contact-lines.ts`, and `lib/in-page-magnet.ts`
150
+ the contact form's action — with its own imports, listed under "Kept — a
151
+ kept file imports them" with the importer; the directory stays with no
152
+ page, so its route still 404s;
153
+ - drops the module from `.ailk-site-manifest.json` (its `modules` and the
154
+ roster rows) and from `architecture.yaml`'s `# Modules:` header, keeping
155
+ the web app's import permissions;
156
+ - regenerates the module flags, the paid-sections registry and — unless you
157
+ have edited it — the nav, which links nothing on a site with no public page.
158
+
159
+ A file the site has edited, or a page the site authored inside a removed
160
+ directory, stops the run before anything is written and is listed; `--force`
161
+ removes those too. Removing `marketing` while `docs-blog` is installed is
162
+ refused — remove `docs-blog` first. A `docs-blog` removal leaves the bundle's
163
+ `package.json`, `next.config.mjs` and `jest.config.cjs` patches in place
164
+ (listed under "Left in place"), so the site still builds; reverse them by
165
+ hand if you want the module's dependencies gone.
166
+
167
+ A site without `marketing` redirects `/` to `/account` (signed out, on to
168
+ sign-in), serves `robots.txt` disallowing every path, sends
169
+ `X-Robots-Tag: noindex` on every response, and answers 404 at
170
+ `/sitemap.xml` and `/llms.txt`; a site that keeps `marketing` or `docs-blog`
171
+ is byte-identical to today. The route code stays and reads the module list
172
+ (`apps/web/lib/modules.generated.ts`), so nothing needs restoring if a module
173
+ comes back.
174
+
113
175
  ### Apply a design to an existing site
114
176
 
115
177
  A site that already exists can wear a different design with one command. Name
@@ -362,11 +424,26 @@ it; the merge itself still never loses content.
362
424
  never a shell. Fetching an old template version (when it is not the running
363
425
  package's own) reads the public npm registry at
364
426
  `https://registry.npmjs.org`, overridable with `AILK_NPM_REGISTRY_URL` for a
365
- mirror. `update` never touches content, env files, `.claude/`, lockfiles or
366
- any `package.json` — those stay `add-module`'s and the consumer version
367
- bump's job. Commit before running `update`, and review the diff after: a
427
+ mirror. `update` never touches content, except to give an mdx-body page
428
+ with no `# ` heading its title and description (below), nor env files,
429
+ `.claude/`, lockfiles or any `package.json` — those stay `add-module`'s and
430
+ the consumer version bump's job. Commit before running `update`, and review the diff after: a
368
431
  clean merge is still worth reading.
369
432
 
433
+ **Page headings (#7495).** A page your `apps/web` renders in
434
+ `bodyMode: "mdx-body"` takes its `<h1>` and intro from its own body: the
435
+ template prints neither frontmatter `title` nor `description` on it. So every
436
+ `update` run, after the template files, gives each such page under
437
+ `content/<locale>/<Type>/` whose body has no `# ` heading its `title` as
438
+ `# <title>` and its `description` as the paragraph under it, inserted at the
439
+ top of the body (after any leading `import`/`export` lines), and lists it
440
+ under **Page heading added**. Nothing else in the file changes. A page with
441
+ one `# ` heading is left exactly as it is; a page with several, or one whose
442
+ `title` or `description` is written in a form `update` does not read (a
443
+ folded `>` block, say), is listed under **Needs review** for you to fix.
444
+ Which pages are mdx-body is read from your own `apps/web/app` routes, and the
445
+ step runs again harmlessly on every update.
446
+
370
447
  ### Adopting an existing site
371
448
 
372
449
  A site that create-ailk did not scaffold, or that was scaffolded before
@@ -35,7 +35,8 @@ export function readArchitectureHeader(archYaml) {
35
35
  }
36
36
  let modules;
37
37
  try {
38
- modules = normalizeModules(modulesLine.split(","));
38
+ // `none` is the empty set `deriveArchitecture` writes (#10040).
39
+ modules = modulesLine.trim() === "none" ? [] : normalizeModules(modulesLine.split(","));
39
40
  }
40
41
  catch (err) {
41
42
  throw new AddModuleRefusedError(`architecture.yaml's \`# Modules:\` header is not a module list: ${err.message}`);
@@ -3,6 +3,7 @@ import { type InstallReportRow } from "./lib/extract-module-bundle.js";
3
3
  import { type ModulePatch } from "./module-architecture.js";
4
4
  import { type ModuleName } from "./parse-args.js";
5
5
  import type { MarkerMap, WorkspaceVersionMap } from "./substitute.js";
6
+ import { type RenderNavConfigOptions } from "./surfaces.js";
6
7
  import { type SyncRoutesResult } from "./sync-routes.js";
7
8
  import { type Topology } from "./topology.js";
8
9
  /**
@@ -97,7 +98,7 @@ export declare function isUneditedNav(existing: string, before: string[]): boole
97
98
  * (`isUneditedNav`). An edited nav is the customer's file: left as-is and
98
99
  * reported `skipped` with the change, like any other.
99
100
  */
100
- export declare function syncNavConfig(targetAbs: string, before: string[], after: string[]): InstallReportRow | undefined;
101
+ export declare function syncNavConfig(targetAbs: string, before: string[], after: string[], renderOptions?: Omit<RenderNavConfigOptions, "serviceSlugs">): InstallReportRow | undefined;
101
102
  /** The report `add-module` prints. */
102
103
  export declare function formatAddModuleReport(result: AddModuleResult, moduleName: string): string;
103
104
  export interface AddModuleArgs {
@@ -34,13 +34,14 @@ import { LEGACY_NAV_RENDERERS } from "./legacy-nav-renderers.js";
34
34
  import { writeAuthSecretFile } from "./lib/auth-secret-file.js";
35
35
  import { appendEnvRows } from "./lib/declared-env.js";
36
36
  import { diffTwoStrings, extractModuleBundle, } from "./lib/extract-module-bundle.js";
37
+ import { publicTreeRemoved } from "./lib/site-modules.js";
37
38
  import { normalizeModules } from "./module-architecture.js";
38
39
  import { MODULE_FLAGS_PATH, PAID_MCP_TOOLS_PATH, PAID_SECTIONS_PATH, writePaidSections, } from "./paid-sections.js";
39
40
  import { ParseError, parseModules } from "./parse-args.js";
40
41
  import { writeSpokeFlowsReadme } from "./seed-flows.js";
41
42
  import { SITE_ROSTER_PATH, moduleRosterRows, planRosterAppend, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
42
43
  import { spokeCheckoutSite, spokeEnvRowOptions, writeSpokeCheckoutSlice, } from "./spoke-checkout.js";
43
- import { MODULE_SURFACES, NAV_CONFIG_PATH, isUnservedSurfaceContent, readNavConfigStamp, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
44
+ import { MODULE_SURFACES, NAV_CONFIG_PATH, isRemovedPublicRoutePath, isUnservedSurfaceContent, readNavConfigStamp, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
44
45
  import { syncRoutes } from "./sync-routes.js";
45
46
  import { TopologyError, isSpokeExcluded, readSiteTopology, readSpokeShape, spokeDestination, spokeModuleRefusal, } from "./topology.js";
46
47
  /**
@@ -159,8 +160,12 @@ export async function applyModuleToScaffold(opts) {
159
160
  // writes a seed under a gateable surface this site's own
160
161
  // `architecture.yaml` header (resolved above, into `surfaces`) does
161
162
  // not enable.
163
+ // #10040 D10 — a site that dropped `marketing` carries no public route
164
+ // tree, so a bundle's file inside one (commerce's dev/purchase launcher,
165
+ // a flow checkout action) is not written either.
162
166
  skip: (destination) => (spoke !== undefined && isSpokeExcluded(destination, spoke)) ||
163
- isUnservedSurfaceContent(destination, surfaces),
167
+ isUnservedSurfaceContent(destination, surfaces) ||
168
+ (publicTreeRemoved(targetAbs) && isRemovedPublicRoutePath(destination)),
164
169
  ...(spoke !== undefined
165
170
  ? {
166
171
  // #7876 D10 — a module flow file lands in flows/ as declared data.
@@ -313,7 +318,7 @@ export function isUneditedNav(existing, before) {
313
318
  * (`isUneditedNav`). An edited nav is the customer's file: left as-is and
314
319
  * reported `skipped` with the change, like any other.
315
320
  */
316
- export function syncNavConfig(targetAbs, before, after) {
321
+ export function syncNavConfig(targetAbs, before, after, renderOptions = {}) {
317
322
  const navPath = join(targetAbs, NAV_CONFIG_PATH);
318
323
  // #6878 D6 — a finished site already has `content/en/Service` (it exists
319
324
  // on ANY site that ever enabled the `services` surface), so read the
@@ -324,7 +329,7 @@ export function syncNavConfig(targetAbs, before, after) {
324
329
  .filter((f) => f.endsWith(".mdx"))
325
330
  .map((f) => f.slice(0, -".mdx".length))
326
331
  : [];
327
- const wanted = renderNavConfig(after, { serviceSlugs });
332
+ const wanted = renderNavConfig(after, { ...renderOptions, serviceSlugs });
328
333
  const existing = existsSync(navPath)
329
334
  ? readFileSync(navPath, "utf8")
330
335
  : undefined;
package/dist/add-route.js CHANGED
@@ -19,9 +19,12 @@
19
19
  * never over a file the site already has.
20
20
  *
21
21
  * The result is the site a fresh scaffold with `--surfaces <route>` would
22
- * have been. That scaffold adds no row to `.ailk-site-manifest.json` for a
23
- * surface (the roster names only the template's own pages), so neither does
24
- * this. A route the site already declares is a no-op: a second run changes
22
+ * have been, with one exception: a surface with generated seeds
23
+ * (`templates`, #10051 D10) gets them only from `add-route`. A scaffold built
24
+ * with `--surfaces templates` has the two routes and no seeds, so
25
+ * `/templates` answers 404 until a hub seed is written. That scaffold adds
26
+ * no row to `.ailk-site-manifest.json` for a surface (the roster names only
27
+ * the template's own pages), so neither does this. A route the site already declares is a no-op: a second run changes
25
28
  * nothing, even where the site has since deleted a seed.
26
29
  *
27
30
  * `create-ailk add-route magnet --path <path> --flow <slug>` (#8840 D12) is
@@ -46,6 +49,7 @@ import { substituteMarkers } from "./substitute.js";
46
49
  import { GATEABLE_SURFACES, MODULE_SURFACES, parseSurfaces, resolveSurfaces, } from "./surfaces.js";
47
50
  import { syncRoutes } from "./sync-routes.js";
48
51
  import { BASE_LAYER_DIR } from "./template-manifest.js";
52
+ import { templatePageSeeds } from "./template-pages.js";
49
53
  import { TopologyError, readSiteTopology } from "./topology.js";
50
54
  /** A refusal the CLI exits 2 on, thrown before anything is written. */
51
55
  export class AddRouteRefusedError extends Error {
@@ -115,6 +119,7 @@ export function addRouteToScaffold(opts) {
115
119
  forceSurfaces: [route],
116
120
  });
117
121
  const report = seedSurfaceContent(targetAbs, route, opts);
122
+ seedGeneratedContent(targetAbs, route, report);
118
123
  const navRow = syncNavConfig(targetAbs, current.surfaces, surfaces);
119
124
  if (navRow !== undefined)
120
125
  report.push(navRow);
@@ -183,25 +188,47 @@ function seedSurfaceContent(targetAbs, route, opts) {
183
188
  const wanted = isText
184
189
  ? Buffer.from(substituteMarkers(readFileSync(src, "utf8"), opts.markers))
185
190
  : readFileSync(src);
186
- const dest = join(targetAbs, path);
187
- if (existsSync(dest)) {
188
- const existing = readFileSync(dest);
189
- if (existing.equals(wanted))
190
- continue;
191
- const encoding = isText ? "utf8" : "latin1";
192
- report.push({
193
- path,
194
- action: "skipped",
195
- wanted: diffTwoStrings(path, existing.toString(encoding), wanted.toString(encoding), isText),
196
- });
197
- continue;
198
- }
199
- mkdirSync(dirname(dest), { recursive: true });
200
- writeFileSync(dest, wanted);
201
- report.push({ path, action: "written" });
191
+ writeSeed(targetAbs, path, wanted, isText, report);
202
192
  }
203
193
  return report;
204
194
  }
195
+ /**
196
+ * Write one seed file unless the site already has it. Identical bytes are
197
+ * silent; different bytes are reported `skipped` with the change the seed
198
+ * wanted, and the site's file is left as it is.
199
+ */
200
+ function writeSeed(targetAbs, path, wanted, isText, report) {
201
+ const dest = join(targetAbs, path);
202
+ if (existsSync(dest)) {
203
+ const existing = readFileSync(dest);
204
+ if (existing.equals(wanted))
205
+ return;
206
+ const encoding = isText ? "utf8" : "latin1";
207
+ report.push({
208
+ path,
209
+ action: "skipped",
210
+ wanted: diffTwoStrings(path, existing.toString(encoding), wanted.toString(encoding), isText),
211
+ });
212
+ return;
213
+ }
214
+ mkdirSync(dirname(dest), { recursive: true });
215
+ writeFileSync(dest, wanted);
216
+ report.push({ path, action: "written" });
217
+ }
218
+ /**
219
+ * Seeds a surface derives from a registry rather than copies from a layer
220
+ * (#10051 D10): the `templates` family seeds one TemplatePage per AILK
221
+ * template from `TEMPLATE_MANIFESTS`, so a new template never needs a new
222
+ * seed file. Written after the static seeds, never over a site's file.
223
+ */
224
+ const GENERATED_SEEDS = {
225
+ templates: () => templatePageSeeds(),
226
+ };
227
+ function seedGeneratedContent(targetAbs, route, report) {
228
+ for (const { path, text } of GENERATED_SEEDS[route]?.() ?? []) {
229
+ writeSeed(targetAbs, path, Buffer.from(text), true, report);
230
+ }
231
+ }
205
232
  /** Every file under `root/dir`, recorded site-relative → absolute source. */
206
233
  function collectFiles(root, dir, out) {
207
234
  const abs = join(root, dir);
@@ -0,0 +1,56 @@
1
+ /** Per content type: mdx-body always, or only while `sections[]` is empty. */
2
+ export type MdxBodyRule = Map<string, "always" | "unless-sections">;
3
+ /**
4
+ * Scan `<appsRoot>/apps/web/app/**\/page.{ts,tsx}` for `renderPage({…})` calls
5
+ * naming `bodyMode: "mdx-body"`. A call whose `bodyMode` sits in a conditional
6
+ * spread is `"unless-sections"`, any other `"always"`; `"always"` wins when two
7
+ * routes name one type. A content hub's `HubConfig` reads the same way, from
8
+ * its `body`. A missing `apps/web/app` gives an empty map.
9
+ */
10
+ export declare function mdxBodyRule(appsRoot: string): MdxBodyRule;
11
+ /** The routes' own `composed` test: mdx-body unless `sections` is a non-empty array. */
12
+ export declare function isMdxBodyPage(type: string, frontmatter: Record<string, unknown>, rule: MdxBodyRule): boolean;
13
+ /**
14
+ * ATX level-1 headings (`^ {0,3}# +\S`) outside fenced code blocks and
15
+ * multi-line `{/* … *\/}` comments. `##`, `#text` and Setext headings do not
16
+ * count. A fence closes, as in CommonMark, on a run of its own character at
17
+ * least as long as the opening one, with nothing after it.
18
+ */
19
+ export declare function countBodyHeadings(body: string): number;
20
+ /**
21
+ * Text for an MDX body line: whitespace runs collapsed, trimmed, and every
22
+ * `\`, `{`, `<`, `` ` ``, `*`, `_` and `[` backslash-escaped.
23
+ */
24
+ export declare function mdxText(text: string): string;
25
+ /**
26
+ * The new file text for an mdx-body page with zero `# ` headings — its
27
+ * `# <title>` and description paragraph inserted at the top of the body —
28
+ * else `undefined`. A page the subset reader refuses, with no frontmatter, or
29
+ * with an empty `title` or `description`, is never written.
30
+ */
31
+ export declare function addBodyHeading(raw: string, rule: MdxBodyRule, type: string): string | undefined;
32
+ export interface BodyHeadingPlan {
33
+ writes: Array<{
34
+ path: string;
35
+ text: string;
36
+ }>;
37
+ refused: Array<{
38
+ path: string;
39
+ reason: string;
40
+ }>;
41
+ }
42
+ /** What `migrateBodyHeadings` would write, and the heading-less pages it cannot. Never throws for a page. */
43
+ export declare function planBodyHeadings(contentDir: string, rule: MdxBodyRule): BodyHeadingPlan;
44
+ /** Every mdx-body page whose body does not have exactly one `# ` heading. */
45
+ export declare function headingViolations(contentDir: string, rule: MdxBodyRule): Array<{
46
+ path: string;
47
+ count: number;
48
+ }>;
49
+ /** Write each planned page; the paths written and the refused rows. */
50
+ export declare function migrateBodyHeadings(contentDir: string, rule: MdxBodyRule): {
51
+ written: string[];
52
+ refused: Array<{
53
+ path: string;
54
+ reason: string;
55
+ }>;
56
+ };