blume 1.5.2 → 1.6.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 (194) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/cli/index.js +3639 -1377
  3. package/dist/cli/index.js.map +103 -91
  4. package/dist/types/ai/component-markdown.d.ts +79 -0
  5. package/dist/types/components/layout/nav-utils.d.ts +60 -0
  6. package/dist/types/core/base-path.d.ts +9 -0
  7. package/dist/types/core/config-input.d.ts +206 -4
  8. package/dist/types/core/config.d.ts +6 -4
  9. package/dist/types/core/data.d.ts +23 -1
  10. package/dist/types/core/github.d.ts +35 -0
  11. package/dist/types/core/i18n-ui.d.ts +8 -0
  12. package/dist/types/core/navigation.d.ts +69 -0
  13. package/dist/types/core/schema.d.ts +117 -1
  14. package/dist/types/core/sources/types.d.ts +31 -6
  15. package/dist/types/core/types.d.ts +23 -2
  16. package/dist/types/markdown/features.d.ts +21 -0
  17. package/dist/types/openapi/references.d.ts +21 -1
  18. package/dist/types/seo/jsonld.d.ts +105 -0
  19. package/dist/types/theme/fonts.d.ts +34 -4
  20. package/docs/_snippets/include-demo.mdx +7 -0
  21. package/docs/advanced/api-reference.mdx +3 -3
  22. package/docs/advanced/custom-pages.mdx +1 -1
  23. package/docs/advanced/graphql.mdx +84 -0
  24. package/docs/advanced/meta.ts +8 -1
  25. package/docs/configuration/ai.mdx +21 -3
  26. package/docs/configuration/index.mdx +24 -0
  27. package/docs/configuration/search.mdx +13 -1
  28. package/docs/configuration/seo.mdx +27 -0
  29. package/docs/configuration/theming.mdx +17 -0
  30. package/docs/content/components.mdx +7 -0
  31. package/docs/content/includes.mdx +68 -0
  32. package/docs/content/meta.ts +1 -0
  33. package/docs/content/navigation.mdx +25 -0
  34. package/docs/content/sources.mdx +42 -1
  35. package/docs/content/syntax.mdx +69 -1
  36. package/docs/content/versioning.mdx +15 -9
  37. package/docs/reference/cli.mdx +2 -1
  38. package/package.json +23 -14
  39. package/skills/blume-migrate/SKILL.md +16 -7
  40. package/skills/blume-migrate/references/docusaurus.md +5 -3
  41. package/skills/blume-migrate/references/fumadocs.md +10 -2
  42. package/skills/blume-migrate/references/mintlify.md +3 -2
  43. package/skills/blume-migrate/references/nextra.md +2 -2
  44. package/skills/blume-migrate/references/starlight.md +1 -1
  45. package/src/ai/agent-readability.ts +2 -1
  46. package/src/ai/ask-data.ts +2 -1
  47. package/src/ai/component-markdown.ts +199 -36
  48. package/src/ai/llms.ts +93 -6
  49. package/src/ai/markdown.ts +2 -2
  50. package/src/ai/mcp/discovery.ts +10 -2
  51. package/src/ai/mcp/server.ts +74 -2
  52. package/src/astro/generate.ts +183 -116
  53. package/src/astro/include-hmr.ts +81 -0
  54. package/src/astro/include-refresh.ts +0 -0
  55. package/src/astro/index.ts +3 -5
  56. package/src/astro/templates.ts +125 -76
  57. package/src/cli/commands/build.ts +84 -15
  58. package/src/cli/init/questions.ts +1 -0
  59. package/src/cli/init/scaffold.ts +27 -4
  60. package/src/components/colors.ts +142 -0
  61. package/src/components/content/Badge.astro +5 -12
  62. package/src/components/content/Callout.astro +19 -36
  63. package/src/components/content/Card.astro +15 -21
  64. package/src/components/content/Component.astro +10 -1
  65. package/src/components/content/GithubInfo.astro +28 -9
  66. package/src/components/content/Tabs.astro +27 -5
  67. package/src/components/content/github-info.ts +20 -5
  68. package/src/components/dropdown-dismiss.ts +122 -0
  69. package/src/components/layout/Fonts.astro +15 -8
  70. package/src/components/layout/Header.astro +44 -0
  71. package/src/components/layout/LanguageSwitcher.astro +9 -1
  72. package/src/components/layout/NavSelector.astro +12 -3
  73. package/src/components/layout/NavTree.astro +6 -18
  74. package/src/components/layout/PageActions.astro +29 -8
  75. package/src/components/layout/PageLayout.astro +10 -1
  76. package/src/components/layout/ReferenceLayout.astro +6 -1
  77. package/src/components/layout/RootLayout.astro +46 -15
  78. package/src/components/layout/Search.astro +36 -4
  79. package/src/components/layout/TableOfContents.astro +8 -2
  80. package/src/components/layout/head-scripts.ts +53 -1
  81. package/src/components/openapi/ApiOverview.astro +13 -3
  82. package/src/components/openapi/AsyncApiOperation.astro +7 -14
  83. package/src/components/openapi/GraphqlChip.astro +33 -0
  84. package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
  85. package/src/components/openapi/GraphqlOperation.astro +186 -0
  86. package/src/components/openapi/GraphqlType.astro +154 -0
  87. package/src/components/openapi/MethodBadge.astro +3 -14
  88. package/src/components/openapi/Operation.astro +12 -5
  89. package/src/components/openapi/OperationPanel.astro +43 -0
  90. package/src/components/openapi/RequestPanel.astro +5 -10
  91. package/src/components/openapi/Responses.astro +1 -16
  92. package/src/components/openapi/graphql-helpers.ts +466 -0
  93. package/src/components/openapi/playground-client.ts +15 -0
  94. package/src/components/openapi/sample-panels.ts +45 -0
  95. package/src/components/openapi/snippets.ts +13 -35
  96. package/src/core/base-path.ts +11 -0
  97. package/src/core/config-input.ts +209 -2
  98. package/src/core/config.ts +6 -4
  99. package/src/core/content-assets.ts +15 -4
  100. package/src/core/data.ts +18 -2
  101. package/src/core/diagnostics.ts +8 -0
  102. package/src/core/frontmatter.ts +20 -8
  103. package/src/core/github.ts +71 -0
  104. package/src/core/graph.ts +22 -8
  105. package/src/core/heading-markers.ts +96 -0
  106. package/src/core/i18n-ui.ts +11 -0
  107. package/src/core/includes.ts +632 -0
  108. package/src/core/last-modified.ts +36 -11
  109. package/src/core/links.ts +79 -13
  110. package/src/core/meta.ts +2 -1
  111. package/src/core/nav-diagnostics.ts +11 -2
  112. package/src/core/navigation.ts +27 -6
  113. package/src/core/project-graph.ts +61 -9
  114. package/src/core/schema.ts +226 -35
  115. package/src/core/server-features.ts +5 -9
  116. package/src/core/sources/github-releases.ts +2 -2
  117. package/src/core/sources/normalize.ts +502 -115
  118. package/src/core/sources/notion.ts +43 -8
  119. package/src/core/sources/obsidian.ts +1038 -0
  120. package/src/core/sources/read.ts +36 -1
  121. package/src/core/sources/resolve.ts +34 -1
  122. package/src/core/sources/types.ts +28 -6
  123. package/src/core/sources/watch.ts +12 -8
  124. package/src/core/tsconfig-aliases.ts +48 -35
  125. package/src/core/types.ts +25 -2
  126. package/src/core/ui-packs/ar.ts +1 -0
  127. package/src/core/ui-packs/bg.ts +2 -0
  128. package/src/core/ui-packs/bn.ts +1 -0
  129. package/src/core/ui-packs/ca.ts +2 -0
  130. package/src/core/ui-packs/cs.ts +1 -0
  131. package/src/core/ui-packs/da.ts +1 -0
  132. package/src/core/ui-packs/de.ts +2 -0
  133. package/src/core/ui-packs/el.ts +2 -0
  134. package/src/core/ui-packs/es.ts +2 -0
  135. package/src/core/ui-packs/fa.ts +1 -0
  136. package/src/core/ui-packs/fi.ts +1 -0
  137. package/src/core/ui-packs/fr.ts +2 -0
  138. package/src/core/ui-packs/he.ts +1 -0
  139. package/src/core/ui-packs/hi.ts +1 -0
  140. package/src/core/ui-packs/hr.ts +2 -0
  141. package/src/core/ui-packs/hu.ts +2 -0
  142. package/src/core/ui-packs/id.ts +2 -0
  143. package/src/core/ui-packs/it.ts +1 -0
  144. package/src/core/ui-packs/ja.ts +2 -0
  145. package/src/core/ui-packs/ko.ts +2 -0
  146. package/src/core/ui-packs/nl.ts +2 -0
  147. package/src/core/ui-packs/no.ts +2 -0
  148. package/src/core/ui-packs/pl.ts +2 -0
  149. package/src/core/ui-packs/pt-br.ts +2 -0
  150. package/src/core/ui-packs/pt.ts +2 -0
  151. package/src/core/ui-packs/ro.ts +2 -0
  152. package/src/core/ui-packs/ru.ts +2 -0
  153. package/src/core/ui-packs/sk.ts +1 -0
  154. package/src/core/ui-packs/sr.ts +1 -0
  155. package/src/core/ui-packs/sv.ts +2 -0
  156. package/src/core/ui-packs/th.ts +1 -0
  157. package/src/core/ui-packs/tr.ts +2 -0
  158. package/src/core/ui-packs/uk.ts +2 -0
  159. package/src/core/ui-packs/vi.ts +1 -0
  160. package/src/core/ui-packs/zh-tw.ts +1 -0
  161. package/src/core/ui-packs/zh.ts +1 -0
  162. package/src/core/version-cut.ts +21 -3
  163. package/src/core/yaml.ts +26 -0
  164. package/src/deploy/function-bundle.ts +251 -0
  165. package/src/eval/schema.ts +3 -1
  166. package/src/markdown/code-title.ts +22 -16
  167. package/src/markdown/features.ts +21 -0
  168. package/src/markdown/fence-meta.ts +50 -0
  169. package/src/markdown/heading-anchors.ts +198 -37
  170. package/src/markdown/include.ts +247 -0
  171. package/src/markdown/index.ts +43 -34
  172. package/src/markdown/language-icon.ts +2 -2
  173. package/src/markdown/mdast.ts +7 -3
  174. package/src/markdown/ts2js.ts +264 -0
  175. package/src/openapi/asyncapi.ts +4 -1
  176. package/src/openapi/graphql-build.ts +293 -0
  177. package/src/openapi/graphql.ts +212 -0
  178. package/src/openapi/model.ts +38 -5
  179. package/src/openapi/parse.ts +34 -0
  180. package/src/openapi/proxy.ts +30 -5
  181. package/src/openapi/references.ts +89 -13
  182. package/src/openapi/render-mdx.ts +48 -8
  183. package/src/openapi/scalar.ts +5 -12
  184. package/src/openapi/source.ts +91 -23
  185. package/src/registry/eject.ts +11 -0
  186. package/src/search/documents.ts +229 -37
  187. package/src/search/orama-index.ts +9 -5
  188. package/src/seo/jsonld.ts +293 -51
  189. package/src/theme/code-block-padding.ts +16 -0
  190. package/src/theme/entry.ts +65 -11
  191. package/src/theme/fonts.ts +189 -16
  192. package/src/translate/prompts.ts +2 -0
  193. package/src/translate/run.ts +7 -0
  194. package/src/translate/work-list.ts +0 -0
package/src/core/links.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
 
3
- import { basename, dirname, join } from "pathe";
3
+ import { basename, dirname, join, relative, resolve } from "pathe";
4
4
 
5
5
  import { stripBasePath, withBasePath } from "./base-path.ts";
6
6
  import {
@@ -37,6 +37,34 @@ interface LinkSite {
37
37
  line: number;
38
38
  }
39
39
 
40
+ /**
41
+ * Message context for a link written inside an included partial whose verdict
42
+ * depends on *which* page splices it (relative paths resolve against the
43
+ * including page's route; bare anchors against its headings). Without it the
44
+ * same partial line can be reported broken while being valid from another
45
+ * includer — with no way to tell the reports apart.
46
+ */
47
+ const includedBySuffix = (link: PageLink, page: PageRecord): string =>
48
+ link.file ? ` (included by ${basename(page.sourcePath ?? page.id)})` : "";
49
+
50
+ /**
51
+ * Invert the include image rebase for display: expansion rewrote a partial's
52
+ * relative image target into the including page's directory so it resolves,
53
+ * but the diagnostic points at the partial — report the path the author
54
+ * actually wrote there, not the rebased one they never typed.
55
+ */
56
+ const authoredImageTarget = (
57
+ target: string,
58
+ pagePath: string,
59
+ partialPath: string
60
+ ): string => {
61
+ const authored = relative(
62
+ dirname(partialPath),
63
+ resolve(dirname(pagePath), target)
64
+ );
65
+ return authored.startsWith(".") ? authored : `./${authored}`;
66
+ };
67
+
40
68
  /** An external link occurrence queued for a network probe. */
41
69
  interface ExternalRef extends LinkSite {
42
70
  url: string;
@@ -130,7 +158,10 @@ const buildAnchorIndex = (pages: PageRecord[]): Map<string, Set<string>> => {
130
158
  for (const page of pages) {
131
159
  anchors.set(
132
160
  page.route,
133
- new Set(page.headings.map((heading) => heading.slug))
161
+ new Set([
162
+ ...page.headings.map((heading) => heading.slug),
163
+ ...page.anchors,
164
+ ])
134
165
  );
135
166
  }
136
167
  return anchors;
@@ -141,15 +172,19 @@ const checkAnchor = (
141
172
  route: string,
142
173
  fragment: string,
143
174
  site: LinkSite,
144
- ctx: LinkContext
175
+ ctx: LinkContext,
176
+ via = ""
145
177
  ): Diagnostic | null => {
146
- if (ctx.anchors.get(route)?.has(fragment.toLowerCase())) {
178
+ const anchors = ctx.anchors.get(route);
179
+ // Exact first: a `[#custom-id]` pin may contain uppercase, which the
180
+ // lowercase fallback (lenient matching for slugger-generated ids) would miss.
181
+ if (anchors?.has(fragment) || anchors?.has(fragment.toLowerCase())) {
147
182
  return null;
148
183
  }
149
184
  return {
150
185
  ...site,
151
186
  code: "BLUME_BROKEN_ANCHOR",
152
- message: `No heading on ${route} matches anchor #${fragment}.`,
187
+ message: `No anchor target on ${route} matches #${fragment}${via}.`,
153
188
  severity: "warning",
154
189
  };
155
190
  };
@@ -161,7 +196,8 @@ const checkPathLink = (
161
196
  page: PageRecord,
162
197
  link: PageLink,
163
198
  site: LinkSite,
164
- ctx: LinkContext
199
+ ctx: LinkContext,
200
+ via = ""
165
201
  ): LinkResult => {
166
202
  // Page routes carry the site-wide base; an absolute author path is written
167
203
  // as if mounted at root, so base it for the route lookup (idempotent — a
@@ -170,7 +206,7 @@ const checkPathLink = (
170
206
  // (e.g. `/releases/v1.0`) isn't misread as a missing asset.
171
207
  const route = toRoute(withBasePath(ctx.basePath, resolved));
172
208
  if (ctx.routes.has(route)) {
173
- return fragment ? checkAnchor(route, fragment, site, ctx) : null;
209
+ return fragment ? checkAnchor(route, fragment, site, ctx, via) : null;
174
210
  }
175
211
  // A custom `.astro` page or generated route serves this path, but its
176
212
  // headings aren't indexed — accept any fragment rather than false-flag it.
@@ -203,10 +239,16 @@ const checkPathLink = (
203
239
  if (resolveRelativeImage(dirname(page.sourcePath), link.target)) {
204
240
  return null;
205
241
  }
242
+ // A partial-origin image was rebased into the page's directory for
243
+ // resolution but reports against the partial — cite the path as
244
+ // authored there, so file, path, and "next to" agree.
245
+ const authored = link.file
246
+ ? authoredImageTarget(link.target, page.sourcePath, link.file)
247
+ : link.target;
206
248
  return {
207
249
  ...site,
208
250
  code: "BLUME_BROKEN_ASSET",
209
- message: `Image ${link.target} was not found next to ${basename(page.sourcePath)}.`,
251
+ message: `Image ${authored} was not found next to ${basename(link.file ?? page.sourcePath)}.`,
210
252
  severity: "warning",
211
253
  suggestion:
212
254
  "Add the file next to the page source or fix the reference.",
@@ -228,7 +270,7 @@ const checkPathLink = (
228
270
  return {
229
271
  ...site,
230
272
  code: "BLUME_BROKEN_LINK",
231
- message: `Broken link to ${link.target}: no page resolves to ${route}.`,
273
+ message: `Broken link to ${link.target}${via}: no page resolves to ${route}.`,
232
274
  severity: "error",
233
275
  suggestion: "Check the path, or create the target page.",
234
276
  };
@@ -268,7 +310,9 @@ const classifyLink = (
268
310
  const { target } = link;
269
311
  const site: LinkSite = {
270
312
  column: link.column,
271
- file: page.sourcePath ?? page.id,
313
+ // A link written inside an included partial reports against the partial
314
+ // file, so the author fixes it where it lives.
315
+ file: link.file ?? page.sourcePath ?? page.id,
272
316
  line: link.line,
273
317
  };
274
318
 
@@ -298,14 +342,19 @@ const classifyLink = (
298
342
  }
299
343
  rawPath = decodePercent(rawPath);
300
344
 
345
+ // A relative path (or a bare anchor) means something different from every
346
+ // including page; an absolute path means the same thing everywhere — so
347
+ // only includer-dependent targets carry the "included by" context, and
348
+ // identical absolute-target reports collapse in the final dedupe.
349
+ const via = rawPath.startsWith("/") ? "" : includedBySuffix(link, page);
301
350
  if (rawPath === "") {
302
- return fragment ? checkAnchor(page.route, fragment, site, ctx) : null;
351
+ return fragment ? checkAnchor(page.route, fragment, site, ctx, via) : null;
303
352
  }
304
353
 
305
354
  const resolved = rawPath.startsWith("/")
306
355
  ? rawPath
307
356
  : resolveRelative(page.route, rawPath, isIndexPage(page));
308
- return checkPathLink(resolved, fragment, page, link, site, ctx);
357
+ return checkPathLink(resolved, fragment, page, link, site, ctx, via);
309
358
  };
310
359
 
311
360
  /**
@@ -371,5 +420,22 @@ export const validateLinks = async (
371
420
  diagnostics.push(...(await checkExternalLinks(external)));
372
421
  }
373
422
 
374
- return diagnostics;
423
+ // A partial spliced into several pages (or every locale of one) yields the
424
+ // same diagnostic once per including page record; byte-identical reports
425
+ // add noise without information, so only the first of each survives.
426
+ const seen = new Set<string>();
427
+ return diagnostics.filter((diagnostic) => {
428
+ const key = [
429
+ diagnostic.code,
430
+ diagnostic.file ?? "",
431
+ diagnostic.line ?? "",
432
+ diagnostic.column ?? "",
433
+ diagnostic.message,
434
+ ].join("\n");
435
+ if (seen.has(key)) {
436
+ return false;
437
+ }
438
+ seen.add(key);
439
+ return true;
440
+ });
375
441
  };
package/src/core/meta.ts CHANGED
@@ -5,6 +5,7 @@ import { diagnosticsFromZod } from "./diagnostics.ts";
5
5
  import { createModuleLoader } from "./load-module.ts";
6
6
  import { folderMetaSchema } from "./schema.ts";
7
7
  import type { FolderMeta } from "./schema.ts";
8
+ import { trimChar } from "./trim.ts";
8
9
  import type { Diagnostic } from "./types.ts";
9
10
 
10
11
  const META_FILES = [
@@ -50,7 +51,7 @@ export interface FolderMetaSource {
50
51
  * source keys to `docs/provider`, exactly the group path navigation builds.
51
52
  */
52
53
  const metaKeyFor = (prefix: string | undefined, dir: string): string => {
53
- const clean = prefix ? prefix.replaceAll(/^\/+|\/+$/gu, "") : "";
54
+ const clean = prefix ? trimChar(prefix, "/") : "";
54
55
  if (!clean) {
55
56
  return dir;
56
57
  }
@@ -1,5 +1,6 @@
1
1
  import { isAssetIcon } from "../theme/icon-kind.ts";
2
2
  import { hasIcon } from "../theme/icons.ts";
3
+ import { isInternalPath } from "./base-path.ts";
3
4
  import type { Diagnostic, NavNode, Navigation, PageRecord } from "./types.ts";
4
5
 
5
6
  /**
@@ -120,12 +121,20 @@ export const validateNavTargets = (
120
121
  label: link.label,
121
122
  path: link.href,
122
123
  })),
124
+ ...(navigation.actions ?? []).map((action) => ({
125
+ label: action.label,
126
+ path: action.href,
127
+ })),
128
+ ...(navigation.cta
129
+ ? [{ label: navigation.cta.label, path: navigation.cta.href }]
130
+ : []),
123
131
  ];
124
132
  const diagnostics: Diagnostic[] = [];
125
133
  const seen = new Set<string>();
126
134
  for (const { label, path } of targets) {
127
- // Only internal, non-anchor paths can be checked against routes.
128
- if (!path.startsWith("/") || path.startsWith("/#") || seen.has(path)) {
135
+ // Only internal, non-anchor paths can be checked against routes. A
136
+ // protocol-relative `//host/path` is external despite its leading slash.
137
+ if (!isInternalPath(path) || path.startsWith("/#") || seen.has(path)) {
129
138
  continue;
130
139
  }
131
140
  if (!resolvesToPages(routes, path.split("#")[0] ?? path)) {
@@ -9,6 +9,7 @@ import type {
9
9
  import type {
10
10
  Diagnostic,
11
11
  FeaturedLink,
12
+ HeaderAction,
12
13
  NavNode,
13
14
  Navigation,
14
15
  NavSelector,
@@ -788,6 +789,8 @@ const withTabHrefs = (tabs: NavTab[], sidebar: NavNode[]): NavTab[] =>
788
789
  * their exact authored shape.
789
790
  */
790
791
  interface NavChrome {
792
+ actions: HeaderAction[];
793
+ cta: HeaderAction | null;
791
794
  featured: FeaturedLink[];
792
795
  selectors: NavSelector[];
793
796
  tabs: NavTab[];
@@ -796,26 +799,33 @@ interface NavChrome {
796
799
  const rebaseNavChrome = (
797
800
  basePath: string,
798
801
  options: {
802
+ actions?: HeaderAction[];
803
+ cta?: HeaderAction | null;
799
804
  featured?: FeaturedLink[];
800
805
  selectors?: NavSelector[];
801
806
  tabs?: NavTab[];
802
807
  }
803
808
  ): NavChrome => {
809
+ const actions = options.actions ?? [];
810
+ const cta = options.cta ?? null;
804
811
  const featured = options.featured ?? [];
805
812
  const selectors = options.selectors ?? [];
806
813
  const tabs = options.tabs ?? [];
807
814
  if (!basePath) {
808
- return { featured, selectors, tabs };
815
+ return { actions, cta, featured, selectors, tabs };
809
816
  }
817
+ const rebaseHref = <T extends { href: string }>(item: T): T => ({
818
+ ...item,
819
+ href: withBasePath(basePath, item.href),
820
+ });
810
821
  const rebasePath = <T extends { path: string }>(item: T): T => ({
811
822
  ...item,
812
823
  path: withBasePath(basePath, item.path),
813
824
  });
814
825
  return {
815
- featured: featured.map((link) => ({
816
- ...link,
817
- href: withBasePath(basePath, link.href),
818
- })),
826
+ actions: actions.map(rebaseHref),
827
+ cta: cta ? rebaseHref(cta) : null,
828
+ featured: featured.map(rebaseHref),
819
829
  selectors: selectors.map((selector) => ({
820
830
  ...selector,
821
831
  items: selector.items.map(rebasePath),
@@ -838,8 +848,12 @@ const rebaseNavChrome = (
838
848
  export const buildNavigation = (
839
849
  pages: PageRecord[],
840
850
  options: {
851
+ /** Plain links in the header, left of the icon buttons. */
852
+ actions?: HeaderAction[];
841
853
  /** Site-wide route mount point (`""` or `/seg`); applied to config paths. */
842
854
  basePath?: string;
855
+ /** The single primary call to action in the header. */
856
+ cta?: HeaderAction | null;
843
857
  folderMeta: Map<string, FolderMeta>;
844
858
  /** Global display mode for every sidebar group (default `flat`). */
845
859
  display?: SidebarDisplay;
@@ -891,7 +905,10 @@ export const buildNavigation = (
891
905
  // Content-derived sidebar routes are already based via `page.route`; the
892
906
  // based tab paths also feed tab-scoping below, so they must agree with the
893
907
  // based content routes.
894
- const { featured, selectors, tabs } = rebaseNavChrome(basePath, options);
908
+ const { actions, cta, featured, selectors, tabs } = rebaseNavChrome(
909
+ basePath,
910
+ options
911
+ );
895
912
  const byRoute = new Map(
896
913
  pages.map((page) => [
897
914
  options.refByLogical ? page.translationKey : page.route,
@@ -939,6 +956,8 @@ export const buildNavigation = (
939
956
  basePath
940
957
  );
941
958
  return {
959
+ actions,
960
+ cta,
942
961
  featured,
943
962
  root: rootTabPath,
944
963
  selectors,
@@ -960,6 +979,8 @@ export const buildNavigation = (
960
979
  diagnostics
961
980
  );
962
981
  return {
982
+ actions,
983
+ cta,
963
984
  featured,
964
985
  root: rootTabPath,
965
986
  selectors,
@@ -1,11 +1,13 @@
1
- import { relative } from "pathe";
1
+ import { isAbsolute, relative } from "pathe";
2
2
 
3
3
  import { loadConfig } from "./config.ts";
4
4
  import { applyDeploymentEnv } from "./deployment-env.ts";
5
5
  import { buildContentGraph } from "./graph.ts";
6
6
  import { i18nDiagnostics } from "./i18n.ts";
7
+ import { expandIncludes, hasIncludeStatements } from "./includes.ts";
7
8
  import {
8
9
  gitLastModifiedTimes,
10
+ gitRepositoryRoot,
9
11
  lastModifiedShallowWarning,
10
12
  resolveLastModifiedConfig,
11
13
  } from "./last-modified.ts";
@@ -14,7 +16,7 @@ import { discoverFolderMeta } from "./meta.ts";
14
16
  import type { FolderMetaSource } from "./meta.ts";
15
17
  import { resolveProjectContext } from "./project.ts";
16
18
  import type { ResolvedConfig } from "./schema.ts";
17
- import { normalizeEntry } from "./sources/normalize.ts";
19
+ import { normalizeEntry, strippedLineOffset } from "./sources/normalize.ts";
18
20
  import { resolveDocsCollection, resolveSources } from "./sources/resolve.ts";
19
21
  import type { ContentSource, SourceLoadResult } from "./sources/types.ts";
20
22
  import type {
@@ -42,6 +44,15 @@ export interface ConfigOverrides {
42
44
  output?: ResolvedConfig["deployment"]["output"];
43
45
  }
44
46
 
47
+ /**
48
+ * Whether `path` sits at or under `dir`. A relative result that is itself
49
+ * absolute means a different drive root on Windows, which is outside too.
50
+ */
51
+ export const isWithin = (dir: string, path: string): boolean => {
52
+ const rel = relative(dir, path);
53
+ return !(rel.startsWith("..") || isAbsolute(rel));
54
+ };
55
+
45
56
  /** Apply CLI config overrides onto a resolved config (returns a new object). */
46
57
  const applyConfigOverrides = (
47
58
  config: ResolvedConfig,
@@ -268,6 +279,32 @@ export const scanProject = async (
268
279
  }),
269
280
  ]);
270
281
 
282
+ // Expand `<include>` statements before normalization, so heading/link/
283
+ // component extraction sees the spliced content every render surface shows.
284
+ // Filesystem entries only: includes resolve as filesystem paths bounded by
285
+ // the owning source's content root. Unresolvable statements stay verbatim
286
+ // and surface as error diagnostics here.
287
+ const includeDiagnostics: Diagnostic[] = [];
288
+ await Promise.all(
289
+ loaded.flatMap(({ source, entries }) => {
290
+ if (source.staged || !source.contentRoot) {
291
+ return [];
292
+ }
293
+ return entries.map(async (entry) => {
294
+ if (!entry.sourcePath || !hasIncludeStatements(entry.body.text)) {
295
+ return;
296
+ }
297
+ const expansion = await expandIncludes(entry.body.text, {
298
+ contentRoot: source.contentRoot,
299
+ lineOffset: strippedLineOffset(entry.raw, entry.body.text),
300
+ sourcePath: entry.sourcePath,
301
+ });
302
+ entry.expanded = expansion;
303
+ includeDiagnostics.push(...expansion.errors);
304
+ });
305
+ })
306
+ );
307
+
271
308
  // Folder meta contributed by the sources themselves (the OpenAPI source
272
309
  // labels each tag directory with the spec's own tag name). It applies to
273
310
  // every locale, so it merges into the shared map — beneath user-authored
@@ -300,13 +337,21 @@ export const scanProject = async (
300
337
  const fsPaths = pages
301
338
  .map((page) => page.sourcePath)
302
339
  .filter((path): path is string => path !== undefined);
303
- // The git pathspecs must cover where the pages actually live: each
304
- // filesystem source's own root, which diverges from the global
305
- // `content.root` when a source configures a non-default `root`.
306
- const contentRoots = sources.flatMap((source) =>
307
- source.staged || !source.contentRoot ? [] : [source.contentRoot]
340
+ // The git pathspecs must cover where the pages actually live: each local
341
+ // source's own on-disk root — a filesystem source's `root`, or a staged
342
+ // local source's tree (an Obsidian vault) — which diverges from the global
343
+ // `content.root`. A root outside the repository would fail the log outright
344
+ // and can never yield dates, so those are dropped up front.
345
+ const gitRoot = gitRepositoryRoot(context.root);
346
+ const contentRoots = sources
347
+ .flatMap((source) => (source.contentRoot ? [source.contentRoot] : []))
348
+ .filter((dir) => gitRoot !== null && isWithin(gitRoot, dir));
349
+ const gitTimes = gitLastModifiedTimes(
350
+ context.root,
351
+ contentRoots,
352
+ fsPaths,
353
+ gitRoot
308
354
  );
309
- const gitTimes = gitLastModifiedTimes(context.root, contentRoots, fsPaths);
310
355
  for (const page of pages) {
311
356
  if (!page.lastModified && page.sourcePath) {
312
357
  page.lastModified = gitTimes.get(page.sourcePath);
@@ -314,8 +359,14 @@ export const scanProject = async (
314
359
  }
315
360
  // A shallow CI clone (Vercel, actions/checkout) silently drops most dates;
316
361
  // surface that instead of letting production diverge from local builds.
362
+ // Only pages the log could have dated count — a page outside every covered
363
+ // root stays undated no matter how deep the clone is, and the warning's
364
+ // suggested fix cannot help it.
317
365
  const undated = pages.filter(
318
- (page) => page.sourcePath && !page.lastModified
366
+ (page) =>
367
+ page.sourcePath &&
368
+ !page.lastModified &&
369
+ contentRoots.some((dir) => isWithin(dir, page.sourcePath ?? ""))
319
370
  ).length;
320
371
  lastModifiedWarnings.push(
321
372
  ...lastModifiedShallowWarning(context.root, undated)
@@ -344,6 +395,7 @@ export const scanProject = async (
344
395
  context,
345
396
  diagnostics: [
346
397
  ...contentDiagnostics,
398
+ ...includeDiagnostics,
347
399
  ...folderMeta.diagnostics,
348
400
  ...entryIdDiagnostics(pages, resolveDocsCollection(config, context).base),
349
401
  ...graph.diagnostics,