blume 1.6.2 → 1.6.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 (79) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cli/index.js +307 -68
  3. package/dist/cli/index.js.map +28 -23
  4. package/dist/types/ai/component-markdown.d.ts +14 -0
  5. package/docs/01-quickstart.mdx +2 -2
  6. package/docs/02-deployment.mdx +5 -5
  7. package/docs/{07-faq.mdx → 08-faq.mdx} +7 -7
  8. package/docs/advanced/blog.mdx +3 -3
  9. package/docs/advanced/changelog.mdx +2 -2
  10. package/docs/advanced/custom-pages.mdx +4 -4
  11. package/docs/advanced/meta.ts +1 -1
  12. package/docs/configuration/ask-ai.mdx +179 -0
  13. package/docs/configuration/index.mdx +8 -7
  14. package/docs/configuration/meta.ts +1 -2
  15. package/docs/configuration/search.mdx +1 -1
  16. package/docs/configuration/theming.mdx +1 -1
  17. package/docs/content/components.mdx +1 -1
  18. package/docs/content/i18n.mdx +7 -1
  19. package/docs/content/index.mdx +1 -1
  20. package/docs/content/navigation.mdx +2 -2
  21. package/docs/content/syntax.mdx +1 -1
  22. package/docs/discoverability/agent-discovery.mdx +196 -0
  23. package/docs/discoverability/index.mdx +48 -0
  24. package/docs/discoverability/json-api.mdx +58 -0
  25. package/docs/discoverability/llms-txt.mdx +68 -0
  26. package/docs/discoverability/markdown.mdx +76 -0
  27. package/docs/discoverability/mcp.mdx +64 -0
  28. package/docs/discoverability/meta.ts +18 -0
  29. package/docs/discoverability/metadata.mdx +82 -0
  30. package/docs/discoverability/open-graph.mdx +113 -0
  31. package/docs/discoverability/rss.mdx +24 -0
  32. package/docs/discoverability/sitemap-and-robots.mdx +95 -0
  33. package/docs/discoverability/structured-data.mdx +51 -0
  34. package/docs/index.mdx +5 -5
  35. package/docs/reference/eval.mdx +1 -1
  36. package/docs/reference/meta.ts +1 -1
  37. package/docs/reference/translate.mdx +1 -0
  38. package/package.json +18 -18
  39. package/src/ai/component-markdown.ts +17 -2
  40. package/src/ai/llms.ts +3 -10
  41. package/src/ai/markdown.ts +3 -10
  42. package/src/ai/openapi-components.ts +123 -0
  43. package/src/ai/serializers.ts +24 -0
  44. package/src/astro/templates.ts +42 -12
  45. package/src/audit/checks/links.ts +1 -8
  46. package/src/audit/checks/llms.ts +5 -4
  47. package/src/audit/redirects.ts +4 -3
  48. package/src/audit/run.ts +6 -8
  49. package/src/audit/url.ts +33 -0
  50. package/src/cli/commands/validate.ts +1 -0
  51. package/src/components/content/Component.astro +65 -68
  52. package/src/components/content/Tabs.astro +24 -9
  53. package/src/components/content/example-pane.ts +6 -0
  54. package/src/components/layout/LocaleLinks.astro +42 -0
  55. package/src/components/layout/PageLayout.astro +5 -3
  56. package/src/components/layout/ReferenceLayout.astro +5 -0
  57. package/src/components/layout/RootLayout.astro +110 -39
  58. package/src/components/layout/search-locale.ts +13 -0
  59. package/src/components/openapi/ApiOverview.astro +7 -39
  60. package/src/components/openapi/ApiTagOperations.astro +2 -1
  61. package/src/components/openapi/AsyncApiOperation.astro +3 -2
  62. package/src/components/openapi/GraphqlOperation.astro +3 -2
  63. package/src/components/openapi/Operation.astro +3 -2
  64. package/src/core/i18n.ts +13 -2
  65. package/src/core/links.ts +33 -1
  66. package/src/core/locale-links.ts +163 -0
  67. package/src/core/sources/normalize.ts +57 -7
  68. package/src/markdown/package-commands.ts +27 -3
  69. package/src/openapi/graphql.ts +29 -0
  70. package/src/openapi/model.ts +69 -0
  71. package/src/openapi/render-mdx.ts +3 -2
  72. package/src/openapi/signature.ts +18 -0
  73. package/src/search/documents.ts +4 -9
  74. package/src/theme/code-block-padding.ts +0 -8
  75. package/src/theme/entry.ts +33 -27
  76. package/src/translate/anchors.ts +91 -0
  77. package/src/translate/validate.ts +8 -3
  78. package/docs/configuration/ai.mdx +0 -613
  79. package/docs/configuration/seo.mdx +0 -364
package/dist/cli/index.js CHANGED
@@ -1619,6 +1619,11 @@ var pageSite = (context, page, key) => {
1619
1619
  };
1620
1620
 
1621
1621
  // src/audit/url.ts
1622
+ var serverRoutes = (context) => {
1623
+ const mcp = context.project.config.ai?.mcp;
1624
+ return mcp?.enabled ? [normalizePath(mcp.route)] : [];
1625
+ };
1626
+ var isServed = (context, path) => context.byUrl.has(path) || context.files.has(path) || context.files.has(`${path}/index.html`) || serverRoutes(context).includes(path);
1622
1627
  var NON_HTTP_SCHEME = /^(?!https?:)[a-z][a-z0-9+.-]*:/iu;
1623
1628
  var decodePath = (path) => {
1624
1629
  try {
@@ -2349,7 +2354,6 @@ var buildGraph = (pages, origin, deployBase = "") => {
2349
2354
  var orphanPages = (pages, graph, homeUrl = "/") => pages.filter((page) => page.indexable && page.url !== "/" && page.url !== homeUrl && (graph.contentIn.get(page.url)?.size ?? 0) === 0);
2350
2355
 
2351
2356
  // src/audit/checks/links.ts
2352
- var isServed = (context, path) => context.byUrl.has(path) || context.files.has(path) || context.files.has(`${path}/index.html`);
2353
2357
  var MAGIC_FRAGMENTS = new Set(["", "top"]);
2354
2358
  var anchorResolves = (target, fragment) => {
2355
2359
  if (MAGIC_FRAGMENTS.has(fragment)) {
@@ -2500,7 +2504,7 @@ var llmsChecks = {
2500
2504
  continue;
2501
2505
  }
2502
2506
  listed.add(path);
2503
- if (!context.byUrl.has(path) && !context.files.has(path)) {
2507
+ if (!isServed(context, path)) {
2504
2508
  found.push(finding("BLUME_AUDIT_LLMS_TXT_STALE_ENTRY", { file: llms.file, line: entry.line, url: entry.url }, `llms.txt lists ${path}, which the build does not serve.`));
2505
2509
  }
2506
2510
  }
@@ -4915,6 +4919,7 @@ import { getEntry, render } from "astro:content";
4915
4919
  import type { CollectionKey } from "astro:content";
4916
4920
  import RootLayout from "blume/components/layout/RootLayout.astro";
4917
4921
  import { withBase } from "blume/components/islands/base-path.ts";
4922
+ import { stripBasePath, withBasePath } from "blume/core/base-path.ts";
4918
4923
  import { resolveSlot } from "blume/components/layout/overrides.ts";
4919
4924
  import Accordion from "blume/components/content/Accordion.astro";
4920
4925
  import AccordionItem from "blume/components/content/AccordionItem.astro";
@@ -4951,6 +4956,7 @@ import TypeTable from "blume/components/content/TypeTable.astro";
4951
4956
  import Visibility from "blume/components/content/Visibility.astro";
4952
4957
  import YouTube from "blume/components/content/YouTube.astro";
4953
4958
  import Icon from "blume/components/Icon.astro";
4959
+ import LocaleLinks from "blume/components/layout/LocaleLinks.astro";
4954
4960
  import ApiOverview from "blume/components/openapi/ApiOverview.astro";
4955
4961
  import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
4956
4962
  import Operation from "blume/components/openapi/Operation.astro";
@@ -5138,7 +5144,22 @@ const localeAlternates =
5138
5144
  const defaultAlt = i18n ? (alternates ?? []).find((alt) => alt.locale === i18n.defaultLocale) : null;
5139
5145
  const xDefault = defaultAlt && base ? absolute(defaultAlt.path) : null;
5140
5146
 
5141
- const logicalRoute = i18n ? stripLocale(route, locale) : route;
5147
+ // \`route\` arrives with \`basePath\` already applied, so the locale segment it
5148
+ // carries sits *after* the base (\`/docs/ja/guide\`), while \`localePrefix\` is
5149
+ // base-less (\`/ja\`). Stripping and re-adding a locale therefore happen in
5150
+ // base-less space, with the base re-applied at the end — the same
5151
+ // \`withBasePath(basePath, localizeRoute(...))\` composition the manifest uses to
5152
+ // build every real route. Done in based space, \`stripLocale\` matches nothing
5153
+ // and \`localizeRoute\` prepends a second prefix (\`/ja/docs/ja/guide\`). Only a
5154
+ // switcher entry for a locale with no real translation reaches this fallback:
5155
+ // a partially translated hand-written page for its missing locales, or a
5156
+ // generated reference (which has no \`alternates\` at all) for every locale
5157
+ // but its own.
5158
+ const mountLocalized = (logical: string, codeArg: string) =>
5159
+ withBasePath(data.config.basePath, localizeRoute(logical, codeArg));
5160
+ const logicalRoute = i18n
5161
+ ? stripLocale(stripBasePath(data.config.basePath, route), locale)
5162
+ : route;
5142
5163
  const localeSwitch = i18n
5143
5164
  ? i18n.locales.map((l) => {
5144
5165
  const alt = (alternates ?? []).find((x) => x.locale === l.code);
@@ -5146,7 +5167,7 @@ const localeSwitch = i18n
5146
5167
  code: l.code,
5147
5168
  current: l.code === locale,
5148
5169
  dir: l.dir,
5149
- href: alt ? alt.path : localizeRoute(logicalRoute, l.code),
5170
+ href: alt ? alt.path : mountLocalized(logicalRoute, l.code),
5150
5171
  label: l.label,
5151
5172
  untranslated: !alt,
5152
5173
  };
@@ -5160,12 +5181,9 @@ const localeSwitch = i18n
5160
5181
  // (manifest \`versionAlternates\` paths arrive with the base already applied).
5161
5182
  const versionRootFor = (id: string) => {
5162
5183
  const logical = id ? \`/\${id}\` : "/";
5163
- const localized = i18n ? localizeRoute(logical, locale) : logical;
5164
- const mount = data.config.basePath;
5165
- if (!mount) {
5166
- return localized;
5167
- }
5168
- return localized === "/" ? mount : \`\${mount}\${localized}\`;
5184
+ return i18n
5185
+ ? mountLocalized(logical, locale)
5186
+ : withBasePath(data.config.basePath, logical);
5169
5187
  };
5170
5188
  const samePageSwitch = versionsConfig
5171
5189
  ? versionsConfig.switcher.redirect === "same-page"
@@ -5274,7 +5292,9 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
5274
5292
  >
5275
5293
  <h1>{title}</h1>
5276
5294
  {frontmatter.description && <p class="text-lg text-muted-foreground">{frontmatter.description}</p>}
5277
- <Content components={components} />
5295
+ <LocaleLinks locale={locale}>
5296
+ <Content components={components} />
5297
+ </LocaleLinks>
5278
5298
  </LayoutComponent>
5279
5299
  `;
5280
5300
  };
@@ -5956,7 +5976,7 @@ const Example = entry.Component;
5956
5976
  const bodyStyle = getComputedStyle(document.body);
5957
5977
  const paddingPx =
5958
5978
  parseFloat(bodyStyle.paddingTop) + parseFloat(bodyStyle.paddingBottom);
5959
- new ResizeObserver(() => {
5979
+ const report = () => {
5960
5980
  window.parent.postMessage(
5961
5981
  {
5962
5982
  height:
@@ -5965,7 +5985,19 @@ const Example = entry.Component;
5965
5985
  },
5966
5986
  window.location.origin
5967
5987
  );
5968
- }).observe(wrapper);
5988
+ };
5989
+ new ResizeObserver(report).observe(wrapper);
5990
+ // The parent asks for a fresh report when its listener comes up, in
5991
+ // case the first one above was posted before anyone was listening.
5992
+ window.addEventListener("message", (event) => {
5993
+ if (
5994
+ event.source === window.parent &&
5995
+ event.origin === window.location.origin &&
5996
+ event.data?.type === "blume:example-height-request"
5997
+ ) {
5998
+ report();
5999
+ }
6000
+ });
5969
6001
  })();
5970
6002
  </script>
5971
6003
  </body>
@@ -6341,7 +6373,7 @@ var pathOnly = (value) => {
6341
6373
  const cut = value.search(/[?#]/u);
6342
6374
  return cut === -1 ? value : value.slice(0, cut);
6343
6375
  };
6344
- var resolveRedirects = (redirects, pageUrls) => {
6376
+ var resolveRedirects = (redirects, served) => {
6345
6377
  const byFrom = new Map;
6346
6378
  for (const redirect of redirects) {
6347
6379
  byFrom.set(normalizePath(redirect.from), redirect);
@@ -6375,7 +6407,7 @@ var resolveRedirects = (redirects, pageUrls) => {
6375
6407
  }
6376
6408
  const destination = chain.at(-1) ?? from;
6377
6409
  const external = /^https?:\/\//iu.test(destination);
6378
- if (!(external || pageUrls.has(destination))) {
6410
+ if (!(external || served(destination))) {
6379
6411
  return { ...redirect, chain, outcome: "broken" };
6380
6412
  }
6381
6413
  return {
@@ -6463,7 +6495,7 @@ var runAudit = async (options) => {
6463
6495
  ...redirect,
6464
6496
  from: withBasePath(basePath, redirect.from),
6465
6497
  to: withBasePath(basePath, redirect.to)
6466
- })), new Set([...byUrl.keys(), ...crawl.files.keys()].map((path) => normalizePath(path)))),
6498
+ })), (path) => isServed({ byUrl, files: crawl.files, project }, path)),
6467
6499
  robots: crawl.robots,
6468
6500
  sitemap: crawl.sitemap,
6469
6501
  sources: await readSources(crawl.pages),
@@ -12386,13 +12418,20 @@ var toHeading = (depth, raw, slugger, isRefDefined) => {
12386
12418
  const markers = parseHeadingMarkers(unescaped, isRefDefined);
12387
12419
  const text = markers.text.trim();
12388
12420
  if (text === "" && (markers.id !== undefined || markers.toc !== undefined)) {
12389
- return { depth, slug: slugger.slug(unescaped), text: unescaped };
12421
+ return {
12422
+ heading: { depth, slug: slugger.slug(unescaped), text: unescaped },
12423
+ pinned: false
12424
+ };
12390
12425
  }
12391
12426
  if (markers.id !== undefined) {
12392
12427
  occupySlug(slugger, markers.id);
12393
- return { depth, slug: markers.id, text };
12428
+ return { heading: { depth, slug: markers.id, text }, pinned: true };
12394
12429
  }
12395
- return { depth, slug: slugger.slug(text), text };
12430
+ return { heading: { depth, slug: slugger.slug(text), text }, pinned: false };
12431
+ };
12432
+ var pushHeading = (headings, state, scanned, line) => {
12433
+ headings.push(scanned.heading);
12434
+ state.sites.push({ line, pinned: scanned.pinned });
12396
12435
  };
12397
12436
  var noteCurlyMarker = (text, line, state) => {
12398
12437
  const id2 = text.match(BARE_CURLY_MARKER)?.groups?.id;
@@ -12406,7 +12445,7 @@ var scanContentLine = (line, state, slugger, headings, isRefDefined) => {
12406
12445
  if (atx?.groups) {
12407
12446
  const depth = atx.groups.hashes?.length ?? 1;
12408
12447
  const text = (atx.groups.text ?? "").trim();
12409
- headings.push(toHeading(depth, text, slugger, isRefDefined));
12448
+ pushHeading(headings, state, toHeading(depth, text, slugger, isRefDefined), state.line);
12410
12449
  noteCurlyMarker(text, state.line, state);
12411
12450
  state.paragraph = [];
12412
12451
  return;
@@ -12415,7 +12454,7 @@ var scanContentLine = (line, state, slugger, headings, isRefDefined) => {
12415
12454
  if (setext?.groups && state.paragraph.length > 0) {
12416
12455
  const depth = setext.groups.marker?.startsWith("=") ? 1 : 2;
12417
12456
  const text = state.paragraph.join(" ").trim();
12418
- headings.push(toHeading(depth, text, slugger, isRefDefined));
12457
+ pushHeading(headings, state, toHeading(depth, text, slugger, isRefDefined), state.line - 1);
12419
12458
  noteCurlyMarker(text, state.paragraphStart, state);
12420
12459
  state.paragraph = [];
12421
12460
  return;
@@ -12467,7 +12506,8 @@ var scanBody = (body) => {
12467
12506
  paragraph: [],
12468
12507
  paragraphStart: 0,
12469
12508
  promptDepth: 0,
12470
- promptTag: false
12509
+ promptTag: false,
12510
+ sites: []
12471
12511
  };
12472
12512
  const { lines, offset } = linesWithoutFrontMatter(body);
12473
12513
  const definedLabels = refDefinitionLabels(lines);
@@ -12485,7 +12525,12 @@ var scanBody = (body) => {
12485
12525
  anchors.add(id2);
12486
12526
  }
12487
12527
  }
12488
- return { anchors: [...anchors], curlyMarkers: state.curlyMarkers, headings };
12528
+ return {
12529
+ anchors: [...anchors],
12530
+ curlyMarkers: state.curlyMarkers,
12531
+ headings,
12532
+ sites: state.sites
12533
+ };
12489
12534
  };
12490
12535
  var extractHeadings = (body) => scanBody(body).headings;
12491
12536
  var MD_LINK = /\[(?<label>(?:[^[\]]|\[[^\]]*\])*)\]\((?<target>(?:[^()\s]|\([^()\s]*\))+)(?<title>\s+"[^"]*")?\)/gu;
@@ -13415,6 +13460,33 @@ var operationKey = (method, path, operationId) => {
13415
13460
  const fromId = operationId ? slugify(splitIdentifier(operationId)) : "";
13416
13461
  return fromId || slugify(`${method}-${path}`);
13417
13462
  };
13463
+ var specOf = (specs, source) => Object.hasOwn(specs, source) ? specs[source] : undefined;
13464
+ var operationOf = (spec, id2) => Object.hasOwn(spec.operations, id2) ? spec.operations[id2] : undefined;
13465
+ var specAddresses = (spec) => {
13466
+ const addresses = [];
13467
+ if (spec.kind === "graphql") {
13468
+ if (spec.endpoint) {
13469
+ addresses.push(spec.endpoint);
13470
+ }
13471
+ return { addresses, label: "Endpoint" };
13472
+ }
13473
+ if (spec.kind === "asyncapi") {
13474
+ const servers = spec.document.servers ?? {};
13475
+ for (const server of Object.values(servers)) {
13476
+ if (server?.host) {
13477
+ addresses.push(`${server.protocol ? `${server.protocol}://` : ""}${server.host}${server.pathname ?? ""}`);
13478
+ }
13479
+ }
13480
+ return { addresses, label: "Servers" };
13481
+ }
13482
+ const declared = spec.document.servers;
13483
+ for (const server of Array.isArray(declared) ? declared : []) {
13484
+ if (server.url) {
13485
+ addresses.push(server.url);
13486
+ }
13487
+ }
13488
+ return { addresses, label: "Base URL" };
13489
+ };
13418
13490
  var isOperation = (value) => typeof value === "object" && value !== null;
13419
13491
  var hasTagName = (tag) => typeof tag.name === "string";
13420
13492
  var tagSlugger = () => {
@@ -13673,6 +13745,19 @@ var GRAPHQL_GROUP_LABELS = {
13673
13745
  union: "Unions"
13674
13746
  };
13675
13747
  var isGraphqlOperationKind = (method) => GRAPHQL_OPERATION_KINDS.some((kind) => kind === method);
13748
+ var GRAPHQL_TYPE_KEYWORDS = {
13749
+ enum: "enum",
13750
+ input: "input",
13751
+ interface: "interface",
13752
+ object: "type",
13753
+ scalar: "scalar",
13754
+ union: "union"
13755
+ };
13756
+ var graphqlSignature = (operation) => {
13757
+ const member = operation.method;
13758
+ const keyword = isGraphqlOperationKind(member) ? member : GRAPHQL_TYPE_KEYWORDS[member];
13759
+ return `${keyword} ${operation.path}`;
13760
+ };
13676
13761
  var byName = (a, b) => a.name < b.name ? -1 : 1;
13677
13762
  var extractGraphqlOperations = (document, baseRoute) => {
13678
13763
  const collector = operationCollector(baseRoute, new Map);
@@ -14115,6 +14200,9 @@ var columnsPrefix = (text, max) => {
14115
14200
  return text.slice(0, end);
14116
14201
  };
14117
14202
 
14203
+ // src/openapi/signature.ts
14204
+ var operationSignature = (spec, operation) => spec.kind === "graphql" ? graphqlSignature(operation) : `${operation.method.toUpperCase()} ${operation.path}`;
14205
+
14118
14206
  // src/openapi/render-mdx.ts
14119
14207
  var MDX_UNSAFE = /[<{}]/gu;
14120
14208
  var ENTITIES = new Map([
@@ -14207,7 +14295,7 @@ ${component}` : component;
14207
14295
  var operationMdx = (spec, operation, reference) => {
14208
14296
  const method = operation.method.toUpperCase();
14209
14297
  const graphql = spec.kind === "graphql";
14210
- const fallbackTitle = graphql ? operation.path : `${method} ${operation.path}`;
14298
+ const fallbackTitle = graphql ? operation.path : operationSignature(spec, operation);
14211
14299
  const title = operation.summary || fallbackTitle;
14212
14300
  const description = operation.description.trim() === operation.summary.trim() ? "" : operation.description;
14213
14301
  const flags = {};
@@ -16989,6 +17077,13 @@ var youtube = ({ props }) => {
16989
17077
  const title = isString4(props.title) && props.title !== "" ? props.title : "Watch on YouTube";
16990
17078
  return `[${title}](https://www.youtube.com/watch?v=${videoId}${start})`;
16991
17079
  };
17080
+ var inlineCode = (text) => {
17081
+ const runs = text.match(/`+/gu);
17082
+ const longest = runs ? Math.max(...runs.map((run) => run.length)) : 0;
17083
+ const delimiter = "`".repeat(longest + 1);
17084
+ const padded = text.startsWith("`") || text.endsWith("`") ? ` ${text} ` : text;
17085
+ return `${delimiter}${padded}${delimiter}`;
17086
+ };
16992
17087
  var fencedBlock = (lang, code) => {
16993
17088
  const trimmed = code.replace(/(?<!\n)\n+$/u, "");
16994
17089
  const runs = trimmed.match(/`+/gu);
@@ -17090,6 +17185,67 @@ var downlevelComponents = (source, components, frontmatter) => {
17090
17185
  return splices.length > 0 ? applySplices(source, splices) : source;
17091
17186
  };
17092
17187
 
17188
+ // src/ai/openapi-components.ts
17189
+ var inlineText = (text) => text.trim().replaceAll(/\s+/gu, " ").replaceAll(/[\\`*_[\]<>~]/gu, String.raw`\$&`);
17190
+ var listItem2 = (signature, operation) => {
17191
+ const tail = [
17192
+ inlineText(operation.summary),
17193
+ operation.deprecated ? "Deprecated." : ""
17194
+ ].filter(Boolean).join(" ");
17195
+ return `- [${inlineCode(signature)}](${linkDestination(operation.route)})${tail ? ` — ${tail}` : ""}`;
17196
+ };
17197
+ var openapiComponentSerializers = (specs) => {
17198
+ const spec = (source) => isString4(source) ? specOf(specs, source) : undefined;
17199
+ return {
17200
+ ApiOverview: ({ props }) => {
17201
+ const data = spec(props.source);
17202
+ if (!data) {
17203
+ return null;
17204
+ }
17205
+ const { addresses, label } = specAddresses(data);
17206
+ const lines = [
17207
+ data.version ? `Version ${inlineText(data.version)}` : "",
17208
+ addresses.length > 0 ? `${label}: ${addresses.map(inlineCode).join(", ")}` : ""
17209
+ ].filter(Boolean);
17210
+ return lines.length > 0 ? lines.join(`
17211
+
17212
+ `) : null;
17213
+ },
17214
+ ApiTagOperations: ({ props }) => {
17215
+ const { tag } = props;
17216
+ const data = spec(props.source);
17217
+ if (!(data && isString4(tag))) {
17218
+ return null;
17219
+ }
17220
+ const items = Object.values(data.operations).filter((operation) => operation.tagSlug === tag).map((operation) => listItem2(operationSignature(data, operation), operation));
17221
+ return items.length > 0 ? items.join(`
17222
+ `) : null;
17223
+ },
17224
+ Operation: ({ props }) => {
17225
+ const { id: id2 } = props;
17226
+ const data = spec(props.source);
17227
+ if (!(data && isString4(id2))) {
17228
+ return null;
17229
+ }
17230
+ const operation = operationOf(data, id2);
17231
+ if (!operation) {
17232
+ return null;
17233
+ }
17234
+ const signature = inlineCode(operationSignature(data, operation));
17235
+ return operation.deprecated ? `${signature}
17236
+
17237
+ **Deprecated.**` : signature;
17238
+ }
17239
+ };
17240
+ };
17241
+
17242
+ // src/ai/serializers.ts
17243
+ var projectComponentSerializers = (project) => ({
17244
+ ...exampleComponentSerializers(project.examples ?? {}),
17245
+ ...openapiComponentSerializers(project.sources.find(isOpenApiSource)?.openApiData() ?? {}),
17246
+ ...project.config.ai.markdownComponents
17247
+ });
17248
+
17093
17249
  // src/ai/skills.ts
17094
17250
  import { createHash as createHash3 } from "node:crypto";
17095
17251
  import { readdir as readdir2, readFile as readFile12, stat as stat3 } from "node:fs/promises";
@@ -17405,10 +17561,7 @@ ${details}` : header;
17405
17561
  var buildFull = async (project) => {
17406
17562
  const { config } = project;
17407
17563
  const pages = eligiblePages(project, { versions: "current" }).toSorted((a, b) => a.route.localeCompare(b.route));
17408
- const components = {
17409
- ...exampleComponentSerializers(project.examples ?? {}),
17410
- ...config.ai.markdownComponents
17411
- };
17564
+ const components = projectComponentSerializers(project);
17412
17565
  const sections = await Promise.all(pages.map(async (page) => {
17413
17566
  let raw = await readExpandedEntryText(project, page);
17414
17567
  if (page.sourcePath) {
@@ -17449,10 +17602,7 @@ var agentMarkdown = (entry) => entry.md ?? entry.mdx;
17449
17602
  var markdownTokenCount = (text) => Math.ceil(text.length / 4);
17450
17603
  var buildRawMarkdown = async (project) => {
17451
17604
  const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));
17452
- const components = {
17453
- ...exampleComponentSerializers(project.examples ?? {}),
17454
- ...project.config.ai.markdownComponents
17455
- };
17605
+ const components = projectComponentSerializers(project);
17456
17606
  const readRoute = async (route) => {
17457
17607
  const page = pageById.get(route.id);
17458
17608
  if (page) {
@@ -18594,10 +18744,7 @@ var buildSearchDocuments = async (project, options) => {
18594
18744
  const page = pageById.get(route.id);
18595
18745
  return page ? contentIndexable(page, project.config) : false;
18596
18746
  });
18597
- const components = {
18598
- ...exampleComponentSerializers(project.examples ?? {}),
18599
- ...project.config.ai.markdownComponents
18600
- };
18747
+ const components = projectComponentSerializers(project);
18601
18748
  return await Promise.all(indexable.map(async (route) => {
18602
18749
  const page = pageById.get(route.id);
18603
18750
  const body = await pageBody(project, page, options, components);
@@ -20288,9 +20435,9 @@ var resolveSearchPopular = (popular, basePath) => popular.map(({ href, icon, lab
20288
20435
 
20289
20436
  // src/theme/code-block-padding.ts
20290
20437
  var CODE_PADDING_BLOCK_REM = 1;
20291
- var FLUSH_CODE_PADDING_TOP_REM = 2.5;
20292
20438
 
20293
20439
  // src/theme/entry.ts
20440
+ var CODE_SCROLL_INSET_REM = 0.375;
20294
20441
  var DARK_VARIANT = `/* Dark mode is driven by data-theme on the <html> element. */
20295
20442
  @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));`;
20296
20443
  var COLOR_SCHEME_DEFAULTS = `:root {
@@ -20748,8 +20895,9 @@ blume-diff {
20748
20895
  max-height: 24rem;
20749
20896
  overflow: auto;
20750
20897
  /* The small bottom inset keeps the horizontal thumb off the last line's
20751
- descenders now that scrollbars are visible. */
20752
- padding: 0 1.25rem 0.375rem;
20898
+ descenders now that scrollbars are visible. The pre gives up the same
20899
+ amount below (next rule), so the inset adds no height to the block. */
20900
+ padding: 0 1.25rem ${CODE_SCROLL_INSET_REM}rem;
20753
20901
  /* Thin theme-colored scrollbars, matching the sidebar treatment, so a
20754
20902
  height-capped block reads as scrollable instead of simply ending.
20755
20903
  Safari before 18.2 supports neither property and falls back to the
@@ -20758,6 +20906,14 @@ blume-diff {
20758
20906
  scrollbar-width: thin;
20759
20907
  }
20760
20908
 
20909
+ /* The scroller's bottom inset comes out of the pre's own block padding: the
20910
+ text still sits one full inset above the frame's bottom edge, the thumb sits
20911
+ in the gap, and a one-line command is no taller than it was before the
20912
+ scroller existed. */
20913
+ .prose :where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *):has(> code)) {
20914
+ padding-bottom: calc(${CODE_PADDING_BLOCK_REM}rem - ${CODE_SCROLL_INSET_REM}rem);
20915
+ }
20916
+
20761
20917
  /* The dark border token is too close to the page background to read as a
20762
20918
  scrollbar thumb; derive a brighter one from the muted foreground instead. */
20763
20919
  :root[data-theme="dark"]
@@ -20864,27 +21020,19 @@ blume-tabs pre[data-language],
20864
21020
  padding-top: ${CODE_PADDING_BLOCK_REM}rem;
20865
21021
  }
20866
21022
 
20867
- /* Room for the copy button. The docs layout injects it into every \`.prose pre\`
20868
- absolutely positioned against the pre's border box, 1.875rem tall, at
20869
- \`top-2.5\` in the flush contexts above and \`top-2\` elsewhere and it is
20870
- unconditional, so wherever no language bar exists to hold it, it painted
20871
- over the first line of code: any line long enough to reach the button's
20872
- strip (a curl invocation, an install command, an import path) went under
20873
- it. Keyed on the attribute the docs layout stamps on <body>, so the strip
20874
- only appears where the injector runs (PageLayout pages run none), and on
20875
- \`.astro-code\` so the playground's response pre created after the
20876
- injector ran, never given a button keeps its plain inset. The first
20877
- selector is the injector's own flush predicate; the second covers the
20878
- remaining bar-less Shiki case, an untitled <CodeBlock> in plain prose, from
20879
- first paint. The third covers a raw \`<pre><code>\` written in prose — no
20880
- \`.astro-code\`, no language bar, but the injector gives it a button all
20881
- the same — keyed on that injected button, which is exactly what separates
20882
- it from the playground's pre. API panel blocks match, but their own
20883
- !important padding wins and they hide the injected button. */
20884
- [data-blume-code-copy] :is(blume-tabs, .not-prose) pre.astro-code,
20885
- [data-blume-code-copy] .prose pre.astro-code:not([data-language]),
20886
- [data-blume-code-copy] .prose pre:not([data-language]):has(> [data-blume-copy]) {
20887
- padding-top: ${FLUSH_CODE_PADDING_TOP_REM}rem;
21023
+ /* Where no language bar holds the copy button a flush block in tabs or a
21024
+ not-prose component, a bar-less block in prose the docs layout still pins
21025
+ one over the block's top-right corner, so a first line long enough to reach
21026
+ it would end underneath. A code switcher (CodeGroup, ts2js) hosts the button
21027
+ in its tab strip instead and needs nothing here; for every block that keeps
21028
+ an overlay button, give the scrolling code element enough end padding that
21029
+ the line's tail clears the button at the end of its scroll. Keyed on the
21030
+ injected button itself (which the playground's client-created response pre
21031
+ never gets), and \`.prose\`-scoped to outrank the base \`:where(pre code)\`
21032
+ inset. Not a vertical insetthat reserved a strip above every one-line
21033
+ command and read as a rendering bug. */
21034
+ .prose :is(blume-tabs pre, .not-prose pre, pre:not([data-language])):not(.twoslash, .twoslash pre, blume-panel-tabs *):has(> [data-blume-copy]) > code {
21035
+ padding-inline-end: 3.5rem;
20888
21036
  }
20889
21037
 
20890
21038
  blume-tabs pre[data-language]::before,
@@ -26160,6 +26308,57 @@ Write the titles in this style: ${target.style}`}
26160
26308
 
26161
26309
  ${JSON.stringify(titles, null, 2)}`;
26162
26310
 
26311
+ // src/translate/anchors.ts
26312
+ var ATX_CLOSE = /\s+#+$/u;
26313
+ var structureOf = (headings) => headings.map((heading) => heading.depth).join(",");
26314
+ var pinLine = (line, id2) => {
26315
+ const eol = line.endsWith("\r") ? "\r" : "";
26316
+ const body = line.slice(0, line.length - eol.length).trimEnd();
26317
+ const close = body.match(ATX_CLOSE)?.[0] ?? "";
26318
+ const head = body.slice(0, body.length - close.length);
26319
+ return `${head} [#${id2}]${close}${eol}`;
26320
+ };
26321
+ var pinHeadingAnchors = (sourceText, translatedText) => {
26322
+ const source = scanBody(sourceText);
26323
+ const translated = scanBody(translatedText);
26324
+ if (structureOf(source.headings) !== structureOf(translated.headings)) {
26325
+ return {
26326
+ pinned: 0,
26327
+ skipped: `heading structure differs (source has ${source.headings.length} headings, translation has ${translated.headings.length})`,
26328
+ text: translatedText
26329
+ };
26330
+ }
26331
+ const lines = translatedText.split(`
26332
+ `);
26333
+ let pinned = 0;
26334
+ for (const [index, heading] of translated.headings.entries()) {
26335
+ const site = translated.sites[index];
26336
+ const sourceId = source.headings[index]?.slug;
26337
+ if (!site || site.pinned || sourceId === undefined || heading.slug === sourceId) {
26338
+ continue;
26339
+ }
26340
+ lines[site.line - 1] = pinLine(lines[site.line - 1] ?? "", sourceId);
26341
+ pinned += 1;
26342
+ }
26343
+ if (pinned === 0) {
26344
+ return { pinned, text: translatedText };
26345
+ }
26346
+ const text2 = lines.join(`
26347
+ `);
26348
+ const rendered = scanBody(text2).headings.map((heading) => heading.slug);
26349
+ const expected = source.headings.map((heading) => heading.slug);
26350
+ if (rendered.join(`
26351
+ `) !== expected.join(`
26352
+ `)) {
26353
+ return {
26354
+ pinned: 0,
26355
+ skipped: "pinned anchors did not reproduce the source heading ids",
26356
+ text: translatedText
26357
+ };
26358
+ }
26359
+ return { pinned, text: text2 };
26360
+ };
26361
+
26163
26362
  // src/translate/validate.ts
26164
26363
  var FRONTMATTER_OPEN = /^---\r?\n/u;
26165
26364
  var FENCE_LINE = /^\s*(?:```|~~~)/u;
@@ -26211,18 +26410,19 @@ var validateTranslation = (sourceText, agentText) => {
26211
26410
  } catch {
26212
26411
  return { ok: false, reason: "frontmatter does not parse as YAML" };
26213
26412
  }
26214
- const body = ensureTrailingNewline(parsed.content.replace(/^\r?\n/u, ""));
26215
- if (body.trim() === "") {
26413
+ const rawBody = ensureTrailingNewline(parsed.content.replace(/^\r?\n/u, ""));
26414
+ if (rawBody.trim() === "") {
26216
26415
  return { ok: false, reason: "translation has an empty body" };
26217
26416
  }
26218
26417
  const sourceFences = countFenceLines(source.content);
26219
- const candidateFences = countFenceLines(body);
26418
+ const candidateFences = countFenceLines(rawBody);
26220
26419
  if (sourceFences !== candidateFences) {
26221
26420
  return {
26222
26421
  ok: false,
26223
26422
  reason: `code fence count changed (source has ${sourceFences}, translation has ${candidateFences})`
26224
26423
  };
26225
26424
  }
26425
+ const body = pinHeadingAnchors(source.content, rawBody).text;
26226
26426
  if (!sourceHasFrontmatter) {
26227
26427
  return { ok: true, text: body };
26228
26428
  }
@@ -26793,10 +26993,38 @@ import { join as join50 } from "pathe";
26793
26993
  // src/core/links.ts
26794
26994
  import { existsSync as existsSync27 } from "node:fs";
26795
26995
  import { basename as basename8, dirname as dirname20, join as join49, relative as relative25, resolve as resolve16 } from "pathe";
26996
+
26997
+ // src/core/locale-links.ts
26998
+ var decodePercent = (value) => {
26999
+ try {
27000
+ return decodeURIComponent(value);
27001
+ } catch {
27002
+ return value;
27003
+ }
27004
+ };
27005
+ var hasLocalePrefix = (path, i18n) => i18n.locales.some((locale) => {
27006
+ const prefix = localePrefix(locale.code, i18n);
27007
+ return prefix !== "" && (path === prefix || path.startsWith(`${prefix}/`));
27008
+ });
27009
+ var localizeLinkPath = (path, options) => {
27010
+ const { basePath, i18n, locale, routes } = options;
27011
+ if (localePrefix(locale, i18n) === "") {
27012
+ return path;
27013
+ }
27014
+ const rest = normalizePath(stripBasePath(basePath, path));
27015
+ if (hasLocalePrefix(rest, i18n)) {
27016
+ return path;
27017
+ }
27018
+ const localized = withBasePath(basePath, localizeRoute(rest, locale, i18n));
27019
+ return routes.has(localized) || routes.has(decodePercent(localized)) ? localized : path;
27020
+ };
27021
+ var routeSets = new WeakMap;
27022
+
27023
+ // src/core/links.ts
26796
27024
  var HTTP = /^https?:\/\//iu;
26797
27025
  var PROTOCOL_RELATIVE = /^\/\//u;
26798
27026
  var SCHEME = /^[a-z][a-z0-9+.-]*:/iu;
26799
- var decodePercent = (value) => {
27027
+ var decodePercent2 = (value) => {
26800
27028
  try {
26801
27029
  return decodeURIComponent(value);
26802
27030
  } catch {
@@ -26865,8 +27093,17 @@ var checkAnchor = (route, fragment, site, ctx, via = "") => {
26865
27093
  severity: "warning"
26866
27094
  };
26867
27095
  };
27096
+ var servedRoute = (authored, page2, ctx) => ctx.i18n ? localizeLinkPath(authored, {
27097
+ basePath: ctx.basePath,
27098
+ i18n: ctx.i18n,
27099
+ locale: page2.locale,
27100
+ routes: {
27101
+ has: (route) => ctx.routes.has(route) || ctx.extraRoutes.has(route)
27102
+ }
27103
+ }) : authored;
26868
27104
  var checkPathLink = (resolved, fragment, page2, link, site, ctx, via = "") => {
26869
- const route = toRoute(withBasePath(ctx.basePath, resolved));
27105
+ const authoredRoute = toRoute(withBasePath(ctx.basePath, resolved));
27106
+ const route = servedRoute(authoredRoute, page2, ctx);
26870
27107
  if (ctx.routes.has(route)) {
26871
27108
  return fragment ? checkAnchor(route, fragment, site, ctx, via) : null;
26872
27109
  }
@@ -26950,13 +27187,13 @@ var classifyLink = (page2, link, ctx, onExternal) => {
26950
27187
  return null;
26951
27188
  }
26952
27189
  const hashIndex = target.indexOf("#");
26953
- const fragment = decodePercent(hashIndex === -1 ? "" : target.slice(hashIndex + 1));
27190
+ const fragment = decodePercent2(hashIndex === -1 ? "" : target.slice(hashIndex + 1));
26954
27191
  let rawPath = hashIndex === -1 ? target : target.slice(0, hashIndex);
26955
27192
  const queryIndex = rawPath.indexOf("?");
26956
27193
  if (queryIndex !== -1) {
26957
27194
  rawPath = rawPath.slice(0, queryIndex);
26958
27195
  }
26959
- rawPath = decodePercent(rawPath);
27196
+ rawPath = decodePercent2(rawPath);
26960
27197
  const via = rawPath.startsWith("/") ? "" : includedBySuffix(link, page2);
26961
27198
  if (rawPath === "") {
26962
27199
  return fragment ? checkAnchor(page2.route, fragment, site, ctx, via) : null;
@@ -26970,6 +27207,7 @@ var validateLinks = async (graph, options) => {
26970
27207
  anchors: buildAnchorIndex(graph.pages),
26971
27208
  basePath,
26972
27209
  extraRoutes: new Set((options.extraRoutes ?? []).map(toRoute)),
27210
+ i18n: options.i18n ?? null,
26973
27211
  publicDir: options.publicDir,
26974
27212
  redirects: new Set((options.redirects ?? []).map((redirect) => toRoute(withBasePath(basePath, redirect.from)))),
26975
27213
  routes: new Set(graph.routes.keys())
@@ -27059,6 +27297,7 @@ var validateCommand = defineCommand14({
27059
27297
  basePath: project.config.basePath,
27060
27298
  checkExternal: Boolean(args.external),
27061
27299
  extraRoutes,
27300
+ i18n: project.config.i18n,
27062
27301
  publicDir: existsSync28(publicDir) ? publicDir : null,
27063
27302
  redirects: project.config.redirects
27064
27303
  }));
@@ -27377,5 +27616,5 @@ process.on("unhandledRejection", (error) => {
27377
27616
  });
27378
27617
  runMain(main, { rawArgs: normalizeHostArgs(process.argv.slice(2)) });
27379
27618
 
27380
- //# debugId=9AB8E5F1D71CCEF764756E2164756E21
27619
+ //# debugId=4199DBFD57FB41A964756E2164756E21
27381
27620
  //# sourceMappingURL=index.js.map