create-ailk 0.27.0 → 0.28.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 (210) 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/env-catalog.js +20 -0
  16. package/dist/lib/site-modules.d.ts +8 -0
  17. package/dist/lib/site-modules.js +34 -0
  18. package/dist/module-architecture.js +23 -14
  19. package/dist/paid-sections.d.ts +17 -0
  20. package/dist/paid-sections.js +39 -0
  21. package/dist/programmatic.d.ts +16 -0
  22. package/dist/programmatic.js +73 -4
  23. package/dist/remove-module.d.ts +98 -0
  24. package/dist/remove-module.js +620 -0
  25. package/dist/seed-flows.d.ts +2 -2
  26. package/dist/seed-flows.js +16 -9
  27. package/dist/site-roster.js +4 -1
  28. package/dist/starter-grid.d.ts +26 -0
  29. package/dist/starter-grid.js +34 -0
  30. package/dist/surfaces.d.ts +26 -0
  31. package/dist/surfaces.js +105 -9
  32. package/dist/sync-routes.d.ts +7 -0
  33. package/dist/sync-routes.js +53 -6
  34. package/dist/template-pages.d.ts +36 -0
  35. package/dist/template-pages.js +129 -0
  36. package/dist/topology.d.ts +2 -1
  37. package/dist/topology.js +4 -2
  38. package/dist/update.d.ts +3 -1
  39. package/dist/update.js +38 -4
  40. package/package.json +3 -3
  41. package/release-scope.json +2 -1
  42. package/site-templates/aliases/hub.TEMPLATE-MANIFEST.json +115 -0
  43. package/templates/.env.example +5 -2
  44. package/templates/apps/api/.env.example +9 -5
  45. package/templates/apps/api/CLAUDE.md +14 -9
  46. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +37 -0
  47. package/templates/apps/api/src/vercel-handler.ts +8 -1
  48. package/templates/apps/mcp/CLAUDE.md +18 -10
  49. package/templates/apps/web/.env.example +25 -11
  50. package/templates/apps/web/CLAUDE.md +45 -4
  51. package/templates/apps/web/__tests__/setup.ts +7 -0
  52. package/templates/apps/web/app/.well-known/mcp.json/route.ts +7 -7
  53. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +61 -2
  54. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +81 -0
  55. package/templates/apps/web/app/[locale]/(authed)/account/connect/__tests__/page.test.tsx +197 -0
  56. package/templates/apps/web/app/[locale]/(authed)/account/connect/page.tsx +152 -0
  57. package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +2 -2
  58. package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +76 -21
  59. package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +46 -16
  60. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +20 -3
  61. package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +180 -2
  62. package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +6 -2
  63. package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +122 -0
  64. package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +1 -1
  65. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +20 -3
  66. package/templates/apps/web/app/[locale]/case-studies/__tests__/hub-children.test.tsx +65 -0
  67. package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +6 -2
  68. package/templates/apps/web/app/[locale]/case-studies/page.tsx +23 -11
  69. package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +11 -1
  70. package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
  71. package/templates/apps/web/app/[locale]/consent.actions.ts +44 -6
  72. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +4 -4
  73. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +269 -42
  74. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +88 -30
  75. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +20 -3
  76. package/templates/apps/web/app/[locale]/courses/__tests__/hub-children.test.tsx +32 -0
  77. package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +6 -2
  78. package/templates/apps/web/app/[locale]/courses/page.tsx +22 -6
  79. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +15 -2
  80. package/templates/apps/web/app/[locale]/events/__tests__/hub-children.test.tsx +32 -0
  81. package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +8 -2
  82. package/templates/apps/web/app/[locale]/events/page.tsx +23 -7
  83. package/templates/apps/web/app/[locale]/features/__tests__/hub-children.test.tsx +32 -0
  84. package/templates/apps/web/app/[locale]/features/page.tsx +23 -25
  85. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +95 -0
  86. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/save.actions.test.ts +193 -0
  87. package/templates/apps/web/app/[locale]/flows/[slug]/email-moment.ts +92 -0
  88. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +10 -0
  89. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +513 -0
  90. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +122 -0
  91. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.no-commerce.test.tsx +195 -0
  92. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/landing.test.tsx +293 -0
  93. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +397 -0
  94. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/checkout-seam.ts +74 -0
  95. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +264 -0
  96. package/templates/apps/web/app/[locale]/flows/[slug]/save.actions.ts +75 -0
  97. package/templates/apps/web/app/[locale]/for/__tests__/hub-children.test.tsx +32 -0
  98. package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +5 -1
  99. package/templates/apps/web/app/[locale]/for/page.tsx +23 -25
  100. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +20 -3
  101. package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
  102. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +25 -4
  103. package/templates/apps/web/app/[locale]/integrations/__tests__/hub-children.test.tsx +38 -0
  104. package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +8 -2
  105. package/templates/apps/web/app/[locale]/integrations/page.tsx +23 -11
  106. package/templates/apps/web/app/[locale]/layout.tsx +157 -139
  107. package/templates/apps/web/app/[locale]/page.tsx +51 -3
  108. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +16 -3
  109. package/templates/apps/web/app/[locale]/partners/__tests__/hub-children.test.tsx +32 -0
  110. package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +6 -2
  111. package/templates/apps/web/app/[locale]/partners/page.tsx +22 -6
  112. package/templates/apps/web/app/[locale]/portfolio/__tests__/hub-children.test.tsx +38 -0
  113. package/templates/apps/web/app/[locale]/portfolio/page.tsx +25 -24
  114. package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +321 -0
  115. package/templates/apps/web/app/[locale]/pricing/[slug]/page.tsx +87 -0
  116. package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
  117. package/templates/apps/web/app/[locale]/pricing/__tests__/pricing-pages.test.tsx +215 -0
  118. package/templates/apps/web/app/[locale]/pricing/page.tsx +56 -341
  119. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +21 -4
  120. package/templates/apps/web/app/[locale]/products/__tests__/hub-children.test.tsx +32 -0
  121. package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +8 -2
  122. package/templates/apps/web/app/[locale]/products/page.tsx +22 -6
  123. package/templates/apps/web/app/[locale]/projects/__tests__/hub-children.test.tsx +32 -0
  124. package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -0
  125. package/templates/apps/web/app/[locale]/projects/page.tsx +18 -11
  126. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +25 -4
  127. package/templates/apps/web/app/[locale]/resources/__tests__/hub-children.test.tsx +38 -0
  128. package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +6 -2
  129. package/templates/apps/web/app/[locale]/resources/page.tsx +20 -10
  130. package/templates/apps/web/app/[locale]/services/[slug]/[child]/page.tsx +68 -0
  131. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +64 -9
  132. package/templates/apps/web/app/[locale]/services/__tests__/hub-children.test.tsx +158 -0
  133. package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +12 -2
  134. package/templates/apps/web/app/[locale]/services/__tests__/service-detail-sections.test.tsx +223 -0
  135. package/templates/apps/web/app/[locale]/services/__tests__/service-families.test.tsx +368 -0
  136. package/templates/apps/web/app/[locale]/services/page.tsx +45 -19
  137. package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
  138. package/templates/apps/web/app/[locale]/team/__tests__/hub-children.test.tsx +32 -0
  139. package/templates/apps/web/app/[locale]/team/page.tsx +25 -24
  140. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +24 -3
  141. package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
  142. package/templates/apps/web/app/[locale]/templates/[slug]/__tests__/page.test.tsx +203 -0
  143. package/templates/apps/web/app/[locale]/templates/[slug]/page.tsx +81 -0
  144. package/templates/apps/web/app/[locale]/templates/__tests__/page.test.tsx +236 -0
  145. package/templates/apps/web/app/[locale]/templates/page.tsx +108 -0
  146. package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
  147. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +24 -3
  148. package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
  149. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +204 -2
  150. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +151 -33
  151. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +233 -7
  152. package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +92 -41
  153. package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +77 -39
  154. package/templates/apps/web/app/llms.txt/route.ts +41 -3
  155. package/templates/apps/web/app/robots.txt/route.ts +5 -1
  156. package/templates/apps/web/app/sitemap.xml/route.ts +45 -3
  157. package/templates/apps/web/auth.ts +102 -50
  158. package/templates/apps/web/components/signed-in-nav.tsx +8 -1
  159. package/templates/apps/web/lib/__tests__/bot-check.test.ts +254 -0
  160. package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +212 -13
  161. package/templates/apps/web/lib/__tests__/deck-page.test.ts +13 -1
  162. package/templates/apps/web/lib/__tests__/docs-index.test.ts +93 -0
  163. package/templates/apps/web/lib/__tests__/docs-path.test.ts +168 -0
  164. package/templates/apps/web/lib/__tests__/hub-children.fixtures.ts +278 -0
  165. package/templates/apps/web/lib/__tests__/nav-label-keys.test.ts +138 -0
  166. package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +178 -2
  167. package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +183 -0
  168. package/templates/apps/web/lib/__tests__/page-chrome.test.ts +412 -0
  169. package/templates/apps/web/lib/__tests__/pricing-page.test.ts +142 -0
  170. package/templates/apps/web/lib/__tests__/relay.test.ts +202 -10
  171. package/templates/apps/web/lib/__tests__/route-family.test.ts +57 -0
  172. package/templates/apps/web/lib/__tests__/service-listing.test.ts +111 -0
  173. package/templates/apps/web/lib/__tests__/services-path.test.ts +96 -0
  174. package/templates/apps/web/lib/__tests__/site-header.test.ts +151 -0
  175. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +148 -6
  176. package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +84 -0
  177. package/templates/apps/web/lib/__tests__/template-listing.test.ts +158 -0
  178. package/templates/apps/web/lib/auth-secret.ts +57 -0
  179. package/templates/apps/web/lib/bot-check.ts +138 -0
  180. package/templates/apps/web/lib/consent-visitor.ts +81 -7
  181. package/templates/apps/web/lib/deck-page.ts +6 -3
  182. package/templates/apps/web/lib/docs-index.ts +100 -0
  183. package/templates/apps/web/lib/docs-path.ts +94 -0
  184. package/templates/apps/web/lib/hub-children.tsx +318 -0
  185. package/templates/apps/web/lib/modules.generated.ts +9 -0
  186. package/templates/apps/web/lib/nav-label-check.ts +61 -0
  187. package/templates/apps/web/lib/nav-label-keys.ts +78 -0
  188. package/templates/apps/web/lib/newsletter.actions.ts +31 -6
  189. package/templates/apps/web/lib/offerings-pricing.ts +135 -6
  190. package/templates/apps/web/lib/page-chrome.ts +367 -0
  191. package/templates/apps/web/lib/posting-title.ts +26 -4
  192. package/templates/apps/web/lib/pricing-page.ts +430 -0
  193. package/templates/apps/web/lib/relay.ts +31 -106
  194. package/templates/apps/web/lib/route-family.ts +69 -0
  195. package/templates/apps/web/lib/service-listing.ts +89 -0
  196. package/templates/apps/web/lib/services-path.ts +97 -0
  197. package/templates/apps/web/lib/site-header.ts +94 -0
  198. package/templates/apps/web/lib/telemetry-scrub.ts +68 -3
  199. package/templates/apps/web/lib/template-listing.ts +169 -0
  200. package/templates/apps/web/messages/en.json +86 -30
  201. package/templates/apps/web/middleware.ts +171 -11
  202. package/templates/apps/web/next.config.mjs +211 -2
  203. package/templates/apps/web/package.json +1 -1
  204. package/templates/apps/web/scripts/assert-content-bundled.ts +28 -6
  205. package/templates/apps/web/scripts/check-nav-labels.ts +39 -0
  206. package/templates/apps/web/sentry.server.config.ts +30 -2
  207. package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -9
  208. package/templates/apps/web/src/lib/rate-limit.ts +68 -27
  209. package/templates/apps/web/src/lib/routes.ts +15 -1
  210. package/versions.json +48 -48
@@ -12,8 +12,10 @@
12
12
  * In order (D7): the site must be a spoke; every `flows/*.json` must parse
13
13
  * and carry a `slug` equal to its file name; `--dry-run` stops there; the hub
14
14
  * origin comes from `--hub-url`, else the `AILK_API_URL` environment
15
- * variable, else the `AILK_API_URL=` line of `apps/web/.env.local`, and must
16
- * be `https:` (or `http:` on a loopback host); the key comes from the
15
+ * variable, else (for `--dry-run` only) the `AILK_API_URL=` line of
16
+ * `apps/web/.env.local`, and must be `https:` (or `http:` on a loopback
17
+ * host); a send whose origin only that file names is refused, because a
18
+ * repo file never decides where the key goes (#9969 D8); the key comes from the
17
19
  * `AILK_SPOKE_KEY` environment variable and nowhere else. Every refusal is
18
20
  * thrown before the first request. The hub origin and its source are printed
19
21
  * before any request, so where the key goes is visible before it is sent.
@@ -256,6 +258,9 @@ export async function seedFlows(opts) {
256
258
  return { ...(hub !== undefined && { hub }), flows: outcomes, exitCode: 0 };
257
259
  }
258
260
  const hub = resolveHub(targetDir, opts.hubUrl, env);
261
+ if (hub.source === "apps/web/.env.local") {
262
+ throw new SeedFlowsRefusedError(`seed-flows: the hub origin ${hub.origin} was read from apps/web/.env.local. Confirm it with --hub-url ${hub.origin} (or set ${HUB_ORIGIN_ENV_VAR}) — the spoke key is sent only to an origin you name. Nothing was sent.`);
263
+ }
259
264
  const key = env[SPOKE_KEY_ENV_VAR]?.trim() ?? "";
260
265
  if (key === "") {
261
266
  throw new SeedFlowsRefusedError(`seed-flows: set ${SPOKE_KEY_ENV_VAR} to the spoke key your hub issued. It is read from the environment only — never a flag or a file. Nothing was sent.`);
@@ -297,11 +302,12 @@ Run this as part of each rollout, from the site's root, with the spoke key
297
302
  your hub issued set as \`AILK_SPOKE_KEY\` in the environment (a shell or CI
298
303
  secret — never a file in this repo):
299
304
 
300
- npx create-ailk seed-flows .
305
+ npx create-ailk seed-flows . --hub-url <the hub's origin>
301
306
 
302
- The hub origin comes from \`--hub-url\`, else \`AILK_API_URL\`, else the
303
- \`AILK_API_URL=\` line of \`apps/web/.env.local\`. The key is read from the
304
- environment only. Each flow answers \`created\`, \`updated\` or \`unchanged\`,
307
+ The hub origin comes from \`--hub-url\`, else \`AILK_API_URL\`; the
308
+ \`AILK_API_URL=\` line of \`apps/web/.env.local\` is read for \`--dry-run\`
309
+ only, and a send whose origin only that file names is refused. The key is
310
+ read from the environment only. Each flow answers \`created\`, \`updated\` or \`unchanged\`,
305
311
  so running it again is safe. \`--dry-run\` checks the files and sends nothing.
306
312
 
307
313
  The hub's operator must first turn seeding on (\`AILK_SPOKE_FLOW_SEED=true\`
@@ -360,9 +366,10 @@ Arguments:
360
366
  <dir> The spoke site. Defaults to the current dir.
361
367
 
362
368
  Options:
363
- --hub-url <origin> The hub's origin. Else AILK_API_URL, else the
364
- AILK_API_URL= line of apps/web/.env.local. https:
365
- only (http: on localhost or 127.0.0.1).
369
+ --hub-url <origin> The hub's origin. Else AILK_API_URL. The
370
+ AILK_API_URL= line of apps/web/.env.local is read
371
+ for --dry-run only. https: only (http: on
372
+ localhost or 127.0.0.1).
366
373
  --dry-run Check every flows/*.json and print what would be sent.
367
374
  Sends nothing and reads no key.
368
375
  -h, --help Show this help.
@@ -31,6 +31,7 @@ import { AddModuleRefusedError, readArchitectureHeader, } from "./add-module-cor
31
31
  import { DESIGN_SEED_PAGES } from "./design-home.js";
32
32
  import { locatePageSections } from "./design-presets.js";
33
33
  import { diffTwoStrings } from "./lib/extract-module-bundle.js";
34
+ import { publicTreeRemoved } from "./lib/site-modules.js";
34
35
  import { MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
35
36
  import { substituteMarkers } from "./substitute.js";
36
37
  import { isUnservedSurfaceContent, resolveSurfaces } from "./surfaces.js";
@@ -695,7 +696,9 @@ export function siteRosterProblems(opts) {
695
696
  seen.add(route);
696
697
  }
697
698
  const contributions = new Map();
698
- for (const moduleName of modules) {
699
+ // #10040 D3 — a site that dropped `marketing` carries no public route tree,
700
+ // so no module's public pages are expected on its roster either.
701
+ for (const moduleName of publicTreeRemoved(opts.targetDir) ? [] : modules) {
699
702
  let refs;
700
703
  try {
701
704
  refs = moduleRosterRows(moduleName, opts.siteTemplatesDir);
@@ -18,6 +18,12 @@ import type { CategorySlug } from "./category-profile.js";
18
18
  import type { ModuleName } from "./parse-args.js";
19
19
  import type { TemplateLayout } from "./site-chrome.js";
20
20
  import type { StarterDesign, StarterShape, TemplateRosterRow } from "./template-manifest.js";
21
+ /**
22
+ * The roster routes a hub keeps (#10040): the sign-in pair and the three legal
23
+ * pages. Declared here, beside the rosters they are cut from, so this file
24
+ * stays import-free at runtime; the hub alias's `drop` is derived from it.
25
+ */
26
+ export declare const HUB_KEPT_ROUTES: readonly string[];
21
27
  /** The shapes a starter can be built from — every StarterShape but the reserved one. */
22
28
  export type BuildableShape = Exclude<StarterShape, "local-business-product">;
23
29
  /** How an alias's own rows differ from its shape's standard roster. */
@@ -148,6 +154,26 @@ export declare const ALIASES: Readonly<{
148
154
  readonly before: "/portfolio";
149
155
  };
150
156
  };
157
+ /**
158
+ * The hub starter (#10040, decision 4): a site that signs people in and
159
+ * serves the account area, the API and the Admin, with no public pages.
160
+ * The shape is `organization-service` (the account service is the
161
+ * operator's organization), every public row of it dropped, and `modules`
162
+ * is empty — `marketing` deliberately absent, the Admin and the rest ride
163
+ * the key's `--modules`. Treeless (spec 6892 D1): its seed layer is the
164
+ * shape's tree, which `scaffold()` then strips through the same
165
+ * `remove-module marketing` path a live hub runs.
166
+ */
167
+ readonly hub: {
168
+ readonly shape: "organization-service";
169
+ readonly design: "standard";
170
+ readonly modules: readonly [];
171
+ readonly categories: readonly ["accounting", "legal", "real-estate"];
172
+ readonly ownTree: false;
173
+ readonly differences: {
174
+ readonly drop: string[];
175
+ };
176
+ };
151
177
  }>;
152
178
  /**
153
179
  * The coordinates a keyless `--shape S` builds (#6757 D5) — the composed
@@ -1,3 +1,15 @@
1
+ /**
2
+ * The roster routes a hub keeps (#10040): the sign-in pair and the three legal
3
+ * pages. Declared here, beside the rosters they are cut from, so this file
4
+ * stays import-free at runtime; the hub alias's `drop` is derived from it.
5
+ */
6
+ export const HUB_KEPT_ROUTES = [
7
+ "/sign-in",
8
+ "/sign-up",
9
+ "/privacy",
10
+ "/terms",
11
+ "/working-agreement",
12
+ ];
1
13
  /**
2
14
  * The pages every starter carries whatever its shape (#6658 D5,
3
15
  * `docs/decisions/starter-roster-page-set.md` D4/D7): a glossary and a
@@ -471,6 +483,28 @@ export const ALIASES = Object.freeze({
471
483
  before: "/portfolio",
472
484
  },
473
485
  },
486
+ /**
487
+ * The hub starter (#10040, decision 4): a site that signs people in and
488
+ * serves the account area, the API and the Admin, with no public pages.
489
+ * The shape is `organization-service` (the account service is the
490
+ * operator's organization), every public row of it dropped, and `modules`
491
+ * is empty — `marketing` deliberately absent, the Admin and the rest ride
492
+ * the key's `--modules`. Treeless (spec 6892 D1): its seed layer is the
493
+ * shape's tree, which `scaffold()` then strips through the same
494
+ * `remove-module marketing` path a live hub runs.
495
+ */
496
+ hub: {
497
+ shape: "organization-service",
498
+ design: "standard",
499
+ modules: [],
500
+ categories: ["accounting", "legal", "real-estate"],
501
+ ownTree: false,
502
+ differences: {
503
+ drop: SHAPE_ROSTERS["organization-service"]
504
+ .map((row) => row.route)
505
+ .filter((route) => !HUB_KEPT_ROUTES.includes(route)),
506
+ },
507
+ },
474
508
  });
475
509
  /**
476
510
  * The coordinates a keyless `--shape S` builds (#6757 D5) — the composed
@@ -1,3 +1,5 @@
1
+ /** Template-relative prefix every `apps/web` route directory sits under. */
2
+ export declare const ROUTE_ROOT = "apps/web/app/[locale]";
1
3
  /**
2
4
  * Route paths (site-relative, for the record written into `architecture.yaml`)
3
5
  * that every scaffold carries.
@@ -9,6 +11,24 @@
9
11
  * is authored per form, when the form exists.
10
12
  */
11
13
  export declare const CORE_SURFACE_PATHS: readonly string[];
14
+ /**
15
+ * The `app/[locale]/` entries a site keeps when it drops the `marketing`
16
+ * module (#10040, the operator's decisions of 2026-10-26): the sign-in group,
17
+ * the authed account area, and the three legal pages the kept sign-up form
18
+ * and account pages link through `consentPoliciesFor`. Every OTHER directory
19
+ * under `app/[locale]/` is the public route tree `remove-module marketing`
20
+ * deletes, `sync-routes` stops copying and `update` stops adding (spec
21
+ * 10040 D2, D10). One constant, three readers.
22
+ */
23
+ export declare const HUB_KEPT_ROUTE_DIRS: readonly string[];
24
+ /**
25
+ * Whether a `/`-separated scaffold path (`apps/web/app/[locale]/about/page.tsx`)
26
+ * lies inside a public route directory — one `remove-module marketing` removes.
27
+ * A file directly under `app/[locale]/` (the layout, the home page, the
28
+ * consent pair) is not: those stay. Read by `sync-routes` and `update` on a
29
+ * site whose manifest lists no `marketing` (`lib/site-modules.ts`, spec 10040 D10).
30
+ */
31
+ export declare function isRemovedPublicRoutePath(destRel: string): boolean;
12
32
  /** Template-relative route directories that are always copied. */
13
33
  export declare const CORE_ROUTE_DIRS: readonly string[];
14
34
  /**
@@ -220,6 +240,12 @@ export interface RenderNavConfigOptions {
220
240
  * surface is on (there is nothing to link yet).
221
241
  */
222
242
  serviceSlugs?: readonly string[];
243
+ /**
244
+ * `false` on a site without the `marketing` module (#10040 D2): the public
245
+ * tree is gone, so the nav and footer link nothing — empty `navItems` and
246
+ * `footerColumns` under the same self-hashing stamp. Absent → `true`.
247
+ */
248
+ publicPages?: boolean;
223
249
  }
224
250
  /**
225
251
  * Render `apps/web/lib/nav-config.ts` for an enabled surface set.
package/dist/surfaces.js CHANGED
@@ -32,7 +32,7 @@
32
32
  */
33
33
  import { createHash } from "node:crypto";
34
34
  /** Template-relative prefix every `apps/web` route directory sits under. */
35
- const ROUTE_ROOT = "apps/web/app/[locale]";
35
+ export const ROUTE_ROOT = "apps/web/app/[locale]";
36
36
  /**
37
37
  * Route paths (site-relative, for the record written into `architecture.yaml`)
38
38
  * that every scaffold carries.
@@ -56,6 +56,38 @@ export const CORE_SURFACE_PATHS = [
56
56
  // module's own route (the docs-blog `blog/`) win over a same-named MDX.
57
57
  "/{slug}",
58
58
  ];
59
+ /**
60
+ * The `app/[locale]/` entries a site keeps when it drops the `marketing`
61
+ * module (#10040, the operator's decisions of 2026-10-26): the sign-in group,
62
+ * the authed account area, and the three legal pages the kept sign-up form
63
+ * and account pages link through `consentPoliciesFor`. Every OTHER directory
64
+ * under `app/[locale]/` is the public route tree `remove-module marketing`
65
+ * deletes, `sync-routes` stops copying and `update` stops adding (spec
66
+ * 10040 D2, D10). One constant, three readers.
67
+ */
68
+ export const HUB_KEPT_ROUTE_DIRS = [
69
+ "(auth)",
70
+ "(authed)",
71
+ "privacy",
72
+ "terms",
73
+ "working-agreement",
74
+ ];
75
+ /**
76
+ * Whether a `/`-separated scaffold path (`apps/web/app/[locale]/about/page.tsx`)
77
+ * lies inside a public route directory — one `remove-module marketing` removes.
78
+ * A file directly under `app/[locale]/` (the layout, the home page, the
79
+ * consent pair) is not: those stay. Read by `sync-routes` and `update` on a
80
+ * site whose manifest lists no `marketing` (`lib/site-modules.ts`, spec 10040 D10).
81
+ */
82
+ export function isRemovedPublicRoutePath(destRel) {
83
+ const prefix = `${ROUTE_ROOT}/`;
84
+ if (!destRel.startsWith(prefix))
85
+ return false;
86
+ const rest = destRel.slice(prefix.length).split("/");
87
+ if (rest.length < 2)
88
+ return false;
89
+ return !HUB_KEPT_ROUTE_DIRS.includes(rest[0]);
90
+ }
59
91
  /** Template-relative route directories that are always copied. */
60
92
  export const CORE_ROUTE_DIRS = [
61
93
  `${ROUTE_ROOT}/about`,
@@ -316,6 +348,16 @@ export const GATEABLE_SURFACES = [
316
348
  routes: [`${ROUTE_ROOT}/tech-articles`],
317
349
  content: ["content/<locale>/TechArticle"],
318
350
  },
351
+ {
352
+ // #10051 D9 — the templates route family: the /templates hub that lists
353
+ // every TemplatePage itself, and /templates/<slug>. No roster names it,
354
+ // so a fresh scaffold excludes it; `add-route templates` installs it and
355
+ // seeds one TemplatePage per AILK template. No `navLabelKey`: the site
356
+ // links it from its own nav (the `alternatives` rule).
357
+ id: "templates",
358
+ routes: [`${ROUTE_ROOT}/templates`],
359
+ content: ["content/<locale>/TemplatesList", "content/<locale>/TemplatePage"],
360
+ },
319
361
  {
320
362
  id: "terms",
321
363
  routes: [`${ROUTE_ROOT}/terms`],
@@ -619,6 +661,9 @@ export const ROUTE_SINCE = {
619
661
  portfolio: "0.27.0",
620
662
  "portfolio/[slug]": "0.27.0",
621
663
  pricing: "0.22.0",
664
+ // #10044 D9 — a pricing page per offering. Genuinely new, so it attaches at
665
+ // the fixed group's next release line (the line `templates` attached at).
666
+ "pricing/[slug]": "0.50.0",
622
667
  privacy: "0.22.0",
623
668
  products: "0.0.0",
624
669
  "products/[slug]": "0.0.0",
@@ -638,6 +683,9 @@ export const ROUTE_SINCE = {
638
683
  "resources/[slug]": "0.0.0",
639
684
  services: "0.0.0",
640
685
  "services/[slug]": "0.0.0",
686
+ // #10043 D11 — a Service under its category. Genuinely new, so it attaches
687
+ // at the fixed group's next release line (the line `templates` attached at).
688
+ "services/[slug]/[child]": "0.50.0",
641
689
  software: "0.0.0",
642
690
  "software/[slug]": "0.0.0",
643
691
  // #7744 — the team profile pages, a new route pair at today's line.
@@ -708,6 +756,13 @@ export const ROUTE_SINCE = {
708
756
  // #8515 — the client's onboarding page. Genuinely new, so it attaches at
709
757
  // today's fixed-group version, the same rule #7430 applied.
710
758
  "(authed)/account/onboarding": "0.40.0",
759
+ // #9966 D2 — the hub's consent page. Genuinely new, so it attaches at the
760
+ // fixed group's next release line.
761
+ "(authed)/account/connect": "0.50.0",
762
+ // #10051 D9 — the templates route family. Genuinely new, so it attaches at
763
+ // the fixed group's next release line.
764
+ templates: "0.50.0",
765
+ "templates/[slug]": "0.50.0",
711
766
  // The leads inbox (#5678). The ROUTE ships in every scaffold, Free
712
767
  // included — lead capture is a never-gate surface, so reading a lead back
713
768
  // is one too. Whether it shows a ROW is a separate question with two
@@ -828,6 +883,11 @@ export const ROUTE_DEPENDENCIES = {
828
883
  "apps/web/lib/auth-routes.ts",
829
884
  ],
830
885
  "(auth)/sign-in": ["apps/web/lib/auth-env.ts"],
886
+ // #10051 D9 — the /templates hub imports the route-family helper and the
887
+ // template-listing mapping, its detail route the mapping; each lists both
888
+ // so the pair travels together to a site scaffolded before them.
889
+ templates: ["apps/web/lib/route-family.ts", "apps/web/lib/template-listing.ts"],
890
+ "templates/[slug]": ["apps/web/lib/route-family.ts", "apps/web/lib/template-listing.ts"],
831
891
  // #8765 D8/D9 — the sign-up page links its agreements through the one
832
892
  // localized policy map; the carried-cookie mint (`sign-up.actions.ts`) is
833
893
  // deleted, since the server now refuses and records the agreements.
@@ -1102,6 +1162,34 @@ export const ROUTE_DEPENDENCIES = {
1102
1162
  pricing: [
1103
1163
  "apps/web/lib/offerings-pricing.ts",
1104
1164
  "apps/web/lib/api-origin.generated.ts",
1165
+ // #10044 D9 — the shared Pricing render and the offering pages' list.
1166
+ "apps/web/lib/pricing-page.ts",
1167
+ "apps/web/lib/route-family.ts",
1168
+ ],
1169
+ // #10044 D9 — one offering's pricing page renders through the same lib,
1170
+ // which imports the route-family helper's `DetailPage` type.
1171
+ "pricing/[slug]": [
1172
+ "apps/web/lib/offerings-pricing.ts",
1173
+ "apps/web/lib/api-origin.generated.ts",
1174
+ "apps/web/lib/pricing-page.ts",
1175
+ "apps/web/lib/route-family.ts",
1176
+ ],
1177
+ // #10043 D11 — the services hub maps its links through the services
1178
+ // listing; a service page lists its category's children through it and the
1179
+ // route-family helper, and the nested route redirects with it. The
1180
+ // listing reads the base from `services-path.ts`, which the middleware
1181
+ // already carries; it is listed so a site whose middleware predates it
1182
+ // still compiles (a dependency copy never overwrites).
1183
+ services: ["apps/web/lib/service-listing.ts", "apps/web/lib/services-path.ts"],
1184
+ "services/[slug]": [
1185
+ "apps/web/lib/route-family.ts",
1186
+ "apps/web/lib/service-listing.ts",
1187
+ "apps/web/lib/services-path.ts",
1188
+ ],
1189
+ "services/[slug]/[child]": [
1190
+ "apps/web/lib/route-family.ts",
1191
+ "apps/web/lib/service-listing.ts",
1192
+ "apps/web/lib/services-path.ts",
1105
1193
  ],
1106
1194
  };
1107
1195
  /**
@@ -1136,6 +1224,8 @@ export const ROUTE_MESSAGE_NAMESPACES = {
1136
1224
  "(authed)/account/artifacts/[pieceId]": ["account"],
1137
1225
  // #8515 — the onboarding page.
1138
1226
  "(authed)/account/onboarding": ["account"],
1227
+ // #9966 D2 — the hub's consent page reads its own namespace.
1228
+ "(authed)/account/connect": ["hubConnect"],
1139
1229
  "(authed)/admin": ["admin"],
1140
1230
  "(authed)/leads": ["leadsInbox"],
1141
1231
  // #5977 — every string on both editor routes comes from the `edit`
@@ -1155,7 +1245,14 @@ export const ROUTE_MESSAGE_NAMESPACES = {
1155
1245
  // #8438 D21 — the unsubscribe landing's own namespace.
1156
1246
  unsubscribe: ["unsubscribe"],
1157
1247
  // #8927 D6 — the Offerings cards' CTA, recommendation and period phrases.
1158
- pricing: ["pricing", "pricingOfferings"],
1248
+ // #10044 D8 — the offering pricing pages list's heading and link label.
1249
+ pricing: ["pricing", "pricingOfferings", "pricingPages"],
1250
+ "pricing/[slug]": ["pricing", "pricingOfferings"],
1251
+ // #10051 D8 — the /templates cards, actions and empty state.
1252
+ templates: ["templates"],
1253
+ "templates/[slug]": ["templates"],
1254
+ // #10043 D11 — a category's list of its services (heading, link label).
1255
+ "services/[slug]": ["services"],
1159
1256
  };
1160
1257
  /** Template-relative path of the generated site-nav registry. */
1161
1258
  export const NAV_CONFIG_PATH = "apps/web/lib/nav-config.ts";
@@ -1409,6 +1506,9 @@ export function renderNavConfig(enabled, options) {
1409
1506
  "from ",
1410
1507
  '"@working-theory/ui";',
1411
1508
  ].join("");
1509
+ const publicPages = options?.publicPages !== false;
1510
+ const navEntries = publicPages ? entries : [];
1511
+ const footerColumnEntries = publicPages ? footerEntries : [];
1412
1512
  const body = `// nav-config.ts — site-nav registry.
1413
1513
  //
1414
1514
  // GENERATED by create-ailk from the declared route surfaces. Every entry here
@@ -1425,10 +1525,8 @@ export function renderNavConfig(enabled, options) {
1425
1525
  // \`apps/web/messages/<locale>.json\`. Paths are locale-agnostic — Nav prefixes
1426
1526
  // them with the active locale at render.
1427
1527
  ${navItemImport}
1428
- ${servicesHub ? 'import { ArrowRight } from "lucide-react";\n' : ""}
1429
- export const navItems: NavItem[] = [
1430
- ${entries.join("\n")}
1431
- ];
1528
+ ${servicesHub && publicPages ? 'import { ArrowRight } from "lucide-react";\n' : ""}
1529
+ export const navItems: NavItem[] = ${navEntries.length > 0 ? `[\n${navEntries.join("\n")}\n]` : "[]"};
1432
1530
 
1433
1531
  // Footer column groups. Keys only — the layout resolves each \`headingKey\`
1434
1532
  // and \`labelKey\` through the same \`nav\` labels as the header.
@@ -1437,9 +1535,7 @@ export interface FooterColumnConfig {
1437
1535
  items: { labelKey: string; path: string }[];
1438
1536
  }
1439
1537
 
1440
- export const footerColumns: FooterColumnConfig[] = [
1441
- ${footerEntries.join("\n")}
1442
- ];
1538
+ export const footerColumns: FooterColumnConfig[] = ${footerColumnEntries.length > 0 ? `[\n${footerColumnEntries.join("\n")}\n]` : "[]"};
1443
1539
  `;
1444
1540
  // D1 — a first-line stamp holding the SHA-256 of the rest of the file
1445
1541
  // (`body`), so an unedited nav proves itself whatever generator wrote it.
@@ -78,6 +78,13 @@ export interface SyncRoutesResult {
78
78
  missingMessageNamespaces: string[];
79
79
  /** Gateable surface ids the target does not carry, sorted. */
80
80
  skippedSurfaces: string[];
81
+ /**
82
+ * The template route dirs left out of scope because the site has dropped the
83
+ * `marketing` module (#10040 D10): its manifest lists no `marketing`,
84
+ * so every dir outside `HUB_KEPT_ROUTE_DIRS` is deliberate absence, never
85
+ * drift — not reported missing, not copied, `forceSurfaces` included.
86
+ */
87
+ publicTreeRemoved: string[];
81
88
  /**
82
89
  * When `apply` added a route the template exempts in its structured-data
83
90
  * test and the site's copy of that test is site-edited (its bytes differ
@@ -42,10 +42,11 @@ import { copyTemplate, renderTemplateFile } from "./copy.js";
42
42
  import { SPOKE_BILLING_ACTION_PATH } from "./derive-architecture.js";
43
43
  import { appendEnvRows } from "./lib/declared-env.js";
44
44
  import { ENV_CATALOG } from "./lib/env-catalog.js";
45
+ import { publicTreeRemoved } from "./lib/site-modules.js";
45
46
  import { BILLING_ACTION, BILLING_ACTION_MARKER, RETIRED_CHECKOUT_E2E_MARKER, RETIRED_CHECKOUT_E2E_WORKFLOW_PATH, RETIRED_SPOKE_CHECKOUT_PATHS, } from "./spoke-checkout.js";
46
- import { GATEABLE_SURFACES, NON_ROUTE_DIRS, ROUTE_DEPENDENCIES, ROUTE_MESSAGE_NAMESPACES, ROUTE_SINCE, surfaceForRouteDir, } from "./surfaces.js";
47
+ import { GATEABLE_SURFACES, HUB_KEPT_ROUTE_DIRS, NON_ROUTE_DIRS, ROUTE_DEPENDENCIES, ROUTE_MESSAGE_NAMESPACES, ROUTE_SINCE, surfaceForRouteDir, } from "./surfaces.js";
47
48
  import { TEMPLATE_RECORD_PATH, parseTemplateRecord, serializeTemplateRecord, sha256Hex, } from "./template-record.js";
48
- import { SPOKE_SEAM_FILES, isSpokeExcluded, readSpokeShape, } from "./topology.js";
49
+ import { SPOKE_SEAM_FILES, isSpokeExcluded, readSpokeShape, spokeExclusions, } from "./topology.js";
49
50
  /** Template-relative prefix every `apps/web` route directory sits under. */
50
51
  const ROUTE_ROOT = "apps/web/app/[locale]";
51
52
  /** The site test that fails a route neither rendered through `renderPage` nor in its `NO_STRUCTURED_DATA_ROUTES` (#8704). */
@@ -172,9 +173,13 @@ function walkRouteImports(route, ctx) {
172
173
  const differs = new Set();
173
174
  const unresolved = [];
174
175
  const reached = [];
176
+ // A file under a seed the spoke does not carry is never copied, so its
177
+ // imports are not walked (the hub's consent page under a hub sign-in spoke's
178
+ // account, #9966 D2).
175
179
  const queue = regularFilesUnder(ctx.scratch, route)
176
180
  .map((rel) => `${ROUTE_ROOT}/${rel}`)
177
- .filter((path) => ownerRoute(path, ctx.missingRoutes) === route);
181
+ .filter((path) => ownerRoute(path, ctx.missingRoutes) === route)
182
+ .filter((path) => ctx.spoke === undefined || !isSpokeExcluded(path, ctx.spoke));
178
183
  const visited = new Set(queue);
179
184
  for (let from = queue.shift(); from !== undefined; from = queue.shift()) {
180
185
  if (!SOURCE_FILE.test(from))
@@ -432,6 +437,20 @@ function regularFilesUnder(targetAbs, rel) {
432
437
  .sort()
433
438
  .flatMap((name) => regularFilesUnder(targetAbs, `${rel}/${name}`));
434
439
  }
440
+ /**
441
+ * The outermost paths strictly under route dir `route` of `routeRoot` that a
442
+ * spoke does not carry, relative to `route` — a directory is named once and
443
+ * not descended, so the copier skips it whole instead of leaving it empty.
444
+ */
445
+ function spokeExcludedUnder(routeRoot, route, spoke) {
446
+ const walk = (rel) => readdirSync(join(routeRoot, route, rel), { withFileTypes: true }).flatMap((entry) => {
447
+ const child = rel === "" ? entry.name : `${rel}/${entry.name}`;
448
+ if (isSpokeExcluded(`${ROUTE_ROOT}/${route}/${child}`, spoke))
449
+ return [child];
450
+ return entry.isDirectory() ? walk(child) : [];
451
+ });
452
+ return walk("");
453
+ }
435
454
  /** The first line of a text file. */
436
455
  function firstLine(text) {
437
456
  const eol = text.indexOf("\n");
@@ -608,7 +627,19 @@ export function syncRoutes(opts) {
608
627
  return (spoke !== undefined &&
609
628
  (SPOKE_SEAM_FILES.includes(path) || isSpokeExcluded(path, spoke)));
610
629
  };
630
+ // #10040 D10 — a site that dropped `marketing` carries no public route
631
+ // tree: every template route dir outside the kept set is out of scope.
632
+ const publicTreeSkipped = [];
633
+ const marketingRemoved = publicTreeRemoved(targetAbs);
611
634
  const inScope = listTemplateRouteDirs(opts.templatesDir)
635
+ .filter((route) => {
636
+ if (!marketingRemoved)
637
+ return true;
638
+ if (HUB_KEPT_ROUTE_DIRS.includes(route.split("/")[0]))
639
+ return true;
640
+ publicTreeSkipped.push(route);
641
+ return false;
642
+ })
612
643
  .map(reportFor)
613
644
  // "authed" (#5014): the `(authed)` route group ships unconditionally in
614
645
  // every hub scaffold, the same as "core" — it is checked regardless of
@@ -719,6 +750,11 @@ export function syncRoutes(opts) {
719
750
  const appliedDependencies = [];
720
751
  if (opts.apply === true) {
721
752
  const writable = missingRoutes.filter((r) => !refusedSet.has(r));
753
+ // #6786 — the closure a fresh spoke scaffold drops, not only its seeds:
754
+ // a template file that imports a seed goes with it here too.
755
+ const spokeShape = spoke === undefined
756
+ ? undefined
757
+ : { ...spoke, exclusions: spokeExclusions(opts.templatesDir, spoke) };
722
758
  for (const route of writable) {
723
759
  applied.push(route);
724
760
  // An ancestor route dir's subtree copy already brings this one.
@@ -734,9 +770,16 @@ export function syncRoutes(opts) {
734
770
  versions: opts.versions,
735
771
  markers,
736
772
  // #9406 DD4 — hold back each refused sub-route from the ancestor's copy.
737
- excludePaths: refused
738
- .filter((r) => r.startsWith(route + "/"))
739
- .map((r) => r.slice(route.length + 1)),
773
+ // #6786 — on a spoke, hold back every file the spoke does not carry,
774
+ // so an ancestor's subtree copy lands what a fresh spoke scaffold does.
775
+ excludePaths: [
776
+ ...refused
777
+ .filter((r) => r.startsWith(route + "/"))
778
+ .map((r) => r.slice(route.length + 1)),
779
+ ...(spokeShape === undefined
780
+ ? []
781
+ : spokeExcludedUnder(templateRouteRoot, route, spokeShape)),
782
+ ],
740
783
  });
741
784
  }
742
785
  for (const dep of missingDependencies) {
@@ -766,6 +809,7 @@ export function syncRoutes(opts) {
766
809
  appliedDependencies,
767
810
  missingMessageNamespaces,
768
811
  skippedSurfaces,
812
+ publicTreeRemoved: publicTreeSkipped,
769
813
  structuredDataEntries: structuredDataEntriesFor(targetAbs, opts.templatesDir, applied),
770
814
  retiredCheckout: spoke !== undefined
771
815
  ? retireLegacySpokeCheckout(targetAbs, opts.apply === true)
@@ -838,6 +882,9 @@ export function formatSyncRoutesReport(result, apply) {
838
882
  if (result.skippedSurfaces.length > 0) {
839
883
  lines.push(`[create-ailk] Surfaces this site does not carry (skipped): ${result.skippedSurfaces.join(", ")}`);
840
884
  }
885
+ if (result.publicTreeRemoved.length > 0) {
886
+ lines.push(`[create-ailk] Public route tree not carried (marketing removed, #10040): ${result.publicTreeRemoved.length} template route(s) skipped`);
887
+ }
841
888
  // #9406 DD5 — per missing route, what its imports copy, leave, or cannot
842
889
  // resolve; then the routes a dangling import holds back.
843
890
  if (result.routeImports.length > 0) {
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The templates route family's generated seeds (#10051 D10) — the /templates
3
+ * hub seed plus one placeholder TemplatePage per AILK template, derived from
4
+ * `TEMPLATE_MANIFESTS` so a new template never needs a hand-written seed.
5
+ * `add-route templates` writes them (`GENERATED_SEEDS` in `add-route.ts`);
6
+ * the site then edits or deletes them like any content.
7
+ *
8
+ * A seed never invents a fact: its copy is placeholder (`placeholder: true`,
9
+ * renderPage's replace-before-launch label), it has no thumbnail, and its Get
10
+ * it action is `/contact`. Only a template whose `templateDelivery` is
11
+ * `"free"` gets `price: 0` — a paid template's price is the site's to set.
12
+ *
13
+ * Pure (no fs). create-ailk has no YAML dependency by design, so every
14
+ * frontmatter value is written as `JSON.stringify(value)` — a JSON scalar,
15
+ * array or object is valid YAML flow syntax (the `derive-architecture.ts`
16
+ * precedent).
17
+ */
18
+ import { type ModuleName } from "./parse-args.js";
19
+ import { type TemplateManifest } from "./template-manifest.js";
20
+ /** One "What's included" row. */
21
+ export interface IncludeRow {
22
+ title: string;
23
+ description: string;
24
+ }
25
+ /** What every template carries whatever its modules: the base site. */
26
+ export declare const BASE_INCLUDE: IncludeRow;
27
+ /** One row per module, typed exhaustively so a new module fails type-check. */
28
+ export declare const MODULE_INCLUDES: Readonly<Record<ModuleName, IncludeRow>>;
29
+ /**
30
+ * The hub seed, then one TemplatePage seed per manifest, sorted by name.
31
+ * `manifests` defaults to the frozen registry; a test may pass its own.
32
+ */
33
+ export declare function templatePageSeeds(manifests?: Readonly<Record<string, TemplateManifest>>): {
34
+ path: string;
35
+ text: string;
36
+ }[];