blume 1.1.2 → 1.1.4

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 (61) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/cli/index.js +284 -109
  3. package/dist/cli/index.js.map +33 -33
  4. package/dist/types/ai/component-markdown.d.ts +10 -0
  5. package/dist/types/core/config-input.d.ts +44 -0
  6. package/dist/types/core/data.d.ts +2 -0
  7. package/dist/types/core/i18n-ui.d.ts +24 -24
  8. package/dist/types/core/schema.d.ts +282 -114
  9. package/dist/types/core/types.d.ts +14 -0
  10. package/dist/types/openapi/references.d.ts +5 -0
  11. package/docs/advanced/api-reference.mdx +20 -0
  12. package/docs/configuration/index.mdx +27 -0
  13. package/package.json +1 -1
  14. package/src/ai/component-markdown.ts +28 -0
  15. package/src/ai/llms.ts +11 -2
  16. package/src/ai/markdown.ts +12 -6
  17. package/src/ai/mcp/server.ts +29 -6
  18. package/src/astro/examples.ts +13 -0
  19. package/src/astro/generate.ts +141 -58
  20. package/src/astro/templates.ts +65 -21
  21. package/src/audit/checks/duplicates.ts +15 -6
  22. package/src/audit/checks/indexability.ts +11 -2
  23. package/src/audit/checks/network.ts +22 -8
  24. package/src/audit/checks/sitemap.ts +42 -16
  25. package/src/audit/redirects.ts +12 -1
  26. package/src/audit/run.ts +13 -3
  27. package/src/audit/url.ts +21 -2
  28. package/src/cli/commands/audit.ts +21 -6
  29. package/src/cli/commands/dev.ts +19 -2
  30. package/src/components/content/Frame.astro +4 -1
  31. package/src/components/content/Prompt.astro +4 -1
  32. package/src/components/content/Tooltip.astro +4 -1
  33. package/src/components/content/Update.astro +45 -0
  34. package/src/components/islands/ask-ai.tsx +19 -2
  35. package/src/components/islands/hooks.ts +38 -11
  36. package/src/components/layout/Logo.astro +2 -2
  37. package/src/components/layout/RootLayout.astro +27 -7
  38. package/src/components/layout/Search.astro +5 -1
  39. package/src/components/layout/head-scripts.ts +22 -5
  40. package/src/components/openapi/ApiTagOperations.astro +17 -8
  41. package/src/core/config-input.ts +45 -0
  42. package/src/core/data.ts +2 -0
  43. package/src/core/date-format.ts +17 -0
  44. package/src/core/deployment-env.ts +7 -2
  45. package/src/core/graph.ts +7 -1
  46. package/src/core/i18n.ts +10 -2
  47. package/src/core/navigation.ts +7 -3
  48. package/src/core/project-graph.ts +9 -0
  49. package/src/core/schema.ts +64 -0
  50. package/src/core/sources/normalize.ts +69 -8
  51. package/src/core/sources/notion.ts +4 -2
  52. package/src/core/sources/sanity.ts +5 -3
  53. package/src/core/types.ts +16 -0
  54. package/src/markdown/code-title.ts +7 -1
  55. package/src/openapi/model.ts +31 -2
  56. package/src/openapi/references.ts +6 -0
  57. package/src/openapi/render-mdx.ts +12 -7
  58. package/src/openapi/scalar.ts +4 -0
  59. package/src/registry/eject.ts +6 -3
  60. package/src/theme/entry.ts +7 -0
  61. package/src/theme/twoslash.ts +10 -0
package/dist/cli/index.js CHANGED
@@ -1581,6 +1581,13 @@ var normalizePath = (path) => {
1581
1581
  const trimmed = path.replace(/\/+$/u, "");
1582
1582
  return trimmed === "" ? "/" : trimmed;
1583
1583
  };
1584
+ var decodePath = (path) => {
1585
+ try {
1586
+ return decodeURI(path);
1587
+ } catch {
1588
+ return path;
1589
+ }
1590
+ };
1584
1591
  var siteOrigin = (site) => {
1585
1592
  if (!site) {
1586
1593
  return null;
@@ -1611,7 +1618,7 @@ var resolveHref = (pageUrl, href, origin, deployBase = "") => {
1611
1618
  return {
1612
1619
  hash: parsed.hash.slice(1),
1613
1620
  kind: "self-origin",
1614
- path: normalizePath(stripBasePath(deployBase, parsed.pathname))
1621
+ path: normalizePath(stripBasePath(deployBase, decodePath(parsed.pathname)))
1615
1622
  };
1616
1623
  }
1617
1624
  return { kind: "external", url: parsed.toString() };
@@ -1626,7 +1633,7 @@ var resolveHref = (pageUrl, href, origin, deployBase = "") => {
1626
1633
  return {
1627
1634
  hash: resolved.hash.slice(1),
1628
1635
  kind: "internal",
1629
- path: normalizePath(stripBasePath(deployBase, resolved.pathname))
1636
+ path: normalizePath(stripBasePath(deployBase, decodePath(resolved.pathname)))
1630
1637
  };
1631
1638
  };
1632
1639
 
@@ -1825,17 +1832,20 @@ var contentChecks = {
1825
1832
  };
1826
1833
 
1827
1834
  // src/audit/checks/duplicates.ts
1828
- var isNonCanonical = (page) => {
1835
+ var isNonCanonical = (page, deployBase) => {
1829
1836
  if (!page.canonical) {
1830
1837
  return false;
1831
1838
  }
1832
1839
  try {
1833
- return new URL(page.canonical).pathname.replace(/\/$/u, "") !== page.url.replace(/\/$/u, "");
1840
+ return stripBasePath(deployBase, decodePath(new URL(page.canonical).pathname)).replace(/\/$/u, "") !== page.url.replace(/\/$/u, "");
1834
1841
  } catch {
1835
1842
  return false;
1836
1843
  }
1837
1844
  };
1838
- var comparable = (context) => context.pages.filter((page) => page.indexable && !page.route?.fallback && !isNonCanonical(page));
1845
+ var comparable = (context) => {
1846
+ const deployBase = normalizeBasePath(context.project.config.deployment.base);
1847
+ return context.pages.filter((page) => page.indexable && !page.route?.fallback && !isNonCanonical(page, deployBase));
1848
+ };
1839
1849
  var reportGroups = (context, pages, id, key, describe, frontmatterKey) => {
1840
1850
  const groups = new Map;
1841
1851
  for (const page of pages) {
@@ -2009,12 +2019,12 @@ var PLATFORMS = [
2009
2019
  {
2010
2020
  adapter: "vercel",
2011
2021
  detect: (env) => Boolean(env.VERCEL),
2012
- site: (env) => toUrl(env.VERCEL_PROJECT_PRODUCTION_URL ?? env.VERCEL_URL)
2022
+ site: (env) => toUrl(env.VERCEL_PROJECT_PRODUCTION_URL) ?? toUrl(env.VERCEL_URL)
2013
2023
  },
2014
2024
  {
2015
2025
  adapter: "netlify",
2016
2026
  detect: (env) => Boolean(env.NETLIFY),
2017
- site: (env) => toUrl(env.URL ?? env.DEPLOY_PRIME_URL ?? env.DEPLOY_URL)
2027
+ site: (env) => toUrl(env.URL) ?? toUrl(env.DEPLOY_PRIME_URL) ?? toUrl(env.DEPLOY_URL)
2018
2028
  },
2019
2029
  {
2020
2030
  adapter: "cloudflare",
@@ -2073,7 +2083,7 @@ var canonicalChecks = (context, page) => {
2073
2083
  }
2074
2084
  return found;
2075
2085
  }
2076
- const target = normalizePath(canonical.pathname);
2086
+ const target = normalizePath(stripBasePath(normalizeBasePath(context.project.config.deployment.base), decodePath(canonical.pathname)));
2077
2087
  if (target === normalizePath(page.url)) {
2078
2088
  return found;
2079
2089
  }
@@ -2401,7 +2411,7 @@ var probeAll = async (urls, options = {}) => {
2401
2411
  var CLIENT_ERROR = 400;
2402
2412
  var SERVER_ERROR = 500;
2403
2413
  var SLOW_MS = 1500;
2404
- var liveUrl = (origin, page) => new URL(page.url, origin).toString();
2414
+ var liveUrl = (origin, page, deployBase) => new URL(`${deployBase}${page.url}`, origin).toString();
2405
2415
  var badResponse = (context, page, result) => {
2406
2416
  const site = pageSite(context, page);
2407
2417
  if (result.timedOut) {
@@ -2445,12 +2455,13 @@ var networkChecks = {
2445
2455
  return [];
2446
2456
  }
2447
2457
  const found = [];
2448
- const targets = context.pages.map((page) => liveUrl(origin, page));
2449
- const robotsUrl = new URL("/robots.txt", origin).toString();
2450
- const sitemapUrl = new URL("/sitemap.xml", origin).toString();
2458
+ const deployBase = normalizeBasePath(context.project.config.deployment.base);
2459
+ const targets = context.pages.map((page) => liveUrl(origin, page, deployBase));
2460
+ const robotsUrl = new URL(`${deployBase}/robots.txt`, origin).toString();
2461
+ const sitemapUrl = new URL(`${deployBase}/sitemap.xml`, origin).toString();
2451
2462
  const results = await probeAll([...targets, robotsUrl, sitemapUrl]);
2452
2463
  for (const page of context.pages) {
2453
- const result = results.get(liveUrl(origin, page));
2464
+ const result = results.get(liveUrl(origin, page, deployBase));
2454
2465
  if (!result) {
2455
2466
  continue;
2456
2467
  }
@@ -2717,23 +2728,23 @@ var MAX_SITEMAP_BYTES = 50 * 1024 * 1024;
2717
2728
  var MAX_SITEMAP_URLS = 50000;
2718
2729
  var LASTMOD_SLACK_MS = 24 * 60 * 60 * 1000;
2719
2730
  var ERROR_ROUTES2 = new Set(["/404", "/500"]);
2720
- var sitemapPaths = (context) => {
2731
+ var sitemapPaths = (context, deployBase) => {
2721
2732
  const paths = new Map;
2722
2733
  for (const loc of context.sitemap?.urls ?? []) {
2723
2734
  try {
2724
- paths.set(normalizePath(new URL(loc).pathname), loc);
2735
+ paths.set(normalizePath(stripBasePath(deployBase, decodePath(new URL(loc).pathname))), loc);
2725
2736
  } catch {}
2726
2737
  }
2727
2738
  return paths;
2728
2739
  };
2729
- var canonicalPath = (canonical) => {
2740
+ var canonicalPath = (canonical, deployBase) => {
2730
2741
  try {
2731
- return normalizePath(new URL(canonical).pathname);
2742
+ return normalizePath(stripBasePath(deployBase, decodePath(new URL(canonical).pathname)));
2732
2743
  } catch {
2733
2744
  return null;
2734
2745
  }
2735
2746
  };
2736
- var checkListedUrl = (context, loc, origin, file) => {
2747
+ var checkListedUrl = (context, loc, origin, file, deployBase) => {
2737
2748
  let parsed;
2738
2749
  try {
2739
2750
  parsed = new URL(loc);
@@ -2747,7 +2758,7 @@ var checkListedUrl = (context, loc, origin, file) => {
2747
2758
  finding("BLUME_AUDIT_SITEMAP_OUT_OF_SCOPE", { file, url: loc }, `sitemap.xml lists ${loc}, which is on another origin.`)
2748
2759
  ];
2749
2760
  }
2750
- const path = normalizePath(stripBasePath(normalizeBasePath(context.project.config.deployment.base), parsed.pathname));
2761
+ const path = normalizePath(stripBasePath(deployBase, decodePath(parsed.pathname)));
2751
2762
  const page = context.byUrl.get(path);
2752
2763
  if (!page) {
2753
2764
  const redirect = context.redirects.find((entry) => normalizePath(entry.from) === path);
@@ -2759,7 +2770,7 @@ var checkListedUrl = (context, loc, origin, file) => {
2759
2770
  if (!page.indexable) {
2760
2771
  found.push(finding("BLUME_AUDIT_NOINDEX_IN_SITEMAP", pageSite(context, page, ["noindex"]), `${path} is in the sitemap but declares robots "${page.robots}".`));
2761
2772
  }
2762
- const canonical = page.canonical && canonicalPath(page.canonical);
2773
+ const canonical = page.canonical && canonicalPath(page.canonical, deployBase);
2763
2774
  if (canonical && canonical !== path) {
2764
2775
  found.push(finding("BLUME_AUDIT_NON_CANONICAL_IN_SITEMAP", pageSite(context, page, ["seo", "canonical"]), `${path} is in the sitemap but canonicalizes to ${canonical}.`));
2765
2776
  }
@@ -2795,9 +2806,10 @@ var sitemapChecks = {
2795
2806
  }
2796
2807
  }
2797
2808
  const origin = siteOrigin(site);
2798
- const listed = sitemapPaths(context);
2809
+ const deployBase = normalizeBasePath(context.project.config.deployment.base);
2810
+ const listed = sitemapPaths(context, deployBase);
2799
2811
  for (const loc of sitemap.urls) {
2800
- found.push(...checkListedUrl(context, loc, origin, sitemap.file));
2812
+ found.push(...checkListedUrl(context, loc, origin, sitemap.file, deployBase));
2801
2813
  }
2802
2814
  for (const page of context.pages) {
2803
2815
  if (!page.indexable || ERROR_ROUTES2.has(page.url) || listed.has(normalizePath(page.url))) {
@@ -3106,6 +3118,7 @@ var referencesFor = (kind, block, defaultLabel, renderer, display, basePath) =>
3106
3118
  label,
3107
3119
  renderer,
3108
3120
  route,
3121
+ scalar: block.scalar,
3109
3122
  slug: routeSlug(route),
3110
3123
  spec: source.spec,
3111
3124
  theme: block.theme
@@ -3467,6 +3480,13 @@ var astroOutDir = (context) => context.distDir ?? `${context.root}/dist`;
3467
3480
  var adapterRoot = (context) => dirname5(astroOutDir(context));
3468
3481
  var REACT_EXCLUDE = String.raw`exclude: [/\/node_modules\/\.vite\//]`;
3469
3482
  var reactIntegration = (compilerPath) => compilerPath ? `react({ babel: { plugins: [[${JSON.stringify(compilerPath)}, { target: "19" }]] }, ${REACT_EXCLUDE} })` : `react({ ${REACT_EXCLUDE} })`;
3483
+ var devWatchOption = (outDir, contentWatchesRuntimeDir) => contentWatchesRuntimeDir ? `
3484
+ // Astro's cache dir sits inside the docs collection, whose watcher would
3485
+ // otherwise churn (and can loop) on Astro's own writes. Trade-off: .md
3486
+ // body edits need a dev-server restart in this layout.
3487
+ watch: {
3488
+ ignored: ${JSON.stringify([join8(outDir, ".astro", "**")])},
3489
+ },` : "";
3470
3490
  var astroConfigTemplate = (options) => {
3471
3491
  const { context, config, needsReact, pages, dataPath, themePath } = options;
3472
3492
  const {
@@ -3553,6 +3573,7 @@ var astroConfigTemplate = (options) => {
3553
3573
  integrations.push("svelte()");
3554
3574
  }
3555
3575
  integrations.push(`blumeIntegration(${JSON.stringify({ base: deployment.base, contentRoutes, pages })})`);
3576
+ const watchOption = devWatchOption(context.outDir, options.contentWatchesRuntimeDir);
3556
3577
  return `// Generated by Blume. Do not edit; this file is recreated on each run.
3557
3578
  ${defineConfigImport}
3558
3579
  import mdx from "@astrojs/mdx";
@@ -3627,22 +3648,17 @@ export default defineConfig({
3627
3648
  server: {
3628
3649
  fs: {
3629
3650
  allow: ${JSON.stringify(fsAllow)},
3630
- },
3631
- // Keep the file watcher out of Astro's own cache dir. In a migrated
3632
- // (root-rooted) project the docs collection is rooted at the project dir,
3633
- // so its glob-loader watcher would otherwise fire on every write Astro
3634
- // makes under .blume/.astro (data-store.json, content module manifests,
3635
- // self-hosted fonts) -- pure noise the loader logs as "No entry type
3636
- // found". Vite appends this to its default ignores.
3637
- watch: {
3638
- ignored: ${JSON.stringify([join8(context.outDir, ".astro", "**")])},
3639
- },
3651
+ },${watchOption}
3640
3652
  },
3641
3653
  },
3642
3654
  });
3643
3655
  `;
3644
3656
  };
3645
3657
  var stagedContentDir = (outDir) => join8(outDir, "content");
3658
+ var runtimeDirWithin = (base, outDir) => {
3659
+ const rel = relative5(base, outDir);
3660
+ return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : null;
3661
+ };
3646
3662
  var astroGlobBase = (base) => isAbsolute(base) ? pathToFileURL(base).href : base;
3647
3663
  var contentConfigTemplate = (options) => {
3648
3664
  const { context, config } = options;
@@ -3650,8 +3666,8 @@ var contentConfigTemplate = (options) => {
3650
3666
  const collectionBase = options.collection?.base ?? context.contentRoot;
3651
3667
  const includeGlobs = options.collection?.include ?? config.content.include;
3652
3668
  const excludeGlobs = options.collection?.exclude ?? config.content.exclude;
3653
- const outDirRel = relative5(collectionBase, context.outDir);
3654
- const outDirIgnore = outDirRel && !outDirRel.startsWith("..") && !isAbsolute(outDirRel) ? [`!${outDirRel}/**`] : [];
3669
+ const outDirRel = runtimeDirWithin(collectionBase, context.outDir);
3670
+ const outDirIgnore = outDirRel ? [`!${outDirRel}/**`] : [];
3655
3671
  const filesystem = options.filesystem ?? true;
3656
3672
  const docsPattern = filesystem ? [
3657
3673
  ...includeGlobs,
@@ -4327,6 +4343,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
4327
4343
  page={{ title: seo.title ?? title, description: seo.description ?? frontmatter.description, route }}
4328
4344
  headings={headings}
4329
4345
  toc={data.config.toc}
4346
+ dateFormat={data.config.dateFormat}
4330
4347
  themeMode={data.config.theme.mode}
4331
4348
  fontCssVars={data.fontCssVars}
4332
4349
  searchEnabled={data.config.search.enabled}
@@ -4365,6 +4382,7 @@ import RootLayout from "blume/components/layout/RootLayout.astro";
4365
4382
  import Update from "blume/components/content/Update.astro";
4366
4383
  import { withBase } from "blume/components/islands/base-path.ts";
4367
4384
  import { resolveSlot } from "blume/components/layout/overrides.ts";
4385
+ import { resolveDateFormatOptions } from "blume/core/date-format.ts";
4368
4386
  import { layoutOverrides } from "../generated/components.ts";
4369
4387
  import data from "blume:data";
4370
4388
 
@@ -4392,8 +4410,9 @@ const localeMeta = i18n
4392
4410
  const dir = localeMeta?.dir ?? "ltr";
4393
4411
  const htmlLang = i18n ? i18n.defaultLocale : "en";
4394
4412
 
4395
- // Formatted in the same locale as the chrome, and in UTC, to match the
4396
- // per-page "last updated" stamp.
4413
+ // Formatted in the same locale as the chrome, and with the configured
4414
+ // \`dateFormat\` (UTC by default), to match the per-page "last updated" stamp.
4415
+ const dateFormatOptions = resolveDateFormatOptions(data.config.dateFormat);
4397
4416
  const formatDate = (value: string | null | undefined) => {
4398
4417
  if (!value) {
4399
4418
  return;
@@ -4401,10 +4420,7 @@ const formatDate = (value: string | null | undefined) => {
4401
4420
  const date = new Date(value);
4402
4421
  return Number.isNaN(date.getTime())
4403
4422
  ? undefined
4404
- : new Intl.DateTimeFormat(htmlLang, {
4405
- dateStyle: "long",
4406
- timeZone: "UTC",
4407
- }).format(date);
4423
+ : new Intl.DateTimeFormat(htmlLang, dateFormatOptions).format(date);
4408
4424
  };
4409
4425
 
4410
4426
  const slugify = (text: string) =>
@@ -5124,6 +5140,10 @@ var crawlStaticDir = async (options) => {
5124
5140
  };
5125
5141
 
5126
5142
  // src/audit/redirects.ts
5143
+ var pathOnly = (value) => {
5144
+ const cut = value.search(/[?#]/u);
5145
+ return cut === -1 ? value : value.slice(0, cut);
5146
+ };
5127
5147
  var resolveRedirects = (redirects, pageUrls) => {
5128
5148
  const byFrom = new Map;
5129
5149
  for (const redirect of redirects) {
@@ -5139,7 +5159,7 @@ var resolveRedirects = (redirects, pageUrls) => {
5139
5159
  chain.push(current);
5140
5160
  break;
5141
5161
  }
5142
- const next = normalizePath(current);
5162
+ const next = normalizePath(pathOnly(current));
5143
5163
  if (seen.has(next)) {
5144
5164
  chain.push(next);
5145
5165
  return {
@@ -5221,8 +5241,9 @@ var matches = (id, terms) => {
5221
5241
  var runAudit = async (options) => {
5222
5242
  const { project } = options;
5223
5243
  const staticDir = deployStaticDir(project.config, project.context);
5244
+ const basePath = normalizeBasePath(project.config.basePath);
5224
5245
  const crawl = await crawlStaticDir({
5225
- basePath: normalizeBasePath(project.config.basePath),
5246
+ basePath,
5226
5247
  manifest: project.manifest,
5227
5248
  staticDir
5228
5249
  });
@@ -5239,7 +5260,11 @@ var runAudit = async (options) => {
5239
5260
  origin,
5240
5261
  pages: crawl.pages,
5241
5262
  project,
5242
- redirects: resolveRedirects(project.config.redirects, new Set([...byUrl.keys(), ...crawl.files.keys()].map((path) => normalizePath(path)))),
5263
+ redirects: resolveRedirects(project.config.redirects.map((redirect) => ({
5264
+ ...redirect,
5265
+ from: withBasePath(basePath, redirect.from),
5266
+ to: withBasePath(basePath, redirect.to)
5267
+ })), new Set([...byUrl.keys(), ...crawl.files.keys()].map((path) => normalizePath(path)))),
5243
5268
  robots: crawl.robots,
5244
5269
  sitemap: crawl.sitemap,
5245
5270
  sources: await readSources(crawl.pages),
@@ -7994,6 +8019,19 @@ var lastModifiedConfigSchema = z2.union([
7994
8019
  z2.boolean(),
7995
8020
  z2.strictObject({ type: z2.enum(["git", "frontmatter"]).default("git") })
7996
8021
  ]);
8022
+ var dateFormatConfigSchema = z2.strictObject({
8023
+ calendar: z2.string().optional(),
8024
+ dateStyle: z2.enum(["full", "long", "medium", "short"]).optional(),
8025
+ day: z2.enum(["numeric", "2-digit"]).optional(),
8026
+ era: z2.enum(["long", "short", "narrow"]).optional(),
8027
+ month: z2.enum(["numeric", "2-digit", "long", "short", "narrow"]).optional(),
8028
+ numberingSystem: z2.string().optional(),
8029
+ timeZone: z2.string().optional(),
8030
+ weekday: z2.enum(["long", "short", "narrow"]).optional(),
8031
+ year: z2.enum(["numeric", "2-digit"]).optional()
8032
+ }).refine((value) => value.dateStyle === undefined || value.weekday === undefined && value.era === undefined && value.year === undefined && value.month === undefined && value.day === undefined, {
8033
+ message: "dateFormat.dateStyle can't be combined with weekday/era/year/month/day; use one or the other."
8034
+ });
7997
8035
  var codeConfigSchema = z2.strictObject({
7998
8036
  icons: z2.boolean().default(true),
7999
8037
  wrap: z2.boolean().default(false)
@@ -8012,12 +8050,14 @@ var openapiSourceSchema = z2.strictObject({
8012
8050
  route: z2.string().optional(),
8013
8051
  spec: z2.string()
8014
8052
  });
8053
+ var scalarConfigSchema = z2.record(z2.string(), z2.unknown()).optional();
8015
8054
  var openapiConfigSchema = z2.strictObject({
8016
8055
  codeSamples: z2.array(z2.string()).default(["curl", "js", "python"]),
8017
8056
  enabled: z2.boolean().default(false),
8018
8057
  expandSchemas: z2.boolean().default(false),
8019
8058
  renderer: z2.enum(["blume", "scalar"]).default("blume"),
8020
8059
  route: z2.string().default("/reference"),
8060
+ scalar: scalarConfigSchema,
8021
8061
  sources: z2.array(openapiSourceSchema).default([]),
8022
8062
  spec: z2.string().optional(),
8023
8063
  theme: z2.string().optional()
@@ -8025,6 +8065,7 @@ var openapiConfigSchema = z2.strictObject({
8025
8065
  var asyncapiConfigSchema = z2.strictObject({
8026
8066
  enabled: z2.boolean().default(false),
8027
8067
  route: z2.string().default("/events"),
8068
+ scalar: scalarConfigSchema,
8028
8069
  sources: z2.array(openapiSourceSchema).default([]),
8029
8070
  spec: z2.string().optional(),
8030
8071
  theme: z2.string().optional()
@@ -8069,6 +8110,7 @@ var blumeConfigSchema = z2.strictObject({
8069
8110
  banner: bannerConfigSchema.optional(),
8070
8111
  basePath: z2.string().optional().transform((value) => normalizeBasePath(value)),
8071
8112
  content: contentConfigSchema.default({}),
8113
+ dateFormat: dateFormatConfigSchema.default({ dateStyle: "long" }),
8072
8114
  deployment: deploymentConfigSchema.default({}),
8073
8115
  description: z2.string().optional(),
8074
8116
  examples: examplesConfigSchema.default("examples"),
@@ -8173,9 +8215,10 @@ var detectLocale = (parts, i18n) => {
8173
8215
  var localePlacement = (rel, ext, i18n) => {
8174
8216
  const base = rel.slice(0, rel.length - ext.length);
8175
8217
  if (base.endsWith(".$")) {
8218
+ const shared = `${base.slice(0, -2)}${ext}`;
8176
8219
  return {
8177
8220
  locales: i18n.locales.map((locale2) => locale2.code),
8178
- navPath: `${base.slice(0, -2)}${ext}`
8221
+ navPath: i18n.parser === "dir" ? detectLocale(shared.split("/"), i18n).rest.join("/") : shared
8179
8222
  };
8180
8223
  }
8181
8224
  if (i18n.parser === "dot") {
@@ -8655,7 +8698,8 @@ var normalizeRef = (ref) => {
8655
8698
  return "/";
8656
8699
  }
8657
8700
  const withSlash = ref.startsWith("/") ? ref : `/${ref}`;
8658
- const trimmed = withSlash.endsWith("/index") ? withSlash.slice(0, -"/index".length) : withSlash;
8701
+ const noTrailing = withSlash.replace(/\/+$/u, "");
8702
+ const trimmed = noTrailing.endsWith("/index") ? noTrailing.slice(0, -"/index".length) : noTrailing;
8659
8703
  return trimmed === "" ? "/" : trimmed;
8660
8704
  };
8661
8705
  var routeForRef = (ref, byRoute, basePath) => {
@@ -8862,7 +8906,10 @@ var buildLocaleNavigation = (code, pages, fallback, fallbackByKey, options, i18n
8862
8906
  basePath: options.basePath ?? "",
8863
8907
  diagnostics,
8864
8908
  display: options.navigation.sidebar.display,
8865
- featured: options.navigation.featured,
8909
+ featured: options.navigation.featured?.map((link) => ({
8910
+ ...link,
8911
+ href: localizePath(link.href)
8912
+ })),
8866
8913
  folderMeta: options.folderMeta,
8867
8914
  localizedRoot: localizeRoute("/", code, i18n),
8868
8915
  metaPrefix: i18n.parser === "dir" && code !== i18n.defaultLocale ? code : "",
@@ -9158,6 +9205,7 @@ var WORD_SPLIT2 = /[-_]/u;
9158
9205
  var stripNumericPrefix = (segment) => segment.replace(NUMERIC_PREFIX2, "");
9159
9206
  var groupLabel = (segment) => segment.match(GROUP_FOLDER2)?.groups?.label ?? null;
9160
9207
  var slugify2 = (text) => text.toLowerCase().trim().replaceAll(/[^\w\s-]/gu, "").replaceAll(/[\s_]+/gu, "-").replaceAll(/-+/gu, "-").replaceAll(/^-|-$/gu, "");
9208
+ var slugifyPath = (text) => text.split("/").map(slugify2).filter(Boolean).join("/");
9161
9209
  var titleCase = (value) => value.split(WORD_SPLIT2).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
9162
9210
  var addRouteSegment = (part, segments, groups) => {
9163
9211
  if (part === "") {
@@ -9187,11 +9235,18 @@ var mapRoute = (relativePath) => {
9187
9235
  };
9188
9236
  var CODE_FENCE = /^(?<delimiter>```|~~~)/u;
9189
9237
  var nextFenceState = (line, fence) => {
9190
- const delimiter = line.trimStart().match(CODE_FENCE)?.groups?.delimiter;
9238
+ const trimmed = line.trimStart();
9239
+ const delimiter = trimmed.match(CODE_FENCE)?.groups?.delimiter;
9191
9240
  if (delimiter === undefined) {
9192
9241
  return fence;
9193
9242
  }
9194
9243
  if (fence === null) {
9244
+ if (delimiter === "```") {
9245
+ const run = trimmed.match(/^`+/u)?.[0].length ?? 0;
9246
+ if (trimmed.slice(run).includes("`")) {
9247
+ return fence;
9248
+ }
9249
+ }
9195
9250
  return delimiter;
9196
9251
  }
9197
9252
  return fence === delimiter ? null : fence;
@@ -9209,6 +9264,9 @@ var linesWithoutFrontMatter = (body) => {
9209
9264
  if (!/^-{3}\s*$/u.test(lines[0] ?? "")) {
9210
9265
  return lines;
9211
9266
  }
9267
+ if ((lines[1] ?? "").trim() === "") {
9268
+ return lines;
9269
+ }
9212
9270
  const close = lines.findIndex((line, index) => index > 0 && FRONT_MATTER_CLOSE.test(line));
9213
9271
  return close === -1 ? lines : lines.slice(close + 1);
9214
9272
  };
@@ -9288,8 +9346,10 @@ var extractHeadings = (body) => {
9288
9346
  }
9289
9347
  return headings;
9290
9348
  };
9291
- var MD_LINK = /\[[^\]]*\]\((?<target>[^)\s]+)(?:\s+"[^"]*")?\)/gu;
9349
+ var MD_LINK = /\[(?<label>(?:[^[\]]|\[[^\]]*\])*)\]\((?<target>(?:[^()\s]|\([^()\s]*\))+)(?<title>\s+"[^"]*")?\)/gu;
9350
+ var MD_IMAGE = /!\[[^\]]*\]\((?<target>(?:[^()\s]|\([^()\s]*\))+)(?<title>\s+"[^"]*")?\)/gu;
9292
9351
  var INLINE_CODE = /`[^`]*`/gu;
9352
+ var targetOffsetIn = (matched, target, title) => matched.length - 1 - (title?.length ?? 0) - target.length;
9293
9353
  var scanLinkLine = (line, lineNumber, fence, links) => {
9294
9354
  const next = nextFenceState(line, fence);
9295
9355
  if (fence !== null || next !== null) {
@@ -9301,12 +9361,24 @@ var scanLinkLine = (line, lineNumber, fence, links) => {
9301
9361
  if (target === undefined || match.index === undefined) {
9302
9362
  continue;
9303
9363
  }
9304
- const targetOffset = match.index + match[0].indexOf("](") + "](".length;
9364
+ const targetOffset = targetOffsetIn(match[0], target, match.groups?.title);
9305
9365
  links.push({
9306
- column: targetOffset + 1,
9366
+ column: match.index + targetOffset + 1,
9307
9367
  line: lineNumber,
9308
9368
  target
9309
9369
  });
9370
+ const label = match[0].slice(0, targetOffset - "](".length);
9371
+ for (const image of label.matchAll(MD_IMAGE)) {
9372
+ const imageTarget = image.groups?.target;
9373
+ if (imageTarget === undefined || image.index === undefined) {
9374
+ continue;
9375
+ }
9376
+ links.push({
9377
+ column: match.index + image.index + targetOffsetIn(image[0], imageTarget, image.groups?.title) + 1,
9378
+ line: lineNumber,
9379
+ target: imageTarget
9380
+ });
9381
+ }
9310
9382
  }
9311
9383
  return next;
9312
9384
  };
@@ -9580,6 +9652,24 @@ var operationKey = (method, path, operationId) => {
9580
9652
  return fromId || slugify3(`${method}-${path}`);
9581
9653
  };
9582
9654
  var isOperation = (value) => typeof value === "object" && value !== null;
9655
+ var tagSlugger = () => {
9656
+ const assigned = new Map;
9657
+ const taken = new Set;
9658
+ return (name) => {
9659
+ const existing = assigned.get(name);
9660
+ if (existing) {
9661
+ return existing;
9662
+ }
9663
+ const base = slugify3(name) || "operations";
9664
+ let slug = base;
9665
+ for (let suffix = 2;taken.has(slug); suffix += 1) {
9666
+ slug = `${base}-${suffix}`;
9667
+ }
9668
+ taken.add(slug);
9669
+ assigned.set(name, slug);
9670
+ return slug;
9671
+ };
9672
+ };
9583
9673
  var extractOperations = (document, baseRoute) => {
9584
9674
  const operations = [];
9585
9675
  const tagOrder = [];
@@ -9587,6 +9677,7 @@ var extractOperations = (document, baseRoute) => {
9587
9677
  const tagMeta = new Map((document.tags ?? []).map((tag) => [tag.name, tag.description ?? ""]));
9588
9678
  const seen = new Set;
9589
9679
  const warnings = [];
9680
+ const slugForTag = tagSlugger();
9590
9681
  for (const [path, rawItem] of Object.entries(document.paths ?? {})) {
9591
9682
  const item = rawItem;
9592
9683
  if (!item) {
@@ -9602,7 +9693,7 @@ var extractOperations = (document, baseRoute) => {
9602
9693
  continue;
9603
9694
  }
9604
9695
  const tag = operation.tags?.[0] ?? UNTAGGED;
9605
- const tagSlug = slugify3(tag) || "operations";
9696
+ const tagSlug = slugForTag(tag);
9606
9697
  if (!tagsSeen.has(tag)) {
9607
9698
  tagsSeen.add(tag);
9608
9699
  tagOrder.push(tag);
@@ -9629,7 +9720,7 @@ var extractOperations = (document, baseRoute) => {
9629
9720
  const tags = tagOrder.map((name) => ({
9630
9721
  description: tagMeta.get(name) ?? "",
9631
9722
  name,
9632
- slug: slugify3(name) || "operations"
9723
+ slug: slugForTag(name)
9633
9724
  }));
9634
9725
  return { operations, tags, warnings };
9635
9726
  };
@@ -9779,15 +9870,14 @@ var parseSpec = async (spec, root, options = {}) => {
9779
9870
  };
9780
9871
 
9781
9872
  // src/openapi/render-mdx.ts
9782
- var MDX_UNSAFE = /[<>{}]/gu;
9873
+ var MDX_UNSAFE = /[<{}]/gu;
9783
9874
  var ENTITIES = {
9784
9875
  "<": "&lt;",
9785
- ">": "&gt;",
9786
9876
  "{": "&#123;",
9787
9877
  "}": "&#125;"
9788
9878
  };
9789
9879
  var MDX_ESM_KEYWORD = /^(?<keyword>import|export)\b/gmu;
9790
- var BACKTICK_CODE = /(?<bt>`+)[\s\S]*?\k<bt>/gu;
9880
+ var BACKTICK_CODE = /(?<!`)(?<bt>`+)(?!`)[\s\S]*?(?<!`)\k<bt>(?!`)/gu;
9791
9881
  var escapeProse = (text) => text.replace(MDX_UNSAFE, (char) => ENTITIES[char] ?? char).replace(MDX_ESM_KEYWORD, (keyword) => `&#${keyword.codePointAt(0)};${keyword.slice(1)}`);
9792
9882
  var mdxSafe = (text) => {
9793
9883
  let out = "";
@@ -10399,7 +10489,7 @@ import { join as join15 } from "pathe";
10399
10489
  // src/core/sources/assets.ts
10400
10490
  import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
10401
10491
  import { extname as extname4, join as join14 } from "pathe";
10402
- var MD_IMAGE = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
10492
+ var MD_IMAGE2 = /!\[(?<alt>[^\]]*)\]\((?<url>[^)\s]+)\)/gu;
10403
10493
  var REMOTE = /^https?:\/\//u;
10404
10494
  var SAFE_EXT = /^\.[a-z0-9]+$/iu;
10405
10495
  var CODE_FENCE_BLOCK = /^(?<fence>`{3,}|~{3,})[^\n]*\n[\s\S]*?^\k<fence>[^\n]*(?=\n|$)/gmu;
@@ -10418,7 +10508,7 @@ var materializeAssets = async (markdown, ctx) => {
10418
10508
  return `\x00blume-fence-${fences.length - 1}\x00`;
10419
10509
  });
10420
10510
  const urls = new Set;
10421
- for (const match of masked.matchAll(MD_IMAGE)) {
10511
+ for (const match of masked.matchAll(MD_IMAGE2)) {
10422
10512
  const url = match.groups?.url;
10423
10513
  if (url && REMOTE.test(url)) {
10424
10514
  urls.add(url);
@@ -10444,7 +10534,7 @@ var materializeAssets = async (markdown, ctx) => {
10444
10534
  });
10445
10535
  }
10446
10536
  }));
10447
- const rewritten = masked.replaceAll(MD_IMAGE, (match, alt, url) => {
10537
+ const rewritten = masked.replaceAll(MD_IMAGE2, (match, alt, url) => {
10448
10538
  const local = rewrites.get(url);
10449
10539
  return local ? `![${alt}](${local})` : match;
10450
10540
  }).replaceAll(FENCE_TOKEN, (token, index) => fences[Number(index)] ?? token);
@@ -10679,7 +10769,7 @@ ${nested}`;
10679
10769
  data.sidebar = { order };
10680
10770
  }
10681
10771
  const slugProp = richToMarkdown(page.properties[props.slug ?? "Slug"]?.rich_text);
10682
- const slug = slugify2(slugProp || title) || page.id;
10772
+ const slug = slugifyPath(slugProp || title) || page.id;
10683
10773
  return { data, slug };
10684
10774
  };
10685
10775
  const toEntry2 = async (client, page) => {
@@ -10886,7 +10976,7 @@ var sanitySource = (options, ctx) => {
10886
10976
  let snapshot = new Map;
10887
10977
  const toEntry2 = (doc) => {
10888
10978
  const slugValue = asString(getPath(doc, fields.slug ?? "slug.current")) ?? asString(doc._id) ?? "untitled";
10889
- const slug = slugify2(slugValue) || slugify2(asString(doc._id) ?? "") || "untitled";
10979
+ const slug = slugifyPath(slugValue) || slugify2(asString(doc._id) ?? "") || "untitled";
10890
10980
  const data = {};
10891
10981
  const title = asString(getPath(doc, fields.title ?? "title"));
10892
10982
  const description = asString(getPath(doc, fields.description ?? "description"));
@@ -11246,6 +11336,16 @@ var shouldFail = (result, gate) => {
11246
11336
  const failing = failingSeverities(gate);
11247
11337
  return result.diagnostics.some((d) => failing.has(d.severity));
11248
11338
  };
11339
+ var launchAgentCode = async (bin, prompt) => {
11340
+ try {
11341
+ return await launchAgent(bin, prompt);
11342
+ } catch (error) {
11343
+ if (error?.code !== "ENOENT") {
11344
+ throw error;
11345
+ }
11346
+ return WINDOWS_COMMAND_NOT_FOUND;
11347
+ }
11348
+ };
11249
11349
  var auditCommand = defineCommand2({
11250
11350
  args: {
11251
11351
  claude: {
@@ -11351,12 +11451,7 @@ var auditCommand = defineCommand2({
11351
11451
  process.stderr.write(` Handing ${count} finding${count === 1 ? "" : "s"} to ${cli.name}…
11352
11452
 
11353
11453
  `);
11354
- let code;
11355
- try {
11356
- code = await launchAgent(cli.bin, fixPrompt(report));
11357
- } catch {
11358
- code = WINDOWS_COMMAND_NOT_FOUND;
11359
- }
11454
+ const code = await launchAgentCode(cli.bin, fixPrompt(report));
11360
11455
  if (code === WINDOWS_COMMAND_NOT_FOUND) {
11361
11456
  logger.error(`${cli.name} (\`${cli.bin}\`) was not found on PATH. Install it with \`${cli.install}\`.`);
11362
11457
  process.exit(1);
@@ -11734,6 +11829,22 @@ var youtube = ({ props }) => {
11734
11829
  const title = typeof props.title === "string" && props.title !== "" ? props.title : "Watch on YouTube";
11735
11830
  return `[${title}](https://www.youtube.com/watch?v=${videoId}${start})`;
11736
11831
  };
11832
+ var fencedBlock = (lang, code) => {
11833
+ const trimmed = code.replace(/(?<!\n)\n+$/u, "");
11834
+ const runs = trimmed.match(/`+/gu);
11835
+ const longest = runs ? Math.max(...runs.map((run) => run.length)) : 0;
11836
+ const fence = "`".repeat(Math.max(3, longest + 1));
11837
+ return `${fence}${lang}
11838
+ ${trimmed}
11839
+ ${fence}`;
11840
+ };
11841
+ var exampleComponentSerializers = (examples) => ({
11842
+ Component: ({ props }) => {
11843
+ const path = typeof props.path === "string" ? props.path : undefined;
11844
+ const example = path === undefined ? undefined : examples[path];
11845
+ return example ? fencedBlock(example.lang, example.source) : null;
11846
+ }
11847
+ });
11737
11848
  var SERIALIZERS = {
11738
11849
  Callout: callout,
11739
11850
  Steps: steps,
@@ -11929,10 +12040,14 @@ var buildIndex = (project) => {
11929
12040
  var buildFull = async (project) => {
11930
12041
  const { config } = project;
11931
12042
  const pages = eligiblePages(project).toSorted((a, b) => a.route.localeCompare(b.route));
12043
+ const components = {
12044
+ ...exampleComponentSerializers(project.examples ?? {}),
12045
+ ...config.ai.markdownComponents
12046
+ };
11932
12047
  const sections = await Promise.all(pages.map(async (page) => {
11933
12048
  const raw = await readEntryText(project, page);
11934
12049
  const parsed = frontmatter_default(raw);
11935
- const body = downlevelComponents(applyAgentVisibility(parsed.content), config.ai.markdownComponents, parsed.data).trim();
12050
+ const body = downlevelComponents(applyAgentVisibility(parsed.content), components, parsed.data).trim();
11936
12051
  const url = pageUrl(page.route, config.deployment.site, normalizeBasePath(config.deployment.base));
11937
12052
  return [`# ${page.title}`, `Source: ${url}`, "", body].join(`
11938
12053
  `);
@@ -12539,6 +12654,7 @@ import {
12539
12654
  lstat,
12540
12655
  mkdir as mkdir6,
12541
12656
  readFile as readFile14,
12657
+ readlink,
12542
12658
  realpath,
12543
12659
  rename,
12544
12660
  rm as rm2,
@@ -12547,7 +12663,7 @@ import {
12547
12663
  } from "node:fs/promises";
12548
12664
  import { createRequire as createRequire5 } from "node:module";
12549
12665
  import { pathToFileURL as pathToFileURL3 } from "node:url";
12550
- import { basename as basename3, dirname as dirname9, join as join26, normalize as normalize3, relative as relative13 } from "pathe";
12666
+ import { basename as basename3, dirname as dirname9, join as join26, normalize as normalize3, relative as relative13, resolve as resolve7 } from "pathe";
12551
12667
  import { glob as glob7 } from "tinyglobby";
12552
12668
 
12553
12669
  // src/ai/ask-data.ts
@@ -12574,6 +12690,10 @@ import { readFile as readFile10 } from "node:fs/promises";
12574
12690
  var agentMarkdown = (entry) => entry.md ?? entry.mdx;
12575
12691
  var buildRawMarkdown = async (project) => {
12576
12692
  const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
12693
+ const components = {
12694
+ ...exampleComponentSerializers(project.examples ?? {}),
12695
+ ...project.config.ai.markdownComponents
12696
+ };
12577
12697
  const readRoute = async (route) => {
12578
12698
  const page = pageById.get(route.id);
12579
12699
  if (page) {
@@ -12583,7 +12703,7 @@ var buildRawMarkdown = async (project) => {
12583
12703
  };
12584
12704
  const entries = await Promise.all(project.manifest.routes.map(async (route) => {
12585
12705
  const source = applyAgentVisibility(await readRoute(route));
12586
- const md = downlevelComponents(source, project.config.ai.markdownComponents, frontmatter_default(source).data);
12706
+ const md = downlevelComponents(source, components, frontmatter_default(source).data);
12587
12707
  const entry = md === source ? { mdx: source } : { md, mdx: source };
12588
12708
  return [route.path, entry];
12589
12709
  }));
@@ -13351,7 +13471,8 @@ var buildReferenceFiles = async (options) => {
13351
13471
  content: scalarReferenceTemplate({
13352
13472
  configuration: {
13353
13473
  ...spec.config,
13354
- ...themeConfiguration(config, ref.theme)
13474
+ ...themeConfiguration(config, ref.theme),
13475
+ ...ref.scalar
13355
13476
  },
13356
13477
  dataImport: `${"../".repeat(depth + 1)}generated/data.json`,
13357
13478
  route: ref.route,
@@ -13582,6 +13703,13 @@ ${THEME_MAPPING}
13582
13703
  letter-spacing: 0;
13583
13704
  }
13584
13705
 
13706
+ /* A heading can carry one long unbreakable token — an OpenAPI operation's title
13707
+ is \`METHOD /very/long/{path}\` when the spec sets no summary — which would run
13708
+ off the content column. Break it across lines instead of overflowing. */
13709
+ .prose :where(h1, h2, h3, h4, h5, h6) {
13710
+ overflow-wrap: break-word;
13711
+ }
13712
+
13585
13713
  .prose :where(h1) {
13586
13714
  font-size: 3rem;
13587
13715
  line-height: 1.1;
@@ -14132,6 +14260,16 @@ var OVERRIDES = `
14132
14260
  padding-right: 1.25rem;
14133
14261
  }
14134
14262
 
14263
+ /* Twoslash blocks keep \`overflow: visible\` so popups can escape, which means
14264
+ long lines can't scroll — they'd push past the viewport on narrow screens.
14265
+ Wrap them on mobile instead; hover popups still position against the token. */
14266
+ @media (max-width: 640px) {
14267
+ .prose pre.twoslash code {
14268
+ white-space: pre-wrap;
14269
+ overflow-wrap: anywhere;
14270
+ }
14271
+ }
14272
+
14135
14273
  /* The rich renderer renders each popup's type signature as a nested Shiki
14136
14274
  pre. Strip the code-block chrome (border, radius, padding, background) so it
14137
14275
  sits flush inside the popup, which owns the frame. */
@@ -14410,6 +14548,10 @@ var discoverExamples = async (root, pattern = "examples") => {
14410
14548
  }
14411
14549
  return { examples, warnings };
14412
14550
  };
14551
+ var exampleMarkdownLookup = (examples) => Object.fromEntries(examples.map((example) => [
14552
+ example.path,
14553
+ { lang: example.lang, source: example.source }
14554
+ ]));
14413
14555
 
14414
14556
  // src/astro/generate.ts
14415
14557
  var BLUME_SRC = join26(packageRoot(), "src");
@@ -14421,12 +14563,12 @@ var canResolveFrom = (fromDir, spec) => {
14421
14563
  return false;
14422
14564
  }
14423
14565
  };
14424
- var resolveReactCompiler = (config, needsReact) => {
14566
+ var resolveReactCompiler = (config, needsReact, pkgDir = packageRoot()) => {
14425
14567
  if (!(needsReact && config.react.compiler)) {
14426
14568
  return null;
14427
14569
  }
14428
14570
  try {
14429
- return createRequire5(pathToFileURL3(join26(packageRoot(), "_.js")).href).resolve("babel-plugin-react-compiler");
14571
+ return createRequire5(pathToFileURL3(join26(pkgDir, "_.js")).href).resolve("babel-plugin-react-compiler");
14430
14572
  } catch {
14431
14573
  return null;
14432
14574
  }
@@ -14441,12 +14583,13 @@ var resolveAstroPackageJson = (modulesDir) => {
14441
14583
  return null;
14442
14584
  }
14443
14585
  };
14444
- var resolvedAstroPath = (fromDir) => {
14586
+ var resolvedAstroHit = (fromDir) => {
14445
14587
  let dir = normalize3(fromDir);
14446
14588
  while (true) {
14447
- const resolved = resolveAstroPackageJson(join26(dir, "node_modules"));
14448
- if (resolved) {
14449
- return resolved;
14589
+ const modulesDir = join26(dir, "node_modules");
14590
+ const pkg = resolveAstroPackageJson(modulesDir);
14591
+ if (pkg) {
14592
+ return { modulesDir, pkg };
14450
14593
  }
14451
14594
  const parent = dirname9(dir);
14452
14595
  if (parent === dir) {
@@ -14455,6 +14598,13 @@ var resolvedAstroPath = (fromDir) => {
14455
14598
  dir = parent;
14456
14599
  }
14457
14600
  };
14601
+ var sameRealDir = (a, b) => {
14602
+ try {
14603
+ return realpathSync(a) === realpathSync(b);
14604
+ } catch {
14605
+ return false;
14606
+ }
14607
+ };
14458
14608
  var depsCandidates = (pkgDir) => [
14459
14609
  join26(pkgDir, "node_modules"),
14460
14610
  dirname9(pkgDir)
@@ -14471,6 +14621,11 @@ var linkDepsJunction = async (link, depsDir) => {
14471
14621
  if (!existing.isSymbolicLink()) {
14472
14622
  return;
14473
14623
  }
14624
+ try {
14625
+ if (resolve7(dirname9(link), await readlink(link)) === resolve7(depsDir)) {
14626
+ return;
14627
+ }
14628
+ } catch {}
14474
14629
  await rm2(link, { force: true });
14475
14630
  }
14476
14631
  await mkdir6(dirname9(link), { recursive: true });
@@ -14523,16 +14678,17 @@ var ensureDepsLink = async (outDir, pkgDir = packageRoot()) => {
14523
14678
  const mdxDir = candidateHolding(pkgDir, "@astrojs", "mdx");
14524
14679
  const blumeAstro = resolveAstroPackageJson(astroDir);
14525
14680
  await dropStaleDepsLink(join26(outDir, "node_modules"), pkgDir);
14526
- const outDirAstro = resolvedAstroPath(outDir);
14527
- const astroCorrect = blumeAstro !== null && outDirAstro === blumeAstro;
14528
- if (astroCorrect && mdxDir === astroDir) {
14681
+ const outDirHit = resolvedAstroHit(outDir);
14682
+ const astroCorrect = blumeAstro !== null && outDirHit?.pkg === blumeAstro;
14683
+ const walkLandsInDeps = astroCorrect && outDirHit !== null && sameRealDir(outDirHit.modulesDir, astroDir);
14684
+ if (walkLandsInDeps && mdxDir === astroDir) {
14529
14685
  return null;
14530
14686
  }
14531
14687
  if (mdxDir && (mdxDir === astroDir || astroCorrect)) {
14532
14688
  await linkDepsJunction(join26(outDir, "node_modules"), mdxDir);
14533
14689
  return null;
14534
14690
  }
14535
- return astroConflictWarning(blumeAstro, outDirAstro);
14691
+ return astroConflictWarning(blumeAstro, outDirHit?.pkg ?? null);
14536
14692
  };
14537
14693
  var ISLAND_FRAMEWORK_DEPS = {
14538
14694
  svelte: "@astrojs/svelte",
@@ -14561,6 +14717,15 @@ var deploymentAdapterWarnings = (deployment, root) => {
14561
14717
  }
14562
14718
  return [];
14563
14719
  };
14720
+ var searchProviderWarnings = (provider, root, pkgDir = packageRoot()) => {
14721
+ const warnings = [];
14722
+ for (const dep of searchProviderMeta(provider).runtimeDeps) {
14723
+ if (!(canResolveFrom(root, dep) || canResolveFrom(pkgDir, dep))) {
14724
+ warnings.push(`Search provider "${provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`);
14725
+ }
14726
+ }
14727
+ return warnings;
14728
+ };
14564
14729
  var examplesCssFile = (root, config) => config.examples.css ? join26(root, config.examples.css) : null;
14565
14730
  var writeExamplesPreview = async (options) => {
14566
14731
  const { config, hasExamples, root, srcDir, write } = options;
@@ -14823,6 +14988,7 @@ var buildRuntimeData = (project) => {
14823
14988
  basePath: config.basePath,
14824
14989
  codeThemes: config.markdown.codeBlocks.theme,
14825
14990
  codeWrap: config.markdown.code.wrap,
14991
+ dateFormat: config.dateFormat,
14826
14992
  description: config.description,
14827
14993
  favicon: resolveFavicon(project),
14828
14994
  feedback: config.feedback,
@@ -14965,6 +15131,7 @@ var writeNotFoundPage = async (write, srcDir, pages, contentPages) => {
14965
15131
  }
14966
15132
  await write(join26(srcDir, "pages", "404.astro"), notFoundPageTemplate());
14967
15133
  };
15134
+ var diagnosticWarning = (diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message;
14968
15135
  var buildComponentSlots = async (componentsFile) => {
14969
15136
  const analysis = componentsFile ? analyzeComponentOverrides(await readFile14(componentsFile, "utf-8"), componentsFile) : null;
14970
15137
  return {
@@ -14973,6 +15140,7 @@ var buildComponentSlots = async (componentsFile) => {
14973
15140
  warnings: analysis ? analysis.warnings : []
14974
15141
  };
14975
15142
  };
15143
+ var contentWatchesRuntimeDir = (hasFilesystemSource, collectionBase, context) => hasFilesystemSource && runtimeDirWithin(collectionBase, context.outDir) !== null;
14976
15144
  var generateRuntime = async (project) => {
14977
15145
  const { context, config } = project;
14978
15146
  const out = context.outDir;
@@ -15018,6 +15186,7 @@ var generateRuntime = async (project) => {
15018
15186
  tags: overrideTags,
15019
15187
  warnings: overrideWarnings
15020
15188
  } = componentSlots;
15189
+ project.examples = exampleMarkdownLookup(exampleDiscovery.examples);
15021
15190
  const frameworks = new Set([
15022
15191
  ...islandDiscovery.islands.map((island) => island.framework),
15023
15192
  ...exampleDiscovery.examples.map((example) => example.framework),
@@ -15032,6 +15201,7 @@ var generateRuntime = async (project) => {
15032
15201
  pages.push(...mcp.discoveryPages);
15033
15202
  const hasStaged = staged.size > 0;
15034
15203
  const hasFilesystemSource = project.sources.some((source) => !source.staged);
15204
+ const docsCollection = resolveDocsCollection(config, context);
15035
15205
  const [structural] = await Promise.all([
15036
15206
  Promise.all([
15037
15207
  write(join26(out, "astro.config.mjs"), astroConfigTemplate({
@@ -15039,6 +15209,7 @@ var generateRuntime = async (project) => {
15039
15209
  askPath,
15040
15210
  config,
15041
15211
  contentRoutes: project.manifest.routes.map((route) => route.path),
15212
+ contentWatchesRuntimeDir: contentWatchesRuntimeDir(hasFilesystemSource, docsCollection.base, context),
15042
15213
  context,
15043
15214
  dataPath,
15044
15215
  examplesPath,
@@ -15056,7 +15227,7 @@ var generateRuntime = async (project) => {
15056
15227
  write(join26(out, "tsconfig.json"), runtimeTsconfigTemplate()),
15057
15228
  write(join26(srcDir, "env.d.ts"), envTemplate()),
15058
15229
  write(join26(srcDir, "content.config.ts"), contentConfigTemplate({
15059
- collection: resolveDocsCollection(config, context),
15230
+ collection: docsCollection,
15060
15231
  config,
15061
15232
  context,
15062
15233
  filesystem: hasFilesystemSource,
@@ -15160,18 +15331,12 @@ var generateRuntime = async (project) => {
15160
15331
  warnings.push(...[
15161
15332
  ...validateNavTargets(project.graph.navigation, navTargetRoutes),
15162
15333
  ...validateSearchPopularIcons(config.search.popular)
15163
- ].map((diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message));
15334
+ ].map(diagnosticWarning));
15164
15335
  const knownComponentTags = new Set([
15165
15336
  ...islandDiscovery.islands.map((island) => island.name),
15166
15337
  ...overrideTags
15167
15338
  ]);
15168
- warnings.push(...validateUsedComponents(project.graph.pages, knownComponentTags, new Set(registry.map((item) => item.name))).map((diagnostic) => diagnostic.suggestion ? `${diagnostic.message} ${diagnostic.suggestion}` : diagnostic.message));
15169
- for (const dep of searchProviderMeta(config.search.provider).runtimeDeps) {
15170
- if (!(canResolveFrom(context.root, dep) || canResolveFrom(packageRoot(), dep))) {
15171
- warnings.push(`Search provider "${config.search.provider}" needs "${dep}", which isn't installed. Run \`npm install ${dep}\` (or your package manager's equivalent).`);
15172
- }
15173
- }
15174
- warnings.push(...deploymentAdapterWarnings(config.deployment, context.root), ...islandFrameworkWarnings(frameworks, context.root));
15339
+ warnings.push(...validateUsedComponents(project.graph.pages, knownComponentTags, new Set(registry.map((item) => item.name))).map(diagnosticWarning), ...searchProviderWarnings(config.search.provider, context.root), ...deploymentAdapterWarnings(config.deployment, context.root), ...islandFrameworkWarnings(frameworks, context.root));
15175
15340
  if (hasScalarReferences(config)) {
15176
15341
  const references = await buildReferenceFiles({
15177
15342
  config,
@@ -15196,7 +15361,7 @@ var generateRuntime = async (project) => {
15196
15361
 
15197
15362
  // src/cli/env.ts
15198
15363
  import { existsSync as existsSync15, readFileSync as readFileSync10 } from "node:fs";
15199
- import { dirname as dirname10, join as join27, resolve as resolve7 } from "pathe";
15364
+ import { dirname as dirname10, join as join27, resolve as resolve8 } from "pathe";
15200
15365
  var ENV_LINE = /^\s*(?:export\s+)?(?<key>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?<value>.*?)\s*$/u;
15201
15366
  var DOUBLE_QUOTED2 = /^"(?<body>[\s\S]*)"$/u;
15202
15367
  var SINGLE_QUOTED = /^'(?<body>[\s\S]*)'$/u;
@@ -15248,7 +15413,7 @@ var loadFile = (path) => {
15248
15413
  } catch {}
15249
15414
  };
15250
15415
  var loadEnvFiles = (startDir) => {
15251
- let dir = resolve7(startDir);
15416
+ let dir = resolve8(startDir);
15252
15417
  let done = false;
15253
15418
  while (!done) {
15254
15419
  loadFile(join27(dir, ".env.local"));
@@ -15690,6 +15855,7 @@ var checkCommand = defineCommand4({
15690
15855
  import { watch } from "node:fs";
15691
15856
  import { dev } from "astro";
15692
15857
  import { defineCommand as defineCommand5 } from "citty";
15858
+ import { basename as basename4, dirname as dirname11 } from "pathe";
15693
15859
 
15694
15860
  // src/astro/integration.ts
15695
15861
  var overlayServer = null;
@@ -15868,17 +16034,26 @@ var devCommand = defineCommand5({
15868
16034
  if (boundPort !== port) {
15869
16035
  runRegenerate();
15870
16036
  }
16037
+ const dirTargets = [project.context.pagesRoot].filter((target) => target !== null);
15871
16038
  const fileTargets = [
15872
- project.context.pagesRoot,
15873
16039
  project.context.configFile,
15874
16040
  project.context.themeFile,
15875
16041
  project.context.componentsFile
15876
16042
  ].filter((target) => target !== null);
15877
16043
  const disposers = [
15878
16044
  ...project.sources.map((source) => source.watch?.(regenerate)),
15879
- ...fileTargets.map((target) => {
16045
+ ...dirTargets.map((target) => {
15880
16046
  const watcher = watch(target, { recursive: true }, regenerate);
15881
16047
  return () => watcher.close();
16048
+ }),
16049
+ ...fileTargets.map((target) => {
16050
+ const name = basename4(target);
16051
+ const watcher = watch(dirname11(target), (_event, filename) => {
16052
+ if (!filename || filename === name) {
16053
+ regenerate();
16054
+ }
16055
+ });
16056
+ return () => watcher.close();
15882
16057
  })
15883
16058
  ].filter((dispose) => dispose !== undefined);
15884
16059
  const shutdown = async () => {
@@ -16112,6 +16287,8 @@ var eject = async (root) => {
16112
16287
  const askEnabled = config.ai.ask?.enabled ?? false;
16113
16288
  const exportPdf = config.export.pdf;
16114
16289
  const exportEpub = config.export.epub;
16290
+ const examples = await discoverExamples(root, config.examples.source);
16291
+ project.examples = exampleMarkdownLookup(examples.examples);
16115
16292
  const [
16116
16293
  pages,
16117
16294
  needsReactRaw,
@@ -16119,8 +16296,7 @@ var eject = async (root) => {
16119
16296
  userTheme,
16120
16297
  userExamplesCss,
16121
16298
  rawMarkdown,
16122
- islands,
16123
- examples
16299
+ islands
16124
16300
  ] = await Promise.all([
16125
16301
  context.pagesRoot ? discoverPages(context.pagesRoot) : Promise.resolve([]),
16126
16302
  detectNeedsReact(root),
@@ -16128,8 +16304,7 @@ var eject = async (root) => {
16128
16304
  context.themeFile ? readFile15(context.themeFile, "utf-8") : Promise.resolve(""),
16129
16305
  readExamplesCss(root, config.examples.css),
16130
16306
  buildRawMarkdown(project),
16131
- discoverIslands(root),
16132
- discoverExamples(root, config.examples.source)
16307
+ discoverIslands(root)
16133
16308
  ]);
16134
16309
  const frameworks = new Set([
16135
16310
  ...islands.islands.map((island) => island.framework),
@@ -16399,7 +16574,7 @@ var updatePackageScripts = async (root) => {
16399
16574
  // src/cli/init/scaffold.ts
16400
16575
  import { existsSync as existsSync19 } from "node:fs";
16401
16576
  import { mkdir as mkdir8, writeFile as writeFile11 } from "node:fs/promises";
16402
- import { basename as basename4, dirname as dirname11, isAbsolute as isAbsolute9, join as join33, relative as relative15 } from "pathe";
16577
+ import { basename as basename5, dirname as dirname12, isAbsolute as isAbsolute9, join as join33, relative as relative15 } from "pathe";
16403
16578
 
16404
16579
  // src/core/package-json.ts
16405
16580
  var toPackageName = (raw) => raw.toLowerCase().replaceAll(/[^a-z0-9._-]+/gu, "-").replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
@@ -16609,7 +16784,7 @@ var extraDepsFor = (sources) => ({
16609
16784
  var buildPlan = (root, answers) => {
16610
16785
  const files = [
16611
16786
  {
16612
- content: blumePackageJson(toPackageName(basename4(root)), extraDepsFor(answers.sources)),
16787
+ content: blumePackageJson(toPackageName(basename5(root)), extraDepsFor(answers.sources)),
16613
16788
  path: join33(root, "package.json")
16614
16789
  },
16615
16790
  { content: buildConfig(answers), path: join33(root, "blume.config.ts") }
@@ -16624,14 +16799,14 @@ var writeFileSafe = async (file, log) => {
16624
16799
  log.info(`Skipped existing ${file.path}`);
16625
16800
  return false;
16626
16801
  }
16627
- await mkdir8(dirname11(file.path), { recursive: true });
16802
+ await mkdir8(dirname12(file.path), { recursive: true });
16628
16803
  await writeFile11(file.path, file.content, "utf-8");
16629
16804
  log.success(`Created ${file.path}`);
16630
16805
  return true;
16631
16806
  };
16632
16807
  var applyPlan = async (files, log) => {
16633
16808
  const created = await Promise.all(files.map((file) => writeFileSafe(file, log)));
16634
- const createdPackage = files.some((file, index) => created[index] && basename4(file.path) === "package.json");
16809
+ const createdPackage = files.some((file, index) => created[index] && basename5(file.path) === "package.json");
16635
16810
  return { createdPackage };
16636
16811
  };
16637
16812
  var envVarsFor = (sources) => [
@@ -16718,10 +16893,10 @@ The blume package remains importable.`);
16718
16893
  // src/cli/commands/init.ts
16719
16894
  import * as clack from "@clack/prompts";
16720
16895
  import { defineCommand as defineCommand8 } from "citty";
16721
- import { resolve as resolve9 } from "pathe";
16896
+ import { resolve as resolve10 } from "pathe";
16722
16897
 
16723
16898
  // src/cli/init/questions.ts
16724
- import { basename as basename5, resolve as resolve8 } from "pathe";
16899
+ import { basename as basename6, resolve as resolve9 } from "pathe";
16725
16900
  var cancelled = (value) => typeof value === "symbol";
16726
16901
  var collectAnswers = async (prompter, flags, defaults) => {
16727
16902
  const directory = flags.directory ?? await prompter.text({
@@ -16732,9 +16907,9 @@ var collectAnswers = async (prompter, flags, defaults) => {
16732
16907
  if (cancelled(directory)) {
16733
16908
  return null;
16734
16909
  }
16735
- const root = resolve8(defaults.cwd, directory);
16910
+ const root = resolve9(defaults.cwd, directory);
16736
16911
  const title = await prompter.text({
16737
- initialValue: titleize(basename5(root)),
16912
+ initialValue: titleize(basename6(root)),
16738
16913
  message: "What's your docs site called?",
16739
16914
  validate: (value) => value?.trim() ? undefined : "Give your docs site a name."
16740
16915
  });
@@ -16901,7 +17076,7 @@ var initCommand = defineCommand8({
16901
17076
  title: "My Docs"
16902
17077
  };
16903
17078
  }
16904
- const root = resolve9(cwd, answers.directory);
17079
+ const root = resolve10(cwd, answers.directory);
16905
17080
  if (validateContentDir(root, answers.contentDir) !== undefined) {
16906
17081
  logger.error(`Invalid --content-dir "${answers.contentDir}" (must be a path inside the project).`);
16907
17082
  process.exit(1);
@@ -17008,7 +17183,7 @@ import { join as join37 } from "pathe";
17008
17183
 
17009
17184
  // src/core/links.ts
17010
17185
  import { existsSync as existsSync21 } from "node:fs";
17011
- import { basename as basename6, join as join36 } from "pathe";
17186
+ import { basename as basename7, join as join36 } from "pathe";
17012
17187
  var HTTP = /^https?:\/\//iu;
17013
17188
  var PROTOCOL_RELATIVE = /^\/\//u;
17014
17189
  var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
@@ -17023,7 +17198,7 @@ var DOC_EXT = /\.(?:md|mdx)$/iu;
17023
17198
  var FILE_EXT = /\.[a-z0-9]+$/iu;
17024
17199
  var assetIsPresent = (resolved, ctx) => ctx.publicDir !== null && existsSync21(join36(ctx.publicDir, resolved));
17025
17200
  var NUMERIC_PREFIX3 = /^\d+[-_.]/u;
17026
- var isIndexPage = (page2) => /^index\.(?:md|mdx)$/iu.test(basename6(page2.navPath).replace(NUMERIC_PREFIX3, ""));
17201
+ var isIndexPage = (page2) => /^index\.(?:md|mdx)$/iu.test(basename7(page2.navPath).replace(NUMERIC_PREFIX3, ""));
17027
17202
  var applyRelativePart = (segments, part) => {
17028
17203
  if (part === "" || part === ".") {
17029
17204
  return;
@@ -17299,5 +17474,5 @@ process.on("unhandledRejection", (error) => {
17299
17474
  });
17300
17475
  runMain(main);
17301
17476
 
17302
- //# debugId=7D02660AA9B8797064756E2164756E21
17477
+ //# debugId=5D342B2E16EE6E0464756E2164756E21
17303
17478
  //# sourceMappingURL=index.js.map