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.
- package/CHANGELOG.md +25 -0
- package/dist/cli/index.js +307 -68
- package/dist/cli/index.js.map +28 -23
- package/dist/types/ai/component-markdown.d.ts +14 -0
- package/docs/01-quickstart.mdx +2 -2
- package/docs/02-deployment.mdx +5 -5
- package/docs/{07-faq.mdx → 08-faq.mdx} +7 -7
- package/docs/advanced/blog.mdx +3 -3
- package/docs/advanced/changelog.mdx +2 -2
- package/docs/advanced/custom-pages.mdx +4 -4
- package/docs/advanced/meta.ts +1 -1
- package/docs/configuration/ask-ai.mdx +179 -0
- package/docs/configuration/index.mdx +8 -7
- package/docs/configuration/meta.ts +1 -2
- package/docs/configuration/search.mdx +1 -1
- package/docs/configuration/theming.mdx +1 -1
- package/docs/content/components.mdx +1 -1
- package/docs/content/i18n.mdx +7 -1
- package/docs/content/index.mdx +1 -1
- package/docs/content/navigation.mdx +2 -2
- package/docs/content/syntax.mdx +1 -1
- package/docs/discoverability/agent-discovery.mdx +196 -0
- package/docs/discoverability/index.mdx +48 -0
- package/docs/discoverability/json-api.mdx +58 -0
- package/docs/discoverability/llms-txt.mdx +68 -0
- package/docs/discoverability/markdown.mdx +76 -0
- package/docs/discoverability/mcp.mdx +64 -0
- package/docs/discoverability/meta.ts +18 -0
- package/docs/discoverability/metadata.mdx +82 -0
- package/docs/discoverability/open-graph.mdx +113 -0
- package/docs/discoverability/rss.mdx +24 -0
- package/docs/discoverability/sitemap-and-robots.mdx +95 -0
- package/docs/discoverability/structured-data.mdx +51 -0
- package/docs/index.mdx +5 -5
- package/docs/reference/eval.mdx +1 -1
- package/docs/reference/meta.ts +1 -1
- package/docs/reference/translate.mdx +1 -0
- package/package.json +18 -18
- package/src/ai/component-markdown.ts +17 -2
- package/src/ai/llms.ts +3 -10
- package/src/ai/markdown.ts +3 -10
- package/src/ai/openapi-components.ts +123 -0
- package/src/ai/serializers.ts +24 -0
- package/src/astro/templates.ts +42 -12
- package/src/audit/checks/links.ts +1 -8
- package/src/audit/checks/llms.ts +5 -4
- package/src/audit/redirects.ts +4 -3
- package/src/audit/run.ts +6 -8
- package/src/audit/url.ts +33 -0
- package/src/cli/commands/validate.ts +1 -0
- package/src/components/content/Component.astro +65 -68
- package/src/components/content/Tabs.astro +24 -9
- package/src/components/content/example-pane.ts +6 -0
- package/src/components/layout/LocaleLinks.astro +42 -0
- package/src/components/layout/PageLayout.astro +5 -3
- package/src/components/layout/ReferenceLayout.astro +5 -0
- package/src/components/layout/RootLayout.astro +110 -39
- package/src/components/layout/search-locale.ts +13 -0
- package/src/components/openapi/ApiOverview.astro +7 -39
- package/src/components/openapi/ApiTagOperations.astro +2 -1
- package/src/components/openapi/AsyncApiOperation.astro +3 -2
- package/src/components/openapi/GraphqlOperation.astro +3 -2
- package/src/components/openapi/Operation.astro +3 -2
- package/src/core/i18n.ts +13 -2
- package/src/core/links.ts +33 -1
- package/src/core/locale-links.ts +163 -0
- package/src/core/sources/normalize.ts +57 -7
- package/src/markdown/package-commands.ts +27 -3
- package/src/openapi/graphql.ts +29 -0
- package/src/openapi/model.ts +69 -0
- package/src/openapi/render-mdx.ts +3 -2
- package/src/openapi/signature.ts +18 -0
- package/src/search/documents.ts +4 -9
- package/src/theme/code-block-padding.ts +0 -8
- package/src/theme/entry.ts +33 -27
- package/src/translate/anchors.ts +91 -0
- package/src/translate/validate.ts +8 -3
- package/docs/configuration/ai.mdx +0 -613
- package/docs/configuration/seo.mdx +0 -364
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
import data from "blume:data";
|
|
3
|
+
|
|
4
|
+
import { normalizeBasePath } from "../../core/base-path.ts";
|
|
5
|
+
import { localePrefix } from "../../core/i18n.ts";
|
|
6
|
+
import {
|
|
7
|
+
localizeContentLinks,
|
|
8
|
+
localizeHref,
|
|
9
|
+
routeSetFor,
|
|
10
|
+
} from "../../core/locale-links.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Keep a translated page's content links inside the reader's locale. Content
|
|
14
|
+
* is compiled once per file, so its `<a href>`s point at the default locale's
|
|
15
|
+
* routes; rendered under `/fr/…` (a real translation, a fallback page, or a
|
|
16
|
+
* shared `page.$.mdx`), each root-relative page link moves to `/fr/…` when
|
|
17
|
+
* that route is served, and keeps its authored target otherwise. Default-locale
|
|
18
|
+
* pages (no prefix) and single-locale sites pass the slot through untouched.
|
|
19
|
+
*/
|
|
20
|
+
interface Props {
|
|
21
|
+
/** Locale of the route being rendered — a fallback route's own, not its content's. */
|
|
22
|
+
locale: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { locale } = Astro.props;
|
|
26
|
+
const i18n = data.config.i18n;
|
|
27
|
+
|
|
28
|
+
const html =
|
|
29
|
+
i18n && localePrefix(locale, i18n) !== ""
|
|
30
|
+
? localizeContentLinks(await Astro.slots.render("default"), (href) =>
|
|
31
|
+
localizeHref(href, {
|
|
32
|
+
basePath: data.config.basePath,
|
|
33
|
+
deployBase: normalizeBasePath(import.meta.env.BASE_URL),
|
|
34
|
+
i18n,
|
|
35
|
+
locale,
|
|
36
|
+
routes: routeSetFor(data.routes),
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
: null;
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
{html === null ? <slot /> : <Fragment set:html={html} />}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
import { buildStructuredData } from "../../seo/jsonld.ts";
|
|
36
36
|
import { normalizeXHandle } from "../../seo/x-handle.ts";
|
|
37
37
|
import { withBase } from "../islands/base-path.ts";
|
|
38
|
+
import { searchLocaleFor } from "./search-locale.ts";
|
|
38
39
|
import "blume:theme";
|
|
39
40
|
import { ClientRouter } from "astro:transitions";
|
|
40
41
|
import Analytics from "./Analytics.astro";
|
|
@@ -160,9 +161,10 @@ const strings = ui ?? EN_UI;
|
|
|
160
161
|
// from a not-yet-regenerated snapshot) still renders instead of coming out
|
|
161
162
|
// blank — the PageActions pattern.
|
|
162
163
|
const navStrings = { ...EN_UI.nav, ...strings.nav };
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
// Scope search to the active language on a multi-locale site. Read from the
|
|
165
|
+
// i18n snapshot, not the switcher list, so pages rendered without one still
|
|
166
|
+
// filter (see `searchLocaleFor`).
|
|
167
|
+
const searchLocale = searchLocaleFor(data.config.i18n, locale);
|
|
166
168
|
const pageTitle = page?.title ?? site.title;
|
|
167
169
|
const description = page?.description ?? site.description;
|
|
168
170
|
const route = page?.route ?? "/";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import "blume:theme";
|
|
3
|
+
import data from "blume:data";
|
|
3
4
|
import type { BlumeFavicon } from "../../core/data.ts";
|
|
4
5
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
5
6
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
THEME_INIT_SCRIPT,
|
|
17
18
|
} from "./head-scripts.ts";
|
|
18
19
|
import Header from "./Header.astro";
|
|
20
|
+
import { searchLocaleFor } from "./search-locale.ts";
|
|
19
21
|
|
|
20
22
|
// A minimal shell for the Scalar API/AsyncAPI reference: Blume's banner + navbar
|
|
21
23
|
// on top, then a full-height region the reference mounts into. Unlike RootLayout
|
|
@@ -91,6 +93,8 @@ const {
|
|
|
91
93
|
} = Astro.props;
|
|
92
94
|
|
|
93
95
|
const strings = ui ?? EN_UI;
|
|
96
|
+
// Scope search to the reference page's language on a multi-locale site.
|
|
97
|
+
const searchLocale = searchLocaleFor(data.config.i18n, locale);
|
|
94
98
|
|
|
95
99
|
const bannerKey = banner?.dismissible ? banner.key : null;
|
|
96
100
|
---
|
|
@@ -126,6 +130,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
126
130
|
navigation={navigation}
|
|
127
131
|
route={route}
|
|
128
132
|
searchEnabled={searchEnabled}
|
|
133
|
+
searchLocale={searchLocale}
|
|
129
134
|
site={site}
|
|
130
135
|
/>
|
|
131
136
|
<div
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
} from "./nav-utils.ts";
|
|
59
59
|
import NavTree from "./NavTree.astro";
|
|
60
60
|
import { resolveSlot } from "./overrides.ts";
|
|
61
|
+
import { searchLocaleFor } from "./search-locale.ts";
|
|
61
62
|
import PageActions from "./PageActions.astro";
|
|
62
63
|
import PageFeedback from "./PageFeedback.astro";
|
|
63
64
|
import Pagination from "./Pagination.astro";
|
|
@@ -289,9 +290,10 @@ const strings = ui ?? EN_UI;
|
|
|
289
290
|
const navStrings = { ...EN_UI.nav, ...strings.nav };
|
|
290
291
|
const actionStrings = { ...EN_UI.actions, ...strings.actions };
|
|
291
292
|
const contentStrings = { ...EN_UI.content, ...strings.content };
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
293
|
+
// Scope search to the active language on a multi-locale site. Read from the
|
|
294
|
+
// i18n snapshot, not the switcher list, so pages rendered without one still
|
|
295
|
+
// filter (see `searchLocaleFor`).
|
|
296
|
+
const searchLocale = searchLocaleFor(data.config.i18n, locale);
|
|
295
297
|
// TOC entries: the configured heading range, or none when the TOC is disabled
|
|
296
298
|
// (an empty list makes TableOfContents render nothing).
|
|
297
299
|
// API operation pages own a two-column body (docs + request panel) and their own
|
|
@@ -798,9 +800,55 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
798
800
|
// Opaque icon chip over the code (in the style of Lina's code block),
|
|
799
801
|
// with the check icon scale-swapping in over the copy icon after a copy.
|
|
800
802
|
const buttonClass =
|
|
801
|
-
"
|
|
803
|
+
"inline-flex size-[1.875rem] select-none items-center justify-center rounded-md text-muted-foreground bg-background transition-colors [&_svg]:pointer-events-none [&_svg]:shrink-0";
|
|
804
|
+
// Pinned over the block's top-right corner when the button lives in
|
|
805
|
+
// the pre; a strip-hosted button (see codeGroupActions) is in flow.
|
|
806
|
+
const overlayClass = "absolute right-3 z-[2]";
|
|
802
807
|
const idleClasses = ["hover:bg-muted", "hover:text-foreground"];
|
|
803
808
|
|
|
809
|
+
// A tab panel that is nothing but code: a CodeGroup/ts2js block (the
|
|
810
|
+
// pre is the panel) or a <Tab> holding only fences and the icon
|
|
811
|
+
// template — the same predicate Tab.astro's flush-padding selector uses.
|
|
812
|
+
const isCodePanel = (panel: Element) =>
|
|
813
|
+
panel.matches("pre, figure") ||
|
|
814
|
+
(panel.matches("[data-blume-tab-panel]") &&
|
|
815
|
+
panel.querySelector(":scope > pre") !== null &&
|
|
816
|
+
Array.from(panel.children).every((child) =>
|
|
817
|
+
child.matches("pre, template")
|
|
818
|
+
));
|
|
819
|
+
|
|
820
|
+
// The tab group's actions slot, when the group is a code switcher —
|
|
821
|
+
// every panel a bare block — so one copy button in the strip serves
|
|
822
|
+
// whichever panel is showing. A group with prose panels (or none) gets
|
|
823
|
+
// null and keeps a button in each pre.
|
|
824
|
+
const codeGroupActions = (pre: Element) => {
|
|
825
|
+
const tabs = pre.closest("blume-tabs");
|
|
826
|
+
const content = tabs?.querySelector(":scope > [data-blume-tab-content]");
|
|
827
|
+
const panels = content ? Array.from(content.children) : [];
|
|
828
|
+
if (!tabs || panels.length === 0 || !panels.every(isCodePanel)) {
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
return tabs.querySelector<HTMLElement>(
|
|
832
|
+
":scope > * > [data-blume-tab-actions]"
|
|
833
|
+
);
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
// The block the strip's copy button should read: the visible panel's
|
|
837
|
+
// pre. Panels toggle a `hidden` class once the tabs element upgrades;
|
|
838
|
+
// before that only the first is painted, and it is also the first
|
|
839
|
+
// match here.
|
|
840
|
+
const activeCodePre = (tabs: Element) => {
|
|
841
|
+
const content = tabs.querySelector(":scope > [data-blume-tab-content]");
|
|
842
|
+
const panel =
|
|
843
|
+
Array.from(content?.children ?? []).find(
|
|
844
|
+
(child) => !child.classList.contains("hidden")
|
|
845
|
+
) ?? content?.firstElementChild;
|
|
846
|
+
if (!panel) {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
return panel.matches("pre") ? panel : panel.querySelector("pre");
|
|
850
|
+
};
|
|
851
|
+
|
|
804
852
|
const languageLabels: Record<string, string> = {
|
|
805
853
|
astro: "Astro",
|
|
806
854
|
bash: "Bash",
|
|
@@ -836,41 +884,16 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
836
884
|
const copiedLabel =
|
|
837
885
|
document.body.getAttribute("data-i18n-copied") || "Copied!";
|
|
838
886
|
|
|
839
|
-
for
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
continue;
|
|
847
|
-
}
|
|
848
|
-
const language = pre.getAttribute("data-language");
|
|
849
|
-
if (language) {
|
|
850
|
-
pre.setAttribute(
|
|
851
|
-
"data-language",
|
|
852
|
-
languageLabels[language.toLowerCase()] ?? language
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
pre.classList.add("group", "relative");
|
|
856
|
-
// The code element is the scroll container (see the theme entry), but
|
|
857
|
-
// Shiki's tab stop lands on the pre, which no longer scrolls. Move the
|
|
858
|
-
// stop to the code so keyboard users can actually scroll the block
|
|
859
|
-
// (WCAG 2.1.1 — the same rule the table wrapper handles). Twoslash and
|
|
860
|
-
// API-panel blocks keep the pre as their scroller, so theirs stays.
|
|
861
|
-
const scroller = pre.querySelector("code");
|
|
862
|
-
if (scroller && !pre.matches(".twoslash, blume-panel-tabs *")) {
|
|
863
|
-
scroller.setAttribute("tabindex", "0");
|
|
864
|
-
pre.removeAttribute("tabindex");
|
|
865
|
-
}
|
|
887
|
+
// `placement` is the extra class set for an overlay button (empty for
|
|
888
|
+
// one hosted by a tab strip); `readPre` resolves the block to copy at
|
|
889
|
+
// click time, since a strip button follows the active panel.
|
|
890
|
+
const createCopyButton = (
|
|
891
|
+
placement: string,
|
|
892
|
+
readPre: () => Element | null
|
|
893
|
+
) => {
|
|
866
894
|
const button = document.createElement("button");
|
|
867
895
|
button.type = "button";
|
|
868
|
-
|
|
869
|
-
// offset; pick the Tailwind class by context instead of a CSS override.
|
|
870
|
-
const topClass = pre.closest("blume-tabs, .not-prose")
|
|
871
|
-
? "top-2.5"
|
|
872
|
-
: "top-2";
|
|
873
|
-
button.className = `${buttonClass} ${idleClasses.join(" ")} ${topClass}`;
|
|
896
|
+
button.className = `${buttonClass} ${idleClasses.join(" ")} ${placement}`;
|
|
874
897
|
button.setAttribute("data-blume-copy", "");
|
|
875
898
|
button.setAttribute("aria-label", copyCodeLabel);
|
|
876
899
|
button.innerHTML =
|
|
@@ -896,7 +919,7 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
896
919
|
};
|
|
897
920
|
const flash = createCopyFlash(setChecked, copiedLabel);
|
|
898
921
|
button.addEventListener("click", async () => {
|
|
899
|
-
const code =
|
|
922
|
+
const code = readPre()?.querySelector("code");
|
|
900
923
|
let text = code?.textContent ?? "";
|
|
901
924
|
// Twoslash nests each hover popup's type signature and docs inside
|
|
902
925
|
// the <code>; copying textContent verbatim would interleave them
|
|
@@ -914,7 +937,55 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
914
937
|
flash();
|
|
915
938
|
}
|
|
916
939
|
});
|
|
917
|
-
|
|
940
|
+
return button;
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
for (const pre of document.querySelectorAll(".prose pre")) {
|
|
944
|
+
if (pre.querySelector("[data-blume-copy]")) {
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
// Skip nested <pre>: Twoslash renders each hover popup's type signature
|
|
948
|
+
// as a <pre> inside the code block, which shouldn't get its own button.
|
|
949
|
+
if (pre.parentElement?.closest("pre")) {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const language = pre.getAttribute("data-language");
|
|
953
|
+
if (language) {
|
|
954
|
+
pre.setAttribute(
|
|
955
|
+
"data-language",
|
|
956
|
+
languageLabels[language.toLowerCase()] ?? language
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
pre.classList.add("group", "relative");
|
|
960
|
+
// The code element is the scroll container (see the theme entry), but
|
|
961
|
+
// Shiki's tab stop lands on the pre, which no longer scrolls. Move the
|
|
962
|
+
// stop to the code so keyboard users can actually scroll the block
|
|
963
|
+
// (WCAG 2.1.1 — the same rule the table wrapper handles). Twoslash and
|
|
964
|
+
// API-panel blocks keep the pre as their scroller, so theirs stays.
|
|
965
|
+
const scroller = pre.querySelector("code");
|
|
966
|
+
if (scroller && !pre.matches(".twoslash, blume-panel-tabs *")) {
|
|
967
|
+
scroller.setAttribute("tabindex", "0");
|
|
968
|
+
pre.removeAttribute("tabindex");
|
|
969
|
+
}
|
|
970
|
+
const actions = codeGroupActions(pre);
|
|
971
|
+
if (actions) {
|
|
972
|
+
// One button per code switcher, reading whichever panel shows.
|
|
973
|
+
if (!actions.querySelector("[data-blume-copy]")) {
|
|
974
|
+
const tabs = actions.closest("blume-tabs");
|
|
975
|
+
actions.appendChild(
|
|
976
|
+
createCopyButton("", () => (tabs ? activeCodePre(tabs) : null))
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
// The language-label bar (prose) vs flush code (tabs) need a different
|
|
982
|
+
// offset; pick the Tailwind class by context instead of a CSS override.
|
|
983
|
+
const topClass = pre.closest("blume-tabs, .not-prose")
|
|
984
|
+
? "top-2.5"
|
|
985
|
+
: "top-2";
|
|
986
|
+
pre.appendChild(
|
|
987
|
+
createCopyButton(`${overlayClass} ${topClass}`, () => pre)
|
|
988
|
+
);
|
|
918
989
|
}
|
|
919
990
|
|
|
920
991
|
// Click-to-zoom for content images (gated by `markdown.imageZoom`),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The locale the search dialog should scope to, or `undefined` when scoping
|
|
3
|
+
* is pointless. Derived from the resolved i18n settings rather than from the
|
|
4
|
+
* header's language-switcher entries: the switcher list is only assembled by
|
|
5
|
+
* the content catch-all, so pages rendered without one (custom pages built on
|
|
6
|
+
* `PageLayout`, the changelog index, the 404 page, the API reference shell)
|
|
7
|
+
* would otherwise search every language while showing no "All languages"
|
|
8
|
+
* toggle. A single-locale site (or one without i18n) has nothing to scope.
|
|
9
|
+
*/
|
|
10
|
+
export const searchLocaleFor = (
|
|
11
|
+
i18n: { locales: { code: string }[] } | null,
|
|
12
|
+
locale: string
|
|
13
|
+
): string | undefined => (i18n && i18n.locales.length > 1 ? locale : undefined);
|
|
@@ -1,54 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import { specAddresses, specOf } from "../../openapi/model.ts";
|
|
5
5
|
|
|
6
6
|
// The spec-level metadata block (version + base URLs) at the top of an API
|
|
7
7
|
// overview page. The tag sections that follow are emitted by `overviewMdx` as
|
|
8
8
|
// markdown headings plus `<ApiTagOperations>` lists, so they land in the
|
|
9
|
-
// table of contents.
|
|
9
|
+
// table of contents. The agent surfaces downlevel this block from the same
|
|
10
|
+
// `specAddresses`, so the `.md` page lists what this one shows.
|
|
10
11
|
interface Props {
|
|
11
12
|
source: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const { source } = Astro.props;
|
|
15
|
-
const spec = specs
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// live endpoint stands in. All flatten into one list of address chips.
|
|
20
|
-
const addresses: string[] = [];
|
|
21
|
-
if (spec?.kind === "graphql") {
|
|
22
|
-
if (spec.endpoint) {
|
|
23
|
-
addresses.push(spec.endpoint);
|
|
24
|
-
}
|
|
25
|
-
} else if (spec?.kind === "asyncapi") {
|
|
26
|
-
const servers = (spec.document as AsyncApiDocument).servers ?? {};
|
|
27
|
-
for (const server of Object.values(servers)) {
|
|
28
|
-
if (server?.host) {
|
|
29
|
-
addresses.push(
|
|
30
|
-
`${server.protocol ? `${server.protocol}://` : ""}${server.host}${server.pathname ?? ""}`
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
} else {
|
|
35
|
-
// Hand-written specs sometimes declare `servers` as a bare object; degrade
|
|
36
|
-
// to no address chips instead of throwing mid-build.
|
|
37
|
-
const declared = ((spec?.document ?? {}) as { servers?: { url?: string }[] })
|
|
38
|
-
.servers;
|
|
39
|
-
const servers = Array.isArray(declared) ? declared : [];
|
|
40
|
-
for (const server of servers) {
|
|
41
|
-
if (server.url) {
|
|
42
|
-
addresses.push(server.url);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
let addressLabel = "Base URL";
|
|
47
|
-
if (spec?.kind === "asyncapi") {
|
|
48
|
-
addressLabel = "Servers";
|
|
49
|
-
} else if (spec?.kind === "graphql") {
|
|
50
|
-
addressLabel = "Endpoint";
|
|
51
|
-
}
|
|
16
|
+
const spec = specOf(specs, source);
|
|
17
|
+
const { addresses, label: addressLabel } = spec
|
|
18
|
+
? specAddresses(spec)
|
|
19
|
+
: { addresses: [], label: "" };
|
|
52
20
|
---
|
|
53
21
|
|
|
54
22
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
|
+
import { specOf } from "../../openapi/model.ts";
|
|
3
4
|
import { withBase } from "../islands/base-path.ts";
|
|
4
5
|
import MethodBadge from "./MethodBadge.astro";
|
|
5
6
|
|
|
@@ -14,7 +15,7 @@ interface Props {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const { source, tag } = Astro.props;
|
|
17
|
-
const operations = Object.values(specs
|
|
18
|
+
const operations = Object.values(specOf(specs, source)?.operations ?? {}).filter(
|
|
18
19
|
(operation) => operation.tagSlug === tag
|
|
19
20
|
);
|
|
20
21
|
---
|
|
@@ -4,6 +4,7 @@ import specs from "blume:openapi";
|
|
|
4
4
|
|
|
5
5
|
import type { AsyncApiDocument } from "../../openapi/asyncapi.ts";
|
|
6
6
|
import { asyncApiOperationObject } from "../../openapi/asyncapi.ts";
|
|
7
|
+
import { operationOf, specOf } from "../../openapi/model.ts";
|
|
7
8
|
import { highlightCode } from "../../markdown/index.ts";
|
|
8
9
|
import {
|
|
9
10
|
asyncApiSecurityEntries,
|
|
@@ -47,8 +48,8 @@ interface Props {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const { source, id } = Astro.props;
|
|
50
|
-
const spec = specs
|
|
51
|
-
const ref = spec
|
|
51
|
+
const spec = specOf(specs, source);
|
|
52
|
+
const ref = spec ? operationOf(spec, id) : undefined;
|
|
52
53
|
const document = (spec?.document ?? {}) as AsyncApiDocument;
|
|
53
54
|
const operation = ref ? asyncApiOperationObject(document, ref) : undefined;
|
|
54
55
|
const channel = ref?.channelId
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
graphqlRootField,
|
|
11
11
|
isGraphqlOperationKind,
|
|
12
12
|
} from "../../openapi/graphql.ts";
|
|
13
|
+
import { operationOf, specOf } from "../../openapi/model.ts";
|
|
13
14
|
import { highlightCode } from "../../markdown/index.ts";
|
|
14
15
|
import { DEPRECATED_LABEL_CLASS } from "../colors.ts";
|
|
15
16
|
import {
|
|
@@ -46,8 +47,8 @@ interface Props {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
const { source, id } = Astro.props;
|
|
49
|
-
const spec = specs
|
|
50
|
-
const ref = spec
|
|
50
|
+
const spec = specOf(specs, source);
|
|
51
|
+
const ref = spec ? operationOf(spec, id) : undefined;
|
|
51
52
|
const document = (spec?.document ?? { roots: {}, types: {} }) as GraphqlDocument;
|
|
52
53
|
const isOperation = ref !== undefined && isGraphqlOperationKind(ref.method);
|
|
53
54
|
const field = ref && isOperation ? graphqlRootField(document, ref) : undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import specs from "blume:openapi";
|
|
3
|
+
import { operationOf, specOf } from "../../openapi/model.ts";
|
|
3
4
|
import {
|
|
4
5
|
mergeParameters,
|
|
5
6
|
type ParameterLike,
|
|
@@ -61,8 +62,8 @@ interface FullOperation {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
const { source, id } = Astro.props;
|
|
64
|
-
const spec = specs
|
|
65
|
-
const ref = spec
|
|
65
|
+
const spec = specOf(specs, source);
|
|
66
|
+
const ref = spec ? operationOf(spec, id) : undefined;
|
|
66
67
|
// The AsyncAPI and GraphQL front-ends render their own bodies; the lookups
|
|
67
68
|
// below are OpenAPI-shaped (paths, request/response) and resolve to nothing
|
|
68
69
|
// for them.
|
package/src/core/i18n.ts
CHANGED
|
@@ -48,8 +48,19 @@ export const resolveFallbackLocale = (
|
|
|
48
48
|
return i18n.fallbackLocale ?? i18n.defaultLocale;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The slice of the i18n settings URL routing reads. `ResolvedI18nConfig` and
|
|
53
|
+
* the `blume:data` runtime shape both satisfy it, so route helpers serve the
|
|
54
|
+
* Node-side graph and the rendered page alike.
|
|
55
|
+
*/
|
|
56
|
+
export interface LocaleRouting {
|
|
57
|
+
defaultLocale: string;
|
|
58
|
+
hideDefaultLocalePrefix: boolean;
|
|
59
|
+
locales: { code: string }[];
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
/** URL prefix for a locale: `""` for the hidden default, else `/<code>`. */
|
|
52
|
-
export const localePrefix = (code: string, i18n:
|
|
63
|
+
export const localePrefix = (code: string, i18n: LocaleRouting): string =>
|
|
53
64
|
code === i18n.defaultLocale && i18n.hideDefaultLocalePrefix ? "" : `/${code}`;
|
|
54
65
|
|
|
55
66
|
/**
|
|
@@ -59,7 +70,7 @@ export const localePrefix = (code: string, i18n: ResolvedI18nConfig): string =>
|
|
|
59
70
|
export const localizeRoute = (
|
|
60
71
|
logicalRoute: string,
|
|
61
72
|
code: string,
|
|
62
|
-
i18n:
|
|
73
|
+
i18n: LocaleRouting
|
|
63
74
|
): string => {
|
|
64
75
|
const prefix = localePrefix(code, i18n);
|
|
65
76
|
if (!prefix) {
|
package/src/core/links.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
isRelativeImageTarget,
|
|
8
8
|
resolveRelativeImage,
|
|
9
9
|
} from "./content-assets.ts";
|
|
10
|
+
import type { LocaleRouting } from "./i18n.ts";
|
|
11
|
+
import { localizeLinkPath } from "./locale-links.ts";
|
|
10
12
|
import { gradeExternal, probeAll } from "./probe.ts";
|
|
11
13
|
import type {
|
|
12
14
|
ContentGraph,
|
|
@@ -78,6 +80,8 @@ interface LinkContext {
|
|
|
78
80
|
/** Servable routes outside the graph (custom pages, generated routes); their
|
|
79
81
|
* headings are unknown, so anchors there are accepted unchecked. */
|
|
80
82
|
extraRoutes: Set<string>;
|
|
83
|
+
/** Locale routing, when the site is multi-locale; drives served-route resolution. */
|
|
84
|
+
i18n: LocaleRouting | null;
|
|
81
85
|
publicDir: string | null;
|
|
82
86
|
/** Normalized `redirect.from` paths — valid targets that resolve at runtime. */
|
|
83
87
|
redirects: Set<string>;
|
|
@@ -189,6 +193,30 @@ const checkAnchor = (
|
|
|
189
193
|
};
|
|
190
194
|
};
|
|
191
195
|
|
|
196
|
+
/**
|
|
197
|
+
* The route a link on `page` actually lands on. Rendered under a prefixed
|
|
198
|
+
* locale, a root-relative link moves into that locale when the localized route
|
|
199
|
+
* is served — a real translation (whose own headings then answer the anchor
|
|
200
|
+
* check) or a fallback page (accepted unchecked via `extraRoutes`) — exactly as
|
|
201
|
+
* `LocaleLinks.astro` rewrites it at render time. Otherwise the authored route
|
|
202
|
+
* stands.
|
|
203
|
+
*/
|
|
204
|
+
const servedRoute = (
|
|
205
|
+
authored: string,
|
|
206
|
+
page: PageRecord,
|
|
207
|
+
ctx: LinkContext
|
|
208
|
+
): string =>
|
|
209
|
+
ctx.i18n
|
|
210
|
+
? localizeLinkPath(authored, {
|
|
211
|
+
basePath: ctx.basePath,
|
|
212
|
+
i18n: ctx.i18n,
|
|
213
|
+
locale: page.locale,
|
|
214
|
+
routes: {
|
|
215
|
+
has: (route) => ctx.routes.has(route) || ctx.extraRoutes.has(route),
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
: authored;
|
|
219
|
+
|
|
192
220
|
/** Validate a resolved internal path: asset, route, then optional anchor. */
|
|
193
221
|
const checkPathLink = (
|
|
194
222
|
resolved: string,
|
|
@@ -204,7 +232,8 @@ const checkPathLink = (
|
|
|
204
232
|
// relative link already resolved against the based `page.route`). A real
|
|
205
233
|
// route always wins over the asset-extension heuristic, so a dotted route
|
|
206
234
|
// (e.g. `/releases/v1.0`) isn't misread as a missing asset.
|
|
207
|
-
const
|
|
235
|
+
const authoredRoute = toRoute(withBasePath(ctx.basePath, resolved));
|
|
236
|
+
const route = servedRoute(authoredRoute, page, ctx);
|
|
208
237
|
if (ctx.routes.has(route)) {
|
|
209
238
|
return fragment ? checkAnchor(route, fragment, site, ctx, via) : null;
|
|
210
239
|
}
|
|
@@ -374,6 +403,8 @@ export const validateLinks = async (
|
|
|
374
403
|
* full-route-set resolution in `nav-diagnostics.ts`/`generateRuntime`.
|
|
375
404
|
*/
|
|
376
405
|
extraRoutes?: string[];
|
|
406
|
+
/** Locale routing when i18n is on; links then resolve into the page's locale. */
|
|
407
|
+
i18n?: LocaleRouting | null;
|
|
377
408
|
publicDir: string | null;
|
|
378
409
|
checkExternal?: boolean;
|
|
379
410
|
/** Configured redirects; their `from` paths count as valid link targets. */
|
|
@@ -385,6 +416,7 @@ export const validateLinks = async (
|
|
|
385
416
|
anchors: buildAnchorIndex(graph.pages),
|
|
386
417
|
basePath,
|
|
387
418
|
extraRoutes: new Set((options.extraRoutes ?? []).map(toRoute)),
|
|
419
|
+
i18n: options.i18n ?? null,
|
|
388
420
|
publicDir: options.publicDir,
|
|
389
421
|
redirects: new Set(
|
|
390
422
|
(options.redirects ?? []).map((redirect) =>
|