blume 0.5.4 → 0.6.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 (57) hide show
  1. package/dist/cli/index.js +759 -406
  2. package/dist/cli/index.js.map +27 -25
  3. package/dist/types/core/config-input.d.ts +759 -0
  4. package/dist/types/core/config.d.ts +126 -3
  5. package/dist/types/core/data.d.ts +4 -0
  6. package/dist/types/core/i18n-ui.d.ts +50 -0
  7. package/dist/types/core/schema.d.ts +334 -62
  8. package/dist/types/core/types.d.ts +8 -0
  9. package/dist/types/index.d.ts +2 -1
  10. package/docs/advanced/changelog.mdx +10 -2
  11. package/docs/configuration/ai.mdx +56 -0
  12. package/docs/configuration/index.mdx +0 -2
  13. package/docs/configuration/seo.mdx +59 -1
  14. package/docs/configuration/theming.mdx +14 -9
  15. package/docs/content/meta.mdx +3 -17
  16. package/docs/content/navigation.mdx +41 -4
  17. package/docs/content/syntax.mdx +4 -8
  18. package/package.json +3 -1
  19. package/src/ai/agent-readability.ts +97 -0
  20. package/src/ai/ask-context.ts +131 -8
  21. package/src/ai/ask-data.ts +4 -1
  22. package/src/astro/generate.ts +40 -11
  23. package/src/astro/templates.ts +90 -10
  24. package/src/cli/commands/build.ts +41 -1
  25. package/src/cli/commands/dev.ts +31 -14
  26. package/src/cli/dev-lock.ts +94 -21
  27. package/src/components/content/GithubInfo.astro +11 -10
  28. package/src/components/content/TypeTable.astro +8 -3
  29. package/src/components/content/Update.astro +12 -2
  30. package/src/components/content/changelog-element.ts +62 -0
  31. package/src/components/islands/AskAI.astro +66 -2
  32. package/src/components/islands/ask-ai.tsx +289 -53
  33. package/src/components/layout/Header.astro +1 -1
  34. package/src/components/layout/NavTree.astro +1 -1
  35. package/src/components/layout/PageActions.astro +73 -30
  36. package/src/components/layout/RootLayout.astro +79 -10
  37. package/src/core/config-input.ts +933 -0
  38. package/src/core/config.ts +126 -3
  39. package/src/core/data.ts +4 -0
  40. package/src/core/graph.ts +7 -2
  41. package/src/core/i18n-ui.ts +5 -0
  42. package/src/core/nav-diagnostics.ts +7 -0
  43. package/src/core/navigation.ts +38 -12
  44. package/src/core/schema.ts +130 -22
  45. package/src/core/sources/filesystem.ts +5 -1
  46. package/src/core/sources/watch.ts +43 -12
  47. package/src/core/types.ts +9 -0
  48. package/src/deploy/adapter-output.ts +82 -0
  49. package/src/deploy/robots.ts +37 -4
  50. package/src/index.ts +1 -1
  51. package/src/markdown/index.ts +28 -30
  52. package/src/markdown/math.ts +3 -2
  53. package/src/openapi/scalar.ts +1 -1
  54. package/src/registry/eject.ts +21 -14
  55. package/src/search/documents.ts +9 -2
  56. package/src/theme/entry.ts +7 -3
  57. package/src/theme/palette.ts +21 -14
package/dist/cli/index.js CHANGED
@@ -513,11 +513,155 @@ Next steps:
513
513
  });
514
514
 
515
515
  // src/cli/commands/build.ts
516
- import { existsSync as existsSync14 } from "node:fs";
516
+ import { existsSync as existsSync15 } from "node:fs";
517
517
  import { readdir, stat, writeFile as writeFile7 } from "node:fs/promises";
518
518
  import { build } from "astro";
519
519
  import { defineCommand as defineCommand2 } from "citty";
520
- import { join as join23 } from "pathe";
520
+ import { join as join24 } from "pathe";
521
+
522
+ // src/deploy/xml.ts
523
+ var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
524
+
525
+ // src/deploy/rss.ts
526
+ var capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
527
+ var pageDate = (page) => {
528
+ const raw = page.meta.date ?? page.meta.changelog?.date;
529
+ if (!raw) {
530
+ return;
531
+ }
532
+ const date = new Date(raw);
533
+ return Number.isNaN(date.getTime()) ? undefined : date;
534
+ };
535
+ var buildRssFeeds = (project) => {
536
+ const { config } = project;
537
+ const { rss } = config.seo;
538
+ const { site } = config.deployment;
539
+ if (!(rss.enabled && site)) {
540
+ return [];
541
+ }
542
+ const base = site.replace(/\/$/u, "");
543
+ const feeds = [];
544
+ for (const type of rss.types) {
545
+ const pages = project.graph.pages.filter((page) => page.contentType === type && !(page.meta.draft || page.meta.sidebar.hidden));
546
+ if (pages.length === 0) {
547
+ continue;
548
+ }
549
+ const items = pages.map((page) => ({
550
+ date: pageDate(page),
551
+ description: page.description,
552
+ link: `${base}${page.route}`,
553
+ title: page.title
554
+ })).toSorted((a, b) => (b.date?.getTime() ?? 0) - (a.date?.getTime() ?? 0)).slice(0, rss.limit);
555
+ feeds.push({
556
+ description: config.description,
557
+ items,
558
+ link: base,
559
+ path: `/${type}/rss.xml`,
560
+ title: `${config.title} — ${capitalize(type)}`,
561
+ type
562
+ });
563
+ }
564
+ return feeds;
565
+ };
566
+ var renderItem = (item) => {
567
+ const parts = [
568
+ ` <title>${escapeXml(item.title)}</title>`,
569
+ ` <link>${escapeXml(item.link)}</link>`,
570
+ ` <guid isPermaLink="true">${escapeXml(item.link)}</guid>`
571
+ ];
572
+ if (item.description) {
573
+ parts.push(` <description>${escapeXml(item.description)}</description>`);
574
+ }
575
+ if (item.date) {
576
+ parts.push(` <pubDate>${item.date.toUTCString()}</pubDate>`);
577
+ }
578
+ return ` <item>
579
+ ${parts.join(`
580
+ `)}
581
+ </item>`;
582
+ };
583
+ var renderRssFeed = (feed) => {
584
+ const channel = [
585
+ ` <title>${escapeXml(feed.title)}</title>`,
586
+ ` <link>${escapeXml(feed.link)}</link>`,
587
+ ` <description>${escapeXml(feed.description ?? feed.title)}</description>`,
588
+ ` <atom:link href="${escapeXml(`${feed.link}${feed.path}`)}" rel="self" type="application/rss+xml" />`
589
+ ];
590
+ const items = feed.items.map(renderItem).join(`
591
+ `);
592
+ return `<?xml version="1.0" encoding="UTF-8"?>
593
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
594
+ <channel>
595
+ ${channel.join(`
596
+ `)}
597
+ ${items}
598
+ </channel>
599
+ </rss>
600
+ `;
601
+ };
602
+
603
+ // src/ai/agent-readability.ts
604
+ var USAGE_TOKENS = [
605
+ ["search", "search"],
606
+ ["aiInput", "ai-input"],
607
+ ["aiTrain", "ai-train"]
608
+ ];
609
+ var usagePolicy = (signals) => {
610
+ if (!signals) {
611
+ return null;
612
+ }
613
+ return Object.fromEntries(USAGE_TOKENS.map(([key, token]) => [token, signals[key]]));
614
+ };
615
+ var buildAgentReadability = (project) => {
616
+ const { config } = project;
617
+ if (!config.seo.agentReadability) {
618
+ return null;
619
+ }
620
+ const site = config.deployment.site ?? null;
621
+ const abs = (path) => site ? `${site.replace(/\/+$/u, "")}${path}` : path;
622
+ const artifacts = {
623
+ markdown: {
624
+ contentNegotiation: "text/markdown",
625
+ pattern: abs("/{route}.md")
626
+ }
627
+ };
628
+ if (config.ai.llmsTxt) {
629
+ artifacts.llmsFullTxt = abs("/llms-full.txt");
630
+ artifacts.llmsTxt = abs("/llms.txt");
631
+ }
632
+ if (config.mcp.enabled) {
633
+ artifacts.mcp = {
634
+ discovery: abs("/.well-known/mcp.json"),
635
+ url: abs(config.mcp.route)
636
+ };
637
+ }
638
+ if (config.ai.ask?.enabled) {
639
+ artifacts.askApi = abs("/api/ask");
640
+ }
641
+ if (site && config.seo.sitemap) {
642
+ artifacts.sitemap = abs("/sitemap.xml");
643
+ }
644
+ const feeds = site && config.seo.rss.enabled ? buildRssFeeds(project).map((feed) => abs(feed.path)) : [];
645
+ if (feeds.length > 0) {
646
+ artifacts.feeds = feeds;
647
+ }
648
+ const version = project.manifest?.blumeVersion;
649
+ const manifest = {
650
+ artifacts,
651
+ description: config.description,
652
+ generator: version ? `blume@${version}` : undefined,
653
+ name: config.mcp.name ?? config.title,
654
+ site
655
+ };
656
+ const usage = usagePolicy(config.seo.contentSignals);
657
+ if (usage) {
658
+ manifest.contentUsage = usage;
659
+ }
660
+ if (config.github) {
661
+ manifest.repository = `https://github.com/${config.github.owner}/${config.github.repo}`;
662
+ }
663
+ return manifest;
664
+ };
521
665
 
522
666
  // src/core/frontmatter.ts
523
667
  import baseMatter from "gray-matter";
@@ -700,6 +844,42 @@ var serverFeatures = (config) => {
700
844
  return features;
701
845
  };
702
846
 
847
+ // src/deploy/adapter-output.ts
848
+ import { existsSync as existsSync4 } from "node:fs";
849
+ import { cp, mkdir as mkdir2, rm } from "node:fs/promises";
850
+ import { dirname as dirname4, join as join6 } from "pathe";
851
+ var ADAPTER_OUTPUT_PATHS = {
852
+ netlify: ".netlify",
853
+ vercel: ".vercel/output"
854
+ };
855
+ var deployStaticDir = (config, context) => {
856
+ const { adapter, output } = config.deployment;
857
+ if (output === "server" && adapter === "vercel") {
858
+ return join6(context.root, ".vercel", "output", "static");
859
+ }
860
+ return context.distDir ?? join6(context.root, "dist");
861
+ };
862
+ var surfaceAdapterOutput = async (config, context) => {
863
+ const { adapter, output } = config.deployment;
864
+ if (output !== "server" || !adapter) {
865
+ return { moved: false };
866
+ }
867
+ const rel = ADAPTER_OUTPUT_PATHS[adapter];
868
+ if (!rel) {
869
+ return { moved: false };
870
+ }
871
+ const from = join6(context.outDir, rel);
872
+ const to = join6(context.root, rel);
873
+ if (!existsSync4(from)) {
874
+ return { moved: false };
875
+ }
876
+ await mkdir2(dirname4(to), { recursive: true });
877
+ await rm(to, { force: true, recursive: true });
878
+ await cp(from, to, { recursive: true });
879
+ await rm(from, { force: true, recursive: true });
880
+ return { from, ignore: `${rel.split("/")[0]}/`, moved: true, to };
881
+ };
882
+
703
883
  // src/deploy/redirects.ts
704
884
  var buildNetlifyRedirects = (redirects) => `${redirects.map((redirect) => `${redirect.from} ${redirect.to} ${redirect.status}`).join(`
705
885
  `)}
@@ -720,12 +900,29 @@ var buildRedirectManifest = (redirects) => `${JSON.stringify(redirects.map((redi
720
900
  `;
721
901
 
722
902
  // src/deploy/robots.ts
903
+ var SIGNAL_TOKENS = [
904
+ ["search", "search"],
905
+ ["aiInput", "ai-input"],
906
+ ["aiTrain", "ai-train"]
907
+ ];
908
+ var contentSignalLine = (signals) => {
909
+ if (!signals) {
910
+ return null;
911
+ }
912
+ const tokens = SIGNAL_TOKENS.map(([key, token]) => `${token}=${signals[key] ? "yes" : "no"}`);
913
+ return `Content-Signal: ${tokens.join(", ")}`;
914
+ };
723
915
  var buildRobots = (project) => {
724
916
  const { config } = project;
725
917
  if (!config.seo.robots) {
726
918
  return null;
727
919
  }
728
- const lines = ["User-agent: *", "Allow: /"];
920
+ const lines = ["User-agent: *"];
921
+ const signal = contentSignalLine(config.seo.contentSignals);
922
+ if (signal) {
923
+ lines.push(signal);
924
+ }
925
+ lines.push("Allow: /");
729
926
  const { site } = config.deployment;
730
927
  if (site && config.seo.sitemap) {
731
928
  lines.push("", `Sitemap: ${site.replace(/\/$/u, "")}/sitemap.xml`);
@@ -735,9 +932,6 @@ var buildRobots = (project) => {
735
932
  `;
736
933
  };
737
934
 
738
- // src/deploy/xml.ts
739
- var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
740
-
741
935
  // src/deploy/sitemap.ts
742
936
  var lastmodTag = (value) => {
743
937
  if (!value) {
@@ -762,7 +956,7 @@ ${urls.join(`
762
956
  };
763
957
 
764
958
  // src/search/build.ts
765
- import { join as join6 } from "pathe";
959
+ import { join as join7 } from "pathe";
766
960
  var buildSearchIndex = async (outDir) => {
767
961
  const pagefind = await import("pagefind");
768
962
  const { index } = await pagefind.createIndex({});
@@ -770,7 +964,7 @@ var buildSearchIndex = async (outDir) => {
770
964
  throw new Error("Failed to create Pagefind index.");
771
965
  }
772
966
  const result = await index.addDirectory({ path: outDir });
773
- await index.writeFiles({ outputPath: join6(outDir, "pagefind") });
967
+ await index.writeFiles({ outputPath: join7(outDir, "pagefind") });
774
968
  await pagefind.close();
775
969
  return result.page_count;
776
970
  };
@@ -2676,7 +2870,8 @@ var buildSearchDocuments = async (project, options) => {
2676
2870
  return await Promise.all(indexable.map(async (route) => {
2677
2871
  const page = pageById.get(route.id);
2678
2872
  const raw = page ? await readEntryText(project, page) : "";
2679
- const body = raw ? toPlainText(frontmatter_default(raw).content) : "";
2873
+ const source = raw ? frontmatter_default(raw).content : "";
2874
+ const body = options?.content === "markdown" ? source.trim() : toPlainText(source);
2680
2875
  const tags = page?.meta?.search?.tags;
2681
2876
  const crumb = crumbs.get(route.path);
2682
2877
  return {
@@ -2817,17 +3012,17 @@ var syncSearchProvider = async (project, reporter) => {
2817
3012
 
2818
3013
  // src/cli/dev-lock.ts
2819
3014
  import {
2820
- existsSync as existsSync5,
3015
+ existsSync as existsSync6,
2821
3016
  mkdirSync,
2822
3017
  readFileSync as readFileSync2,
2823
3018
  rmSync,
2824
3019
  writeFileSync
2825
3020
  } from "node:fs";
2826
- import { join as join8 } from "pathe";
3021
+ import { join as join9 } from "pathe";
2827
3022
 
2828
3023
  // src/core/project.ts
2829
- import { existsSync as existsSync4 } from "node:fs";
2830
- import { isAbsolute, join as join7, resolve as resolve2 } from "pathe";
3024
+ import { existsSync as existsSync5 } from "node:fs";
3025
+ import { isAbsolute, join as join8, resolve as resolve2 } from "pathe";
2831
3026
  var CONFIG_FILENAMES = [
2832
3027
  "blume.config.ts",
2833
3028
  "blume.config.mjs",
@@ -2837,22 +3032,22 @@ var THEME_FILENAMES = ["theme.css"];
2837
3032
  var COMPONENTS_FILENAMES = ["components.tsx", "components.ts"];
2838
3033
  var firstExisting = (root, names) => {
2839
3034
  for (const name of names) {
2840
- const candidate = join7(root, name);
2841
- if (existsSync4(candidate)) {
3035
+ const candidate = join8(root, name);
3036
+ if (existsSync5(candidate)) {
2842
3037
  return candidate;
2843
3038
  }
2844
3039
  }
2845
3040
  return null;
2846
3041
  };
2847
3042
  var findConfigFile = (root) => firstExisting(root, CONFIG_FILENAMES);
2848
- var resolveRuntimeDir = (root, runtimeDir = ".blume") => isAbsolute(runtimeDir) ? runtimeDir : join7(resolve2(root), runtimeDir);
3043
+ var resolveRuntimeDir = (root, runtimeDir = ".blume") => isAbsolute(runtimeDir) ? runtimeDir : join8(resolve2(root), runtimeDir);
2849
3044
  var resolveProjectContext = (root, config, options) => {
2850
3045
  const absoluteRoot = resolve2(root);
2851
- const contentRoot = isAbsolute(config.content.root) ? config.content.root : join7(absoluteRoot, config.content.root);
2852
- const pagesPath = join7(absoluteRoot, config.content.pages);
2853
- const pagesRoot = existsSync4(pagesPath) ? pagesPath : null;
3046
+ const contentRoot = isAbsolute(config.content.root) ? config.content.root : join8(absoluteRoot, config.content.root);
3047
+ const pagesPath = join8(absoluteRoot, config.content.pages);
3048
+ const pagesRoot = existsSync5(pagesPath) ? pagesPath : null;
2854
3049
  const outDir = resolveRuntimeDir(absoluteRoot, options?.runtimeDir);
2855
- const distDir = options?.runtimeDir ? join7(outDir, "dist") : join7(absoluteRoot, "dist");
3050
+ const distDir = options?.runtimeDir ? join8(outDir, "dist") : join8(absoluteRoot, "dist");
2856
3051
  return {
2857
3052
  componentsFile: firstExisting(absoluteRoot, COMPONENTS_FILENAMES),
2858
3053
  configFile: findConfigFile(absoluteRoot),
@@ -2866,16 +3061,27 @@ var resolveProjectContext = (root, config, options) => {
2866
3061
  };
2867
3062
 
2868
3063
  // src/cli/dev-lock.ts
2869
- var lockPath = (outDir) => join8(outDir, "dev.lock");
2870
- var isDevLocked = (outDir) => {
2871
- const path = lockPath(outDir);
2872
- if (!existsSync5(path)) {
2873
- return false;
3064
+ var lockPath = (outDir) => join9(outDir, "dev.lock");
3065
+ var isValidPid = (pid) => typeof pid === "number" && Number.isInteger(pid) && pid > 0;
3066
+ var parseLock = (raw) => {
3067
+ let data;
3068
+ try {
3069
+ data = JSON.parse(raw.trim());
3070
+ } catch {
3071
+ return null;
2874
3072
  }
2875
- const pid = Number.parseInt(readFileSync2(path, "utf-8").trim(), 10);
2876
- if (!(Number.isInteger(pid) && pid > 0)) {
2877
- return false;
3073
+ if (isValidPid(data)) {
3074
+ return { pid: data };
2878
3075
  }
3076
+ if (typeof data === "object" && data !== null) {
3077
+ const { pid, port } = data;
3078
+ if (isValidPid(pid)) {
3079
+ return typeof port === "number" ? { pid, port } : { pid };
3080
+ }
3081
+ }
3082
+ return null;
3083
+ };
3084
+ var isProcessAlive = (pid) => {
2879
3085
  try {
2880
3086
  process.kill(pid, 0);
2881
3087
  return true;
@@ -2883,10 +3089,34 @@ var isDevLocked = (outDir) => {
2883
3089
  return error.code === "EPERM";
2884
3090
  }
2885
3091
  };
2886
- var acquireDevLock = (outDir) => {
3092
+ var readDevLock = (outDir) => {
3093
+ const path = lockPath(outDir);
3094
+ if (!existsSync6(path)) {
3095
+ return null;
3096
+ }
3097
+ const lock = parseLock(readFileSync2(path, "utf-8"));
3098
+ return lock && isProcessAlive(lock.pid) ? lock : null;
3099
+ };
3100
+ var writeLock = (outDir, port) => {
3101
+ writeFileSync(lockPath(outDir), JSON.stringify({
3102
+ pid: process.pid,
3103
+ ...port === undefined ? {} : { port }
3104
+ }));
3105
+ };
3106
+ var ownsLock = (outDir) => {
2887
3107
  const path = lockPath(outDir);
3108
+ if (!existsSync6(path)) {
3109
+ return false;
3110
+ }
3111
+ try {
3112
+ return parseLock(readFileSync2(path, "utf-8"))?.pid === process.pid;
3113
+ } catch {
3114
+ return false;
3115
+ }
3116
+ };
3117
+ var acquireDevLock = (outDir, port) => {
2888
3118
  mkdirSync(outDir, { recursive: true });
2889
- writeFileSync(path, String(process.pid));
3119
+ writeLock(outDir, port);
2890
3120
  let released = false;
2891
3121
  return () => {
2892
3122
  if (released) {
@@ -2894,38 +3124,46 @@ var acquireDevLock = (outDir) => {
2894
3124
  }
2895
3125
  released = true;
2896
3126
  try {
2897
- if (existsSync5(path) && readFileSync2(path, "utf-8").trim() === String(process.pid)) {
2898
- rmSync(path, { force: true });
3127
+ if (ownsLock(outDir)) {
3128
+ rmSync(lockPath(outDir), { force: true });
2899
3129
  }
2900
3130
  } catch {}
2901
3131
  };
2902
3132
  };
3133
+ var updateDevLockPort = (outDir, port) => {
3134
+ if (ownsLock(outDir)) {
3135
+ writeLock(outDir, port);
3136
+ }
3137
+ };
3138
+ var describeDevLock = (lock) => lock.port === undefined ? "" : ` at http://localhost:${lock.port}`;
2903
3139
  var refuseIfDevRunning = (root, action, runtimeDir) => {
2904
- if (isDevLocked(resolveRuntimeDir(root, runtimeDir))) {
2905
- logger.error(`A \`blume dev\` server is running against .blume; ${action} would corrupt it. Stop the dev server, or re-run with --isolated to build/verify against .blume-verify without touching it.`);
3140
+ const lock = readDevLock(resolveRuntimeDir(root, runtimeDir));
3141
+ if (lock) {
3142
+ logger.error(`A \`blume dev\` server is running${describeDevLock(lock)}; ${action} would corrupt its .blume runtime. Reuse that server, stop it first, or re-run with --isolated to build/verify against .blume-verify without touching it.`);
2906
3143
  process.exit(1);
2907
3144
  }
2908
3145
  };
2909
3146
 
2910
3147
  // src/astro/generate.ts
2911
- import { existsSync as existsSync9, readFileSync as readFileSync6, realpathSync } from "node:fs";
3148
+ import { existsSync as existsSync10, readFileSync as readFileSync6, realpathSync } from "node:fs";
2912
3149
  import {
2913
3150
  lstat,
2914
- mkdir as mkdir4,
3151
+ mkdir as mkdir5,
2915
3152
  readFile as readFile10,
2916
3153
  rename,
2917
- rm,
3154
+ rm as rm2,
2918
3155
  symlink,
2919
3156
  writeFile as writeFile5
2920
3157
  } from "node:fs/promises";
2921
3158
  import { createRequire as createRequire5 } from "node:module";
2922
3159
  import { pathToFileURL as pathToFileURL2 } from "node:url";
2923
- import { basename as basename2, dirname as dirname7, join as join16, normalize as normalize3, relative as relative7 } from "pathe";
3160
+ import { basename as basename2, dirname as dirname8, join as join17, normalize as normalize3, relative as relative7 } from "pathe";
2924
3161
  import { glob as glob4 } from "tinyglobby";
2925
3162
 
2926
3163
  // src/ai/ask-data.ts
2927
3164
  var buildAskData = async (project) => {
2928
3165
  const documents = await buildSearchDocuments(project, {
3166
+ content: "markdown",
2929
3167
  includeWhenDisabled: true
2930
3168
  });
2931
3169
  return {
@@ -3158,8 +3396,8 @@ var validateUsedComponents = (pages, extraTags, registryNames) => {
3158
3396
  };
3159
3397
 
3160
3398
  // src/core/component-overrides.ts
3161
- import { existsSync as existsSync6 } from "node:fs";
3162
- import { dirname as dirname4, extname, isAbsolute as isAbsolute2, resolve as resolve3 } from "pathe";
3399
+ import { existsSync as existsSync7 } from "node:fs";
3400
+ import { dirname as dirname5, extname, isAbsolute as isAbsolute2, resolve as resolve3 } from "pathe";
3163
3401
  import ts from "typescript";
3164
3402
  var GROUPS = ["mdx", "layout", "islands"];
3165
3403
  var FRAMEWORK_BY_EXT = {
@@ -3247,7 +3485,7 @@ var findDefaultExportObject = (sourceFile) => {
3247
3485
  var probeExtension = (base) => {
3248
3486
  for (const extension of COMPONENT_EXTS) {
3249
3487
  const candidate = `${base}.${extension}`;
3250
- if (existsSync6(candidate)) {
3488
+ if (existsSync7(candidate)) {
3251
3489
  return candidate;
3252
3490
  }
3253
3491
  }
@@ -3394,7 +3632,7 @@ var analyzeComponentOverrides = (source, filePath) => {
3394
3632
  return result;
3395
3633
  }
3396
3634
  const imports = collectImports(sourceFile);
3397
- const dir = dirname4(filePath);
3635
+ const dir = dirname5(filePath);
3398
3636
  for (const property of object.properties) {
3399
3637
  if (!ts.isPropertyAssignment(property)) {
3400
3638
  continue;
@@ -3424,6 +3662,7 @@ var uiStringsObject = z.object({
3424
3662
  connectMcp: z.string().default("Connect to MCP"),
3425
3663
  copied: z.string().default("Copied!"),
3426
3664
  copyClaudeCode: z.string().default("Copy Claude Code command"),
3665
+ copyCodex: z.string().default("Copy Codex command"),
3427
3666
  copyMarkdown: z.string().default("Copy as Markdown"),
3428
3667
  copyServerUrl: z.string().default("Copy server URL"),
3429
3668
  edit: z.string().default("Edit on GitHub"),
@@ -3431,11 +3670,15 @@ var uiStringsObject = z.object({
3431
3670
  scrollToTop: z.string().default("Scroll to top")
3432
3671
  }).default({}),
3433
3672
  ask: z.object({
3673
+ clear: z.string().default("Clear conversation"),
3674
+ close: z.string().default("Close"),
3675
+ copy: z.string().default("Copy conversation"),
3434
3676
  empty: z.string().default("Ask a question about the docs."),
3435
3677
  error: z.string().default("Sorry, something went wrong."),
3436
3678
  label: z.string().default("Ask a question"),
3437
3679
  placeholder: z.string().default("Ask a question…"),
3438
3680
  send: z.string().default("Send"),
3681
+ tip: z.string().default("Tip: You can open and close chat with"),
3439
3682
  title: z.string().default("Ask AI")
3440
3683
  }).default({}),
3441
3684
  feedback: z.object({
@@ -3568,6 +3811,9 @@ var collectIcons = (navigation) => {
3568
3811
  push(item.icon, `selector "${item.label}"`);
3569
3812
  }
3570
3813
  }
3814
+ for (const link of navigation.featured) {
3815
+ push(link.icon, `featured link "${link.label}"`);
3816
+ }
3571
3817
  const sidebars = [navigation.sidebar];
3572
3818
  for (const sidebar of sidebars) {
3573
3819
  for (const node of flattenNodes(sidebar)) {
@@ -3597,7 +3843,11 @@ var resolvesToPages = (routes, path) => routes.has(path) || [...routes].some((ro
3597
3843
  var validateNavTargets = (navigation, routes) => {
3598
3844
  const targets = [
3599
3845
  ...navigation.tabs.map((tab) => ({ label: tab.label, path: tab.path })),
3600
- ...navigation.selectors.flatMap((selector) => selector.items.map((item) => ({ label: item.label, path: item.path })))
3846
+ ...navigation.selectors.flatMap((selector) => selector.items.map((item) => ({ label: item.label, path: item.path }))),
3847
+ ...navigation.featured.map((link) => ({
3848
+ label: link.label,
3849
+ path: link.href
3850
+ }))
3601
3851
  ];
3602
3852
  const diagnostics = [];
3603
3853
  const seen = new Set;
@@ -3677,10 +3927,10 @@ var validateNavStructure = (navigation, pages) => [
3677
3927
  ];
3678
3928
 
3679
3929
  // src/core/tsconfig-aliases.ts
3680
- import { existsSync as existsSync7, readFileSync as readFileSync3, statSync } from "node:fs";
3930
+ import { existsSync as existsSync8, readFileSync as readFileSync3, statSync } from "node:fs";
3681
3931
  import { createRequire as createRequire3 } from "node:module";
3682
3932
  import { pathToFileURL } from "node:url";
3683
- import { dirname as dirname5, isAbsolute as isAbsolute3, join as join9, resolve as resolve4 } from "pathe";
3933
+ import { dirname as dirname6, isAbsolute as isAbsolute3, join as join10, resolve as resolve4 } from "pathe";
3684
3934
  var stripJsonComments = (text) => {
3685
3935
  let out = "";
3686
3936
  let inString = false;
@@ -3743,7 +3993,7 @@ var resolveExtends = (spec, fromDir) => {
3743
3993
  return candidates.find(isFile) ?? null;
3744
3994
  }
3745
3995
  try {
3746
- const require_ = createRequire3(pathToFileURL(join9(fromDir, "_.js")).href);
3996
+ const require_ = createRequire3(pathToFileURL(join10(fromDir, "_.js")).href);
3747
3997
  for (const sub of [`${spec}/tsconfig.json`, spec]) {
3748
3998
  try {
3749
3999
  return require_.resolve(sub);
@@ -3753,7 +4003,7 @@ var resolveExtends = (spec, fromDir) => {
3753
4003
  return null;
3754
4004
  };
3755
4005
  var loadPaths = (file, seen) => {
3756
- if (seen.has(file) || !existsSync7(file)) {
4006
+ if (seen.has(file) || !existsSync8(file)) {
3757
4007
  return null;
3758
4008
  }
3759
4009
  seen.add(file);
@@ -3765,7 +4015,7 @@ var loadPaths = (file, seen) => {
3765
4015
  if (options.paths && typeof options.paths === "object") {
3766
4016
  const baseUrl = typeof options.baseUrl === "string" ? options.baseUrl : ".";
3767
4017
  return {
3768
- baseDir: resolve4(dirname5(file), baseUrl),
4018
+ baseDir: resolve4(dirname6(file), baseUrl),
3769
4019
  paths: options.paths
3770
4020
  };
3771
4021
  }
@@ -3774,7 +4024,7 @@ var loadPaths = (file, seen) => {
3774
4024
  if (typeof base !== "string") {
3775
4025
  continue;
3776
4026
  }
3777
- const resolved = resolveExtends(base, dirname5(file));
4027
+ const resolved = resolveExtends(base, dirname6(file));
3778
4028
  const found = resolved ? loadPaths(resolved, seen) : null;
3779
4029
  if (found) {
3780
4030
  return found;
@@ -3795,7 +4045,7 @@ var toAlias = (key, value, baseDir) => {
3795
4045
  return { find, replacement: resolve4(baseDir, target) };
3796
4046
  };
3797
4047
  var resolveTsconfigAliases = (root) => {
3798
- const entry = ["tsconfig.json", "jsconfig.json"].map((name) => join9(root, name)).find((file) => existsSync7(file));
4048
+ const entry = ["tsconfig.json", "jsconfig.json"].map((name) => join10(root, name)).find((file) => existsSync8(file));
3799
4049
  if (!entry) {
3800
4050
  return {};
3801
4051
  }
@@ -3813,84 +4063,6 @@ var resolveTsconfigAliases = (root) => {
3813
4063
  return aliases;
3814
4064
  };
3815
4065
 
3816
- // src/deploy/rss.ts
3817
- var capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
3818
- var pageDate = (page) => {
3819
- const raw = page.meta.date ?? page.meta.changelog?.date;
3820
- if (!raw) {
3821
- return;
3822
- }
3823
- const date = new Date(raw);
3824
- return Number.isNaN(date.getTime()) ? undefined : date;
3825
- };
3826
- var buildRssFeeds = (project) => {
3827
- const { config } = project;
3828
- const { rss } = config.seo;
3829
- const { site } = config.deployment;
3830
- if (!(rss.enabled && site)) {
3831
- return [];
3832
- }
3833
- const base = site.replace(/\/$/u, "");
3834
- const feeds = [];
3835
- for (const type of rss.types) {
3836
- const pages = project.graph.pages.filter((page) => page.contentType === type && !(page.meta.draft || page.meta.sidebar.hidden));
3837
- if (pages.length === 0) {
3838
- continue;
3839
- }
3840
- const items = pages.map((page) => ({
3841
- date: pageDate(page),
3842
- description: page.description,
3843
- link: `${base}${page.route}`,
3844
- title: page.title
3845
- })).toSorted((a, b) => (b.date?.getTime() ?? 0) - (a.date?.getTime() ?? 0)).slice(0, rss.limit);
3846
- feeds.push({
3847
- description: config.description,
3848
- items,
3849
- link: base,
3850
- path: `/${type}/rss.xml`,
3851
- title: `${config.title} — ${capitalize(type)}`,
3852
- type
3853
- });
3854
- }
3855
- return feeds;
3856
- };
3857
- var renderItem = (item) => {
3858
- const parts = [
3859
- ` <title>${escapeXml(item.title)}</title>`,
3860
- ` <link>${escapeXml(item.link)}</link>`,
3861
- ` <guid isPermaLink="true">${escapeXml(item.link)}</guid>`
3862
- ];
3863
- if (item.description) {
3864
- parts.push(` <description>${escapeXml(item.description)}</description>`);
3865
- }
3866
- if (item.date) {
3867
- parts.push(` <pubDate>${item.date.toUTCString()}</pubDate>`);
3868
- }
3869
- return ` <item>
3870
- ${parts.join(`
3871
- `)}
3872
- </item>`;
3873
- };
3874
- var renderRssFeed = (feed) => {
3875
- const channel = [
3876
- ` <title>${escapeXml(feed.title)}</title>`,
3877
- ` <link>${escapeXml(feed.link)}</link>`,
3878
- ` <description>${escapeXml(feed.description ?? feed.title)}</description>`,
3879
- ` <atom:link href="${escapeXml(`${feed.link}${feed.path}`)}" rel="self" type="application/rss+xml" />`
3880
- ];
3881
- const items = feed.items.map(renderItem).join(`
3882
- `);
3883
- return `<?xml version="1.0" encoding="UTF-8"?>
3884
- <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
3885
- <channel>
3886
- ${channel.join(`
3887
- `)}
3888
- ${items}
3889
- </channel>
3890
- </rss>
3891
- `;
3892
- };
3893
-
3894
4066
  // src/openapi/references.ts
3895
4067
  var NON_SLUG = /[^a-z0-9]+/gu;
3896
4068
  var SLUG_EDGES = /^-+|-+$/gu;
@@ -3971,11 +4143,35 @@ var hasScalarReferences = (config) => resolveReferences(config).some((ref) => re
3971
4143
 
3972
4144
  // src/openapi/scalar.ts
3973
4145
  import { readFile as readFile5 } from "node:fs/promises";
3974
- import { isAbsolute as isAbsolute5, join as join11 } from "pathe";
4146
+ import { isAbsolute as isAbsolute5, join as join12 } from "pathe";
3975
4147
 
3976
4148
  // src/astro/templates.ts
3977
- import { existsSync as existsSync8, readFileSync as readFileSync4 } from "node:fs";
3978
- import { dirname as dirname6, isAbsolute as isAbsolute4, join as join10, relative as relative4 } from "pathe";
4149
+ import { existsSync as existsSync9, readFileSync as readFileSync4 } from "node:fs";
4150
+ import { dirname as dirname7, isAbsolute as isAbsolute4, join as join11, relative as relative4 } from "pathe";
4151
+
4152
+ // src/core/sources/watch.ts
4153
+ var BLUME_IGNORE_DIRS = [
4154
+ ".blume",
4155
+ ".cache",
4156
+ ".git",
4157
+ ".next",
4158
+ ".turbo",
4159
+ ".vercel",
4160
+ "dist",
4161
+ "node_modules"
4162
+ ];
4163
+ var baselineScanIgnore = () => BLUME_IGNORE_DIRS.map((dir) => `**/${dir}/**`);
4164
+ var BLUME_WATCH_IGNORE_DIRS = BLUME_IGNORE_DIRS;
4165
+ var excludeDirSegments = (patterns) => patterns.map((pattern) => /^(?<dir>[^*/]+)\/\*\*$/u.exec(pattern)?.groups?.dir).filter((dir) => dir !== undefined);
4166
+ var ignoringWatchListener = (onChange, ignoreDirs = BLUME_WATCH_IGNORE_DIRS) => {
4167
+ const ignore = new Set(ignoreDirs);
4168
+ return (_event, filename) => {
4169
+ if (typeof filename === "string" && filename.split(/[/\\]/u).some((segment) => ignore.has(segment))) {
4170
+ return;
4171
+ }
4172
+ onChange();
4173
+ };
4174
+ };
3979
4175
 
3980
4176
  // src/theme/fonts.ts
3981
4177
  var FALLBACKS = {
@@ -4133,7 +4329,7 @@ var WORKSPACE_MARKERS = [
4133
4329
  "yarn.lock"
4134
4330
  ];
4135
4331
  var hasWorkspacesField = (pkgPath) => {
4136
- if (!existsSync8(pkgPath)) {
4332
+ if (!existsSync9(pkgPath)) {
4137
4333
  return false;
4138
4334
  }
4139
4335
  try {
@@ -4143,14 +4339,14 @@ var hasWorkspacesField = (pkgPath) => {
4143
4339
  return false;
4144
4340
  }
4145
4341
  };
4146
- var hasWorkspaceMarker = (dir) => hasWorkspacesField(join10(dir, "package.json")) || WORKSPACE_MARKERS.some((marker) => existsSync8(join10(dir, marker)));
4342
+ var hasWorkspaceMarker = (dir) => hasWorkspacesField(join11(dir, "package.json")) || WORKSPACE_MARKERS.some((marker) => existsSync9(join11(dir, marker)));
4147
4343
  var findWorkspaceRoot = (start) => {
4148
4344
  let dir = start;
4149
4345
  for (;; ) {
4150
4346
  if (hasWorkspaceMarker(dir)) {
4151
4347
  return dir;
4152
4348
  }
4153
- const parent = dirname6(dir);
4349
+ const parent = dirname7(dir);
4154
4350
  if (parent === dir) {
4155
4351
  return start;
4156
4352
  }
@@ -4265,9 +4461,7 @@ var astroConfigTemplate = (options) => {
4265
4461
  const twoslashTransformer = "transformerTwoslash({ explicitTrigger: true }), ";
4266
4462
  const integrations = [
4267
4463
  `mdx({ processor: blumeMdxProcessor(${JSON.stringify({
4268
- headingAnchors: config.markdown.headingAnchors,
4269
- inline: config.markdown.code.inline,
4270
- math: config.markdown.math
4464
+ headingAnchors: config.markdown.headingAnchors
4271
4465
  })}) })`
4272
4466
  ];
4273
4467
  if (needsReact) {
@@ -4295,8 +4489,7 @@ export default defineConfig({
4295
4489
  integrations: [${integrations.join(", ")}],
4296
4490
  markdown: {
4297
4491
  processor: blumeMarkdownProcessor(${JSON.stringify({
4298
- headingAnchors: config.markdown.headingAnchors,
4299
- inline: config.markdown.code.inline
4492
+ headingAnchors: config.markdown.headingAnchors
4300
4493
  })}),
4301
4494
  shikiConfig: {
4302
4495
  themes: {
@@ -4314,9 +4507,17 @@ export default defineConfig({
4314
4507
  // native bindings resolve at runtime and isolated linkers don't bundle
4315
4508
  // symlinked store copies (which would surface their children as unresolvable
4316
4509
  // imports). See RENDER_EXTERNAL_DEPS / prerenderDepsPlugin.
4510
+ //
4511
+ // The SSR externals go through the legacy \`ssr.external\` key rather than
4512
+ // \`environments.ssr\`: defining a user-owned \`environments.ssr\` block
4513
+ // collides with the internal environment Astro 7 builds the server under and
4514
+ // detaches the adapter's server entrypoint from the rolldown input, so the
4515
+ // SSR entry is emitted as \`index.mjs\` instead of the \`entry.mjs\` the
4516
+ // Vercel adapter's \`astro:build:done\` hook then fails to find. \`prerender\`
4517
+ // is Astro-only and has no legacy equivalent, so it stays under \`environments\`.
4518
+ ssr: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} },
4317
4519
  environments: {
4318
4520
  prerender: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
4319
- ssr: { resolve: { external: ${JSON.stringify(RENDER_EXTERNAL_DEPS)} } },
4320
4521
  },
4321
4522
  resolve: {
4322
4523
  alias: {
@@ -4338,14 +4539,14 @@ export default defineConfig({
4338
4539
  // self-hosted fonts) -- pure noise the loader logs as "No entry type
4339
4540
  // found". Vite appends this to its default ignores.
4340
4541
  watch: {
4341
- ignored: ${JSON.stringify([join10(context.outDir, ".astro", "**")])},
4542
+ ignored: ${JSON.stringify([join11(context.outDir, ".astro", "**")])},
4342
4543
  },
4343
4544
  },
4344
4545
  },
4345
4546
  });
4346
4547
  `;
4347
4548
  };
4348
- var stagedContentDir = (outDir) => join10(outDir, "content");
4549
+ var stagedContentDir = (outDir) => join11(outDir, "content");
4349
4550
  var contentConfigTemplate = (options) => {
4350
4551
  const { context, config } = options;
4351
4552
  const stagedBase = options.stagedBase ?? stagedContentDir(context.outDir);
@@ -4355,7 +4556,7 @@ var contentConfigTemplate = (options) => {
4355
4556
  const docsPattern = filesystem ? [
4356
4557
  ...config.content.include,
4357
4558
  ...(config.content.exclude ?? []).map((pattern) => `!${pattern}`),
4358
- "!**/node_modules/**",
4559
+ ...BLUME_IGNORE_DIRS.filter((dir) => dir !== ".blume").map((dir) => `!**/${dir}/**`),
4359
4560
  ...outDirIgnore
4360
4561
  ] : [];
4361
4562
  const stagedBlock = options.staged ? `
@@ -4691,7 +4892,7 @@ const siteHost = (() => {
4691
4892
 
4692
4893
  export async function GET({ props }) {
4693
4894
  const png = await renderOgImage({
4694
- accent: data.config.theme.accent,
4895
+ accent: data.config.theme.accent.light,
4695
4896
  brand: data.config.title,
4696
4897
  description: data.config.description,
4697
4898
  logo: data.config.logo?.svg,
@@ -4739,7 +4940,7 @@ var catchAllPageTemplate = (options) => {
4739
4940
  const askImport = options.askEnabled ? `import AskAI from "blume/components/islands/AskAI.astro";
4740
4941
  ` : "";
4741
4942
  const askSlot = options.askEnabled ? `
4742
- <AskAI slot="ask" strings={ui.ask} />` : "";
4943
+ <AskAI slot="ask" strings={ui.ask} suggestions={data.config.ask?.suggestions ?? []} />` : "";
4743
4944
  const mathImport = options.mathEnabled ? `import Math from "blume/components/content/Math.astro";
4744
4945
  ` : "";
4745
4946
  const mathEntry = options.mathEnabled ? `Math,
@@ -4991,7 +5192,7 @@ var changelogIndexTemplate = (options) => {
4991
5192
  const askImport = options.askEnabled ? `import AskAI from "blume/components/islands/AskAI.astro";
4992
5193
  ` : "";
4993
5194
  const askSlot = options.askEnabled ? `
4994
- <AskAI slot="ask" />` : "";
5195
+ <AskAI slot="ask" strings={data.ui.ask} suggestions={data.config.ask?.suggestions ?? []} />` : "";
4995
5196
  const clientData = options.needsReact ? `
4996
5197
  clientData={{ config: data.config, navigation: data.navigation, page: { route: "/changelog", title: data.config.title + " changelog" } }}` : "";
4997
5198
  const stagedSpread = options.staged ? `
@@ -5035,6 +5236,20 @@ const slugify = (text) =>
5035
5236
  text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ||
5036
5237
  "update";
5037
5238
 
5239
+ // The major of a version's embedded semver (\`1.2.3\` -> 1, \`pkg@2.0.0\` -> 2), or
5240
+ // null when there is no full major.minor.patch to key on. Drives the changelog's
5241
+ // group-by-major pagination, so it tolerates the scoped tags monorepos publish.
5242
+ const majorVersion = (version) => {
5243
+ const match = /(\\d+)\\.\\d+\\.\\d+/.exec(String(version ?? ""));
5244
+ return match ? Number(match[1]) : null;
5245
+ };
5246
+
5247
+ // Map each entry to its own generated page so the timeline heading can deep-link
5248
+ // to it. The collection entry id matches the route manifest's \`entryId\`.
5249
+ const routeByEntry = new Map(
5250
+ data.routes.map((route) => [route.entryId, route.path])
5251
+ );
5252
+
5038
5253
  const changelogEntries = [
5039
5254
  ...(await getCollection("docs")),${stagedSpread}
5040
5255
  ]
@@ -5056,8 +5271,10 @@ const items = await Promise.all(
5056
5271
  return {
5057
5272
  Content: (await render(entry)).Content,
5058
5273
  date: formatDate(entryDate(entry)),
5274
+ href: routeByEntry.get(entry.id) ?? null,
5059
5275
  id: slugify(label),
5060
5276
  label,
5277
+ major: majorVersion(entry.data.changelog?.version),
5061
5278
  tags: entry.data.changelog?.category
5062
5279
  ? [entry.data.changelog.category]
5063
5280
  : [],
@@ -5065,6 +5282,19 @@ const items = await Promise.all(
5065
5282
  })
5066
5283
  );
5067
5284
 
5285
+ // A changelog is semver-paginated only when every visible release parses as
5286
+ // semver and they span more than one major line. Older majors then collapse
5287
+ // into groups the reader reveals one at a time; otherwise the timeline is flat.
5288
+ const majors = items.every((item) => item.major !== null)
5289
+ ? [...new Set(items.map((item) => item.major))].toSorted((a, b) => b - a)
5290
+ : [];
5291
+ const paginate = majors.length > 1;
5292
+ const majorGroups = majors.map((major) => ({
5293
+ items: items.filter((item) => item.major === major),
5294
+ label: major + ".x",
5295
+ major,
5296
+ }));
5297
+
5068
5298
  const headings = items.map((item) => ({
5069
5299
  depth: 2,
5070
5300
  slug: item.id,
@@ -5096,6 +5326,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
5096
5326
  }}
5097
5327
  headings={headings}
5098
5328
  toc={data.config.toc}
5329
+ contentLayout="bare"
5099
5330
  themeMode={data.config.theme.mode}
5100
5331
  fontCssVars={data.fontCssVars}
5101
5332
  searchEnabled={data.config.search.enabled}
@@ -5114,16 +5345,50 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
5114
5345
  {
5115
5346
  items.length === 0 ? (
5116
5347
  <p>No changelog entries yet.</p>
5348
+ ) : paginate ? (
5349
+ <blume-changelog class="not-prose mt-8 block">
5350
+ {majorGroups[0].items.map(({ Content, href, id, label, date, tags }) => (
5351
+ <Update description={date} href={href} id={id} label={label} tags={tags}>
5352
+ <Content />
5353
+ </Update>
5354
+ ))}
5355
+ {majorGroups.slice(1).map((group) => (
5356
+ <section
5357
+ aria-label={group.label + " releases"}
5358
+ data-changelog-label={group.label}
5359
+ data-changelog-major={group.major}
5360
+ >
5361
+ {group.items.map(({ Content, href, id, label, date, tags }) => (
5362
+ <Update description={date} href={href} id={id} label={label} tags={tags}>
5363
+ <Content />
5364
+ </Update>
5365
+ ))}
5366
+ </section>
5367
+ ))}
5368
+ <div class="mt-10 flex justify-center">
5369
+ <button
5370
+ class="inline-flex items-center gap-2 rounded-full border border-border bg-background px-4 py-2 font-medium text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground"
5371
+ data-changelog-more
5372
+ hidden
5373
+ type="button"
5374
+ >
5375
+ Show older releases
5376
+ </button>
5377
+ </div>
5378
+ </blume-changelog>
5117
5379
  ) : (
5118
5380
  <div class="not-prose mt-8">
5119
- {items.map(({ Content, id, label, date, tags }) => (
5120
- <Update description={date} id={id} label={label} tags={tags}>
5381
+ {items.map(({ Content, href, id, label, date, tags }) => (
5382
+ <Update description={date} href={href} id={id} label={label} tags={tags}>
5121
5383
  <Content />
5122
5384
  </Update>
5123
5385
  ))}
5124
5386
  </div>
5125
5387
  )
5126
5388
  }
5389
+ <script>
5390
+ import "blume/components/content/changelog-element.ts";
5391
+ </script>
5127
5392
  </LayoutComponent>
5128
5393
  `;
5129
5394
  };
@@ -5281,8 +5546,8 @@ var themeRootCss = (theme, options) => [
5281
5546
  ` --blume-accent: ${options.accent};`,
5282
5547
  ...cssToken("--blume-action", options.action),
5283
5548
  ...cssToken("--blume-action-foreground", options.action ? "oklch(1 0 0)" : null),
5284
- ...cssToken("--blume-background", safeColorOrNull(theme.background)),
5285
- ...cssToken("--blume-background-image", theme.backgroundImage ? backgroundImageCss(theme.backgroundImage) : null),
5549
+ ...cssToken("--blume-background", safeColorOrNull(theme.background?.light)),
5550
+ ...cssToken("--blume-background-image", theme.backgroundImage?.light ? backgroundImageCss(theme.backgroundImage.light) : null),
5286
5551
  ` --blume-radius: ${options.radius};`
5287
5552
  ].filter(Boolean).join(`
5288
5553
  `);
@@ -5292,8 +5557,8 @@ var themeDarkCss = (theme, options) => {
5292
5557
  " --blume-accent-foreground: oklch(1 0 0);",
5293
5558
  ...cssToken("--blume-action", options.action),
5294
5559
  ...cssToken("--blume-action-foreground", options.action ? "oklch(1 0 0)" : null),
5295
- ...cssToken("--blume-background", safeColorOrNull(theme.backgroundDark)),
5296
- ...cssToken("--blume-background-image", theme.backgroundImageDark ? backgroundImageCss(theme.backgroundImageDark) : null)
5560
+ ...cssToken("--blume-background", safeColorOrNull(theme.background?.dark)),
5561
+ ...cssToken("--blume-background-image", theme.backgroundImage?.dark ? backgroundImageCss(theme.backgroundImage.dark) : null)
5297
5562
  ].filter(Boolean);
5298
5563
  return `:root[data-theme="dark"] {
5299
5564
  ${tokens.join(`
@@ -5301,20 +5566,22 @@ ${tokens.join(`
5301
5566
  }
5302
5567
  `;
5303
5568
  };
5304
- var resolveAccent = (theme) => presetOrColor(theme.accent);
5569
+ var resolveAccent = (theme) => ({
5570
+ dark: presetOrColor(theme.accent.dark),
5571
+ light: presetOrColor(theme.accent.light)
5572
+ });
5305
5573
  var resolveRadius = (theme) => RADII[theme.radius];
5306
5574
  var buildThemeCss = (theme) => {
5307
- const accent = presetOrColor(theme.accent);
5308
- const accentDark = theme.accentDark ? presetOrColor(theme.accentDark) : null;
5575
+ const accent = resolveAccent(theme);
5309
5576
  const action = theme.action ? presetOrColor(theme.action) : null;
5310
5577
  const radius = RADII[theme.radius];
5311
5578
  const root = themeRootCss(theme, {
5312
- accent,
5579
+ accent: accent.light,
5313
5580
  action,
5314
5581
  radius
5315
5582
  });
5316
5583
  const dark = themeDarkCss(theme, {
5317
- accent: accentDark ?? accent,
5584
+ accent: accent.dark,
5318
5585
  action
5319
5586
  });
5320
5587
  return `/* Generated by Blume from theme config. */
@@ -5347,7 +5614,7 @@ var themeConfiguration = (config, override) => {
5347
5614
  const accent = resolveAccent(config.theme);
5348
5615
  const radius = resolveRadius(config.theme);
5349
5616
  return {
5350
- customCss: `:root,.light-mode,.dark-mode{--scalar-color-accent:${accent};--scalar-radius:${radius};}`,
5617
+ customCss: `:root,.light-mode,.dark-mode{--scalar-color-accent:${accent.light};--scalar-radius:${radius};}.dark-mode{--scalar-color-accent:${accent.dark};}`,
5351
5618
  ...darkModeConfig(config.theme.mode)
5352
5619
  };
5353
5620
  };
@@ -5355,7 +5622,7 @@ var specConfiguration = async (spec, root) => {
5355
5622
  if (URL_SPEC.test(spec)) {
5356
5623
  return { config: { url: spec } };
5357
5624
  }
5358
- const absolute = isAbsolute5(spec) ? spec : join11(root, spec);
5625
+ const absolute = isAbsolute5(spec) ? spec : join12(root, spec);
5359
5626
  try {
5360
5627
  return { config: { content: await readFile5(absolute, "utf-8") } };
5361
5628
  } catch {
@@ -5413,8 +5680,8 @@ var buildReferenceFiles = async (options) => {
5413
5680
  };
5414
5681
 
5415
5682
  // src/core/sources/cache.ts
5416
- import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
5417
- import { join as join12 } from "pathe";
5683
+ import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
5684
+ import { join as join13 } from "pathe";
5418
5685
  var hashText = (text) => {
5419
5686
  let hash = 5381;
5420
5687
  for (let i = 0;i < text.length; i += 1) {
@@ -5441,7 +5708,7 @@ var pollingWatch = (load2, intervalSeconds) => (onChange) => {
5441
5708
  return () => clearInterval(timer);
5442
5709
  };
5443
5710
  var snapshotCache = (cacheDir) => {
5444
- const file = join12(cacheDir, "entries.json");
5711
+ const file = join13(cacheDir, "entries.json");
5445
5712
  return {
5446
5713
  read: async () => {
5447
5714
  try {
@@ -5452,7 +5719,7 @@ var snapshotCache = (cacheDir) => {
5452
5719
  },
5453
5720
  write: async (entries) => {
5454
5721
  try {
5455
- await mkdir2(cacheDir, { recursive: true });
5722
+ await mkdir3(cacheDir, { recursive: true });
5456
5723
  await writeFile3(file, `${JSON.stringify(entries)}
5457
5724
  `, "utf-8");
5458
5725
  } catch {}
@@ -5556,10 +5823,10 @@ var extractOperations = (document, baseRoute) => {
5556
5823
  };
5557
5824
 
5558
5825
  // src/openapi/parse.ts
5559
- import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
5826
+ import { mkdir as mkdir4, readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
5560
5827
  import { setTimeout as sleep } from "node:timers/promises";
5561
5828
  import { normalize as normalize2, upgrade } from "@scalar/openapi-parser";
5562
- import { isAbsolute as isAbsolute6, join as join13 } from "pathe";
5829
+ import { isAbsolute as isAbsolute6, join as join14 } from "pathe";
5563
5830
  var URL_SPEC2 = /^https?:\/\//u;
5564
5831
  var FETCH_TIMEOUT_MS = 15000;
5565
5832
  var MAX_ATTEMPTS = 3;
@@ -5634,7 +5901,7 @@ var fetchSpecText = async (spec) => {
5634
5901
  }
5635
5902
  throw last.error;
5636
5903
  };
5637
- var cacheFileFor = (cacheDir, spec) => join13(cacheDir, `spec-${hashText(spec)}.cache`);
5904
+ var cacheFileFor = (cacheDir, spec) => join14(cacheDir, `spec-${hashText(spec)}.cache`);
5638
5905
  var readCache = async (file) => {
5639
5906
  try {
5640
5907
  return await readFile7(file, "utf-8");
@@ -5644,13 +5911,13 @@ var readCache = async (file) => {
5644
5911
  };
5645
5912
  var writeCache = async (dir, file, text) => {
5646
5913
  try {
5647
- await mkdir3(dir, { recursive: true });
5914
+ await mkdir4(dir, { recursive: true });
5648
5915
  await writeFile4(file, text, "utf-8");
5649
5916
  } catch {}
5650
5917
  };
5651
5918
  var readSpecText = async (spec, root, options) => {
5652
5919
  if (!URL_SPEC2.test(spec)) {
5653
- const absolute = isAbsolute6(spec) ? spec : join13(root, spec);
5920
+ const absolute = isAbsolute6(spec) ? spec : join14(root, spec);
5654
5921
  return { text: await readFile7(absolute, "utf-8"), warnings: [] };
5655
5922
  }
5656
5923
  const cacheFile = options.cacheDir ? cacheFileFor(options.cacheDir, spec) : undefined;
@@ -6252,6 +6519,10 @@ blume-diff {
6252
6519
  font-size: 0.8125rem;
6253
6520
  }
6254
6521
 
6522
+ .prose :where(td, th) :not(pre) > code {
6523
+ white-space: nowrap;
6524
+ }
6525
+
6255
6526
  blume-tabs pre,
6256
6527
  .not-prose > div > pre {
6257
6528
  background: var(--blume-code-background);
@@ -6427,9 +6698,9 @@ pre:has(.line.focused):hover .line:not(.focused) {
6427
6698
  content: none;
6428
6699
  }
6429
6700
 
6430
- /* Inline code highlighting (markdown.code.inline): Shiki colors the tokens of a
6431
- \`code\`{:lang} snippet via the same dual-theme CSS variables as fenced blocks,
6432
- keeping the inline pill background. */
6701
+ /* Inline code highlighting: Shiki colors the tokens of a \`code\`{:lang} snippet
6702
+ via the same dual-theme CSS variables as fenced blocks, keeping the inline
6703
+ pill background. Always on — it only fires on the trailing {:lang} marker. */
6433
6704
  .prose code.blume-inline-code span {
6434
6705
  color: var(--shiki-light);
6435
6706
  }
@@ -6653,12 +6924,12 @@ export const layoutOverrides = { ...(overrides.layout ?? {})${layoutEntries.leng
6653
6924
 
6654
6925
  // src/astro/examples.ts
6655
6926
  import { readFile as readFile9 } from "node:fs/promises";
6656
- import { join as join15, relative as relative5 } from "pathe";
6927
+ import { join as join16, relative as relative5 } from "pathe";
6657
6928
  import { glob as glob2 } from "tinyglobby";
6658
6929
 
6659
6930
  // src/astro/islands.ts
6660
6931
  import { readFile as readFile8 } from "node:fs/promises";
6661
- import { basename, join as join14 } from "pathe";
6932
+ import { basename, join as join15 } from "pathe";
6662
6933
  import { glob } from "tinyglobby";
6663
6934
  var DEFAULT_CLIENT = "visible";
6664
6935
  var VALID_MODES = new Set([
@@ -6687,7 +6958,7 @@ var readClientMode = (source, file, warnings) => {
6687
6958
  return mode;
6688
6959
  };
6689
6960
  var discoverIslands = async (root) => {
6690
- const dir = join14(root, "islands");
6961
+ const dir = join15(root, "islands");
6691
6962
  const matches = await glob(["**/*.{jsx,svelte,tsx,vue}"], {
6692
6963
  absolute: true,
6693
6964
  cwd: dir,
@@ -6750,7 +7021,7 @@ var splitGlobBase = (pattern) => {
6750
7021
  };
6751
7022
  var discoverExamples = async (root, pattern = "examples") => {
6752
7023
  const { base, rest } = GLOB_MAGIC.test(pattern) ? splitGlobBase(pattern) : { base: pattern, rest: DEFAULT_EXAMPLE_GLOB };
6753
- const dir = join15(root, base);
7024
+ const dir = join16(root, base);
6754
7025
  const matches = await glob2([rest], {
6755
7026
  absolute: true,
6756
7027
  cwd: dir,
@@ -6831,10 +7102,10 @@ var customOgRoutes = (pages, siteTitle) => {
6831
7102
  };
6832
7103
 
6833
7104
  // src/astro/generate.ts
6834
- var BLUME_SRC = join16(packageRoot(), "src");
7105
+ var BLUME_SRC = join17(packageRoot(), "src");
6835
7106
  var canResolveFrom = (fromDir, spec) => {
6836
7107
  try {
6837
- createRequire5(pathToFileURL2(join16(fromDir, "_.js")).href).resolve(spec);
7108
+ createRequire5(pathToFileURL2(join17(fromDir, "_.js")).href).resolve(spec);
6838
7109
  return true;
6839
7110
  } catch {
6840
7111
  return false;
@@ -6842,15 +7113,15 @@ var canResolveFrom = (fromDir, spec) => {
6842
7113
  };
6843
7114
  var resolvedAstroPath = (fromDir) => {
6844
7115
  try {
6845
- const pkg = createRequire5(pathToFileURL2(join16(fromDir, "_.js")).href).resolve("astro/package.json");
7116
+ const pkg = createRequire5(pathToFileURL2(join17(fromDir, "_.js")).href).resolve("astro/package.json");
6846
7117
  return realpathSync(pkg);
6847
7118
  } catch {
6848
7119
  return null;
6849
7120
  }
6850
7121
  };
6851
7122
  var blumeDepsDir = (pkgDir = packageRoot()) => {
6852
- const candidates = [join16(pkgDir, "node_modules"), dirname7(pkgDir)];
6853
- return candidates.find((dir) => existsSync9(join16(dir, "astro"))) ?? null;
7123
+ const candidates = [join17(pkgDir, "node_modules"), dirname8(pkgDir)];
7124
+ return candidates.find((dir) => existsSync10(join17(dir, "astro"))) ?? null;
6854
7125
  };
6855
7126
  var linkDepsJunction = async (link, depsDir) => {
6856
7127
  const existing = await lstat(link).catch(() => null);
@@ -6858,9 +7129,9 @@ var linkDepsJunction = async (link, depsDir) => {
6858
7129
  if (!existing.isSymbolicLink()) {
6859
7130
  return;
6860
7131
  }
6861
- await rm(link, { force: true });
7132
+ await rm2(link, { force: true });
6862
7133
  }
6863
- await mkdir4(dirname7(link), { recursive: true });
7134
+ await mkdir5(dirname8(link), { recursive: true });
6864
7135
  await symlink(depsDir, link, "junction");
6865
7136
  };
6866
7137
  var readPkgVersion = (pkgJsonPath) => {
@@ -6890,8 +7161,8 @@ var ensureDepsLink = async (outDir, pkgDir = packageRoot()) => {
6890
7161
  if (blumeAstro && outDirAstro === blumeAstro) {
6891
7162
  return null;
6892
7163
  }
6893
- if (existsSync9(join16(depsDir, "@astrojs", "mdx"))) {
6894
- await linkDepsJunction(join16(outDir, "node_modules"), depsDir);
7164
+ if (existsSync10(join17(depsDir, "@astrojs", "mdx"))) {
7165
+ await linkDepsJunction(join17(outDir, "node_modules"), depsDir);
6895
7166
  return null;
6896
7167
  }
6897
7168
  return astroConflictWarning(blumeAstro, outDirAstro);
@@ -6928,6 +7199,15 @@ var detectNeedsReact = async (root) => {
6928
7199
  });
6929
7200
  return matches.length > 0;
6930
7201
  };
7202
+ var detectUsesMath = async (root) => {
7203
+ const files = await glob4(["**/*.mdx"], {
7204
+ cwd: root,
7205
+ ignore: ["**/node_modules/**", "**/.blume/**", "**/dist/**"],
7206
+ onlyFiles: true
7207
+ });
7208
+ const contents = await Promise.all(files.map((file) => readOptional(join17(root, file))));
7209
+ return contents.some((content) => content.includes("$$"));
7210
+ };
6931
7211
  var writeIfChanged = async (path, content) => {
6932
7212
  let existing = null;
6933
7213
  try {
@@ -6938,13 +7218,13 @@ var writeIfChanged = async (path, content) => {
6938
7218
  if (existing === content) {
6939
7219
  return false;
6940
7220
  }
6941
- await mkdir4(dirname7(path), { recursive: true });
7221
+ await mkdir5(dirname8(path), { recursive: true });
6942
7222
  const tmp = `${path}.${process.pid}.tmp`;
6943
7223
  await writeFile5(tmp, content, "utf-8");
6944
7224
  try {
6945
7225
  await rename(tmp, path);
6946
7226
  } catch (error) {
6947
- await rm(tmp, { force: true });
7227
+ await rm2(tmp, { force: true });
6948
7228
  throw error;
6949
7229
  }
6950
7230
  return true;
@@ -6955,7 +7235,7 @@ var pruneOrphans = async (srcDir, written) => {
6955
7235
  cwd: srcDir,
6956
7236
  onlyFiles: true
6957
7237
  });
6958
- await Promise.all(existing.map((path) => normalize3(path)).filter((path) => !written.has(path)).map((path) => rm(path, { force: true })));
7238
+ await Promise.all(existing.map((path) => normalize3(path)).filter((path) => !written.has(path)).map((path) => rm2(path, { force: true })));
6959
7239
  };
6960
7240
  var collectStaged = (project) => {
6961
7241
  const staged = new Map;
@@ -6970,11 +7250,11 @@ var writeStagedContent = async (out, staged) => {
6970
7250
  const contentDir = stagedContentDir(out);
6971
7251
  const written = new Set;
6972
7252
  await Promise.all([...staged].map(async ([entryId, text]) => {
6973
- const path = join16(contentDir, entryId);
7253
+ const path = join17(contentDir, entryId);
6974
7254
  written.add(normalize3(path));
6975
7255
  await writeIfChanged(path, text);
6976
7256
  }));
6977
- if (existsSync9(contentDir)) {
7257
+ if (existsSync10(contentDir)) {
6978
7258
  await pruneOrphans(contentDir, written);
6979
7259
  }
6980
7260
  };
@@ -6993,9 +7273,9 @@ var resolveLogo = (project) => {
6993
7273
  if (light && light === dark && light.toLowerCase().endsWith(".svg")) {
6994
7274
  const rel = light.replace(/^\//u, "");
6995
7275
  const file = [
6996
- join16(project.context.root, "public", rel),
6997
- join16(project.context.root, rel)
6998
- ].find((path) => existsSync9(path));
7276
+ join17(project.context.root, "public", rel),
7277
+ join17(project.context.root, rel)
7278
+ ].find((path) => existsSync10(path));
6999
7279
  if (file) {
7000
7280
  return { alt, href: brandHref, svg: readFileSync6(file, "utf-8"), text };
7001
7281
  }
@@ -7023,7 +7303,7 @@ var faviconType = (name) => {
7023
7303
  };
7024
7304
  var inlineDataUri = (file, type) => `data:${type};base64,${readFileSync6(file).toString("base64")}`;
7025
7305
  var defaultFavicon = () => ({
7026
- href: inlineDataUri(join16(BLUME_SRC, "assets", "icon.png"), "image/png"),
7306
+ href: inlineDataUri(join17(BLUME_SRC, "assets", "icon.png"), "image/png"),
7027
7307
  type: "image/png"
7028
7308
  });
7029
7309
  var APPLE_ICON_CANDIDATES = [
@@ -7035,13 +7315,13 @@ var APPLE_ICON_CANDIDATES = [
7035
7315
  var resolveIconFile = (project, candidates) => {
7036
7316
  const { root } = project.context;
7037
7317
  for (const name of candidates) {
7038
- if (existsSync9(join16(root, "public", name))) {
7318
+ if (existsSync10(join17(root, "public", name))) {
7039
7319
  return { href: `/${name}`, type: faviconType(name) };
7040
7320
  }
7041
7321
  }
7042
7322
  for (const name of candidates) {
7043
- const file = join16(root, name);
7044
- if (existsSync9(file)) {
7323
+ const file = join17(root, name);
7324
+ if (existsSync10(file)) {
7045
7325
  const type = faviconType(name);
7046
7326
  return { href: inlineDataUri(file, type ?? "image/x-icon"), type };
7047
7327
  }
@@ -7097,6 +7377,7 @@ var buildRuntimeData = (project) => {
7097
7377
  const navigationByLocale = i18n ? Object.fromEntries(i18n.locales.map(({ code }) => [
7098
7378
  code,
7099
7379
  withReferenceTabs(graph.navigationByLocale[code] ?? {
7380
+ featured: [],
7100
7381
  selectors: [],
7101
7382
  sidebar: [],
7102
7383
  tabs: []
@@ -7106,6 +7387,7 @@ var buildRuntimeData = (project) => {
7106
7387
  config: {
7107
7388
  analytics: config.analytics ?? null,
7108
7389
  appleIcon: resolveAppleIcon(project),
7390
+ ask: config.ai.ask?.enabled ? { suggestions: config.ai.ask.suggestions } : null,
7109
7391
  banner: resolveBanner(config),
7110
7392
  codeWrap: config.markdown.code.wrap,
7111
7393
  description: config.description,
@@ -7167,7 +7449,7 @@ var buildRuntimeData = (project) => {
7167
7449
  var planMcp = (project, srcDir) => {
7168
7450
  const { config } = project;
7169
7451
  const { route } = config.mcp;
7170
- const dir = join16(srcDir, "blume-mcp");
7452
+ const dir = join17(srcDir, "blume-mcp");
7171
7453
  const base = {
7172
7454
  dir,
7173
7455
  discoveryPages: [],
@@ -7191,11 +7473,11 @@ var planMcp = (project, srcDir) => {
7191
7473
  ...base,
7192
7474
  discoveryPages: [
7193
7475
  {
7194
- entrypoint: join16(dir, "discovery.ts"),
7476
+ entrypoint: join17(dir, "discovery.ts"),
7195
7477
  pattern: "/.well-known/mcp.json"
7196
7478
  },
7197
7479
  {
7198
- entrypoint: join16(dir, "server-card.ts"),
7480
+ entrypoint: join17(dir, "server-card.ts"),
7199
7481
  pattern: "/.well-known/mcp/server-card.json"
7200
7482
  }
7201
7483
  ],
@@ -7214,11 +7496,11 @@ var writeMcpFiles = async (project, plan, write) => {
7214
7496
  version: data.version
7215
7497
  };
7216
7498
  await Promise.all([
7217
- write(join16(plan.srcDir, "generated", "mcp-data.json"), `${JSON.stringify(data)}
7499
+ write(join17(plan.srcDir, "generated", "mcp-data.json"), `${JSON.stringify(data)}
7218
7500
  `),
7219
- write(join16(plan.srcDir, "pages", mcpPageFile(plan.route)), mcpEndpointTemplate(plan.route)),
7220
- write(join16(plan.dir, "discovery.ts"), staticJsonEndpointTemplate(buildMcpDiscovery(discoveryInput))),
7221
- write(join16(plan.dir, "server-card.ts"), staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)))
7501
+ write(join17(plan.srcDir, "pages", mcpPageFile(plan.route)), mcpEndpointTemplate(plan.route)),
7502
+ write(join17(plan.dir, "discovery.ts"), staticJsonEndpointTemplate(buildMcpDiscovery(discoveryInput))),
7503
+ write(join17(plan.dir, "server-card.ts"), staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)))
7222
7504
  ]);
7223
7505
  };
7224
7506
  var writeAskFiles = async (project, srcDir, write) => {
@@ -7228,16 +7510,16 @@ var writeAskFiles = async (project, srcDir, write) => {
7228
7510
  }
7229
7511
  const grounded = ask.provider !== "inkeep";
7230
7512
  if (grounded) {
7231
- await write(join16(srcDir, "generated", "ask-data.json"), `${JSON.stringify(await buildAskData(project))}
7513
+ await write(join17(srcDir, "generated", "ask-data.json"), `${JSON.stringify(await buildAskData(project))}
7232
7514
  `);
7233
7515
  }
7234
- await write(join16(srcDir, "pages", "api", "ask.ts"), askEndpointTemplate(resolveAskBackend(ask), grounded));
7516
+ await write(join17(srcDir, "pages", "api", "ask.ts"), askEndpointTemplate(resolveAskBackend(ask), grounded));
7235
7517
  };
7236
7518
  var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
7237
7519
  if (routeIsTaken(pages, contentPages, "/404")) {
7238
7520
  return;
7239
7521
  }
7240
- await write(join16(srcDir, "pages", "404.astro"), notFoundPageTemplate());
7522
+ await write(join17(srcDir, "pages", "404.astro"), notFoundPageTemplate());
7241
7523
  };
7242
7524
  var shouldGenerateChangelog = (project) => {
7243
7525
  const hasChangelog = project.graph.pages.some((page) => page.contentType === "changelog" && !(page.meta.draft || page.meta.sidebar.hidden));
@@ -7256,12 +7538,12 @@ var buildComponentSlots = async (componentsFile) => {
7256
7538
  var generateRuntime = async (project) => {
7257
7539
  const { context, config } = project;
7258
7540
  const out = context.outDir;
7259
- const srcDir = join16(out, "src");
7260
- const dataPath = join16(srcDir, "generated", "data.json");
7261
- const themePath = join16(srcDir, "generated", "app.css");
7262
- const searchClientPath = join16(srcDir, "generated", "search-client.ts");
7263
- const examplesPath = join16(srcDir, "generated", "examples.ts");
7264
- const openapiPath = join16(srcDir, "generated", "openapi.json");
7541
+ const srcDir = join17(out, "src");
7542
+ const dataPath = join17(srcDir, "generated", "data.json");
7543
+ const themePath = join17(srcDir, "generated", "app.css");
7544
+ const searchClientPath = join17(srcDir, "generated", "search-client.ts");
7545
+ const examplesPath = join17(srcDir, "generated", "examples.ts");
7546
+ const openapiPath = join17(srcDir, "generated", "openapi.json");
7265
7547
  const written = new Set;
7266
7548
  const write = (path, content) => {
7267
7549
  written.add(normalize3(path));
@@ -7271,9 +7553,17 @@ var generateRuntime = async (project) => {
7271
7553
  const askEnabled = config.ai.ask?.enabled ?? false;
7272
7554
  const exportPdf = config.export.pdf;
7273
7555
  const exportEpub = config.export.epub;
7274
- const [pages, detectedReact, userTheme, islandDiscovery, exampleDiscovery] = await Promise.all([
7556
+ const [
7557
+ pages,
7558
+ detectedReact,
7559
+ usesMath,
7560
+ userTheme,
7561
+ islandDiscovery,
7562
+ exampleDiscovery
7563
+ ] = await Promise.all([
7275
7564
  context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
7276
7565
  detectNeedsReact(context.root),
7566
+ detectUsesMath(context.root),
7277
7567
  readOptional(context.themeFile),
7278
7568
  discoverIslands(context.root),
7279
7569
  discoverExamples(context.root, config.examples)
@@ -7298,7 +7588,7 @@ var generateRuntime = async (project) => {
7298
7588
  const hasStaged = staged.size > 0;
7299
7589
  const hasFilesystemSource = project.sources.some((source) => !source.staged);
7300
7590
  const structural = await Promise.all([
7301
- write(join16(out, "astro.config.mjs"), astroConfigTemplate({
7591
+ write(join17(out, "astro.config.mjs"), astroConfigTemplate({
7302
7592
  aliases: resolveTsconfigAliases(context.root),
7303
7593
  config,
7304
7594
  contentRoutes: project.manifest.routes.map((route) => route.path),
@@ -7313,25 +7603,25 @@ var generateRuntime = async (project) => {
7313
7603
  searchClientPath,
7314
7604
  themePath
7315
7605
  })),
7316
- write(join16(out, "package.json"), runtimePackageTemplate(runtimeDependencies({ config, needsReact, needsSvelte, needsVue }))),
7317
- write(join16(out, "tsconfig.json"), runtimeTsconfigTemplate()),
7318
- write(join16(srcDir, "env.d.ts"), envTemplate()),
7319
- write(join16(srcDir, "content.config.ts"), contentConfigTemplate({
7606
+ write(join17(out, "package.json"), runtimePackageTemplate(runtimeDependencies({ config, needsReact, needsSvelte, needsVue }))),
7607
+ write(join17(out, "tsconfig.json"), runtimeTsconfigTemplate()),
7608
+ write(join17(srcDir, "env.d.ts"), envTemplate()),
7609
+ write(join17(srcDir, "content.config.ts"), contentConfigTemplate({
7320
7610
  config,
7321
7611
  context,
7322
7612
  filesystem: hasFilesystemSource,
7323
7613
  staged: hasStaged
7324
7614
  })),
7325
- write(join16(srcDir, "pages", "[...slug].astro"), catchAllPageTemplate({
7615
+ write(join17(srcDir, "pages", "[...slug].astro"), catchAllPageTemplate({
7326
7616
  askEnabled,
7327
7617
  exportEpub,
7328
7618
  exportPdf,
7329
- mathEnabled: config.markdown.math,
7619
+ mathEnabled: usesMath,
7330
7620
  needsReact
7331
7621
  })),
7332
- write(join16(srcDir, "generated", "components.ts"), slotPlan.module),
7333
- write(join16(srcDir, "generated", "islands.ts"), islandMapTemplate(islandDiscovery.islands)),
7334
- write(join16(srcDir, "generated", "examples.ts"), exampleMapTemplate(exampleDiscovery.examples)),
7622
+ write(join17(srcDir, "generated", "components.ts"), slotPlan.module),
7623
+ write(join17(srcDir, "generated", "islands.ts"), islandMapTemplate(islandDiscovery.islands)),
7624
+ write(join17(srcDir, "generated", "examples.ts"), exampleMapTemplate(exampleDiscovery.examples)),
7335
7625
  write(themePath, tailwindEntryTemplate({
7336
7626
  configTokens: `${buildThemeCss(config.theme)}${buildFontsCss(config.theme.fonts)}`,
7337
7627
  sources: [
@@ -7342,16 +7632,16 @@ var generateRuntime = async (project) => {
7342
7632
  userTheme
7343
7633
  }))
7344
7634
  ]);
7345
- await Promise.all(islandDiscovery.islands.map((island) => write(join16(srcDir, "generated", "islands", `${island.name}.astro`), islandWrapperTemplate(island))));
7346
- await Promise.all(slotPlan.wrappers.map((wrapper) => write(join16(srcDir, "generated", "component-slots", `${wrapper.name}.astro`), wrapper.content)));
7347
- await Promise.all(exampleDiscovery.examples.map((example) => write(join16(srcDir, "generated", "examples", `${exampleSlug(example.path)}.astro`), exampleWrapperTemplate(example))));
7635
+ await Promise.all(islandDiscovery.islands.map((island) => write(join17(srcDir, "generated", "islands", `${island.name}.astro`), islandWrapperTemplate(island))));
7636
+ await Promise.all(slotPlan.wrappers.map((wrapper) => write(join17(srcDir, "generated", "component-slots", `${wrapper.name}.astro`), wrapper.content)));
7637
+ await Promise.all(exampleDiscovery.examples.map((example) => write(join17(srcDir, "generated", "examples", `${exampleSlug(example.path)}.astro`), exampleWrapperTemplate(example))));
7348
7638
  await writeAskFiles(project, srcDir, write);
7349
7639
  await writeMcpFiles(project, mcp, write);
7350
7640
  if (config.seo.og.enabled) {
7351
- await write(join16(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes));
7641
+ await write(join17(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes));
7352
7642
  }
7353
7643
  if (shouldGenerateChangelog(project)) {
7354
- await write(join16(srcDir, "pages", "changelog.astro"), changelogIndexTemplate({
7644
+ await write(join17(srcDir, "pages", "changelog.astro"), changelogIndexTemplate({
7355
7645
  askEnabled,
7356
7646
  exportEpub,
7357
7647
  exportPdf,
@@ -7363,27 +7653,27 @@ var generateRuntime = async (project) => {
7363
7653
  await write(searchClientPath, searchClientTemplate(config));
7364
7654
  if (servesStaticIndex(config.search.provider)) {
7365
7655
  const documents = await buildSearchDocuments(project);
7366
- await write(join16(srcDir, "generated", "search.json"), `${JSON.stringify(documents)}
7656
+ await write(join17(srcDir, "generated", "search.json"), `${JSON.stringify(documents)}
7367
7657
  `);
7368
- await write(join16(srcDir, "pages", "blume-search.json.ts"), searchEndpointTemplate());
7658
+ await write(join17(srcDir, "pages", "blume-search.json.ts"), searchEndpointTemplate());
7369
7659
  }
7370
7660
  if (config.search.provider === "mixedbread") {
7371
- await write(join16(srcDir, "pages", "api", "search.ts"), mixedbreadSearchEndpointTemplate(config.search.mixedbread?.storeId ?? ""));
7661
+ await write(join17(srcDir, "pages", "api", "search.ts"), mixedbreadSearchEndpointTemplate(config.search.mixedbread?.storeId ?? ""));
7372
7662
  }
7373
7663
  const rawMarkdown = await buildRawMarkdown(project);
7374
7664
  await Promise.all([
7375
- write(join16(srcDir, "generated", "raw-markdown.json"), `${JSON.stringify(rawMarkdown)}
7665
+ write(join17(srcDir, "generated", "raw-markdown.json"), `${JSON.stringify(rawMarkdown)}
7376
7666
  `),
7377
- write(join16(srcDir, "pages", "[...slug].md.ts"), rawMarkdownEndpointTemplate()),
7378
- write(join16(srcDir, "pages", "[...slug].mdx.ts"), rawMarkdownEndpointTemplate())
7667
+ write(join17(srcDir, "pages", "[...slug].md.ts"), rawMarkdownEndpointTemplate()),
7668
+ write(join17(srcDir, "pages", "[...slug].mdx.ts"), rawMarkdownEndpointTemplate())
7379
7669
  ]);
7380
7670
  const feeds = buildRssFeeds(project);
7381
7671
  if (feeds.length > 0) {
7382
7672
  const feedXml = Object.fromEntries(feeds.map((feed) => [feed.type, renderRssFeed(feed)]));
7383
7673
  await Promise.all([
7384
- write(join16(srcDir, "generated", "rss.json"), `${JSON.stringify(feedXml)}
7674
+ write(join17(srcDir, "generated", "rss.json"), `${JSON.stringify(feedXml)}
7385
7675
  `),
7386
- write(join16(srcDir, "pages", "[section]", "rss.xml.ts"), rssEndpointTemplate())
7676
+ write(join17(srcDir, "pages", "[section]", "rss.xml.ts"), rssEndpointTemplate())
7387
7677
  ]);
7388
7678
  }
7389
7679
  const warnings = [
@@ -7420,13 +7710,13 @@ var generateRuntime = async (project) => {
7420
7710
  root: context.root
7421
7711
  });
7422
7712
  warnings.push(...references.warnings);
7423
- await Promise.all(references.files.map((file) => write(join16(srcDir, "pages", file.pagePath), file.content)));
7713
+ await Promise.all(references.files.map((file) => write(join17(srcDir, "pages", file.pagePath), file.content)));
7424
7714
  }
7425
- await write(join16(srcDir, "generated", "data.json"), buildRuntimeData(project));
7715
+ await write(join17(srcDir, "generated", "data.json"), buildRuntimeData(project));
7426
7716
  const openApiSource2 = project.sources.find(isOpenApiSource);
7427
7717
  await write(openapiPath, `${JSON.stringify(openApiSource2 ? openApiSource2.openApiData() : {})}
7428
7718
  `);
7429
- await write(join16(out, "blume.manifest.json"), `${JSON.stringify(project.manifest, null, 2)}
7719
+ await write(join17(out, "blume.manifest.json"), `${JSON.stringify(project.manifest, null, 2)}
7430
7720
  `);
7431
7721
  await writeStagedContent(out, staged);
7432
7722
  await pruneOrphans(srcDir, written);
@@ -7434,7 +7724,7 @@ var generateRuntime = async (project) => {
7434
7724
  };
7435
7725
 
7436
7726
  // src/core/config.ts
7437
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
7727
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
7438
7728
 
7439
7729
  // src/core/deployment-env.ts
7440
7730
  var toUrl = (value) => {
@@ -7545,7 +7835,6 @@ var pageMetaSchema = pageMetaBaseSchema;
7545
7835
  var sidebarDisplaySchema = z2.enum(["flat", "group", "page"]);
7546
7836
  var folderMetaSchema = z2.object({
7547
7837
  collapsed: z2.boolean().optional(),
7548
- display: sidebarDisplaySchema.optional(),
7549
7838
  icon: iconName.optional(),
7550
7839
  order: z2.number().optional(),
7551
7840
  pages: z2.array(z2.string()).optional(),
@@ -7699,14 +7988,18 @@ var sidebarItemSchema = z2.lazy(() => z2.union([
7699
7988
  var fontSlug = z2.string().refine(isFontSlug, (value) => ({
7700
7989
  message: `Unknown font "${value}". Supported fonts: ${FONT_SLUGS.join(", ")}.`
7701
7990
  }));
7991
+ var perModeValueSchema = z2.union([
7992
+ z2.string(),
7993
+ z2.object({ dark: z2.string().optional(), light: z2.string().optional() }).strict()
7994
+ ]).optional().transform((value) => typeof value === "string" ? { dark: value, light: value } : value);
7702
7995
  var themeConfigSchema = z2.object({
7703
- accent: z2.string().default("blue"),
7704
- accentDark: z2.string().optional(),
7996
+ accent: z2.union([
7997
+ z2.string(),
7998
+ z2.object({ dark: z2.string(), light: z2.string() }).strict()
7999
+ ]).default("blue").transform((value) => typeof value === "string" ? { dark: value, light: value } : value),
7705
8000
  action: z2.string().optional(),
7706
- background: z2.string().optional(),
7707
- backgroundDark: z2.string().optional(),
7708
- backgroundImage: z2.string().optional(),
7709
- backgroundImageDark: z2.string().optional(),
8001
+ background: perModeValueSchema,
8002
+ backgroundImage: perModeValueSchema,
7710
8003
  fonts: z2.object({
7711
8004
  body: fontSlug.default("inter"),
7712
8005
  display: fontSlug.default("inter-tight"),
@@ -7785,7 +8078,11 @@ var aiConfigSchema = z2.object({
7785
8078
  baseUrl: z2.string().url().optional(),
7786
8079
  enabled: z2.boolean().default(false),
7787
8080
  model: z2.string().default("openai/gpt-5.5"),
7788
- provider: z2.enum(askAiProviders).default("gateway")
8081
+ provider: z2.enum(askAiProviders).default("gateway"),
8082
+ suggestions: z2.array(z2.object({
8083
+ icon: iconName.optional(),
8084
+ label: z2.string().min(1)
8085
+ }).strict()).default([])
7789
8086
  }).strict().superRefine((value, ctx) => {
7790
8087
  if (value.provider === "openai-compatible" && !value.baseUrl) {
7791
8088
  ctx.addIssue({
@@ -7797,10 +8094,22 @@ var aiConfigSchema = z2.object({
7797
8094
  }).optional(),
7798
8095
  llmsTxt: z2.boolean().default(false)
7799
8096
  }).strict();
8097
+ var featuredLinkSchema = z2.object({
8098
+ href: z2.string(),
8099
+ icon: iconName.optional(),
8100
+ label: z2.string()
8101
+ }).strict();
7800
8102
  var navigationConfigSchema = z2.object({
8103
+ featured: z2.array(featuredLinkSchema).default([]),
7801
8104
  repo: z2.boolean().default(true),
7802
8105
  selectors: z2.array(navSelectorSchema).default([]),
7803
- sidebar: z2.array(sidebarItemSchema).optional(),
8106
+ sidebar: z2.union([
8107
+ z2.array(sidebarItemSchema),
8108
+ z2.object({
8109
+ display: sidebarDisplaySchema.default("flat"),
8110
+ items: z2.array(sidebarItemSchema).optional()
8111
+ }).strict()
8112
+ ]).default({}).transform((value) => Array.isArray(value) ? { display: "flat", items: value } : value),
7804
8113
  tabs: z2.array(navTabSchema).optional()
7805
8114
  }).strict();
7806
8115
  var exportConfigSchema = z2.union([
@@ -7880,7 +8189,23 @@ var rssConfigSchema = z2.object({
7880
8189
  limit: z2.number().int().positive().default(50),
7881
8190
  types: z2.array(z2.string()).default(["blog", "changelog"])
7882
8191
  }).strict();
8192
+ var contentSignalsObjectSchema = z2.object({
8193
+ aiInput: z2.boolean().default(true),
8194
+ aiTrain: z2.boolean().default(true),
8195
+ search: z2.boolean().default(true)
8196
+ }).strict();
8197
+ var contentSignalsSchema = z2.union([z2.boolean(), contentSignalsObjectSchema]).transform((value) => {
8198
+ if (value === true) {
8199
+ return contentSignalsObjectSchema.parse({});
8200
+ }
8201
+ if (value === false) {
8202
+ return null;
8203
+ }
8204
+ return value;
8205
+ });
7883
8206
  var seoConfigSchema = z2.object({
8207
+ agentReadability: z2.boolean().default(true),
8208
+ contentSignals: contentSignalsSchema.default(true),
7884
8209
  og: ogConfigSchema.default({}),
7885
8210
  robots: z2.boolean().default(true),
7886
8211
  rss: rssConfigSchema.default({}),
@@ -7906,15 +8231,13 @@ var lastModifiedConfigSchema = z2.union([
7906
8231
  ]);
7907
8232
  var codeConfigSchema = z2.object({
7908
8233
  icons: z2.boolean().default(true),
7909
- inline: z2.boolean().default(false),
7910
8234
  wrap: z2.boolean().default(false)
7911
8235
  }).strict();
7912
8236
  var markdownConfigSchema = z2.object({
7913
8237
  code: codeConfigSchema.default({}),
7914
8238
  codeBlocks: codeBlocksConfigSchema.default({}),
7915
8239
  headingAnchors: z2.boolean().default(true),
7916
- imageZoom: z2.boolean().default(true),
7917
- math: z2.boolean().default(false)
8240
+ imageZoom: z2.boolean().default(true)
7918
8241
  }).strict();
7919
8242
  var openapiSourceSchema = z2.object({
7920
8243
  label: z2.string().optional(),
@@ -8000,7 +8323,7 @@ var loadConfig = async (root, options = {}) => {
8000
8323
  }
8001
8324
  const parsed = blumeConfigSchema.safeParse(raw ?? {});
8002
8325
  if (!parsed.success) {
8003
- const source = configFile && existsSync10(configFile) ? readFileSync7(configFile, "utf-8") : undefined;
8326
+ const source = configFile && existsSync11(configFile) ? readFileSync7(configFile, "utf-8") : undefined;
8004
8327
  const diagnostics = diagnosticsFromZod(parsed.error, {
8005
8328
  code: "BLUME_CONFIG_INVALID",
8006
8329
  file: configFile ?? undefined,
@@ -8091,7 +8414,6 @@ var applyFolderMeta = (group, folderMeta, sharedMeta, metaPrefix) => {
8091
8414
  group.icon = meta.icon ?? group.icon;
8092
8415
  group.order = meta.order ?? group.order;
8093
8416
  group.collapsed = meta.collapsed ?? group.collapsed;
8094
- group.display = meta.display ?? group.display;
8095
8417
  if (meta.pages) {
8096
8418
  const rank = new Map(meta.pages.map((key, i) => [key, i]));
8097
8419
  for (const child of group.children) {
@@ -8121,7 +8443,15 @@ var sortNodes = (nodes) => {
8121
8443
  }
8122
8444
  }
8123
8445
  };
8124
- var toNavNode = (node) => {
8446
+ var hoistPages = (nodes) => {
8447
+ const pages = nodes.filter((node) => node.kind === "page");
8448
+ const groups = nodes.filter((node) => node.kind === "group");
8449
+ nodes.splice(0, nodes.length, ...pages, ...groups);
8450
+ for (const group of groups) {
8451
+ hoistPages(group.children);
8452
+ }
8453
+ };
8454
+ var toNavNode = (node, display) => {
8125
8455
  if (node.kind === "page") {
8126
8456
  return {
8127
8457
  badge: node.badge,
@@ -8135,16 +8465,16 @@ var toNavNode = (node) => {
8135
8465
  };
8136
8466
  }
8137
8467
  return {
8138
- children: node.children.map(toNavNode),
8468
+ children: node.children.map((child) => toNavNode(child, display)),
8139
8469
  collapsed: node.collapsed,
8140
- display: node.display,
8470
+ display,
8141
8471
  icon: node.icon,
8142
8472
  kind: "group",
8143
8473
  label: node.label,
8144
8474
  path: node.routePath
8145
8475
  };
8146
8476
  };
8147
- var buildFileSystemSidebar = (pages, folderMeta, sharedMeta, metaPrefix) => {
8477
+ var buildFileSystemSidebar = (pages, folderMeta, sharedMeta, metaPrefix, display) => {
8148
8478
  const root = createGroup("", "", "", 0);
8149
8479
  for (const page of pages) {
8150
8480
  if (page.meta.sidebar.hidden) {
@@ -8175,7 +8505,10 @@ var buildFileSystemSidebar = (pages, folderMeta, sharedMeta, metaPrefix) => {
8175
8505
  }
8176
8506
  applyFolderMeta(root, folderMeta, sharedMeta, metaPrefix);
8177
8507
  sortNodes(root.children);
8178
- return root.children.map(toNavNode);
8508
+ if (display === "flat") {
8509
+ hoistPages(root.children);
8510
+ }
8511
+ return root.children.map((child) => toNavNode(child, display));
8179
8512
  };
8180
8513
  var normalizeRef = (ref) => {
8181
8514
  if (ref === "index") {
@@ -8192,7 +8525,7 @@ var routeForRef = (ref, byRoute) => {
8192
8525
  const normalized = normalizeRef(ref);
8193
8526
  return byRoute.get(normalized)?.route ?? normalized;
8194
8527
  };
8195
- var buildConfigSidebar = (items, byRoute) => {
8528
+ var buildConfigSidebar = (items, byRoute, display) => {
8196
8529
  const nodes = [];
8197
8530
  for (const item of items) {
8198
8531
  if (typeof item === "string") {
@@ -8214,10 +8547,10 @@ var buildConfigSidebar = (items, byRoute) => {
8214
8547
  if (item.items) {
8215
8548
  nodes.push({
8216
8549
  badge: item.badge,
8217
- children: buildConfigSidebar(item.items, byRoute),
8550
+ children: buildConfigSidebar(item.items, byRoute, display),
8218
8551
  collapsed: item.collapsed,
8219
8552
  directory: item.directory,
8220
- display: item.display,
8553
+ display: item.display ?? display,
8221
8554
  icon: item.icon,
8222
8555
  kind: "group",
8223
8556
  label: item.label,
@@ -8252,8 +8585,10 @@ var buildConfigSidebar = (items, byRoute) => {
8252
8585
  return nodes;
8253
8586
  };
8254
8587
  var buildNavigation = (pages, options) => {
8588
+ const featured = options.featured ?? [];
8255
8589
  const selectors = options.selectors ?? [];
8256
8590
  const tabs = options.tabs ?? [];
8591
+ const display = options.display ?? "flat";
8257
8592
  const metaPrefix = options.metaPrefix ?? "";
8258
8593
  const sharedFolderMeta = options.sharedFolderMeta ?? new Map;
8259
8594
  const byRoute = new Map(pages.map((page) => [
@@ -8262,14 +8597,16 @@ var buildNavigation = (pages, options) => {
8262
8597
  ]));
8263
8598
  if (options.sidebar) {
8264
8599
  return {
8600
+ featured,
8265
8601
  selectors,
8266
- sidebar: buildConfigSidebar(options.sidebar, byRoute),
8602
+ sidebar: buildConfigSidebar(options.sidebar, byRoute, display),
8267
8603
  tabs
8268
8604
  };
8269
8605
  }
8270
8606
  return {
8607
+ featured,
8271
8608
  selectors,
8272
- sidebar: buildFileSystemSidebar(pages, options.folderMeta, sharedFolderMeta, metaPrefix),
8609
+ sidebar: buildFileSystemSidebar(pages, options.folderMeta, sharedFolderMeta, metaPrefix, display),
8273
8610
  tabs
8274
8611
  };
8275
8612
  };
@@ -8327,26 +8664,31 @@ var buildContentGraph = (pages, options) => {
8327
8664
  localePages = [...real, ...filled];
8328
8665
  }
8329
8666
  navigationByLocale[code] = buildNavigation(localePages, {
8667
+ display: options.navigation.sidebar.display,
8668
+ featured: options.navigation.featured,
8330
8669
  folderMeta: options.folderMeta,
8331
8670
  metaPrefix: i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "",
8332
8671
  refByLogical: true,
8333
8672
  selectors: options.navigation.selectors,
8334
8673
  sharedFolderMeta: options.sharedFolderMeta,
8335
- sidebar: options.navigation.sidebar,
8674
+ sidebar: options.navigation.sidebar.items,
8336
8675
  tabs
8337
8676
  });
8338
8677
  }
8339
8678
  navigation = navigationByLocale[i18n.defaultLocale] ?? {
8679
+ featured: [],
8340
8680
  selectors: [],
8341
8681
  sidebar: [],
8342
8682
  tabs: []
8343
8683
  };
8344
8684
  } else {
8345
8685
  navigation = buildNavigation(pages, {
8686
+ display: options.navigation.sidebar.display,
8687
+ featured: options.navigation.featured,
8346
8688
  folderMeta: options.folderMeta,
8347
8689
  selectors: options.navigation.selectors,
8348
8690
  sharedFolderMeta: options.sharedFolderMeta,
8349
- sidebar: options.navigation.sidebar,
8691
+ sidebar: options.navigation.sidebar.items,
8350
8692
  tabs: options.navigation.tabs
8351
8693
  });
8352
8694
  }
@@ -8415,7 +8757,7 @@ var gitLastModifiedTimes = (root, contentRoot, sourcePaths) => {
8415
8757
  };
8416
8758
 
8417
8759
  // src/core/meta.ts
8418
- import { basename as basename3, dirname as dirname8, relative as relative9 } from "pathe";
8760
+ import { basename as basename3, dirname as dirname9, relative as relative9 } from "pathe";
8419
8761
  import { glob as glob5 } from "tinyglobby";
8420
8762
  var META_FILES = [
8421
8763
  "**/meta.ts",
@@ -8445,7 +8787,7 @@ var discoverFolderMeta = async (contentRoot) => {
8445
8787
  const shared = new Map;
8446
8788
  const diagnostics = [];
8447
8789
  for (const entry of loaded) {
8448
- const dir = relative9(contentRoot, dirname8(entry.file));
8790
+ const dir = relative9(contentRoot, dirname9(entry.file));
8449
8791
  if (!entry.ok) {
8450
8792
  diagnostics.push({
8451
8793
  code: "BLUME_META_LOAD_FAILED",
@@ -8470,7 +8812,7 @@ var discoverFolderMeta = async (contentRoot) => {
8470
8812
  };
8471
8813
 
8472
8814
  // src/core/sources/normalize.ts
8473
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
8815
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
8474
8816
  import GithubSlugger from "github-slugger";
8475
8817
  import { extname as extname4 } from "pathe";
8476
8818
  var NUMERIC_PREFIX2 = /^\d+[-_.]/u;
@@ -8604,7 +8946,7 @@ var normalizeEntry2 = (entry, ctx) => {
8604
8946
  const ext = format === "mdx" ? ".mdx" : ".md";
8605
8947
  const result = pageMetaSchema.safeParse(entry.data);
8606
8948
  if (!result.success) {
8607
- const source = entry.raw ?? (entry.sourcePath && existsSync11(entry.sourcePath) ? readFileSync8(entry.sourcePath, "utf-8") : undefined);
8949
+ const source = entry.raw ?? (entry.sourcePath && existsSync12(entry.sourcePath) ? readFileSync8(entry.sourcePath, "utf-8") : undefined);
8608
8950
  return {
8609
8951
  diagnostics: diagnosticsFromZod(result.error, {
8610
8952
  code: "BLUME_FRONTMATTER_INVALID",
@@ -8655,35 +8997,20 @@ var normalizeEntry2 = (entry, ctx) => {
8655
8997
  };
8656
8998
 
8657
8999
  // src/core/sources/resolve.ts
8658
- import { join as join21 } from "pathe";
9000
+ import { join as join22 } from "pathe";
8659
9001
 
8660
9002
  // src/core/sources/filesystem.ts
8661
- import { existsSync as existsSync12, watch as fsWatch } from "node:fs";
9003
+ import { existsSync as existsSync13, watch as fsWatch } from "node:fs";
8662
9004
  import { readFile as readFile11 } from "node:fs/promises";
8663
- import { extname as extname5, isAbsolute as isAbsolute7, join as join17, relative as relative10, resolve as resolve5 } from "pathe";
9005
+ import { extname as extname5, isAbsolute as isAbsolute7, join as join18, relative as relative10, resolve as resolve5 } from "pathe";
8664
9006
  import { glob as glob6 } from "tinyglobby";
8665
-
8666
- // src/core/sources/watch.ts
8667
- var BLUME_WATCH_IGNORE_DIRS = [".blume", ".git", "node_modules"];
8668
- var excludeDirSegments = (patterns) => patterns.map((pattern) => /^(?<dir>[^*/]+)\/\*\*$/u.exec(pattern)?.groups?.dir).filter((dir) => dir !== undefined);
8669
- var ignoringWatchListener = (onChange, ignoreDirs = BLUME_WATCH_IGNORE_DIRS) => {
8670
- const ignore = new Set(ignoreDirs);
8671
- return (_event, filename) => {
8672
- if (typeof filename === "string" && filename.split(/[/\\]/u).some((segment) => ignore.has(segment))) {
8673
- return;
8674
- }
8675
- onChange();
8676
- };
8677
- };
8678
-
8679
- // src/core/sources/filesystem.ts
8680
9007
  var filesystemSource = (options) => {
8681
- const contentRoot = isAbsolute7(options.root) ? options.root : join17(resolve5(options.projectRoot), options.root);
9008
+ const contentRoot = isAbsolute7(options.root) ? options.root : join18(resolve5(options.projectRoot), options.root);
8682
9009
  const load2 = async () => {
8683
9010
  const files = await glob6(options.include, {
8684
9011
  absolute: true,
8685
9012
  cwd: contentRoot,
8686
- ignore: options.exclude,
9013
+ ignore: [...options.exclude, ...baselineScanIgnore()],
8687
9014
  onlyFiles: true
8688
9015
  });
8689
9016
  files.sort();
@@ -8702,7 +9029,7 @@ var filesystemSource = (options) => {
8702
9029
  return { diagnostics: [], entries };
8703
9030
  };
8704
9031
  const validate = () => {
8705
- if (!existsSync12(contentRoot)) {
9032
+ if (!existsSync13(contentRoot)) {
8706
9033
  throw new BlumeError({
8707
9034
  code: options.missingCode ?? "BLUME_CONTENT_ROOT_MISSING",
8708
9035
  file: contentRoot,
@@ -8717,7 +9044,7 @@ var filesystemSource = (options) => {
8717
9044
  ...excludeDirSegments(options.exclude)
8718
9045
  ]);
8719
9046
  const watch = (onChange) => {
8720
- if (!existsSync12(contentRoot)) {
9047
+ if (!existsSync13(contentRoot)) {
8721
9048
  return () => {};
8722
9049
  }
8723
9050
  const watcher = fsWatch(contentRoot, { recursive: true }, ignoringWatchListener(onChange, watchIgnoreDirs));
@@ -8728,7 +9055,7 @@ var filesystemSource = (options) => {
8728
9055
  load: load2,
8729
9056
  name: options.name,
8730
9057
  prefix: options.prefix,
8731
- read: (ref) => readFile11(join17(contentRoot, ref), "utf-8"),
9058
+ read: (ref) => readFile11(join18(contentRoot, ref), "utf-8"),
8732
9059
  staged: false,
8733
9060
  validate,
8734
9061
  watch
@@ -9018,11 +9345,11 @@ var mdxRemoteSource = (options, ctx) => {
9018
9345
 
9019
9346
  // src/core/sources/notion.ts
9020
9347
  import { setTimeout as sleep2 } from "node:timers/promises";
9021
- import { join as join19 } from "pathe";
9348
+ import { join as join20 } from "pathe";
9022
9349
 
9023
9350
  // src/core/sources/assets.ts
9024
- import { mkdir as mkdir5, writeFile as writeFile6 } from "node:fs/promises";
9025
- import { extname as extname6, join as join18 } from "pathe";
9351
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
9352
+ import { extname as extname6, join as join19 } from "pathe";
9026
9353
  var MD_IMAGE = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
9027
9354
  var REMOTE = /^https?:\/\//u;
9028
9355
  var SAFE_EXT = /^\.[a-z0-9]+$/iu;
@@ -9050,8 +9377,8 @@ var materializeAssets = async (markdown, ctx) => {
9050
9377
  }
9051
9378
  const bytes = new Uint8Array(await res.arrayBuffer());
9052
9379
  const file = `${hashText(url)}${extFor(url)}`;
9053
- await mkdir5(ctx.assetsDir, { recursive: true });
9054
- await writeFile6(join18(ctx.assetsDir, file), bytes);
9380
+ await mkdir6(ctx.assetsDir, { recursive: true });
9381
+ await writeFile6(join19(ctx.assetsDir, file), bytes);
9055
9382
  rewrites.set(url, `${ctx.assetsBaseUrl}/${file}`);
9056
9383
  } catch (error) {
9057
9384
  diagnostics.push({
@@ -9167,8 +9494,8 @@ ${text}
9167
9494
  };
9168
9495
  var notionSource = (options, ctx) => {
9169
9496
  const props = options.properties ?? {};
9170
- const cache = snapshotCache(ctx?.cacheDir ?? join19(".blume", "cache", options.name));
9171
- const assetsDir = ctx?.assetsDir ?? join19(".blume", "public", "blume-assets", options.name);
9497
+ const cache = snapshotCache(ctx?.cacheDir ?? join20(".blume", "cache", options.name));
9498
+ const assetsDir = ctx?.assetsDir ?? join20(".blume", "public", "blume-assets", options.name);
9172
9499
  const assetsBaseUrl = ctx?.assetsBaseUrl ?? `/blume-assets/${options.name}`;
9173
9500
  let snapshot = new Map;
9174
9501
  const resolveClient = async () => {
@@ -9336,7 +9663,7 @@ ${rendered.join(`
9336
9663
  };
9337
9664
 
9338
9665
  // src/core/sources/sanity.ts
9339
- import { join as join20 } from "pathe";
9666
+ import { join as join21 } from "pathe";
9340
9667
 
9341
9668
  // src/core/sources/portable-text.ts
9342
9669
  var HEADING_STYLES = {
@@ -9475,7 +9802,7 @@ var resolveClient = async (options, preview) => {
9475
9802
  };
9476
9803
  var sanitySource = (options, ctx) => {
9477
9804
  const fields = options.fields ?? {};
9478
- const cache = snapshotCache(ctx?.cacheDir ?? join20(".blume", "cache", options.name));
9805
+ const cache = snapshotCache(ctx?.cacheDir ?? join21(".blume", "cache", options.name));
9479
9806
  let snapshot = new Map;
9480
9807
  const toEntry2 = (doc) => {
9481
9808
  const slugValue = asString(getPath(doc, fields.slug ?? "slug.current")) ?? asString(doc._id) ?? "untitled";
@@ -9550,8 +9877,8 @@ var uniqueNamer = () => {
9550
9877
  };
9551
9878
  var sourceContext = (context, name, runtime) => ({
9552
9879
  assetsBaseUrl: `/blume-assets/${name}`,
9553
- assetsDir: join21(context.outDir, "public", "blume-assets", name),
9554
- cacheDir: join21(context.outDir, "cache", name),
9880
+ assetsDir: join22(context.outDir, "public", "blume-assets", name),
9881
+ cacheDir: join22(context.outDir, "cache", name),
9555
9882
  mode: runtime.mode,
9556
9883
  preview: runtime.preview,
9557
9884
  projectRoot: context.root,
@@ -9741,8 +10068,8 @@ var scanProject = async (root, options = {}) => {
9741
10068
  };
9742
10069
 
9743
10070
  // src/cli/env.ts
9744
- import { existsSync as existsSync13, readFileSync as readFileSync9 } from "node:fs";
9745
- import { dirname as dirname9, join as join22, resolve as resolve6 } from "pathe";
10071
+ import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
10072
+ import { dirname as dirname10, join as join23, resolve as resolve6 } from "pathe";
9746
10073
  var ENV_LINE = /^\s*(?:export\s+)?(?<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?<value>.*?)\s*$/u;
9747
10074
  var DOUBLE_QUOTED2 = /^"(?<body>[\s\S]*)"$/u;
9748
10075
  var SINGLE_QUOTED = /^'(?<body>[\s\S]*)'$/u;
@@ -9780,7 +10107,7 @@ var applyEnv = (parsed) => {
9780
10107
  };
9781
10108
  var loadFile = (path) => {
9782
10109
  try {
9783
- if (existsSync13(path)) {
10110
+ if (existsSync14(path)) {
9784
10111
  applyEnv(parseEnv(readFileSync9(path, "utf-8")));
9785
10112
  }
9786
10113
  } catch {}
@@ -9789,10 +10116,10 @@ var loadEnvFiles = (startDir) => {
9789
10116
  let dir = resolve6(startDir);
9790
10117
  let done = false;
9791
10118
  while (!done) {
9792
- loadFile(join22(dir, ".env.local"));
9793
- loadFile(join22(dir, ".env"));
9794
- const parent = dirname9(dir);
9795
- done = existsSync13(join22(dir, ".git")) || parent === dir;
10119
+ loadFile(join23(dir, ".env.local"));
10120
+ loadFile(join23(dir, ".env"));
10121
+ const parent = dirname10(dir);
10122
+ done = existsSync14(join23(dir, ".git")) || parent === dir;
9796
10123
  dir = parent;
9797
10124
  }
9798
10125
  };
@@ -9922,24 +10249,24 @@ var emitRedirectFiles = async (config, distDir) => {
9922
10249
  if (redirects.length === 0 || config.deployment.output !== "static") {
9923
10250
  return;
9924
10251
  }
9925
- await writeFile7(join23(distDir, "blume-redirects.json"), buildRedirectManifest(redirects), "utf-8");
10252
+ await writeFile7(join24(distDir, "blume-redirects.json"), buildRedirectManifest(redirects), "utf-8");
9926
10253
  const platformFiles = [
9927
10254
  { content: buildNetlifyRedirects(redirects), name: "_redirects" },
9928
10255
  { content: buildVercelConfig(redirects), name: "vercel.json" }
9929
10256
  ];
9930
- await Promise.all(platformFiles.map((file) => existsSync14(join23(distDir, file.name)) ? Promise.resolve() : writeFile7(join23(distDir, file.name), file.content, "utf-8")));
10257
+ await Promise.all(platformFiles.map((file) => existsSync15(join24(distDir, file.name)) ? Promise.resolve() : writeFile7(join24(distDir, file.name), file.content, "utf-8")));
9931
10258
  logger.success(`Emitted redirect files for ${redirects.length} redirect(s)`);
9932
10259
  };
9933
10260
  var formatBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(bytes < 1024 * 100 ? 1 : 0)} kB`;
9934
10261
  var astroAssets = async (distDir, ext) => {
9935
- const astroDir = join23(distDir, "_astro");
9936
- if (!existsSync14(astroDir)) {
10262
+ const astroDir = join24(distDir, "_astro");
10263
+ if (!existsSync15(astroDir)) {
9937
10264
  return [];
9938
10265
  }
9939
10266
  const entries = await readdir(astroDir);
9940
10267
  const files = entries.filter((name) => name.endsWith(`.${ext}`));
9941
10268
  const sized = await Promise.all(files.map(async (name) => {
9942
- const info = await stat(join23(astroDir, name));
10269
+ const info = await stat(join24(astroDir, name));
9943
10270
  return { name, size: info.size };
9944
10271
  }));
9945
10272
  return sized.toSorted((a, b) => b.size - a.size);
@@ -9995,21 +10322,27 @@ var publishBuildArtifacts = async (project, distDir, args) => {
9995
10322
  if (project.config.ai.llmsTxt) {
9996
10323
  const { index, full } = await buildLlmsFiles(project);
9997
10324
  await Promise.all([
9998
- writeFile7(join23(distDir, "llms.txt"), index, "utf-8"),
9999
- writeFile7(join23(distDir, "llms-full.txt"), full, "utf-8")
10325
+ writeFile7(join24(distDir, "llms.txt"), index, "utf-8"),
10326
+ writeFile7(join24(distDir, "llms-full.txt"), full, "utf-8")
10000
10327
  ]);
10001
10328
  logger.success("Generated llms.txt and llms-full.txt");
10002
10329
  }
10003
10330
  const sitemap = buildSitemap(project);
10004
- if (sitemap && !existsSync14(join23(distDir, "sitemap.xml"))) {
10005
- await writeFile7(join23(distDir, "sitemap.xml"), sitemap, "utf-8");
10331
+ if (sitemap && !existsSync15(join24(distDir, "sitemap.xml"))) {
10332
+ await writeFile7(join24(distDir, "sitemap.xml"), sitemap, "utf-8");
10006
10333
  logger.success("Generated sitemap.xml");
10007
10334
  }
10008
10335
  const robots = buildRobots(project);
10009
- if (robots && !existsSync14(join23(distDir, "robots.txt"))) {
10010
- await writeFile7(join23(distDir, "robots.txt"), robots, "utf-8");
10336
+ if (robots && !existsSync15(join24(distDir, "robots.txt"))) {
10337
+ await writeFile7(join24(distDir, "robots.txt"), robots, "utf-8");
10011
10338
  logger.success("Generated robots.txt");
10012
10339
  }
10340
+ const agentReadability = buildAgentReadability(project);
10341
+ if (agentReadability && !existsSync15(join24(distDir, "agent-readability.json"))) {
10342
+ await writeFile7(join24(distDir, "agent-readability.json"), `${JSON.stringify(agentReadability, null, 2)}
10343
+ `, "utf-8");
10344
+ logger.success("Generated agent-readability.json");
10345
+ }
10013
10346
  await emitRedirectFiles(project.config, distDir);
10014
10347
  const { config } = project;
10015
10348
  const features = serverFeatures(config);
@@ -10021,6 +10354,7 @@ var publishBuildArtifacts = async (project, distDir, args) => {
10021
10354
  `Redirects ${config.redirects.length}`,
10022
10355
  `Sitemap ${sitemap ? "yes" : "no (set deployment.site)"}`,
10023
10356
  `Robots ${robots ? "yes" : "no"}`,
10357
+ `Agent JSON ${agentReadability ? "yes" : "no"}`,
10024
10358
  `LLM files ${config.ai.llmsTxt ? "yes" : "no"}`,
10025
10359
  `Server features ${features.length > 0 ? features.join(", ") : "none"}`
10026
10360
  ].join(`
@@ -10106,21 +10440,26 @@ var buildCommand = defineCommand2({
10106
10440
  logLevel: "info",
10107
10441
  root: project.context.outDir
10108
10442
  });
10109
- const distDir = project.context.distDir ?? join23(root, "dist");
10443
+ const distDir = project.context.distDir ?? join24(root, "dist");
10110
10444
  if (runtimeDir) {
10111
10445
  logger.success(`Isolated build OK — output at ${distDir} (not published).`);
10112
10446
  return;
10113
10447
  }
10114
- await publishBuildArtifacts(project, distDir, args);
10448
+ const surfaced = await surfaceAdapterOutput(project.config, project.context);
10449
+ if (surfaced.moved) {
10450
+ logger.success(`Surfaced ${project.config.deployment.adapter} output to ${surfaced.to}`);
10451
+ await ensureGitignore(root, [surfaced.ignore]);
10452
+ }
10453
+ await publishBuildArtifacts(project, deployStaticDir(project.config, project.context), args);
10115
10454
  }
10116
10455
  });
10117
10456
 
10118
10457
  // src/cli/commands/check.ts
10119
- import { existsSync as existsSync15 } from "node:fs";
10458
+ import { existsSync as existsSync16 } from "node:fs";
10120
10459
  import { check } from "@astrojs/check";
10121
10460
  import { sync } from "astro";
10122
10461
  import { defineCommand as defineCommand3 } from "citty";
10123
- import { join as join24 } from "pathe";
10462
+ import { join as join25 } from "pathe";
10124
10463
  var checkCommand = defineCommand3({
10125
10464
  args: {
10126
10465
  isolated: {
@@ -10156,13 +10495,13 @@ var checkCommand = defineCommand3({
10156
10495
  });
10157
10496
  const { outDir } = project.context;
10158
10497
  await sync({ logLevel: "warn", root: outDir });
10159
- const tsconfig = join24(root, "tsconfig.json");
10498
+ const tsconfig = join25(root, "tsconfig.json");
10160
10499
  logger.start(`Type-checking ${project.graph.pages.length} page(s)`);
10161
10500
  const failed = await check({
10162
10501
  minimumFailingSeverity: "error",
10163
10502
  minimumSeverity: "hint",
10164
10503
  root: outDir,
10165
- tsconfig: existsSync15(tsconfig) ? tsconfig : undefined,
10504
+ tsconfig: existsSync16(tsconfig) ? tsconfig : undefined,
10166
10505
  watch: false
10167
10506
  });
10168
10507
  if (failed) {
@@ -10279,6 +10618,14 @@ var devCommand = defineCommand4({
10279
10618
  const explicitPort = parsePort(args.port);
10280
10619
  const port = explicitPort ?? 4321;
10281
10620
  const devServerUrl = `http://localhost:${port}`;
10621
+ const outDir = resolveRuntimeDir(root);
10622
+ const running = readDevLock(outDir);
10623
+ if (running) {
10624
+ logger.error(`A \`blume dev\` server is already running${describeDevLock(running)} in this project. Reuse that server instead of starting a second one — two dev servers would corrupt the shared .blume dir. If it crashed, delete .blume/dev.lock.`);
10625
+ process.exit(1);
10626
+ }
10627
+ const releaseLock = acquireDevLock(outDir, port);
10628
+ process.on("exit", releaseLock);
10282
10629
  const project = await prepareProject({
10283
10630
  devServerUrl,
10284
10631
  mode: "dev",
@@ -10287,12 +10634,6 @@ var devCommand = defineCommand4({
10287
10634
  root,
10288
10635
  strict: args.strict
10289
10636
  });
10290
- if (isDevLocked(project.context.outDir)) {
10291
- logger.error("Another `blume dev` is already running in this project; two dev servers would corrupt the shared .blume dir. Stop the other one first (or delete .blume/dev.lock if it crashed).");
10292
- process.exit(1);
10293
- }
10294
- const releaseLock = acquireDevLock(project.context.outDir);
10295
- process.on("exit", releaseLock);
10296
10637
  const server = await dev({
10297
10638
  logLevel: args.debug ? "debug" : "info",
10298
10639
  root: project.context.outDir,
@@ -10302,6 +10643,9 @@ var devCommand = defineCommand4({
10302
10643
  port: explicitPort
10303
10644
  }
10304
10645
  });
10646
+ if (server.address.port !== port) {
10647
+ updateDevLockPort(outDir, server.address.port);
10648
+ }
10305
10649
  showBlumeErrorOverlay(project.diagnostics);
10306
10650
  const runRegenerate = coalescedRunner(async () => {
10307
10651
  try {
@@ -10429,12 +10773,12 @@ var doctorCommand = defineCommand5({
10429
10773
  // src/cli/commands/eject.ts
10430
10774
  import { readFile as readFile13, writeFile as writeFile9 } from "node:fs/promises";
10431
10775
  import { defineCommand as defineCommand6 } from "citty";
10432
- import { join as join26, relative as relative12 } from "pathe";
10776
+ import { join as join27, relative as relative12 } from "pathe";
10433
10777
 
10434
10778
  // src/registry/eject.ts
10435
- import { existsSync as existsSync16 } from "node:fs";
10436
- import { cp, mkdir as mkdir6, readFile as readFile12, rm as rm2, writeFile as writeFile8 } from "node:fs/promises";
10437
- import { join as join25, relative as relative11 } from "pathe";
10779
+ import { existsSync as existsSync17 } from "node:fs";
10780
+ import { cp as cp2, mkdir as mkdir7, readFile as readFile12, rm as rm3, writeFile as writeFile8 } from "node:fs/promises";
10781
+ import { join as join26, relative as relative11 } from "pathe";
10438
10782
  var POSIX = (path) => path.split("\\").join("/");
10439
10783
  var ejectOpenApiData = (project) => {
10440
10784
  const source = project.sources.find(isOpenApiSource);
@@ -10449,14 +10793,14 @@ var askFiles = async (project, srcDir, genDir) => {
10449
10793
  const files = [
10450
10794
  {
10451
10795
  content: askEndpointTemplate(resolveAskBackend(ask), grounded),
10452
- path: join25(srcDir, "pages", "api", "ask.ts")
10796
+ path: join26(srcDir, "pages", "api", "ask.ts")
10453
10797
  }
10454
10798
  ];
10455
10799
  if (grounded) {
10456
10800
  files.push({
10457
10801
  content: `${JSON.stringify(await buildAskData(project))}
10458
10802
  `,
10459
- path: join25(genDir, "ask-data.json")
10803
+ path: join26(genDir, "ask-data.json")
10460
10804
  });
10461
10805
  }
10462
10806
  return files;
@@ -10464,14 +10808,23 @@ var askFiles = async (project, srcDir, genDir) => {
10464
10808
  var eject = async (root) => {
10465
10809
  const project = await scanProject(root, { mode: "build" });
10466
10810
  const { context, config } = project;
10467
- const srcDir = join25(root, "src");
10468
- const genDir = join25(srcDir, "generated");
10811
+ const srcDir = join26(root, "src");
10812
+ const genDir = join26(srcDir, "generated");
10469
10813
  const askEnabled = config.ai.ask?.enabled ?? false;
10470
10814
  const exportPdf = config.export.pdf;
10471
10815
  const exportEpub = config.export.epub;
10472
- const [pages, needsReactRaw, userTheme, rawMarkdown, islands, examples] = await Promise.all([
10816
+ const [
10817
+ pages,
10818
+ needsReactRaw,
10819
+ usesMath,
10820
+ userTheme,
10821
+ rawMarkdown,
10822
+ islands,
10823
+ examples
10824
+ ] = await Promise.all([
10473
10825
  context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
10474
10826
  detectNeedsReact(root),
10827
+ detectUsesMath(root),
10475
10828
  context.themeFile ? readFile12(context.themeFile, "utf-8") : Promise.resolve(""),
10476
10829
  buildRawMarkdown(project),
10477
10830
  discoverIslands(root),
@@ -10514,14 +10867,14 @@ var eject = async (root) => {
10514
10867
  searchClientPath: "./src/generated/search-client.ts",
10515
10868
  themePath: "./src/generated/app.css"
10516
10869
  }),
10517
- path: join25(root, "astro.config.mjs")
10870
+ path: join26(root, "astro.config.mjs")
10518
10871
  },
10519
10872
  {
10520
10873
  content: runtimeTsconfigTemplate(),
10521
- path: join25(root, "tsconfig.json"),
10874
+ path: join26(root, "tsconfig.json"),
10522
10875
  skipIfExists: true
10523
10876
  },
10524
- { content: envTemplate(), path: join25(srcDir, "env.d.ts") },
10877
+ { content: envTemplate(), path: join26(srcDir, "env.d.ts") },
10525
10878
  {
10526
10879
  content: contentConfigTemplate({
10527
10880
  config,
@@ -10529,29 +10882,29 @@ var eject = async (root) => {
10529
10882
  staged: hasStaged,
10530
10883
  stagedBase: stagedDir
10531
10884
  }),
10532
- path: join25(srcDir, "content.config.ts")
10885
+ path: join26(srcDir, "content.config.ts")
10533
10886
  },
10534
10887
  {
10535
10888
  content: catchAllPageTemplate({
10536
10889
  askEnabled,
10537
10890
  exportEpub,
10538
10891
  exportPdf,
10539
- mathEnabled: config.markdown.math,
10892
+ mathEnabled: usesMath,
10540
10893
  needsReact
10541
10894
  }),
10542
- path: join25(srcDir, "pages", "[...slug].astro")
10895
+ path: join26(srcDir, "pages", "[...slug].astro")
10543
10896
  },
10544
10897
  {
10545
10898
  content: planComponentSlots(componentsImport, null).module,
10546
- path: join25(genDir, "components.ts")
10899
+ path: join26(genDir, "components.ts")
10547
10900
  },
10548
10901
  {
10549
10902
  content: islandMapTemplate(islands.islands),
10550
- path: join25(genDir, "islands.ts")
10903
+ path: join26(genDir, "islands.ts")
10551
10904
  },
10552
10905
  {
10553
10906
  content: exampleMapTemplate(examples.examples),
10554
- path: join25(genDir, "examples.ts")
10907
+ path: join26(genDir, "examples.ts")
10555
10908
  },
10556
10909
  {
10557
10910
  content: tailwindEntryTemplate({
@@ -10563,26 +10916,26 @@ var eject = async (root) => {
10563
10916
  twoslashCss: twoslashCss(),
10564
10917
  userTheme
10565
10918
  }),
10566
- path: join25(genDir, "app.css")
10919
+ path: join26(genDir, "app.css")
10567
10920
  },
10568
- { content: buildRuntimeData(project), path: join25(genDir, "data.json") },
10921
+ { content: buildRuntimeData(project), path: join26(genDir, "data.json") },
10569
10922
  {
10570
10923
  content: `${JSON.stringify(ejectOpenApiData(project))}
10571
10924
  `,
10572
- path: join25(genDir, "openapi.json")
10925
+ path: join26(genDir, "openapi.json")
10573
10926
  },
10574
10927
  {
10575
10928
  content: `${JSON.stringify(rawMarkdown)}
10576
10929
  `,
10577
- path: join25(genDir, "raw-markdown.json")
10930
+ path: join26(genDir, "raw-markdown.json")
10578
10931
  },
10579
10932
  {
10580
10933
  content: rawMarkdownEndpointTemplate(),
10581
- path: join25(srcDir, "pages", "[...slug].md.ts")
10934
+ path: join26(srcDir, "pages", "[...slug].md.ts")
10582
10935
  },
10583
10936
  {
10584
10937
  content: rawMarkdownEndpointTemplate(),
10585
- path: join25(srcDir, "pages", "[...slug].mdx.ts")
10938
+ path: join26(srcDir, "pages", "[...slug].mdx.ts")
10586
10939
  }
10587
10940
  ];
10588
10941
  if (askEnabled) {
@@ -10591,34 +10944,34 @@ var eject = async (root) => {
10591
10944
  if (config.seo.og.enabled) {
10592
10945
  files.push({
10593
10946
  content: ogEndpointTemplate(customOgRoutes(pages, config.title)),
10594
- path: join25(srcDir, "pages", "og", "[...slug].png.ts")
10947
+ path: join26(srcDir, "pages", "og", "[...slug].png.ts")
10595
10948
  });
10596
10949
  }
10597
10950
  if (!routeIsTaken(pages, project.graph.pages, "/404")) {
10598
10951
  files.push({
10599
10952
  content: notFoundPageTemplate(),
10600
- path: join25(srcDir, "pages", "404.astro")
10953
+ path: join26(srcDir, "pages", "404.astro")
10601
10954
  });
10602
10955
  }
10603
10956
  files.push({
10604
10957
  content: searchClientTemplate(config),
10605
- path: join25(genDir, "search-client.ts")
10958
+ path: join26(genDir, "search-client.ts")
10606
10959
  });
10607
10960
  if (servesStaticIndex(config.search.provider)) {
10608
10961
  const documents = await buildSearchDocuments(project);
10609
10962
  files.push({
10610
10963
  content: `${JSON.stringify(documents)}
10611
10964
  `,
10612
- path: join25(genDir, "search.json")
10965
+ path: join26(genDir, "search.json")
10613
10966
  }, {
10614
10967
  content: searchEndpointTemplate(),
10615
- path: join25(srcDir, "pages", "blume-search.json.ts")
10968
+ path: join26(srcDir, "pages", "blume-search.json.ts")
10616
10969
  });
10617
10970
  }
10618
10971
  if (config.search.provider === "mixedbread") {
10619
10972
  files.push({
10620
10973
  content: mixedbreadSearchEndpointTemplate(config.search.mixedbread?.storeId ?? ""),
10621
- path: join25(srcDir, "pages", "api", "search.ts")
10974
+ path: join26(srcDir, "pages", "api", "search.ts")
10622
10975
  });
10623
10976
  }
10624
10977
  const feeds = buildRssFeeds(project);
@@ -10627,10 +10980,10 @@ var eject = async (root) => {
10627
10980
  files.push({
10628
10981
  content: `${JSON.stringify(feedXml)}
10629
10982
  `,
10630
- path: join25(genDir, "rss.json")
10983
+ path: join26(genDir, "rss.json")
10631
10984
  }, {
10632
10985
  content: rssEndpointTemplate(),
10633
- path: join25(srcDir, "pages", "[section]", "rss.xml.ts")
10986
+ path: join26(srcDir, "pages", "[section]", "rss.xml.ts")
10634
10987
  });
10635
10988
  }
10636
10989
  if (hasScalarReferences(config)) {
@@ -10642,38 +10995,38 @@ var eject = async (root) => {
10642
10995
  for (const file of references.files) {
10643
10996
  files.push({
10644
10997
  content: file.content,
10645
- path: join25(srcDir, "pages", file.pagePath)
10998
+ path: join26(srcDir, "pages", file.pagePath)
10646
10999
  });
10647
11000
  }
10648
11001
  }
10649
11002
  files.push(...islands.islands.map((island) => ({
10650
11003
  content: islandWrapperTemplate(island),
10651
- path: join25(genDir, "islands", `${island.name}.astro`)
11004
+ path: join26(genDir, "islands", `${island.name}.astro`)
10652
11005
  })), ...examples.examples.map((example) => ({
10653
11006
  content: exampleWrapperTemplate(example),
10654
- path: join25(genDir, "examples", `${exampleSlug(example.path)}.astro`)
11007
+ path: join26(genDir, "examples", `${exampleSlug(example.path)}.astro`)
10655
11008
  })));
10656
11009
  for (const [entryId, content] of staged) {
10657
- files.push({ content, path: join25(root, stagedDir, entryId) });
11010
+ files.push({ content, path: join26(root, stagedDir, entryId) });
10658
11011
  }
10659
- const written = files.filter((file) => !(file.skipIfExists && existsSync16(file.path)));
11012
+ const written = files.filter((file) => !(file.skipIfExists && existsSync17(file.path)));
10660
11013
  await Promise.all(written.map(async (file) => {
10661
- await mkdir6(join25(file.path, ".."), { recursive: true });
11014
+ await mkdir7(join26(file.path, ".."), { recursive: true });
10662
11015
  await writeFile8(file.path, file.content, "utf-8");
10663
11016
  }));
10664
- const assetsSrc = join25(context.outDir, "public", "blume-assets");
10665
- if (existsSync16(assetsSrc)) {
10666
- await cp(assetsSrc, join25(root, "public", "blume-assets"), {
11017
+ const assetsSrc = join26(context.outDir, "public", "blume-assets");
11018
+ if (existsSync17(assetsSrc)) {
11019
+ await cp2(assetsSrc, join26(root, "public", "blume-assets"), {
10667
11020
  recursive: true
10668
11021
  });
10669
11022
  }
10670
- await rm2(context.outDir, { force: true, recursive: true });
11023
+ await rm3(context.outDir, { force: true, recursive: true });
10671
11024
  return written.map((file) => file.path);
10672
11025
  };
10673
11026
 
10674
11027
  // src/cli/commands/eject.ts
10675
11028
  var updatePackageScripts = async (root) => {
10676
- const pkgPath = join26(root, "package.json");
11029
+ const pkgPath = join27(root, "package.json");
10677
11030
  let pkg;
10678
11031
  try {
10679
11032
  pkg = JSON.parse(await readFile13(pkgPath, "utf-8"));
@@ -10723,10 +11076,10 @@ The blume package remains importable.`);
10723
11076
  });
10724
11077
 
10725
11078
  // src/cli/commands/init.ts
10726
- import { existsSync as existsSync17 } from "node:fs";
10727
- import { mkdir as mkdir7, writeFile as writeFile10 } from "node:fs/promises";
11079
+ import { existsSync as existsSync18 } from "node:fs";
11080
+ import { mkdir as mkdir8, writeFile as writeFile10 } from "node:fs/promises";
10728
11081
  import { defineCommand as defineCommand7 } from "citty";
10729
- import { basename as basename4, dirname as dirname10, isAbsolute as isAbsolute8, join as join27, relative as relative13 } from "pathe";
11082
+ import { basename as basename4, dirname as dirname11, isAbsolute as isAbsolute8, join as join28, relative as relative13 } from "pathe";
10730
11083
 
10731
11084
  // src/core/package-json.ts
10732
11085
  var toPackageName = (raw) => raw.toLowerCase().replaceAll(/[^a-z0-9._-]+/gu, "-").replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
@@ -10778,7 +11131,7 @@ var STARTERS = {
10778
11131
  files: (dir) => [
10779
11132
  {
10780
11133
  content: page("API Reference", "Explore the API.", "# API Reference\n\nYour OpenAPI spec renders at [`/api`](/api). Point `openapi.sources` at your own spec in `blume.config.ts`."),
10781
- path: join27(dir, "index.mdx")
11134
+ path: join28(dir, "index.mdx")
10782
11135
  }
10783
11136
  ]
10784
11137
  },
@@ -10793,7 +11146,7 @@ var STARTERS = {
10793
11146
  files: (dir) => [
10794
11147
  {
10795
11148
  content: page("Introduction", "Welcome to your new Blume docs.", "# Introduction\n\nWrite your docs here, and log releases under `changelog/`."),
10796
- path: join27(dir, "index.mdx")
11149
+ path: join28(dir, "index.mdx")
10797
11150
  },
10798
11151
  {
10799
11152
  content: `---
@@ -10804,7 +11157,7 @@ date: 2026-01-01
10804
11157
 
10805
11158
  The first release. Edit \`${dir}/changelog/v1-0-0.mdx\` or add new entries beside it.
10806
11159
  `,
10807
- path: join27(dir, "changelog", "v1-0-0.mdx")
11160
+ path: join28(dir, "changelog", "v1-0-0.mdx")
10808
11161
  }
10809
11162
  ]
10810
11163
  },
@@ -10817,7 +11170,7 @@ The first release. Edit \`${dir}/changelog/v1-0-0.mdx\` or add new entries besid
10817
11170
  Welcome to **Blume** — markdown-first docs powered by Astro and Vite.
10818
11171
 
10819
11172
  Edit \`${dir}/index.mdx\` to get started, then run \`blume dev\`.`),
10820
- path: join27(dir, "index.mdx")
11173
+ path: join28(dir, "index.mdx")
10821
11174
  }
10822
11175
  ]
10823
11176
  },
@@ -10828,11 +11181,11 @@ Edit \`${dir}/index.mdx\` to get started, then run \`blume dev\`.`),
10828
11181
  content: page("Introduction", "Get started with the SDK.", `# Introduction
10829
11182
 
10830
11183
  Install the SDK and make your first call. See [Installation](/installation).`),
10831
- path: join27(dir, "index.mdx")
11184
+ path: join28(dir, "index.mdx")
10832
11185
  },
10833
11186
  {
10834
11187
  content: page("Installation", "Install the SDK.", "# Installation\n\n```package-install\nyour-sdk\n```"),
10835
- path: join27(dir, "installation.mdx")
11188
+ path: join28(dir, "installation.mdx")
10836
11189
  }
10837
11190
  ]
10838
11191
  }
@@ -10842,11 +11195,11 @@ var commandsFor = (pm) => ({
10842
11195
  install: `${pm} install`
10843
11196
  });
10844
11197
  var writeFileSafe = async (path, content) => {
10845
- if (existsSync17(path)) {
11198
+ if (existsSync18(path)) {
10846
11199
  logger.info(`Skipped existing ${path}`);
10847
11200
  return false;
10848
11201
  }
10849
- await mkdir7(dirname10(path), { recursive: true });
11202
+ await mkdir8(dirname11(path), { recursive: true });
10850
11203
  await writeFile10(path, content, "utf-8");
10851
11204
  logger.success(`Created ${path}`);
10852
11205
  return true;
@@ -10879,7 +11232,7 @@ var initCommand = defineCommand7({
10879
11232
  async run({ args }) {
10880
11233
  const root = process.cwd();
10881
11234
  const contentDir = args["content-dir"] ?? "docs";
10882
- if (isAbsolute8(contentDir) || relative13(root, join27(root, contentDir)).startsWith("..")) {
11235
+ if (isAbsolute8(contentDir) || relative13(root, join28(root, contentDir)).startsWith("..")) {
10883
11236
  logger.error(`Invalid --content-dir "${contentDir}" (must be a path inside the project).`);
10884
11237
  process.exit(1);
10885
11238
  }
@@ -10894,9 +11247,9 @@ var initCommand = defineCommand7({
10894
11247
  process.exit(1);
10895
11248
  }
10896
11249
  const starter = STARTERS[template];
10897
- const createdPackage = await writeFileSafe(join27(root, "package.json"), blumePackageJson(toPackageName(basename4(root))));
10898
- await writeFileSafe(join27(root, "blume.config.ts"), starter.config);
10899
- await Promise.all(starter.files(contentDir).map((file) => writeFileSafe(join27(root, file.path), file.content)));
11250
+ const createdPackage = await writeFileSafe(join28(root, "package.json"), blumePackageJson(toPackageName(basename4(root))));
11251
+ await writeFileSafe(join28(root, "blume.config.ts"), starter.config);
11252
+ await Promise.all(starter.files(contentDir).map((file) => writeFileSafe(join28(root, file.path), file.content)));
10900
11253
  const ignored = await ensureGitignore(root, [".blume/", "dist/"]);
10901
11254
  if (ignored.length > 0) {
10902
11255
  logger.success(`Added ${ignored.join(", ")} to .gitignore`);
@@ -10934,10 +11287,10 @@ var initCommand = defineCommand7({
10934
11287
  });
10935
11288
 
10936
11289
  // src/cli/commands/preview.ts
10937
- import { existsSync as existsSync18 } from "node:fs";
11290
+ import { existsSync as existsSync19 } from "node:fs";
10938
11291
  import { preview } from "astro";
10939
11292
  import { defineCommand as defineCommand8 } from "citty";
10940
- import { join as join28 } from "pathe";
11293
+ import { join as join29 } from "pathe";
10941
11294
  var previewCommand = defineCommand8({
10942
11295
  args: {
10943
11296
  host: { description: "Network host to bind.", type: "string" },
@@ -10951,7 +11304,7 @@ var previewCommand = defineCommand8({
10951
11304
  const root = process.cwd();
10952
11305
  const { config } = await loadConfig(root);
10953
11306
  const context = resolveProjectContext(root, config);
10954
- if (!existsSync18(join28(context.outDir, "astro.config.mjs"))) {
11307
+ if (!existsSync19(join29(context.outDir, "astro.config.mjs"))) {
10955
11308
  logger.error("No build found. Run `blume build` first.");
10956
11309
  process.exit(1);
10957
11310
  }
@@ -10967,9 +11320,9 @@ var previewCommand = defineCommand8({
10967
11320
  });
10968
11321
 
10969
11322
  // src/cli/commands/sync.ts
10970
- import { rm as rm3 } from "node:fs/promises";
11323
+ import { rm as rm4 } from "node:fs/promises";
10971
11324
  import { defineCommand as defineCommand9 } from "citty";
10972
- import { join as join29 } from "pathe";
11325
+ import { join as join30 } from "pathe";
10973
11326
  var syncCommand = defineCommand9({
10974
11327
  args: {
10975
11328
  force: {
@@ -10991,7 +11344,7 @@ var syncCommand = defineCommand9({
10991
11344
  if (args.force) {
10992
11345
  const { config } = await loadConfig(root);
10993
11346
  const context = resolveProjectContext(root, config);
10994
- await rm3(join29(context.outDir, "cache"), { force: true, recursive: true });
11347
+ await rm4(join30(context.outDir, "cache"), { force: true, recursive: true });
10995
11348
  logger.info("Cleared source cache.");
10996
11349
  }
10997
11350
  await prepareProject({
@@ -11006,13 +11359,13 @@ var syncCommand = defineCommand9({
11006
11359
  });
11007
11360
 
11008
11361
  // src/cli/commands/validate.ts
11009
- import { existsSync as existsSync20 } from "node:fs";
11362
+ import { existsSync as existsSync21 } from "node:fs";
11010
11363
  import { defineCommand as defineCommand10 } from "citty";
11011
- import { join as join31 } from "pathe";
11364
+ import { join as join32 } from "pathe";
11012
11365
 
11013
11366
  // src/core/links.ts
11014
- import { existsSync as existsSync19 } from "node:fs";
11015
- import { basename as basename5, join as join30 } from "pathe";
11367
+ import { existsSync as existsSync20 } from "node:fs";
11368
+ import { basename as basename5, join as join31 } from "pathe";
11016
11369
  var HTTP = /^https?:\/\//iu;
11017
11370
  var PROTOCOL_RELATIVE = /^\/\//u;
11018
11371
  var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
@@ -11024,7 +11377,7 @@ var STATUS_NOT_FOUND = 404;
11024
11377
  var STATUS_GONE = 410;
11025
11378
  var STATUS_METHOD_NOT_ALLOWED = 405;
11026
11379
  var STATUS_NOT_IMPLEMENTED = 501;
11027
- var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null && existsSync19(join30(ctx.publicDir, resolved));
11380
+ var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null && existsSync20(join31(ctx.publicDir, resolved));
11028
11381
  var isIndexPage = (page2) => {
11029
11382
  const ref = page2.source?.ref ?? page2.sourcePath ?? "";
11030
11383
  return /^index\.(?:md|mdx)$/iu.test(basename5(ref));
@@ -11267,10 +11620,10 @@ var validateCommand = defineCommand10({
11267
11620
  try {
11268
11621
  const project = await scanProject(root, { mode: "build" });
11269
11622
  diagnostics.push(...project.diagnostics);
11270
- const publicDir = join31(root, "public");
11623
+ const publicDir = join32(root, "public");
11271
11624
  diagnostics.push(...await validateLinks(project.graph, {
11272
11625
  checkExternal: Boolean(args.external),
11273
- publicDir: existsSync20(publicDir) ? publicDir : null,
11626
+ publicDir: existsSync21(publicDir) ? publicDir : null,
11274
11627
  redirects: project.config.redirects
11275
11628
  }));
11276
11629
  } catch (error) {
@@ -11330,5 +11683,5 @@ process.on("unhandledRejection", (error) => {
11330
11683
  });
11331
11684
  runMain(main);
11332
11685
 
11333
- //# debugId=59F90AF7D601CF0764756E2164756E21
11686
+ //# debugId=0628D18D87A2023564756E2164756E21
11334
11687
  //# sourceMappingURL=index.js.map