blume 1.5.0 → 1.5.2

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 (89) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +16 -12
  3. package/dist/cli/index.js +449 -135
  4. package/dist/cli/index.js.map +24 -23
  5. package/dist/types/ai/ask-context.d.ts +78 -0
  6. package/dist/types/core/config-input.d.ts +54 -2
  7. package/dist/types/core/data.d.ts +19 -2
  8. package/dist/types/core/open-in-chat.d.ts +9 -0
  9. package/dist/types/core/schema.d.ts +48 -1
  10. package/dist/types/core/types.d.ts +10 -3
  11. package/dist/types/openapi/references.d.ts +9 -0
  12. package/dist/types/search/orama-index.d.ts +70 -0
  13. package/dist/types/theme/fonts.d.ts +11 -2
  14. package/docs/advanced/api-reference.mdx +67 -5
  15. package/docs/advanced/custom-pages.mdx +5 -1
  16. package/docs/configuration/ai.mdx +35 -0
  17. package/docs/configuration/index.mdx +14 -2
  18. package/docs/configuration/search.mdx +4 -4
  19. package/docs/configuration/theming.mdx +4 -2
  20. package/docs/reference/cli.mdx +2 -2
  21. package/package.json +1 -1
  22. package/skills/blume-migrate/SKILL.md +1 -1
  23. package/skills/blume-migrate/references/mintlify.md +1 -1
  24. package/src/ai/ask-context.ts +51 -11
  25. package/src/ai/mcp/data.ts +3 -2
  26. package/src/ai/mcp/server.ts +3 -2
  27. package/src/assets/icon-dark.png +0 -0
  28. package/src/astro/generate.ts +172 -18
  29. package/src/astro/templates.ts +89 -15
  30. package/src/components/content/AccordionItem.astro +4 -0
  31. package/src/components/content/Update.astro +3 -0
  32. package/src/components/islands/AskAI.astro +6 -0
  33. package/src/components/islands/ask-ai.tsx +39 -9
  34. package/src/components/layout/Analytics.astro +9 -1
  35. package/src/components/layout/Favicon.astro +29 -8
  36. package/src/components/layout/Fonts.astro +23 -3
  37. package/src/components/layout/Header.astro +2 -2
  38. package/src/components/layout/NavSelector.astro +1 -1
  39. package/src/components/layout/PageActions.astro +120 -78
  40. package/src/components/layout/PageFeedback.astro +12 -3
  41. package/src/components/layout/PageLayout.astro +79 -5
  42. package/src/components/layout/ReferenceLayout.astro +12 -9
  43. package/src/components/layout/RootLayout.astro +153 -121
  44. package/src/components/layout/Search.astro +41 -26
  45. package/src/components/layout/drawer-inert.ts +10 -5
  46. package/src/components/layout/head-scripts.ts +34 -16
  47. package/src/components/layout/nav-utils.ts +34 -15
  48. package/src/components/layout/search/orama.ts +3 -2
  49. package/src/components/openapi/AsyncApiOperation.astro +22 -7
  50. package/src/components/openapi/MessageComposer.astro +238 -0
  51. package/src/components/openapi/Operation.astro +26 -12
  52. package/src/components/openapi/PanelTabs.astro +7 -0
  53. package/src/components/openapi/Playground.astro +320 -0
  54. package/src/components/openapi/RequestPanel.astro +1 -0
  55. package/src/components/openapi/async-snippets.ts +20 -7
  56. package/src/components/openapi/async.ts +13 -2
  57. package/src/components/openapi/message-composer.ts +242 -0
  58. package/src/components/openapi/message-model.ts +108 -0
  59. package/src/components/openapi/message.ts +153 -0
  60. package/src/components/openapi/operation-model.ts +260 -0
  61. package/src/components/openapi/playground-client.ts +486 -0
  62. package/src/components/openapi/playground-schema.ts +109 -0
  63. package/src/components/openapi/request.ts +287 -0
  64. package/src/components/openapi/security.ts +0 -56
  65. package/src/components/openapi/snippets.ts +23 -136
  66. package/src/components/openapi/validate-json.ts +144 -0
  67. package/src/components/openapi/ws-client.ts +194 -0
  68. package/src/core/config-input.ts +67 -1
  69. package/src/core/content-assets.ts +66 -15
  70. package/src/core/data.ts +16 -2
  71. package/src/core/last-modified.ts +76 -2
  72. package/src/core/links.ts +30 -4
  73. package/src/core/navigation.ts +26 -1
  74. package/src/core/open-in-chat.ts +17 -0
  75. package/src/core/project-graph.ts +11 -0
  76. package/src/core/schema.ts +60 -1
  77. package/src/core/server-features.ts +11 -0
  78. package/src/core/sources/normalize.ts +10 -2
  79. package/src/core/types.ts +10 -3
  80. package/src/deploy/vercel-negotiation.ts +34 -14
  81. package/src/og/card.ts +3 -1
  82. package/src/openapi/model.ts +7 -0
  83. package/src/openapi/proxy.ts +217 -0
  84. package/src/openapi/references.ts +8 -0
  85. package/src/openapi/source.ts +13 -0
  86. package/src/registry/eject.ts +4 -5
  87. package/src/search/orama-index.ts +109 -36
  88. package/src/theme/entry.ts +15 -2
  89. package/src/theme/fonts.ts +75 -3
package/dist/cli/index.js CHANGED
@@ -3179,11 +3179,13 @@ var referencesFor = (kind, block, defaultLabel, renderer, display, basePath) =>
3179
3179
  var resolveReferences = (config) => [
3180
3180
  ...referencesFor("openapi", config.openapi, "API Reference", config.openapi.renderer, {
3181
3181
  codeSamples: config.openapi.codeSamples,
3182
- expandSchemas: config.openapi.expandSchemas
3182
+ expandSchemas: config.openapi.expandSchemas,
3183
+ playground: config.openapi.playground
3183
3184
  }, config.basePath),
3184
3185
  ...referencesFor("asyncapi", config.asyncapi, "Events", config.asyncapi.renderer, {
3185
3186
  codeSamples: config.asyncapi.codeSamples,
3186
- expandSchemas: config.asyncapi.expandSchemas
3187
+ expandSchemas: config.asyncapi.expandSchemas,
3188
+ playground: config.asyncapi.playground
3187
3189
  }, config.basePath)
3188
3190
  ];
3189
3191
  var referenceRoutes = (config) => resolveReferences(config).map((ref) => ref.renderer === "blume" ? withBasePath(config.basePath, ref.route) : ref.route);
@@ -3607,7 +3609,47 @@ ${lines.join(`
3607
3609
  }
3608
3610
  ` : "";
3609
3611
  };
3610
- var configuredCssVars = (fonts) => buildFontEntries(fonts).map((entry) => entry.cssVariable);
3612
+ var PRELOAD_WEIGHTS = {
3613
+ body: [400, 500],
3614
+ display: [500, 600],
3615
+ mono: [400]
3616
+ };
3617
+ var entryWeights = (entry) => entry.kind === "remote" ? entry.weights : entry.variants.map((variant) => variant.weight);
3618
+ var preloadWeightsFor = (slot, entry) => {
3619
+ const preferred = PRELOAD_WEIGHTS[slot];
3620
+ const weights = entryWeights(entry);
3621
+ const numeric = weights.filter((weight) => typeof weight === "number");
3622
+ const hits = preferred.filter((weight) => numeric.includes(weight));
3623
+ if (hits.length > 0) {
3624
+ return hits;
3625
+ }
3626
+ return numeric.length === weights.length ? numeric : preferred;
3627
+ };
3628
+ var configuredFonts = (fonts) => {
3629
+ if (!fonts) {
3630
+ return [];
3631
+ }
3632
+ const heads = new Map;
3633
+ for (const slot of SLOTS) {
3634
+ const value = fonts[slot];
3635
+ if (value === undefined) {
3636
+ continue;
3637
+ }
3638
+ const entry = resolveFontValue(slot, value);
3639
+ if (!entry) {
3640
+ continue;
3641
+ }
3642
+ const weights = heads.get(entry.cssVariable) ?? new Set;
3643
+ for (const weight of preloadWeightsFor(slot, entry)) {
3644
+ weights.add(weight);
3645
+ }
3646
+ heads.set(entry.cssVariable, weights);
3647
+ }
3648
+ return [...heads].map(([cssVariable, weights]) => ({
3649
+ cssVariable,
3650
+ preloadWeights: [...weights].toSorted((a, b) => a - b)
3651
+ }));
3652
+ };
3611
3653
 
3612
3654
  // src/astro/templates.ts
3613
3655
  var WORKSPACE_MARKERS = [
@@ -3936,6 +3978,11 @@ ${userConfigSetup}export default defineConfig({
3936
3978
  },
3937
3979
  },
3938
3980
  devToolbar: { enabled: false },
3981
+ // The layouts render Astro's <ClientRouter />, and its in-place swaps read
3982
+ // from the prefetch cache — fetching every link on hover/viewport hides the
3983
+ // request latency behind the user's intent, so most navigations swap
3984
+ // instantly.
3985
+ prefetch: { prefetchAll: true },
3939
3986
  vite: {
3940
3987
  plugins: [tailwindcss(), prerenderDepsPlugin(), serverAppResolvePlugin()],
3941
3988
  // Everything hydration can reach must be part of the dev dep optimizer's
@@ -4047,7 +4094,8 @@ export const collections = { docs${options.staged ? ", staged" : ""} };
4047
4094
  `;
4048
4095
  };
4049
4096
  var ASK_FALLBACK_PROMPT = "You are a helpful documentation assistant. Answer using the project's documentation.";
4050
- var askEndpointTemplate = (backend, grounded, instructions) => {
4097
+ var askEndpointTemplate = (backend, grounded, options) => {
4098
+ const instructions = options?.instructions;
4051
4099
  const fallbackPrompt = instructions ? `${ASK_FALLBACK_PROMPT}
4052
4100
 
4053
4101
  ${instructions}` : ASK_FALLBACK_PROMPT;
@@ -4076,7 +4124,14 @@ const provider = createOpenAICompatible({
4076
4124
  }
4077
4125
  if (grounded) {
4078
4126
  imports.push('import { createAskContext } from "blume/ai/ask-context.ts";', 'import askData from "../../generated/ask-data.json";');
4079
- const groundOptions = instructions ? `, { instructions: ${JSON.stringify(instructions)} }` : "";
4127
+ const groundFields = [];
4128
+ if (instructions) {
4129
+ groundFields.push(`instructions: ${JSON.stringify(instructions)}`);
4130
+ }
4131
+ if (options?.retrieval) {
4132
+ groundFields.push(`retrieval: ${JSON.stringify(options.retrieval)}`);
4133
+ }
4134
+ const groundOptions = groundFields.length > 0 ? `, { ${groundFields.join(", ")} }` : "";
4080
4135
  setup += `
4081
4136
  const ground = createAskContext(askData${groundOptions});
4082
4137
  `;
@@ -4414,6 +4469,16 @@ const handler = createMcpFetchHandler(data as McpData);
4414
4469
  export const ALL: APIRoute = ({ request }) => handler(request);
4415
4470
  `;
4416
4471
  };
4472
+ var playgroundProxyTemplate = (origins) => `// Generated by Blume. Do not edit.
4473
+ import type { APIRoute } from "astro";
4474
+ import { createPlaygroundProxyHandler } from "blume/openapi/proxy.ts";
4475
+
4476
+ export const prerender = false;
4477
+
4478
+ const handler = createPlaygroundProxyHandler(${JSON.stringify(origins)});
4479
+
4480
+ export const ALL: APIRoute = ({ request }) => handler(request);
4481
+ `;
4417
4482
  var staticJsonEndpointTemplate = (payload) => `// Generated by Blume. Do not edit.
4418
4483
  export const prerender = true;
4419
4484
 
@@ -4444,7 +4509,7 @@ export function GET({ props }: { props: { section: string } }) {
4444
4509
  });
4445
4510
  }
4446
4511
  `;
4447
- var ogEndpointTemplate = (customRoutes = [], og = {}) => `// Generated by Blume. Do not edit.
4512
+ var ogEndpointTemplate = (customRoutes = [], og = {}, includeChangelog = false) => `// Generated by Blume. Do not edit.
4448
4513
  import { renderOgImage } from "blume/og";
4449
4514
  import type { OgFont, OgFontFamilies } from "blume/og";
4450
4515
  import data from "blume:data";
@@ -4479,7 +4544,10 @@ export function getStaticPaths() {
4479
4544
  }
4480
4545
  for (const route of data.routes) {
4481
4546
  add(route.path === "/" ? "index" : route.path.slice(1), route.title);
4482
- }
4547
+ }${includeChangelog ? `
4548
+ // The generated changelog index is not a content route, so it needs its own
4549
+ // card. Added last: a custom page or content route owning /changelog wins.
4550
+ add("changelog", data.ui.changelog?.title ?? "Changelog");` : ""}
4483
4551
  return paths;
4484
4552
  }
4485
4553
 
@@ -4753,10 +4821,9 @@ const contentLocale =
4753
4821
  const contentDir = i18n
4754
4822
  ? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
4755
4823
  : "ltr";
4756
- const absolute = (path: string) => {
4757
- const p = withBase(path);
4758
- return base + (p === "/" ? "" : p);
4759
- };
4824
+ // The root route keeps its trailing slash (\`https://site/\`) so canonical and
4825
+ // hreflang URLs byte-match the sitemap's <loc> for the home page.
4826
+ const absolute = (path: string) => base + withBase(path);
4760
4827
 
4761
4828
  // An archived page defaults its canonical to the same page in the latest docs
4762
4829
  // when that page still exists — search engines treat the live page as
@@ -4767,7 +4834,7 @@ const canonical =
4767
4834
  (archived && archived.canonical === "latest" && latestVersionAlt && base
4768
4835
  ? absolute(latestVersionAlt.path)
4769
4836
  : base
4770
- ? \`\${base}\${basedRoute === "/" ? "" : encodeURI(basedRoute)}\`
4837
+ ? \`\${base}\${basedRoute === "/" ? "/" : encodeURI(basedRoute)}\`
4771
4838
  : null);
4772
4839
  const effectiveNoindex = Boolean(seo.noindex) || (archived?.noindex ?? false);
4773
4840
 
@@ -4902,6 +4969,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
4902
4969
  feedback={data.config.feedback}
4903
4970
  exportPdf={${options.exportPdf}}
4904
4971
  exportEpub={${options.exportEpub}}
4972
+ openInChat={data.config.openInChat}
4905
4973
  feeds={data.feeds}
4906
4974
  discovery={data.config.discovery}
4907
4975
  siteUrl={data.config.site}
@@ -5061,6 +5129,12 @@ const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
5061
5129
  const basedRoute = withBase("/changelog");
5062
5130
  const canonical = base ? base + basedRoute : null;
5063
5131
 
5132
+ // The generated OG card for this route (the /og endpoint emits it alongside
5133
+ // the content-route cards), absolutized like the catch-all's so crawlers get
5134
+ // a full URL when the site is known.
5135
+ const ogPath = data.config.og.enabled ? withBase("/og/changelog.png") : null;
5136
+ const ogImage = ogPath && base ? base + ogPath : ogPath;
5137
+
5064
5138
  // The page chrome (h1, title, description) comes from the same translatable
5065
5139
  // \`changelog\` group as the reveal button; optional chaining tolerates a
5066
5140
  // not-yet-regenerated data snapshot from before these keys existed.
@@ -5068,7 +5142,10 @@ const changelogTitle = data.ui.changelog?.title ?? "Changelog";
5068
5142
  const changelogDescription =
5069
5143
  data.ui.changelog?.description ??
5070
5144
  "Product updates, new features, and fixes from every release.";
5071
- const pageTitle = data.config.title + " " + changelogTitle;
5145
+ // The layout suffixes "- {site title}" itself, so the page title is just the
5146
+ // changelog's own name — prefixing the site title too would double it
5147
+ // ("Acme Changelog - Acme").
5148
+ const pageTitle = changelogTitle;
5072
5149
 
5073
5150
  const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
5074
5151
  ---
@@ -5100,11 +5177,13 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
5100
5177
  fontCssVars={data.fontCssVars}
5101
5178
  searchEnabled={data.config.search.enabled}
5102
5179
  indexable={true}
5103
- ogImage={null}
5180
+ ogImage={ogImage}
5181
+ ogGenerated={Boolean(ogImage)}
5104
5182
  x={data.config.x}
5105
5183
  canonical={canonical}
5106
5184
  exportPdf={${options.exportPdf}}
5107
5185
  exportEpub={${options.exportEpub}}
5186
+ openInChat={data.config.openInChat}
5108
5187
  feeds={data.feeds}
5109
5188
  discovery={data.config.discovery}
5110
5189
  siteUrl={data.config.site}
@@ -9566,6 +9645,16 @@ var resolveUIStrings = (locale, options) => {
9566
9645
  return dict;
9567
9646
  };
9568
9647
 
9648
+ // src/core/open-in-chat.ts
9649
+ var openInChatProviders = [
9650
+ "v0",
9651
+ "chatgpt",
9652
+ "claude",
9653
+ "t3",
9654
+ "scira",
9655
+ "cursor"
9656
+ ];
9657
+
9569
9658
  // src/core/standard-schema.ts
9570
9659
  var isStandardSchema = (value) => typeof value === "object" && value !== null && typeof value["~standard"]?.validate === "function";
9571
9660
 
@@ -9861,7 +9950,7 @@ var themeConfigSchema = z2.strictObject({
9861
9950
  backgroundImage: perModeValueSchema,
9862
9951
  fonts: z2.strictObject({
9863
9952
  body: fontValueSchema.default("inter"),
9864
- display: fontValueSchema.default("inter-tight"),
9953
+ display: fontValueSchema.default("inter"),
9865
9954
  mono: fontValueSchema.default("ibm-plex-mono")
9866
9955
  }).prefault({}),
9867
9956
  layout: z2.enum(["sidebar"]).default("sidebar"),
@@ -9980,6 +10069,11 @@ var aiConfigSchema = z2.strictObject({
9980
10069
  instructions: z2.string().trim().min(1).optional(),
9981
10070
  model: z2.string().default("openai/gpt-5.5"),
9982
10071
  provider: z2.enum(askAiProviders).default("gateway"),
10072
+ retrieval: z2.strictObject({
10073
+ contextBudget: z2.number().int().positive().optional(),
10074
+ excerptChars: z2.number().int().positive().optional(),
10075
+ maxResults: z2.number().int().positive().optional()
10076
+ }).optional(),
9983
10077
  suggestions: z2.array(z2.strictObject({
9984
10078
  icon: iconName.optional(),
9985
10079
  label: z2.string().min(1)
@@ -10004,6 +10098,17 @@ var aiConfigSchema = z2.strictObject({
10004
10098
  message: "Expected a serializer function."
10005
10099
  })).default({}),
10006
10100
  mcp: mcpConfigSchema.prefault({}),
10101
+ openInChat: z2.union([
10102
+ z2.boolean(),
10103
+ z2.array(z2.enum(openInChatProviders)).refine((value) => new Set(value).size === value.length, {
10104
+ message: "ai.openInChat must not repeat a provider."
10105
+ })
10106
+ ]).default(true).transform((value) => {
10107
+ if (isBoolean(value)) {
10108
+ return value ? [...openInChatProviders] : [];
10109
+ }
10110
+ return value;
10111
+ }),
10007
10112
  skills: z2.string().min(1).optional(),
10008
10113
  webBotAuth: z2.strictObject({
10009
10114
  keys: z2.array(publicJwkSchema).default([])
@@ -10279,6 +10384,13 @@ var referenceConfigSchema = (defaults) => z2.strictObject({
10279
10384
  codeSamples: z2.array(z2.string()).default(defaults.codeSamples),
10280
10385
  enabled: z2.boolean().default(false),
10281
10386
  expandSchemas: z2.boolean().default(false),
10387
+ playground: z2.union([
10388
+ z2.boolean(),
10389
+ z2.strictObject({
10390
+ enabled: z2.boolean().default(true),
10391
+ proxy: z2.union([z2.boolean(), z2.string()]).default(false)
10392
+ })
10393
+ ]).default(true).transform((value) => isBoolean(value) ? { enabled: value, proxy: false } : value),
10282
10394
  renderer: z2.enum(["blume", "scalar"]).default("blume"),
10283
10395
  route: z2.string().default(defaults.route),
10284
10396
  scalar: scalarConfigSchema,
@@ -10718,6 +10830,8 @@ import { extname } from "pathe";
10718
10830
  var NUMERIC_PREFIX = /^(?<order>\d+)[-_.]/u;
10719
10831
  var GROUP_FOLDER = /^\((?<label>.+)\)$/u;
10720
10832
  var WORD_SPLIT = /[-_]/u;
10833
+ var isUnderPath = (route, base) => base === "/" || route === base || route.startsWith(`${base}/`);
10834
+ var isRootTab = (tab, root) => isUnderPath(root, tab.path);
10721
10835
  var humanize = (segment) => segment.replace(NUMERIC_PREFIX, "").split(WORD_SPLIT).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
10722
10836
  var numericOrder = (segment) => {
10723
10837
  const value = segment.match(NUMERIC_PREFIX)?.groups?.order;
@@ -11174,7 +11288,7 @@ var buildNavigation = (pages, options) => {
11174
11288
  tabs: withTabHrefs(tabs, sidebar2)
11175
11289
  };
11176
11290
  }
11177
- const sidebar = buildFileSystemSidebar(pages, options.folderMeta, sharedFolderMeta, metaPrefix, sharedMetaPrefix, display, new Set(tabs.flatMap((tab) => tab.path === rootTabPath ? [] : [tab.path])), diagnostics);
11291
+ const sidebar = buildFileSystemSidebar(pages, options.folderMeta, sharedFolderMeta, metaPrefix, sharedMetaPrefix, display, new Set(tabs.flatMap((tab) => isRootTab(tab, rootTabPath) ? [] : [tab.path])), diagnostics);
11178
11292
  return {
11179
11293
  featured,
11180
11294
  root: rootTabPath,
@@ -11406,6 +11520,15 @@ var buildContentGraph = (pages, options) => {
11406
11520
  // src/core/last-modified.ts
11407
11521
  import { execFileSync } from "node:child_process";
11408
11522
  import { relative as relative7 } from "pathe";
11523
+ var GIT_LOCATION_VARS = new Set([
11524
+ "GIT_COMMON_DIR",
11525
+ "GIT_DIR",
11526
+ "GIT_INDEX_FILE",
11527
+ "GIT_OBJECT_DIRECTORY",
11528
+ "GIT_PREFIX",
11529
+ "GIT_WORK_TREE"
11530
+ ]);
11531
+ var gitEnv = () => Object.fromEntries(Object.entries(process.env).filter(([key]) => !GIT_LOCATION_VARS.has(key)));
11409
11532
  var resolveLastModifiedConfig = (value) => {
11410
11533
  if (value === false) {
11411
11534
  return { enabled: false, source: "git" };
@@ -11433,7 +11556,7 @@ var gitLastModifiedTimes = (root, contentRoots, sourcePaths) => {
11433
11556
  return new Map;
11434
11557
  }
11435
11558
  try {
11436
- const gitRoot = execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf-8" }).trim();
11559
+ const gitRoot = execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf-8", env: gitEnv() }).trim();
11437
11560
  const output = execFileSync("git", [
11438
11561
  "-C",
11439
11562
  root,
@@ -11444,7 +11567,7 @@ var gitLastModifiedTimes = (root, contentRoots, sourcePaths) => {
11444
11567
  "--name-only",
11445
11568
  "--",
11446
11569
  ...contentRoots
11447
- ], { encoding: "utf-8", maxBuffer: 256 * 1024 * 1024 });
11570
+ ], { encoding: "utf-8", env: gitEnv(), maxBuffer: 256 * 1024 * 1024 });
11448
11571
  const byRepoPath = parseGitLog(output);
11449
11572
  const result = new Map;
11450
11573
  for (const sourcePath of sourcePaths) {
@@ -11458,6 +11581,30 @@ var gitLastModifiedTimes = (root, contentRoots, sourcePaths) => {
11458
11581
  return new Map;
11459
11582
  }
11460
11583
  };
11584
+ var isShallowGitRepository = (root) => {
11585
+ try {
11586
+ return execFileSync("git", ["-C", root, "rev-parse", "--is-shallow-repository"], {
11587
+ encoding: "utf-8",
11588
+ env: gitEnv(),
11589
+ stdio: ["ignore", "pipe", "ignore"]
11590
+ }).trim() === "true";
11591
+ } catch {
11592
+ return false;
11593
+ }
11594
+ };
11595
+ var lastModifiedShallowWarning = (root, undatedCount) => {
11596
+ if (undatedCount === 0 || !isShallowGitRepository(root)) {
11597
+ return [];
11598
+ }
11599
+ return [
11600
+ {
11601
+ code: "BLUME_SHALLOW_GIT_HISTORY",
11602
+ message: `lastModified is on, but this build runs in a shallow git clone, so ${undatedCount} page(s) have no git-derived date — their sitemap <lastmod> and "Last updated" stamps are omitted.`,
11603
+ severity: "warning",
11604
+ suggestion: "Fetch full history in CI: set the VERCEL_DEEP_CLONE=true environment variable on Vercel, or fetch-depth: 0 for actions/checkout."
11605
+ }
11606
+ ];
11607
+ };
11461
11608
 
11462
11609
  // src/core/manifest.ts
11463
11610
  var versionAlternateKey = (versionKey, locale) => `${versionKey}\x00${locale}`;
@@ -11829,11 +11976,15 @@ var scanLinkLine = (line, lineNumber, fence, links) => {
11829
11976
  continue;
11830
11977
  }
11831
11978
  const targetOffset = targetOffsetIn(match[0], target, match.groups?.title);
11832
- links.push({
11979
+ const entry = {
11833
11980
  column: match.index + targetOffset + 1,
11834
11981
  line: lineNumber,
11835
11982
  target
11836
- });
11983
+ };
11984
+ if (masked[match.index - 1] === "!") {
11985
+ entry.image = true;
11986
+ }
11987
+ links.push(entry);
11837
11988
  const label = match[0].slice(0, targetOffset - "](".length);
11838
11989
  for (const image of label.matchAll(MD_IMAGE)) {
11839
11990
  const imageTarget = image.groups?.target;
@@ -11842,6 +11993,7 @@ var scanLinkLine = (line, lineNumber, fence, links) => {
11842
11993
  }
11843
11994
  links.push({
11844
11995
  column: match.index + image.index + targetOffsetIn(image[0], imageTarget, image.groups?.title) + 1,
11996
+ image: true,
11845
11997
  line: lineNumber,
11846
11998
  target: imageTarget
11847
11999
  });
@@ -12790,6 +12942,13 @@ var openApiSource = (references, ctx) => {
12790
12942
  try {
12791
12943
  const { document, warnings, operations, tags, extractWarnings } = await parseReference(reference, ctx);
12792
12944
  const info = document.info ?? { title: reference.label, version: "" };
12945
+ const { proxy: configuredProxy } = reference.display.playground;
12946
+ let proxy = false;
12947
+ if (configuredProxy === true) {
12948
+ proxy = withBasePath(reference.basePath, "/_api-proxy");
12949
+ } else if (configuredProxy !== false && configuredProxy !== "") {
12950
+ proxy = configuredProxy;
12951
+ }
12793
12952
  const spec = {
12794
12953
  codeSamples: reference.display.codeSamples,
12795
12954
  description: info.description ?? "",
@@ -12804,6 +12963,7 @@ var openApiSource = (references, ctx) => {
12804
12963
  route: withBasePath(reference.basePath, operation.route)
12805
12964
  }
12806
12965
  ])),
12966
+ playground: { enabled: reference.display.playground.enabled, proxy },
12807
12967
  route: reference.route,
12808
12968
  slug: reference.slug,
12809
12969
  tags,
@@ -14029,6 +14189,7 @@ var scanProject = async (root, options = {}) => {
14029
14189
  } = normalizeLoadedEntries(loaded, config);
14030
14190
  const pages = mode === "build" && !preview ? allPages.filter((page) => !page.meta.draft) : allPages;
14031
14191
  const lastModified = resolveLastModifiedConfig(config.lastModified);
14192
+ const lastModifiedWarnings = [];
14032
14193
  if (lastModified.enabled && lastModified.source === "git") {
14033
14194
  const fsPaths = pages.map((page) => page.sourcePath).filter((path) => path !== undefined);
14034
14195
  const contentRoots = sources.flatMap((source) => source.staged || !source.contentRoot ? [] : [source.contentRoot]);
@@ -14038,6 +14199,8 @@ var scanProject = async (root, options = {}) => {
14038
14199
  page.lastModified = gitTimes.get(page.sourcePath);
14039
14200
  }
14040
14201
  }
14202
+ const undated = pages.filter((page) => page.sourcePath && !page.lastModified).length;
14203
+ lastModifiedWarnings.push(...lastModifiedShallowWarning(context.root, undated));
14041
14204
  }
14042
14205
  const graph = buildContentGraph(pages, {
14043
14206
  basePath: config.basePath,
@@ -14059,7 +14222,8 @@ var scanProject = async (root, options = {}) => {
14059
14222
  ...entryIdDiagnostics(pages, resolveDocsCollection(config, context).base),
14060
14223
  ...graph.diagnostics,
14061
14224
  ...i18nWarnings,
14062
- ...versionWarnings
14225
+ ...versionWarnings,
14226
+ ...lastModifiedWarnings
14063
14227
  ],
14064
14228
  droppedPages,
14065
14229
  graph,
@@ -14250,7 +14414,7 @@ var auditCommand = defineCommand2({
14250
14414
  });
14251
14415
 
14252
14416
  // src/cli/commands/build.ts
14253
- import { existsSync as existsSync18 } from "node:fs";
14417
+ import { existsSync as existsSync17 } from "node:fs";
14254
14418
  import { mkdir as mkdir8, readdir as readdir2, readFile as readFile17, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
14255
14419
  import { build } from "astro";
14256
14420
  import { defineCommand as defineCommand3 } from "citty";
@@ -14423,9 +14587,16 @@ var buildAgentReadability = (project) => {
14423
14587
  };
14424
14588
 
14425
14589
  // src/core/content-assets.ts
14426
- import { existsSync as existsSync9 } from "node:fs";
14590
+ import { readdirSync, statSync } from "node:fs";
14427
14591
  import { readFile as readFile8 } from "node:fs/promises";
14428
- import { dirname as dirname7, extname as extname5, relative as relative11, resolve as resolve5 } from "pathe";
14592
+ import {
14593
+ basename as basename2,
14594
+ dirname as dirname7,
14595
+ extname as extname5,
14596
+ normalize as normalize3,
14597
+ relative as relative11,
14598
+ resolve as resolve5
14599
+ } from "pathe";
14429
14600
  var CONTENT_ASSETS_PREFIX = "/blume-assets/content";
14430
14601
  var IMAGE_EXTENSIONS = new Set([
14431
14602
  ".apng",
@@ -14448,19 +14619,40 @@ var contentAssetParam = (projectRoot, absPath) => {
14448
14619
  }
14449
14620
  return rel;
14450
14621
  };
14451
- var resolveRelativeImage = (sourceDir, target) => {
14452
- if (!isRelativeTarget(target)) {
14453
- return null;
14454
- }
14455
- let decoded = target;
14622
+ var decodeTarget = (target) => {
14456
14623
  try {
14457
- decoded = decodeURI(target);
14458
- } catch {}
14459
- if (!IMAGE_EXTENSIONS.has(extname5(decoded).toLowerCase())) {
14624
+ return decodeURI(target);
14625
+ } catch {
14626
+ return target;
14627
+ }
14628
+ };
14629
+ var isRelativeImageTarget = (target) => isRelativeTarget(target) && IMAGE_EXTENSIONS.has(extname5(decodeTarget(target)).toLowerCase());
14630
+ var existsAsWritten = (abs, segments) => {
14631
+ let current = abs;
14632
+ for (let i = 0;i < segments; i += 1) {
14633
+ const parent = dirname7(current);
14634
+ let entries;
14635
+ try {
14636
+ entries = readdirSync(parent);
14637
+ } catch {
14638
+ return false;
14639
+ }
14640
+ if (!entries.includes(basename2(current))) {
14641
+ return false;
14642
+ }
14643
+ current = parent;
14644
+ }
14645
+ const stat2 = statSync(abs, { throwIfNoEntry: false });
14646
+ return stat2 !== undefined && stat2.isFile();
14647
+ };
14648
+ var resolveRelativeImage = (sourceDir, target) => {
14649
+ if (!isRelativeImageTarget(target)) {
14460
14650
  return null;
14461
14651
  }
14652
+ const decoded = decodeTarget(target);
14653
+ const segments = normalize3(decoded).split("/").filter((part) => part !== "" && part !== "..").length;
14462
14654
  const abs = resolve5(sourceDir, decoded);
14463
- return existsSync9(abs) ? abs : null;
14655
+ return existsAsWritten(abs, segments) ? abs : null;
14464
14656
  };
14465
14657
  var encodedParam = (param) => param.split("/").map(encodeURIComponent).join("/");
14466
14658
  var rewriteLine = (line, toUrl2) => {
@@ -15178,13 +15370,13 @@ var buildSignaturesDirectory = (config) => {
15178
15370
  };
15179
15371
 
15180
15372
  // src/core/gitignore.ts
15181
- import { existsSync as existsSync10 } from "node:fs";
15373
+ import { existsSync as existsSync9 } from "node:fs";
15182
15374
  import { readFile as readFile12, writeFile as writeFile6 } from "node:fs/promises";
15183
15375
  import { join as join19 } from "pathe";
15184
15376
  var gitignoreKey = (line) => line.trim().replace(/\/+$/u, "");
15185
15377
  var ensureGitignore = async (root, entries) => {
15186
15378
  const path = join19(root, ".gitignore");
15187
- const existing = existsSync10(path) ? await readFile12(path, "utf-8") : "";
15379
+ const existing = existsSync9(path) ? await readFile12(path, "utf-8") : "";
15188
15380
  const present = new Set(existing.split(`
15189
15381
  `).flatMap((line) => {
15190
15382
  const key = gitignoreKey(line);
@@ -15212,6 +15404,9 @@ var serverFeatures = (config) => {
15212
15404
  if (config.ai.mcp.enabled) {
15213
15405
  features.push("MCP server");
15214
15406
  }
15407
+ if (config.openapi.enabled && config.openapi.renderer === "blume" && config.openapi.playground.enabled && config.openapi.playground.proxy === true) {
15408
+ features.push("API playground proxy");
15409
+ }
15215
15410
  if (searchProviderMeta(config.search.provider).requiresServer) {
15216
15411
  features.push(`Search (${config.search.provider})`);
15217
15412
  }
@@ -15818,12 +16013,14 @@ var buildNegotiationRoutes = (routePaths, homeTokens) => {
15818
16013
  return { headerRoutes, rewriteRoutes };
15819
16014
  };
15820
16015
  var HOME_SRC = "^/$";
15821
- var isNegotiationRoute = (route) => route.has?.some((condition) => condition.value === ACCEPT_MARKDOWN_HEADER_VALUE) === true || route.continue === true && route.headers?.vary === "Accept" && isString6(route.src) && Object.keys(route).length === 3 || route.continue === true && isString6(route.headers?.link) && route.src === HOME_SRC && Object.keys(route).length === 3;
16016
+ var TRAILING_SLASH_REDIRECT = {
16017
+ headers: { Location: "/$1" },
16018
+ src: "^/(.+)/$",
16019
+ status: 308
16020
+ };
16021
+ var isNegotiationRoute = (route) => route.has?.some((condition) => condition.value === ACCEPT_MARKDOWN_HEADER_VALUE) === true || route.continue === true && route.headers?.vary === "Accept" && isString6(route.src) && Object.keys(route).length === 3 || route.continue === true && isString6(route.headers?.link) && route.src === HOME_SRC && Object.keys(route).length === 3 || route.status === TRAILING_SLASH_REDIRECT.status && route.src === TRAILING_SLASH_REDIRECT.src;
15822
16022
  var injectNegotiationRoutes = (configText, routePaths, homeLinkHeader, contentTypeOverrides, homeTokens) => {
15823
16023
  const overrideEntries = Object.entries(contentTypeOverrides ?? {});
15824
- if (routePaths.length === 0 && !homeLinkHeader && overrideEntries.length === 0) {
15825
- return null;
15826
- }
15827
16024
  let config;
15828
16025
  try {
15829
16026
  config = JSON.parse(configText);
@@ -15849,7 +16046,7 @@ var injectNegotiationRoutes = (configText, routePaths, homeLinkHeader, contentTy
15849
16046
  src: HOME_SRC
15850
16047
  });
15851
16048
  }
15852
- routes.splice(filesystemIndex, 0, ...headerRoutes, ...rewriteRoutes);
16049
+ routes.splice(filesystemIndex, 0, ...headerRoutes, ...rewriteRoutes, TRAILING_SLASH_REDIRECT);
15853
16050
  config.routes = routes;
15854
16051
  return `${JSON.stringify(config, null, "\t")}
15855
16052
  `;
@@ -16158,7 +16355,7 @@ var syncSearchProvider = async (project, reporter) => {
16158
16355
 
16159
16356
  // src/cli/dev-lock.ts
16160
16357
  import {
16161
- existsSync as existsSync11,
16358
+ existsSync as existsSync10,
16162
16359
  mkdirSync,
16163
16360
  readFileSync as readFileSync5,
16164
16361
  rmSync,
@@ -16197,7 +16394,7 @@ var isProcessAlive = (pid) => {
16197
16394
  };
16198
16395
  var readDevLock = (outDir) => {
16199
16396
  const path = lockPath(outDir);
16200
- if (!existsSync11(path)) {
16397
+ if (!existsSync10(path)) {
16201
16398
  return null;
16202
16399
  }
16203
16400
  const lock = parseLock(readFileSync5(path, "utf-8"));
@@ -16224,7 +16421,7 @@ class DevLockHeldError extends Error {
16224
16421
  }
16225
16422
  var ownsLock = (outDir) => {
16226
16423
  const path = lockPath(outDir);
16227
- if (!existsSync11(path)) {
16424
+ if (!existsSync10(path)) {
16228
16425
  return false;
16229
16426
  }
16230
16427
  try {
@@ -16282,7 +16479,7 @@ var refuseIfDevRunning = (root, action, options = {}) => {
16282
16479
 
16283
16480
  // src/astro/generate.ts
16284
16481
  import { createHash as createHash4 } from "node:crypto";
16285
- import { existsSync as existsSync16, readFileSync as readFileSync8, realpathSync } from "node:fs";
16482
+ import { existsSync as existsSync15, readFileSync as readFileSync8, realpathSync } from "node:fs";
16286
16483
  import {
16287
16484
  lstat,
16288
16485
  mkdir as mkdir7,
@@ -16296,7 +16493,7 @@ import { createRequire as createRequire4 } from "node:module";
16296
16493
  import { pathToFileURL as pathToFileURL2 } from "node:url";
16297
16494
  import { imageSize as imageSize2 } from "image-size";
16298
16495
  import pMap6 from "p-map";
16299
- import { basename as basename3, dirname as dirname11, join as join28, normalize as normalize3, relative as relative14, resolve as resolve8 } from "pathe";
16496
+ import { basename as basename4, dirname as dirname11, join as join28, normalize as normalize4, relative as relative14, resolve as resolve8 } from "pathe";
16300
16497
  import { glob as glob7 } from "tinyglobby";
16301
16498
 
16302
16499
  // src/ai/ask-data.ts
@@ -16545,7 +16742,7 @@ var validateUsedComponents = (pages, extraTags, registryNames) => {
16545
16742
  };
16546
16743
 
16547
16744
  // src/core/component-overrides.ts
16548
- import { existsSync as existsSync12 } from "node:fs";
16745
+ import { existsSync as existsSync11 } from "node:fs";
16549
16746
  import { dirname as dirname8, extname as extname7, isAbsolute as isAbsolute6, resolve as resolve6 } from "pathe";
16550
16747
  import ts from "typescript";
16551
16748
  var GROUPS = ["mdx", "layout", "islands"];
@@ -16640,7 +16837,7 @@ var findDefaultExportObject = (sourceFile) => {
16640
16837
  var probeExtension = (base) => {
16641
16838
  for (const extension of COMPONENT_EXTS) {
16642
16839
  const candidate = `${base}.${extension}`;
16643
- if (existsSync12(candidate)) {
16840
+ if (existsSync11(candidate)) {
16644
16841
  return candidate;
16645
16842
  }
16646
16843
  }
@@ -16819,7 +17016,7 @@ var writeTextAtomic = async (path, text) => {
16819
17016
  };
16820
17017
 
16821
17018
  // src/core/tsconfig-aliases.ts
16822
- import { existsSync as existsSync13 } from "node:fs";
17019
+ import { existsSync as existsSync12 } from "node:fs";
16823
17020
  import { parseTsconfig } from "get-tsconfig";
16824
17021
  import { dirname as dirname10, join as join22, resolve as resolve7 } from "pathe";
16825
17022
  var CONFIG_DIR_TEMPLATE = "${configDir}";
@@ -16841,7 +17038,7 @@ var toAlias = (key, value, baseDir, configDir) => {
16841
17038
  };
16842
17039
  };
16843
17040
  var resolveTsconfigAliases = (root) => {
16844
- const entry = ["tsconfig.json", "jsconfig.json"].map((name) => join22(root, name)).find((file) => existsSync13(file));
17041
+ const entry = ["tsconfig.json", "jsconfig.json"].map((name) => join22(root, name)).find((file) => existsSync12(file));
16845
17042
  if (!entry) {
16846
17043
  return {};
16847
17044
  }
@@ -16868,7 +17065,7 @@ var resolveTsconfigAliases = (root) => {
16868
17065
  };
16869
17066
 
16870
17067
  // src/og/derive.ts
16871
- import { existsSync as existsSync14 } from "node:fs";
17068
+ import { existsSync as existsSync13 } from "node:fs";
16872
17069
  import { isAbsolute as isAbsolute7, join as join23 } from "pathe";
16873
17070
  var CARD_WEIGHTS = [400, 600];
16874
17071
  var absoluteSrc = (root, src) => isAbsolute7(src) ? src : join23(root, src);
@@ -16972,11 +17169,11 @@ var missingFontFiles = (options, root) => {
16972
17169
  sources.push(absoluteSrc(root, font.src));
16973
17170
  }
16974
17171
  }
16975
- return [...new Set(sources)].filter((path) => !existsSync14(path));
17172
+ return [...new Set(sources)].filter((path) => !existsSync13(path));
16976
17173
  };
16977
17174
 
16978
17175
  // src/og/logo.ts
16979
- import { existsSync as existsSync15, readFileSync as readFileSync6 } from "node:fs";
17176
+ import { existsSync as existsSync14, readFileSync as readFileSync6 } from "node:fs";
16980
17177
  import { join as join24 } from "pathe";
16981
17178
  var resolveOgLogo = (project, source) => {
16982
17179
  if (!source?.toLowerCase().endsWith(".svg")) {
@@ -16986,7 +17183,7 @@ var resolveOgLogo = (project, source) => {
16986
17183
  const file = [
16987
17184
  join24(project.context.root, "public", relative13),
16988
17185
  join24(project.context.root, relative13)
16989
- ].find((path) => existsSync15(path));
17186
+ ].find((path) => existsSync14(path));
16990
17187
  return file ? readFileSync6(file, "utf-8") : undefined;
16991
17188
  };
16992
17189
 
@@ -17319,7 +17516,11 @@ ${THEME_MAPPING}
17319
17516
  scroll-padding-top: 4.5rem;
17320
17517
  text-rendering: optimizeLegibility;
17321
17518
  }
17322
- /* Headings use the display font (defaults to the body font when unset). */
17519
+ /* Headings use the display font (defaults to the body font when unset).
17520
+ The tightened tracking is part of the theme, not the font: display-tuned
17521
+ families bake it into their metrics, but a text family promoted to
17522
+ headings (including the Inter default) reads loose without it. -0.05em
17523
+ was matched visually against Inter Tight, the previous display default. */
17323
17524
  h1,
17324
17525
  h2,
17325
17526
  h3,
@@ -17327,6 +17528,7 @@ ${THEME_MAPPING}
17327
17528
  h5,
17328
17529
  h6 {
17329
17530
  font-family: var(--font-display);
17531
+ letter-spacing: -0.05em;
17330
17532
  }
17331
17533
  :focus-visible {
17332
17534
  outline: 2px solid var(--blume-accent);
@@ -17347,6 +17549,13 @@ ${THEME_MAPPING}
17347
17549
  }
17348
17550
  }
17349
17551
 
17552
+ /* Same-origin navigations ride Astro's client router (the layouts render
17553
+ ClientRouter), which owns the page-transition styling — including its own
17554
+ prefers-reduced-motion guard — so the theme declares nothing about it. The
17555
+ cross-document view-transition opt-in that briefly lived here is
17556
+ deliberately gone: mixing it with the client router double-animates the
17557
+ full-load navigations the router hands back to the browser. */
17558
+
17350
17559
  /* Code reads left-to-right regardless of page direction; only the surrounding
17351
17560
  chrome mirrors for RTL. Inline code is isolated so LTR identifiers don't
17352
17561
  disturb the bidi flow of right-to-left prose. */
@@ -17382,9 +17591,10 @@ ${THEME_MAPPING}
17382
17591
  line-height: 1.7;
17383
17592
  }
17384
17593
 
17594
+ /* No letter-spacing here: prose headings inherit the base h1-h6 rule's
17595
+ display tracking, same as headings outside the prose column. */
17385
17596
  .prose :where(h1, h2, h3, h4) {
17386
17597
  font-weight: 500;
17387
- letter-spacing: 0;
17388
17598
  }
17389
17599
 
17390
17600
  /* A heading can carry one long unbreakable token — an OpenAPI operation's title
@@ -18125,7 +18335,7 @@ import { glob as glob6 } from "tinyglobby";
18125
18335
  // src/astro/islands.ts
18126
18336
  import { readFile as readFile14 } from "node:fs/promises";
18127
18337
  import pMap4 from "p-map";
18128
- import { basename as basename2, join as join26 } from "pathe";
18338
+ import { basename as basename3, join as join26 } from "pathe";
18129
18339
  import { glob as glob5 } from "tinyglobby";
18130
18340
  var DEFAULT_CLIENT = "visible";
18131
18341
  var READ_CONCURRENCY2 = 16;
@@ -18170,7 +18380,7 @@ var discoverIslands = async (root) => {
18170
18380
  const warnings = [];
18171
18381
  const seen = new Map;
18172
18382
  const collectIsland = (file, source) => {
18173
- const base = basename2(file);
18383
+ const base = basename3(file);
18174
18384
  const ext = base.match(ISLAND_FILE)?.groups?.ext;
18175
18385
  const framework = ext ? FRAMEWORK_BY_EXT2.get(ext) : undefined;
18176
18386
  if (!framework) {
@@ -18298,7 +18508,7 @@ var resolveAstroPackageJson = (modulesDir) => {
18298
18508
  }
18299
18509
  };
18300
18510
  var resolvedAstroHit = (fromDir) => {
18301
- let dir = normalize3(fromDir);
18511
+ let dir = normalize4(fromDir);
18302
18512
  while (true) {
18303
18513
  const modulesDir = join28(dir, "node_modules");
18304
18514
  const pkg = resolveAstroPackageJson(modulesDir);
@@ -18323,7 +18533,7 @@ var depsCandidates = (pkgDir) => [
18323
18533
  join28(pkgDir, "node_modules"),
18324
18534
  dirname11(pkgDir)
18325
18535
  ];
18326
- var candidateHolding = (pkgDir, ...segments) => depsCandidates(pkgDir).find((dir) => existsSync16(join28(dir, ...segments))) ?? null;
18536
+ var candidateHolding = (pkgDir, ...segments) => depsCandidates(pkgDir).find((dir) => existsSync15(join28(dir, ...segments))) ?? null;
18327
18537
  var linkDepsJunction = async (link, depsDir) => {
18328
18538
  let existing;
18329
18539
  try {
@@ -18459,7 +18669,7 @@ var writeExamplesPreview = async (options) => {
18459
18669
  await write(join28(srcDir, "pages", ...config.basePath.split("/").filter(Boolean), "blume-examples", "[...path].astro"), examplesPageTemplate());
18460
18670
  }
18461
18671
  const cssFile = examplesCssFile(root, config);
18462
- return cssFile && !existsSync16(cssFile) ? [
18672
+ return cssFile && !existsSync15(cssFile) ? [
18463
18673
  `examples.css points at "${config.examples.css}", which doesn't exist; previews render without it.`
18464
18674
  ] : [];
18465
18675
  };
@@ -18525,7 +18735,7 @@ var pruneOrphans = async (srcDir, written) => {
18525
18735
  });
18526
18736
  const removals = [];
18527
18737
  for (const path of existing) {
18528
- const normalized = normalize3(path);
18738
+ const normalized = normalize4(path);
18529
18739
  if (!written.has(normalized)) {
18530
18740
  removals.push(rm2(normalized, { force: true }));
18531
18741
  }
@@ -18546,10 +18756,10 @@ var writeStagedContent = async (out, staged) => {
18546
18756
  const written = new Set;
18547
18757
  await Promise.all([...staged].map(async ([entryId, text]) => {
18548
18758
  const path = join28(contentDir, entryId);
18549
- written.add(normalize3(path));
18759
+ written.add(normalize4(path));
18550
18760
  await writeIfChanged(path, text);
18551
18761
  }));
18552
- if (existsSync16(contentDir)) {
18762
+ if (existsSync15(contentDir)) {
18553
18763
  await pruneOrphans(contentDir, written);
18554
18764
  }
18555
18765
  };
@@ -18572,7 +18782,7 @@ var readLogoSvg = (project, source) => {
18572
18782
  const file = [
18573
18783
  join28(project.context.root, "public", rel),
18574
18784
  join28(project.context.root, rel)
18575
- ].find((path) => existsSync16(path));
18785
+ ].find((path) => existsSync15(path));
18576
18786
  return file ? readFileSync8(file, "utf-8") : undefined;
18577
18787
  };
18578
18788
  var isStringShorthand = (value) => typeof value === "string";
@@ -18606,6 +18816,7 @@ var FAVICON_CANDIDATES = [
18606
18816
  "favicon.ico",
18607
18817
  "icon.ico"
18608
18818
  ];
18819
+ var darkSibling = (name) => name.replace(/\.(?=[^.]+$)/u, "-dark.");
18609
18820
  var FAVICON_TYPES = new Map([
18610
18821
  ["ico", "image/x-icon"],
18611
18822
  ["jpeg", "image/jpeg"],
@@ -18619,6 +18830,10 @@ var faviconType = (name) => {
18619
18830
  };
18620
18831
  var inlineDataUri = (file, type) => `data:${type};base64,${readFileSync8(file).toString("base64")}`;
18621
18832
  var defaultFavicon = () => ({
18833
+ dark: {
18834
+ href: inlineDataUri(join28(BLUME_SRC, "assets", "icon-dark.png"), "image/png"),
18835
+ type: "image/png"
18836
+ },
18622
18837
  href: inlineDataUri(join28(BLUME_SRC, "assets", "icon.png"), "image/png"),
18623
18838
  type: "image/png"
18624
18839
  });
@@ -18631,20 +18846,43 @@ var APPLE_ICON_CANDIDATES = [
18631
18846
  var resolveIconFile = (project, candidates) => {
18632
18847
  const { root } = project.context;
18633
18848
  for (const name of candidates) {
18634
- if (existsSync16(join28(root, "public", name))) {
18849
+ if (existsSync15(join28(root, "public", name))) {
18635
18850
  return { href: `/${name}`, type: faviconType(name) };
18636
18851
  }
18637
18852
  }
18638
18853
  for (const name of candidates) {
18639
18854
  const file = join28(root, name);
18640
- if (existsSync16(file)) {
18855
+ if (existsSync15(file)) {
18641
18856
  const type = faviconType(name);
18642
18857
  return { href: inlineDataUri(file, type ?? "image/x-icon"), type };
18643
18858
  }
18644
18859
  }
18645
18860
  return null;
18646
18861
  };
18647
- var resolveFavicon = (project) => resolveIconFile(project, FAVICON_CANDIDATES) ?? defaultFavicon();
18862
+ var resolveFavicon = (project) => {
18863
+ const { root } = project.context;
18864
+ for (const name of FAVICON_CANDIDATES) {
18865
+ if (existsSync15(join28(root, "public", name))) {
18866
+ const type = faviconType(name);
18867
+ const sibling = darkSibling(name);
18868
+ return existsSync15(join28(root, "public", sibling)) ? { dark: { href: `/${sibling}`, type }, href: `/${name}`, type } : { href: `/${name}`, type };
18869
+ }
18870
+ }
18871
+ for (const name of FAVICON_CANDIDATES) {
18872
+ const file = join28(root, name);
18873
+ if (existsSync15(file)) {
18874
+ const type = faviconType(name);
18875
+ const mime = type ?? "image/x-icon";
18876
+ const siblingFile = join28(root, darkSibling(name));
18877
+ return existsSync15(siblingFile) ? {
18878
+ dark: { href: inlineDataUri(siblingFile, mime), type },
18879
+ href: inlineDataUri(file, mime),
18880
+ type
18881
+ } : { href: inlineDataUri(file, mime), type };
18882
+ }
18883
+ }
18884
+ return resolveIconFile(project, FAVICON_CANDIDATES.map(darkSibling)) ?? defaultFavicon();
18885
+ };
18648
18886
  var resolveAppleIcon = (project) => resolveIconFile(project, APPLE_ICON_CANDIDATES);
18649
18887
  var resolveBanner = (config) => {
18650
18888
  const { banner } = config;
@@ -18771,6 +19009,7 @@ var buildRuntimeData = (project) => {
18771
19009
  palette: config.seo.og.palette,
18772
19010
  site: resolveOgSite(config)
18773
19011
  },
19012
+ openInChat: config.ai.openInChat,
18774
19013
  repoUrl,
18775
19014
  search: {
18776
19015
  enabled: config.search.provider !== "none",
@@ -18793,7 +19032,7 @@ var buildRuntimeData = (project) => {
18793
19032
  href: feed.path,
18794
19033
  title: feed.title
18795
19034
  })),
18796
- fontCssVars: configuredCssVars(config.theme.fonts),
19035
+ fontCssVars: configuredFonts(config.theme.fonts),
18797
19036
  navigation: withRepoUrl(graph.navigation),
18798
19037
  navigationByLocale,
18799
19038
  navigationByVersion: Object.fromEntries(Object.entries(graph.navigationByVersion).map(([id2, byLocale]) => [
@@ -18884,6 +19123,30 @@ var writeMcpFiles = async (project, plan, write) => {
18884
19123
  write(join28(plan.dir, "server-card.ts"), staticJsonEndpointTemplate(buildMcpServerCard(discoveryInput)))
18885
19124
  ]);
18886
19125
  };
19126
+ var planPlaygroundProxy = (config, srcDir) => ({
19127
+ enabled: config.openapi.enabled && config.openapi.renderer === "blume" && config.openapi.playground.enabled && config.openapi.playground.proxy === true,
19128
+ entrypoint: join28(srcDir, "blume-openapi", "api-proxy.ts"),
19129
+ pattern: "/_api-proxy"
19130
+ });
19131
+ var specOrigins = (data) => {
19132
+ const origins = new Set;
19133
+ for (const spec of Object.values(data)) {
19134
+ const { servers } = spec.document;
19135
+ for (const server of Array.isArray(servers) ? servers : []) {
19136
+ const url = server.url ?? "";
19137
+ if (url.includes("{")) {
19138
+ continue;
19139
+ }
19140
+ try {
19141
+ origins.add(new URL(url).origin);
19142
+ } catch {}
19143
+ }
19144
+ }
19145
+ return [...origins].toSorted();
19146
+ };
19147
+ var proxyAllowlistWarnings = (enabled, origins) => enabled && origins.length === 0 ? [
19148
+ "openapi.playground.proxy is enabled, but no spec declares an absolute servers[].url (relative and templated URLs carry no origin), so the proxy's allowlist is empty and it will refuse every request. Add an absolute server URL to the spec, or point playground.proxy at an external proxy URL."
19149
+ ] : [];
18887
19150
  var writeAskFiles = async (project, srcDir, write) => {
18888
19151
  const { ask } = project.config.ai;
18889
19152
  if (!(ask?.enabled && !ask.endpoint)) {
@@ -18894,7 +19157,10 @@ var writeAskFiles = async (project, srcDir, write) => {
18894
19157
  await write(join28(srcDir, "generated", "ask-data.json"), `${JSON.stringify(await buildAskData(project))}
18895
19158
  `);
18896
19159
  }
18897
- await write(join28(srcDir, "pages", "api", "ask.ts"), askEndpointTemplate(resolveAskBackend(ask), grounded, ask.instructions));
19160
+ await write(join28(srcDir, "pages", "api", "ask.ts"), askEndpointTemplate(resolveAskBackend(ask), grounded, {
19161
+ instructions: ask.instructions,
19162
+ retrieval: ask.retrieval
19163
+ }));
18898
19164
  };
18899
19165
  var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
18900
19166
  if (routeIsTaken(pages, contentPages, "/404")) {
@@ -18942,7 +19208,7 @@ var generateRuntime = async (project) => {
18942
19208
  const openapiPath = join28(srcDir, "generated", "openapi.json");
18943
19209
  const written = new Set;
18944
19210
  const write = (path, content) => {
18945
- written.add(normalize3(path));
19211
+ written.add(normalize4(path));
18946
19212
  return writeIfChanged(path, content);
18947
19213
  };
18948
19214
  const depsLinkWarning = await ensureDepsLink(out);
@@ -18987,8 +19253,19 @@ var generateRuntime = async (project) => {
18987
19253
  const needsSvelte = frameworks.has("svelte");
18988
19254
  const reactCompilerPath = resolveReactCompiler(config, needsReact);
18989
19255
  const ogRoutes = customOgRoutes(pages, config.title, config.seo.og.titles);
19256
+ const changelogIndex = hasGeneratedChangelog(project, pages);
18990
19257
  const mcp = planMcp(project, srcDir, pages);
18991
19258
  pages.push(...mcp.discoveryPages);
19259
+ const openApiSource2 = project.sources.find(isOpenApiSource);
19260
+ const openApiData = openApiSource2 ? openApiSource2.openApiData() : {};
19261
+ const playgroundProxy = planPlaygroundProxy(config, srcDir);
19262
+ if (playgroundProxy.enabled) {
19263
+ pages.push({
19264
+ entrypoint: playgroundProxy.entrypoint,
19265
+ pattern: playgroundProxy.pattern
19266
+ });
19267
+ }
19268
+ const proxyOrigins = specOrigins(openApiData);
18992
19269
  const hasStaged = staged.size > 0;
18993
19270
  const hasFilesystemSource = project.sources.some((source) => !source.staged);
18994
19271
  const docsCollection = resolveDocsCollection(config, context);
@@ -19053,12 +19330,13 @@ var generateRuntime = async (project) => {
19053
19330
  Promise.all(slotPlan.wrappers.map((wrapper) => write(join28(srcDir, "generated", "component-slots", `${wrapper.name}.astro`), wrapper.content))),
19054
19331
  Promise.all(exampleDiscovery.examples.map((example) => write(join28(srcDir, "generated", "examples", `${exampleSlug(example.path)}.astro`), exampleWrapperTemplate(example)))),
19055
19332
  writeAskFiles(project, srcDir, write),
19056
- writeMcpFiles(project, mcp, write)
19333
+ writeMcpFiles(project, mcp, write),
19334
+ playgroundProxy.enabled ? write(playgroundProxy.entrypoint, playgroundProxyTemplate(proxyOrigins)) : Promise.resolve(false)
19057
19335
  ]);
19058
19336
  if (config.seo.og.enabled) {
19059
- await write(join28(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes, projectOgFonts(project)));
19337
+ await write(join28(srcDir, "pages", "og", "[...slug].png.ts"), ogEndpointTemplate(ogRoutes, projectOgFonts(project), changelogIndex));
19060
19338
  }
19061
- if (hasGeneratedChangelog(project, pages)) {
19339
+ if (changelogIndex) {
19062
19340
  await write(join28(srcDir, "pages", "changelog.astro"), changelogIndexTemplate({
19063
19341
  exportEpub,
19064
19342
  exportPdf,
@@ -19108,6 +19386,7 @@ var generateRuntime = async (project) => {
19108
19386
  }
19109
19387
  const warnings = [
19110
19388
  ...depsLinkWarning ? [depsLinkWarning] : [],
19389
+ ...proxyAllowlistWarnings(playgroundProxy.enabled, proxyOrigins),
19111
19390
  ...reactCompilerWarnings(config, needsReact, reactCompilerPath),
19112
19391
  ...mcp.warnings,
19113
19392
  ...islandDiscovery.warnings,
@@ -19120,7 +19399,7 @@ var generateRuntime = async (project) => {
19120
19399
  ...pages.map((page) => page.pattern),
19121
19400
  ...referenceRoutes(config)
19122
19401
  ]);
19123
- if (hasGeneratedChangelog(project, pages)) {
19402
+ if (changelogIndex) {
19124
19403
  navTargetRoutes.add("/changelog");
19125
19404
  }
19126
19405
  warnings.push(...[
@@ -19141,10 +19420,9 @@ var generateRuntime = async (project) => {
19141
19420
  warnings.push(...references.warnings);
19142
19421
  await Promise.all(references.files.map((file) => write(join28(srcDir, "pages", file.pagePath), file.content)));
19143
19422
  }
19144
- const openApiSource2 = project.sources.find(isOpenApiSource);
19145
19423
  await Promise.all([
19146
19424
  write(join28(srcDir, "generated", "data.json"), buildRuntimeData(project)),
19147
- write(openapiPath, `${JSON.stringify(openApiSource2 ? openApiSource2.openApiData() : {})}
19425
+ write(openapiPath, `${JSON.stringify(openApiData)}
19148
19426
  `),
19149
19427
  write(join28(out, "blume.manifest.json"), `${JSON.stringify(project.manifest, null, 2)}
19150
19428
  `),
@@ -19155,7 +19433,7 @@ var generateRuntime = async (project) => {
19155
19433
  };
19156
19434
 
19157
19435
  // src/cli/env.ts
19158
- import { existsSync as existsSync17 } from "node:fs";
19436
+ import { existsSync as existsSync16 } from "node:fs";
19159
19437
  import { config } from "dotenv";
19160
19438
  import { dirname as dirname12, join as join29, resolve as resolve9 } from "pathe";
19161
19439
  var loadEnvFiles = (startDir) => {
@@ -19165,7 +19443,7 @@ var loadEnvFiles = (startDir) => {
19165
19443
  while (!done) {
19166
19444
  paths.push(join29(dir, ".env.local"), join29(dir, ".env"));
19167
19445
  const parent = dirname12(dir);
19168
- done = existsSync17(join29(dir, ".git")) || parent === dir;
19446
+ done = existsSync16(join29(dir, ".git")) || parent === dir;
19169
19447
  dir = parent;
19170
19448
  }
19171
19449
  config({ path: paths, quiet: true });
@@ -19277,17 +19555,17 @@ var emitRedirectFiles = async (config2, distDir) => {
19277
19555
  { content: buildNetlifyRedirects(redirects), name: "_redirects" },
19278
19556
  { content: buildVercelConfig(redirects), name: "vercel.json" }
19279
19557
  ];
19280
- await Promise.all(platformFiles.map((file) => existsSync18(join30(distDir, file.name)) ? Promise.resolve() : writeFile7(join30(distDir, file.name), file.content, "utf-8")));
19558
+ await Promise.all(platformFiles.map((file) => existsSync17(join30(distDir, file.name)) ? Promise.resolve() : writeFile7(join30(distDir, file.name), file.content, "utf-8")));
19281
19559
  logger.success(`Emitted redirect files for ${redirects.length} redirect(s)`);
19282
19560
  };
19283
19561
  var emitHeaderFiles = async (project, distDir) => {
19284
19562
  const { config: config2 } = project;
19285
- if (!readsHeaderFiles(config2.deployment) || existsSync18(join30(project.context.root, "public", "_headers"))) {
19563
+ if (!readsHeaderFiles(config2.deployment) || existsSync17(join30(project.context.root, "public", "_headers"))) {
19286
19564
  return;
19287
19565
  }
19288
19566
  const ours = buildNetlifyHeaders(config2, buildHomeLinkHeader(config2, markdownRoutePaths(project)));
19289
19567
  const target = join30(distDir, "_headers");
19290
- const existing = existsSync18(target) ? await readFile17(target, "utf-8") : "";
19568
+ const existing = existsSync17(target) ? await readFile17(target, "utf-8") : "";
19291
19569
  await writeFile7(target, existing ? `${existing.trimEnd()}
19292
19570
  ${ours}` : ours, "utf-8");
19293
19571
  logger.success("Emitted _headers (UTF-8 Content-Type + homepage Link header)");
@@ -19298,12 +19576,12 @@ var emitAgentSkills = async (project, distDir) => {
19298
19576
  return;
19299
19577
  }
19300
19578
  const dir = resolve10(project.context.root, configured);
19301
- if (!existsSync18(dir)) {
19579
+ if (!existsSync17(dir)) {
19302
19580
  logger.warn(`ai.skills points at "${configured}" (${dir}), which does not exist; no skills published.`);
19303
19581
  return;
19304
19582
  }
19305
19583
  const outDir = join30(distDir, AGENT_SKILLS_DIR.slice(1));
19306
- if (existsSync18(join30(outDir, "index.json"))) {
19584
+ if (existsSync17(join30(outDir, "index.json"))) {
19307
19585
  return;
19308
19586
  }
19309
19587
  const { skills, warnings } = await collectSkills(dir);
@@ -19337,7 +19615,7 @@ var emitWellKnownFiles = async (config2, distDir) => {
19337
19615
  ];
19338
19616
  for (const file of files) {
19339
19617
  const target = join30(distDir, file.path.slice(1));
19340
- if (!file.content || existsSync18(target)) {
19618
+ if (!file.content || existsSync17(target)) {
19341
19619
  continue;
19342
19620
  }
19343
19621
  await mkdir8(join30(distDir, ".well-known"), { recursive: true });
@@ -19348,7 +19626,7 @@ var emitWellKnownFiles = async (config2, distDir) => {
19348
19626
  var emitVercelNegotiation = async (project, routePaths, root) => {
19349
19627
  const { config: config2 } = project;
19350
19628
  const configPath = join30(root, ".vercel", "output", "config.json");
19351
- if (!existsSync18(configPath)) {
19629
+ if (!existsSync17(configPath)) {
19352
19630
  return;
19353
19631
  }
19354
19632
  const overrides = {};
@@ -19373,7 +19651,7 @@ var emitCloudflareNegotiation = async (project, routePaths) => {
19373
19651
  const { config: config2, context } = project;
19374
19652
  const serverDir = join30(context.distDir ?? join30(context.root, "dist"), "server");
19375
19653
  const wranglerPath = join30(serverDir, "wrangler.json");
19376
- if (!existsSync18(wranglerPath)) {
19654
+ if (!existsSync17(wranglerPath)) {
19377
19655
  warnCloudflareNegotiationSkipped();
19378
19656
  return;
19379
19657
  }
@@ -19404,7 +19682,7 @@ var formatBytes2 = (bytes) => {
19404
19682
  };
19405
19683
  var astroAssets = async (distDir, ext) => {
19406
19684
  const astroDir = join30(distDir, "_astro");
19407
- if (!existsSync18(astroDir)) {
19685
+ if (!existsSync17(astroDir)) {
19408
19686
  return [];
19409
19687
  }
19410
19688
  const entries = await readdir2(astroDir);
@@ -19481,8 +19759,8 @@ var isolatedStaticDir = (config2, context) => {
19481
19759
  var publishLlmsFiles = async (project, distDir) => {
19482
19760
  const indexPath = join30(distDir, "llms.txt");
19483
19761
  const fullPath = join30(distDir, "llms-full.txt");
19484
- const writeIndex = !existsSync18(indexPath);
19485
- const writeFull = !existsSync18(fullPath);
19762
+ const writeIndex = !existsSync17(indexPath);
19763
+ const writeFull = !existsSync17(fullPath);
19486
19764
  if (!(writeIndex || writeFull)) {
19487
19765
  return;
19488
19766
  }
@@ -19515,17 +19793,17 @@ var publishBuildArtifacts = async (project, distDir, args) => {
19515
19793
  await publishLlmsFiles(project, distDir);
19516
19794
  }
19517
19795
  const sitemapFiles = buildSitemapFiles(project);
19518
- if (sitemapFiles && !existsSync18(join30(distDir, "sitemap.xml"))) {
19796
+ if (sitemapFiles && !existsSync17(join30(distDir, "sitemap.xml"))) {
19519
19797
  await Promise.all(sitemapFiles.map((file) => writeFile7(join30(distDir, file.name), file.xml, "utf-8")));
19520
19798
  logger.success(sitemapFiles.length === 1 ? "Generated sitemap.xml" : `Generated sitemap.xml (index of ${sitemapFiles.length - 1} sitemap files)`);
19521
19799
  }
19522
19800
  const robots = buildRobots(project);
19523
- if (robots && !existsSync18(join30(distDir, "robots.txt"))) {
19801
+ if (robots && !existsSync17(join30(distDir, "robots.txt"))) {
19524
19802
  await writeFile7(join30(distDir, "robots.txt"), robots, "utf-8");
19525
19803
  logger.success("Generated robots.txt");
19526
19804
  }
19527
19805
  const agentReadability = buildAgentReadability(project);
19528
- if (agentReadability && !existsSync18(join30(distDir, "agent-readability.json"))) {
19806
+ if (agentReadability && !existsSync17(join30(distDir, "agent-readability.json"))) {
19529
19807
  await writeFile7(join30(distDir, "agent-readability.json"), `${JSON.stringify(agentReadability, null, 2)}
19530
19808
  `, "utf-8");
19531
19809
  logger.success("Generated agent-readability.json");
@@ -19658,7 +19936,7 @@ var buildCommand = defineCommand3({
19658
19936
  });
19659
19937
 
19660
19938
  // src/cli/commands/check.ts
19661
- import { existsSync as existsSync19 } from "node:fs";
19939
+ import { existsSync as existsSync18 } from "node:fs";
19662
19940
  import { check } from "@astrojs/check";
19663
19941
  import { sync } from "astro";
19664
19942
  import { defineCommand as defineCommand4 } from "citty";
@@ -19704,7 +19982,7 @@ var checkCommand = defineCommand4({
19704
19982
  minimumFailingSeverity: "error",
19705
19983
  minimumSeverity: "hint",
19706
19984
  root: outDir,
19707
- tsconfig: existsSync19(tsconfig) ? tsconfig : undefined,
19985
+ tsconfig: existsSync18(tsconfig) ? tsconfig : undefined,
19708
19986
  watch: false
19709
19987
  });
19710
19988
  if (failed) {
@@ -19986,13 +20264,13 @@ import { defineCommand as defineCommand7 } from "citty";
19986
20264
  import { relative as relative17 } from "pathe";
19987
20265
 
19988
20266
  // src/registry/eject.ts
19989
- import { existsSync as existsSync20 } from "node:fs";
20267
+ import { existsSync as existsSync19 } from "node:fs";
19990
20268
  import { cp as cp2, mkdir as mkdir9, readFile as readFile18, rm as rm3, writeFile as writeFile8 } from "node:fs/promises";
19991
20269
  import { join as join33, relative as relative15 } from "pathe";
19992
20270
  var toPosix = (path) => path.split("\\").join("/");
19993
20271
  var LOCAL_BLUME_SOURCE = "../../node_modules/blume/src/**/*.{astro,ts,tsx}";
19994
20272
  var blumeSourceGlob = (root, genDir, resolveBlumeRoot = packageRoot) => {
19995
- if (existsSync20(join33(root, "node_modules", "blume"))) {
20273
+ if (existsSync19(join33(root, "node_modules", "blume"))) {
19996
20274
  return LOCAL_BLUME_SOURCE;
19997
20275
  }
19998
20276
  try {
@@ -20011,7 +20289,7 @@ var askFiles = async (project, srcDir, genDir) => {
20011
20289
  const { ask } = project.config.ai;
20012
20290
  if (!(ask?.enabled && !ask.endpoint)) {
20013
20291
  const endpointPath = join33(srcDir, "pages", "api", "ask.ts");
20014
- if (existsSync20(endpointPath)) {
20292
+ if (existsSync19(endpointPath)) {
20015
20293
  const content = await readFile18(endpointPath, "utf-8");
20016
20294
  if (content.startsWith("// Generated by Blume. Do not edit.")) {
20017
20295
  await rm3(endpointPath, { force: true });
@@ -20023,7 +20301,10 @@ var askFiles = async (project, srcDir, genDir) => {
20023
20301
  const grounded = ask.provider !== "inkeep";
20024
20302
  const files = [
20025
20303
  {
20026
- content: askEndpointTemplate(resolveAskBackend(ask), grounded, ask.instructions),
20304
+ content: askEndpointTemplate(resolveAskBackend(ask), grounded, {
20305
+ instructions: ask.instructions,
20306
+ retrieval: ask.retrieval
20307
+ }),
20027
20308
  path: join33(srcDir, "pages", "api", "ask.ts")
20028
20309
  }
20029
20310
  ];
@@ -20093,7 +20374,7 @@ var changelogFiles = (project, userPages, srcDir, options) => {
20093
20374
  }
20094
20375
  ];
20095
20376
  };
20096
- var readExamplesCss = (root, css) => css && existsSync20(join33(root, css)) ? readFile18(join33(root, css), "utf-8") : Promise.resolve("");
20377
+ var readExamplesCss = (root, css) => css && existsSync19(join33(root, css)) ? readFile18(join33(root, css), "utf-8") : Promise.resolve("");
20097
20378
  var examplesPreviewFiles = (srcDir, basePath, hasExamples) => hasExamples ? [
20098
20379
  {
20099
20380
  content: examplesPageTemplate(),
@@ -20332,13 +20613,13 @@ var eject = async (root) => {
20332
20613
  for (const [entryId, content] of staged) {
20333
20614
  files.push({ content, path: join33(root, stagedDir, entryId) });
20334
20615
  }
20335
- const written = files.filter((file) => !(file.skipIfExists && existsSync20(file.path)));
20616
+ const written = files.filter((file) => !(file.skipIfExists && existsSync19(file.path)));
20336
20617
  await Promise.all(written.map(async (file) => {
20337
20618
  await mkdir9(join33(file.path, ".."), { recursive: true });
20338
20619
  await writeFile8(file.path, file.content, "utf-8");
20339
20620
  }));
20340
20621
  const assetsSrc = join33(context.outDir, "public", "blume-assets");
20341
- if (existsSync20(assetsSrc)) {
20622
+ if (existsSync19(assetsSrc)) {
20342
20623
  await cp2(assetsSrc, join33(root, "public", "blume-assets"), {
20343
20624
  recursive: true
20344
20625
  });
@@ -20395,10 +20676,10 @@ var updatePackageScripts = async (root) => {
20395
20676
  };
20396
20677
 
20397
20678
  // src/cli/init/scaffold.ts
20398
- import { existsSync as existsSync21 } from "node:fs";
20679
+ import { existsSync as existsSync20 } from "node:fs";
20399
20680
  import { mkdir as mkdir10, writeFile as writeFile10 } from "node:fs/promises";
20400
20681
  import { detect } from "package-manager-detector/detect";
20401
- import { basename as basename4, dirname as dirname14, isAbsolute as isAbsolute9, join as join35, relative as relative16 } from "pathe";
20682
+ import { basename as basename5, dirname as dirname14, isAbsolute as isAbsolute9, join as join35, relative as relative16 } from "pathe";
20402
20683
 
20403
20684
  // src/core/package-json.ts
20404
20685
  var toPackageName = (raw) => raw.toLowerCase().replaceAll(/[^a-z0-9._-]+/gu, "-").replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
@@ -20607,7 +20888,7 @@ var extraDepsFor = (sources) => {
20607
20888
  var buildPlan = (root, answers) => {
20608
20889
  const files = [
20609
20890
  {
20610
- content: blumePackageJson(toPackageName(basename4(root)), extraDepsFor(answers.sources)),
20891
+ content: blumePackageJson(toPackageName(basename5(root)), extraDepsFor(answers.sources)),
20611
20892
  path: join35(root, "package.json")
20612
20893
  },
20613
20894
  { content: buildConfig(answers), path: join35(root, "blume.config.ts") }
@@ -20618,7 +20899,7 @@ var buildPlan = (root, answers) => {
20618
20899
  return files;
20619
20900
  };
20620
20901
  var writeFileSafe = async (file, log) => {
20621
- if (existsSync21(file.path)) {
20902
+ if (existsSync20(file.path)) {
20622
20903
  log.info(`Skipped existing ${file.path}`);
20623
20904
  return false;
20624
20905
  }
@@ -20629,7 +20910,7 @@ var writeFileSafe = async (file, log) => {
20629
20910
  };
20630
20911
  var applyPlan = async (files, log) => {
20631
20912
  const created = await Promise.all(files.map((file) => writeFileSafe(file, log)));
20632
- const createdPackage = files.some((file, index) => created[index] && basename4(file.path) === "package.json");
20913
+ const createdPackage = files.some((file, index) => created[index] && basename5(file.path) === "package.json");
20633
20914
  return { createdPackage };
20634
20915
  };
20635
20916
  var envVarsFor = (sources) => [
@@ -20714,7 +20995,7 @@ The blume package remains importable.`);
20714
20995
  });
20715
20996
 
20716
20997
  // src/cli/commands/eval.ts
20717
- import { existsSync as existsSync22 } from "node:fs";
20998
+ import { existsSync as existsSync21 } from "node:fs";
20718
20999
  import { defineCommand as defineCommand8 } from "citty";
20719
21000
  import { join as join39 } from "pathe";
20720
21001
 
@@ -21438,7 +21719,7 @@ var runFixHandoff = async (agent, result, root, threshold) => {
21438
21719
  };
21439
21720
  var runInit = async (agent, file) => {
21440
21721
  const path = join39(process.cwd(), file);
21441
- if (existsSync22(path)) {
21722
+ if (existsSync21(path)) {
21442
21723
  logger.error(`${file} already exists — edit it directly, or pass --file to draft elsewhere.`);
21443
21724
  process.exit(1);
21444
21725
  }
@@ -21576,7 +21857,7 @@ import { defineCommand as defineCommand9 } from "citty";
21576
21857
  import { resolve as resolve12 } from "pathe";
21577
21858
 
21578
21859
  // src/cli/init/questions.ts
21579
- import { basename as basename5, resolve as resolve11 } from "pathe";
21860
+ import { basename as basename6, resolve as resolve11 } from "pathe";
21580
21861
  var cancelled = (value) => typeof value === "symbol";
21581
21862
  var collectAnswers = async (prompter, flags, defaults) => {
21582
21863
  const directory = flags.directory ?? await prompter.text({
@@ -21589,7 +21870,7 @@ var collectAnswers = async (prompter, flags, defaults) => {
21589
21870
  }
21590
21871
  const root = resolve11(defaults.cwd, directory);
21591
21872
  const title = await prompter.text({
21592
- initialValue: titleize(basename5(root)),
21873
+ initialValue: titleize(basename6(root)),
21593
21874
  message: "What's your docs site called?",
21594
21875
  validate: (value) => value?.trim() ? undefined : "Give your docs site a name."
21595
21876
  });
@@ -21816,10 +22097,30 @@ var SCHEMA = {
21816
22097
  };
21817
22098
  var toFacetTerms = (facets) => Object.entries(facets).map(([key, value]) => `${key}:${value}`);
21818
22099
  var BOOST = { description: 2, title: 3 };
21819
- var SEGMENTED_LANGUAGES = new Set(["ja", "ko", "th", "zh"]);
21820
- var BIGRAM_LANGUAGES = new Set(["ja", "zh"]);
22100
+ var LATIN_SCRIPT = "Latn";
22101
+ var parseLocale = (tag) => {
22102
+ const hyphenated = tag.replaceAll("_", "-");
22103
+ try {
22104
+ return new Intl.Locale(hyphenated);
22105
+ } catch {}
22106
+ const [primary = ""] = hyphenated.split("-");
22107
+ try {
22108
+ return new Intl.Locale(primary);
22109
+ } catch {
22110
+ return;
22111
+ }
22112
+ };
22113
+ var resolveLocale = (tag) => parseLocale(tag)?.maximize();
22114
+ var BIGRAM_INDEX_SCRIPTS = new Set(["Hans", "Hant", "Jpan"]);
22115
+ var isBigramLanguage = (language) => {
22116
+ const script = resolveLocale(language)?.script;
22117
+ return script !== undefined && BIGRAM_INDEX_SCRIPTS.has(script);
22118
+ };
21821
22119
  var BIGRAM_SCRIPTS = /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}々〆〇ー゙゚]+$/u;
21822
22120
  var TERM = /[\p{L}\p{M}\p{N}]+(?:(?:['’](?=\p{L})|(?<=\p{N})[.,](?=\p{N}))[\p{L}\p{M}\p{N}]+)*/gu;
22121
+ var LATIN_TERM = /^[\p{Script=Latin}\p{N}'’.,]+$/u;
22122
+ var MARKS = /\p{M}+/gu;
22123
+ var foldDiacritics = (term) => LATIN_TERM.test(term) ? term.normalize("NFD").replace(MARKS, "") : term;
21823
22124
  var addBigrams = (run, tokens) => {
21824
22125
  const characters = [...run];
21825
22126
  if (characters.length === 1) {
@@ -21836,15 +22137,16 @@ var addBigrams = (run, tokens) => {
21836
22137
  };
21837
22138
  var hasSegmenter = (segmenter) => typeof segmenter === "function";
21838
22139
  var segmentingTokenizer = (locale) => {
21839
- const language = locale?.toLowerCase().split(/[-_]/u)[0] ?? "";
21840
- if (!SEGMENTED_LANGUAGES.has(language)) {
22140
+ const resolved = resolveLocale(locale ?? "");
22141
+ if (!resolved?.script || resolved.script === LATIN_SCRIPT) {
21841
22142
  return;
21842
22143
  }
21843
22144
  if (!hasSegmenter(Intl.Segmenter)) {
21844
22145
  return;
21845
22146
  }
22147
+ const { language } = resolved;
21846
22148
  const segmenter = new Intl.Segmenter(language, { granularity: "word" });
21847
- const bigram = BIGRAM_LANGUAGES.has(language);
22149
+ const bigram = isBigramLanguage(language);
21848
22150
  return {
21849
22151
  language,
21850
22152
  normalizationCache: new Map,
@@ -21863,7 +22165,7 @@ var segmentingTokenizer = (locale) => {
21863
22165
  return;
21864
22166
  }
21865
22167
  flush();
21866
- tokens.add(term);
22168
+ tokens.add(foldDiacritics(term));
21867
22169
  };
21868
22170
  for (const segment of segmenter.segment(raw.normalize("NFC").toLowerCase())) {
21869
22171
  if (!segment.isWordLike) {
@@ -21916,7 +22218,7 @@ var queryOramaIndex = async (db, term, limit, filters) => {
21916
22218
  term
21917
22219
  };
21918
22220
  const params = Object.keys(where).length > 0 ? { ...unfiltered, where } : unfiltered;
21919
- const bigrammed = BIGRAM_LANGUAGES.has(db.tokenizer?.language ?? "");
22221
+ const bigrammed = isBigramLanguage(db.tokenizer?.language ?? "");
21920
22222
  const strict = bigrammed ? await search(db, { ...params, threshold: ALL_TOKENS }) : undefined;
21921
22223
  const found = strict && strict.hits.length > 0 ? strict : await search(db, params);
21922
22224
  return found.hits.map((hit) => hit.document);
@@ -22163,7 +22465,7 @@ var mcpStdioCommand = defineCommand10({
22163
22465
  });
22164
22466
 
22165
22467
  // src/cli/commands/preview.ts
22166
- import { existsSync as existsSync23 } from "node:fs";
22468
+ import { existsSync as existsSync22 } from "node:fs";
22167
22469
  import { preview } from "astro";
22168
22470
  import { defineCommand as defineCommand11 } from "citty";
22169
22471
  import { join as join40 } from "pathe";
@@ -22180,7 +22482,7 @@ var previewCommand = defineCommand11({
22180
22482
  const root = process.cwd();
22181
22483
  const { config: config2 } = await loadConfig(root);
22182
22484
  const context = resolveProjectContext(root, config2);
22183
- if (!existsSync23(join40(context.outDir, "astro.config.mjs"))) {
22485
+ if (!existsSync22(join40(context.outDir, "astro.config.mjs"))) {
22184
22486
  logger.error("No build found. Run `blume build` first.");
22185
22487
  process.exit(1);
22186
22488
  }
@@ -22531,7 +22833,7 @@ var createProgressRenderer = (options) => {
22531
22833
  };
22532
22834
 
22533
22835
  // src/translate/run.ts
22534
- import { existsSync as existsSync24 } from "node:fs";
22836
+ import { existsSync as existsSync23 } from "node:fs";
22535
22837
  import { mkdtemp as mkdtemp4, readFile as readFile25 } from "node:fs/promises";
22536
22838
  import { tmpdir as tmpdir4 } from "node:os";
22537
22839
  import pLimit2 from "p-limit";
@@ -22776,7 +23078,7 @@ var runPageItem = async (item, index, context, ledger) => {
22776
23078
  });
22777
23079
  const sourceText = await readFile25(item.sourcePath, "utf-8");
22778
23080
  const target = context.targets.get(item.locale);
22779
- const previousTranslation = existsSync24(item.targetPath) ? await readFile25(item.targetPath, "utf-8") : undefined;
23081
+ const previousTranslation = existsSync23(item.targetPath) ? await readFile25(item.targetPath, "utf-8") : undefined;
22780
23082
  const output = await invokeAgent(context, pagePrompt(sourceText, target, context.source, previousTranslation), index);
22781
23083
  if (output.isError) {
22782
23084
  return done("failed", output.detail ?? "agent failed", output.costUsd);
@@ -22897,7 +23199,7 @@ var runTranslate = async (options) => {
22897
23199
  };
22898
23200
 
22899
23201
  // src/translate/work-list.ts
22900
- import { existsSync as existsSync25 } from "node:fs";
23202
+ import { existsSync as existsSync24 } from "node:fs";
22901
23203
  import { readFile as readFile26 } from "node:fs/promises";
22902
23204
  import { dirname as dirname16, extname as extname8, join as join45, relative as relative20 } from "pathe";
22903
23205
  var PAGE_EXTENSIONS = new Set([".md", ".mdx"]);
@@ -22979,7 +23281,7 @@ var computeWorkList = async (project, ledger, options = {}) => {
22979
23281
  for (const source of meta.metas) {
22980
23282
  knownSources.add(source.sourceRel);
22981
23283
  const targetDir = dirname16(metaTargetPath(source, locale));
22982
- const exists = ["meta.ts", "meta.js", "meta.mjs"].some((name) => existsSync25(join45(targetDir, name)));
23284
+ const exists = ["meta.ts", "meta.js", "meta.mjs"].some((name) => existsSync24(join45(targetDir, name)));
22983
23285
  const hash = hashSource(source.raw);
22984
23286
  const stamp = ledger.files[source.sourceRel]?.[locale];
22985
23287
  const entry = (status) => ({
@@ -23211,13 +23513,13 @@ var translateCommand = defineCommand13({
23211
23513
  });
23212
23514
 
23213
23515
  // src/cli/commands/validate.ts
23214
- import { existsSync as existsSync27 } from "node:fs";
23516
+ import { existsSync as existsSync26 } from "node:fs";
23215
23517
  import { defineCommand as defineCommand14 } from "citty";
23216
23518
  import { join as join47 } from "pathe";
23217
23519
 
23218
23520
  // src/core/links.ts
23219
- import { existsSync as existsSync26 } from "node:fs";
23220
- import { basename as basename6, join as join46 } from "pathe";
23521
+ import { existsSync as existsSync25 } from "node:fs";
23522
+ import { basename as basename7, dirname as dirname17, join as join46 } from "pathe";
23221
23523
  var HTTP = /^https?:\/\//iu;
23222
23524
  var PROTOCOL_RELATIVE = /^\/\//u;
23223
23525
  var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
@@ -23230,9 +23532,9 @@ var decodePercent = (value) => {
23230
23532
  };
23231
23533
  var DOC_EXT = /\.(?:md|mdx)$/iu;
23232
23534
  var FILE_EXT = /\.[a-z0-9]+$/iu;
23233
- var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null && existsSync26(join46(ctx.publicDir, resolved));
23535
+ var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null && existsSync25(join46(ctx.publicDir, resolved));
23234
23536
  var NUMERIC_PREFIX3 = /^\d+[-_.]/u;
23235
- var isIndexPage = (page2) => /^index\.(?:md|mdx)$/iu.test(basename6(page2.navPath).replace(NUMERIC_PREFIX3, ""));
23537
+ var isIndexPage = (page2) => /^index\.(?:md|mdx)$/iu.test(basename7(page2.navPath).replace(NUMERIC_PREFIX3, ""));
23236
23538
  var applyRelativePart = (segments, part) => {
23237
23539
  if (part === "" || part === ".") {
23238
23540
  return;
@@ -23281,7 +23583,7 @@ var checkAnchor = (route, fragment, site, ctx) => {
23281
23583
  severity: "warning"
23282
23584
  };
23283
23585
  };
23284
- var checkPathLink = (resolved, fragment, target, site, ctx) => {
23586
+ var checkPathLink = (resolved, fragment, page2, link, site, ctx) => {
23285
23587
  const route = toRoute(withBasePath(ctx.basePath, resolved));
23286
23588
  if (ctx.routes.has(route)) {
23287
23589
  return fragment ? checkAnchor(route, fragment, site, ctx) : null;
@@ -23297,6 +23599,18 @@ var checkPathLink = (resolved, fragment, target, site, ctx) => {
23297
23599
  if (assetIsPresent(assetPath, ctx)) {
23298
23600
  return null;
23299
23601
  }
23602
+ if (link.image && page2.sourcePath && isRelativeImageTarget(link.target)) {
23603
+ if (resolveRelativeImage(dirname17(page2.sourcePath), link.target)) {
23604
+ return null;
23605
+ }
23606
+ return {
23607
+ ...site,
23608
+ code: "BLUME_BROKEN_ASSET",
23609
+ message: `Image ${link.target} was not found next to ${basename7(page2.sourcePath)}.`,
23610
+ severity: "warning",
23611
+ suggestion: "Add the file next to the page source or fix the reference."
23612
+ };
23613
+ }
23300
23614
  if (ctx.publicDir === null) {
23301
23615
  return "asset-unchecked";
23302
23616
  }
@@ -23311,7 +23625,7 @@ var checkPathLink = (resolved, fragment, target, site, ctx) => {
23311
23625
  return {
23312
23626
  ...site,
23313
23627
  code: "BLUME_BROKEN_LINK",
23314
- message: `Broken link to ${target}: no page resolves to ${route}.`,
23628
+ message: `Broken link to ${link.target}: no page resolves to ${route}.`,
23315
23629
  severity: "error",
23316
23630
  suggestion: "Check the path, or create the target page."
23317
23631
  };
@@ -23364,7 +23678,7 @@ var classifyLink = (page2, link, ctx, onExternal) => {
23364
23678
  return fragment ? checkAnchor(page2.route, fragment, site, ctx) : null;
23365
23679
  }
23366
23680
  const resolved = rawPath.startsWith("/") ? rawPath : resolveRelative(page2.route, rawPath, isIndexPage(page2));
23367
- return checkPathLink(resolved, fragment, target, site, ctx);
23681
+ return checkPathLink(resolved, fragment, page2, link, site, ctx);
23368
23682
  };
23369
23683
  var validateLinks = async (graph, options) => {
23370
23684
  const basePath = options.basePath ?? "";
@@ -23446,7 +23760,7 @@ var validateCommand = defineCommand14({
23446
23760
  basePath: project.config.basePath,
23447
23761
  checkExternal: Boolean(args.external),
23448
23762
  extraRoutes,
23449
- publicDir: existsSync27(publicDir) ? publicDir : null,
23763
+ publicDir: existsSync26(publicDir) ? publicDir : null,
23450
23764
  redirects: project.config.redirects
23451
23765
  }));
23452
23766
  } catch (error) {
@@ -23480,7 +23794,7 @@ var validateCommand = defineCommand14({
23480
23794
  import { defineCommand as defineCommand15 } from "citty";
23481
23795
 
23482
23796
  // src/core/version-cut.ts
23483
- import { existsSync as existsSync28 } from "node:fs";
23797
+ import { existsSync as existsSync27 } from "node:fs";
23484
23798
  import { cp as cp3, readdir as readdir3, readFile as readFile27, rm as rm5 } from "node:fs/promises";
23485
23799
  import { join as join48, relative as relative21 } from "pathe";
23486
23800
  class CutError extends Error {
@@ -23600,7 +23914,7 @@ var cutVersion = async (root, id2, options = {}) => {
23600
23914
  throw new CutError(`Version "${id2}" is already registered in versions.archived.`);
23601
23915
  }
23602
23916
  const dir = join48(contentRoot2, id2);
23603
- if (existsSync28(dir)) {
23917
+ if (existsSync27(dir)) {
23604
23918
  if (!options.force) {
23605
23919
  throw new CutError(`${dir} already exists — pass --force to overwrite the snapshot.`);
23606
23920
  }
@@ -23753,5 +24067,5 @@ process.on("unhandledRejection", (error) => {
23753
24067
  });
23754
24068
  runMain(main);
23755
24069
 
23756
- //# debugId=6377B095BFD3F5AE64756E2164756E21
24070
+ //# debugId=94497B7DFC770E9664756E2164756E21
23757
24071
  //# sourceMappingURL=index.js.map