blume 1.4.3 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +16 -12
- package/dist/cli/index.js +1784 -633
- package/dist/cli/index.js.map +111 -106
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +80 -28
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +19 -3
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +112 -15
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +6 -5
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/dist/types/theme/fonts.d.ts +11 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/advanced/custom-pages.mdx +5 -1
- package/docs/configuration/index.mdx +1 -1
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/configuration/theming.mdx +4 -2
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +2 -1
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +3 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +37 -10
- package/src/ai/ask-context.ts +5 -1
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +40 -16
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +28 -11
- package/src/ai/mcp/server.ts +183 -38
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +6 -2
- package/src/astro/generate.ts +64 -34
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +181 -40
- package/src/audit/agent.ts +2 -2
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +37 -9
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +5 -2
- package/src/audit/snapshot.ts +2 -4
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/build.ts +15 -9
- package/src/cli/commands/dev.ts +2 -0
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eval.ts +7 -3
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +22 -15
- package/src/cli/internal-error.ts +1 -0
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/islands/ask-ai.tsx +33 -25
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Fonts.astro +23 -3
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageLayout.astro +72 -3
- package/src/components/layout/ReferenceLayout.astro +2 -1
- package/src/components/layout/RootLayout.astro +20 -1
- package/src/components/layout/Search.astro +77 -13
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +2 -0
- package/src/components/layout/search/types.ts +13 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +89 -28
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +19 -3
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +33 -7
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/last-modified.ts +49 -0
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/project-graph.ts +26 -3
- package/src/core/schema.ts +214 -68
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/github-releases.ts +39 -31
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +67 -20
- package/src/core/sources/notion.ts +49 -17
- package/src/core/sources/portable-text.ts +32 -11
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +4 -0
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +25 -10
- package/src/deploy/sitemap.ts +33 -1
- package/src/deploy/vercel-negotiation.ts +45 -18
- package/src/eval/report.ts +4 -4
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +6 -8
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +33 -12
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +126 -57
- package/src/openapi/parse.ts +97 -5
- package/src/openapi/references.ts +12 -10
- package/src/openapi/render-mdx.ts +73 -34
- package/src/openapi/scalar.ts +6 -8
- package/src/openapi/source.ts +98 -28
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +25 -5
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +66 -20
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/entry.ts +24 -2
- package/src/theme/fonts.ts +83 -7
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +22 -14
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +9 -5
- package/src/translate/run.ts +10 -4
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
|
@@ -5,7 +5,11 @@ import { withBase } from "../islands/base-path.ts";
|
|
|
5
5
|
import type { ComponentOverride } from "../../core/define-components.ts";
|
|
6
6
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
7
7
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
LocaleSwitchOption,
|
|
10
|
+
Navigation,
|
|
11
|
+
NavSelector as NavSelectorConfig,
|
|
12
|
+
} from "../../core/types.ts";
|
|
9
13
|
import { GITHUB_MARK } from "../github-mark.ts";
|
|
10
14
|
import Icon from "../Icon.astro";
|
|
11
15
|
import LanguageSwitcher from "./LanguageSwitcher.astro";
|
|
@@ -50,8 +54,16 @@ interface Props {
|
|
|
50
54
|
/** Localized chrome labels (nav toggle, sections, GitHub, theme toggle). */
|
|
51
55
|
navStrings?: UIStrings["nav"];
|
|
52
56
|
localeSwitch?: LocaleSwitchOption[];
|
|
57
|
+
/**
|
|
58
|
+
* Auto-populated version switcher, rendered ahead of the configured
|
|
59
|
+
* selectors. `null`/absent when versioning is off — or when the user
|
|
60
|
+
* declares their own `kind: "version"` selector, which then owns the UI.
|
|
61
|
+
*/
|
|
62
|
+
versionSelector?: NavSelectorConfig | null;
|
|
53
63
|
/** Active locale for per-language search filtering. */
|
|
54
64
|
searchLocale?: string;
|
|
65
|
+
/** Viewed docs version for search filtering (`""` = current; `null`/absent = off). */
|
|
66
|
+
searchVersion?: string | null;
|
|
55
67
|
/**
|
|
56
68
|
* Layout-slot overrides forwarded from the root layout. The header honors
|
|
57
69
|
* `Logo` and `Search` here so those pieces can be replaced without swapping
|
|
@@ -77,7 +89,9 @@ const {
|
|
|
77
89
|
switcherStrings,
|
|
78
90
|
navStrings,
|
|
79
91
|
localeSwitch,
|
|
92
|
+
versionSelector,
|
|
80
93
|
searchLocale,
|
|
94
|
+
searchVersion = null,
|
|
81
95
|
layout = {},
|
|
82
96
|
} = Astro.props;
|
|
83
97
|
|
|
@@ -167,6 +181,15 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
|
|
|
167
181
|
)
|
|
168
182
|
}
|
|
169
183
|
<div class="flex-1"></div>
|
|
184
|
+
{
|
|
185
|
+
/* Right-aligned next to the language switcher: the spacer absorbs its
|
|
186
|
+
width, so pages without a version selector (blog, generated references)
|
|
187
|
+
keep the logo, tabs, and the rest of this cluster in place — no layout
|
|
188
|
+
shift when crossing into the docs. */
|
|
189
|
+
versionSelector && (
|
|
190
|
+
<NavSelector align="end" route={route} selector={versionSelector} />
|
|
191
|
+
)
|
|
192
|
+
}
|
|
170
193
|
{
|
|
171
194
|
localeSwitch && localeSwitch.length > 1 && (
|
|
172
195
|
<LanguageSwitcher
|
|
@@ -184,6 +207,7 @@ const clickScript = `(()=>{const dr=()=>{const h=document.querySelector("[data-b
|
|
|
184
207
|
navigation={navigation}
|
|
185
208
|
popularPages={data.config.search.popular}
|
|
186
209
|
strings={searchStrings}
|
|
210
|
+
version={searchVersion}
|
|
187
211
|
/>
|
|
188
212
|
)
|
|
189
213
|
}
|
|
@@ -11,9 +11,16 @@ import { isUnderPath } from "./nav-utils.ts";
|
|
|
11
11
|
interface Props {
|
|
12
12
|
selector: NavSelector;
|
|
13
13
|
route: string;
|
|
14
|
+
/**
|
|
15
|
+
* Which edge the dropdown panel anchors to. `start` (the default) suits the
|
|
16
|
+
* header's leading selector group; the version selector sits at the trailing
|
|
17
|
+
* edge next to the language switcher, where a start-anchored panel would
|
|
18
|
+
* overflow the viewport — pass `end` there.
|
|
19
|
+
*/
|
|
20
|
+
align?: "start" | "end";
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
const { selector, route } = Astro.props;
|
|
23
|
+
const { selector, route, align = "start" } = Astro.props;
|
|
17
24
|
|
|
18
25
|
// The active item is the deepest path the current route sits under (on a path
|
|
19
26
|
// boundary, so `/api` never claims `/api-reference` routes), falling back to
|
|
@@ -46,7 +53,9 @@ const menuRowClass =
|
|
|
46
53
|
size={14}
|
|
47
54
|
/>
|
|
48
55
|
</summary>
|
|
49
|
-
<div
|
|
56
|
+
<div
|
|
57
|
+
class={`absolute z-50 mt-2 min-w-56 rounded-blume border border-border bg-background p-1 shadow-xl ${align === "end" ? "end-0" : "start-0"}`}
|
|
58
|
+
>
|
|
50
59
|
{selector.items.map((item) => (
|
|
51
60
|
<a
|
|
52
61
|
aria-current={item.path === active?.path ? "true" : undefined}
|
|
@@ -45,8 +45,8 @@ const n = { ...EN_UI.nav, ...strings };
|
|
|
45
45
|
const badgeBase =
|
|
46
46
|
"shrink-0 rounded-full px-1.5 py-0.5 font-medium text-[0.65rem] leading-none";
|
|
47
47
|
const badgeClass = `${badgeBase} bg-muted text-muted-foreground`;
|
|
48
|
-
// HTTP-method badges (from
|
|
49
|
-
// every other badge keeps the neutral style.
|
|
48
|
+
// HTTP-method and AsyncAPI-action badges (from a reference's sidebar) are
|
|
49
|
+
// color-coded to match MethodBadge; every other badge keeps the neutral style.
|
|
50
50
|
const METHOD_BADGE: Record<string, string> = {
|
|
51
51
|
DELETE: "bg-red-500/15 text-red-700 dark:text-red-300",
|
|
52
52
|
GET: "bg-green-500/15 text-green-700 dark:text-green-300",
|
|
@@ -55,6 +55,8 @@ const METHOD_BADGE: Record<string, string> = {
|
|
|
55
55
|
PATCH: "bg-yellow-500/20 text-yellow-800 dark:text-yellow-300",
|
|
56
56
|
POST: "bg-blue-500/15 text-blue-700 dark:text-blue-300",
|
|
57
57
|
PUT: "bg-orange-500/15 text-orange-700 dark:text-orange-300",
|
|
58
|
+
RECEIVE: "bg-teal-500/15 text-teal-700 dark:text-teal-300",
|
|
59
|
+
SEND: "bg-violet-500/15 text-violet-700 dark:text-violet-300",
|
|
58
60
|
};
|
|
59
61
|
const badgeClassFor = (badge: string): string => {
|
|
60
62
|
const method = METHOD_BADGE[badge.toUpperCase()];
|
|
@@ -24,12 +24,14 @@ import type {
|
|
|
24
24
|
} from "../../core/data.ts";
|
|
25
25
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
26
26
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
27
|
+
import type { FontHead } from "../../theme/fonts.ts";
|
|
27
28
|
import type { LocaleSwitchOption, Navigation } from "../../core/types.ts";
|
|
28
29
|
import {
|
|
29
30
|
OG_IMAGE_HEIGHT,
|
|
30
31
|
OG_IMAGE_TYPE,
|
|
31
32
|
OG_IMAGE_WIDTH,
|
|
32
33
|
} from "../../og/dimensions.ts";
|
|
34
|
+
import { buildStructuredData } from "../../seo/jsonld.ts";
|
|
33
35
|
import { normalizeXHandle } from "../../seo/x-handle.ts";
|
|
34
36
|
import { withBase } from "../islands/base-path.ts";
|
|
35
37
|
import "blume:theme";
|
|
@@ -57,7 +59,7 @@ interface Props {
|
|
|
57
59
|
*/
|
|
58
60
|
page?: { title?: string; description?: string; route?: string };
|
|
59
61
|
themeMode: "system" | "light" | "dark";
|
|
60
|
-
fontCssVars?: string[];
|
|
62
|
+
fontCssVars?: (string | FontHead)[];
|
|
61
63
|
searchEnabled: boolean;
|
|
62
64
|
/**
|
|
63
65
|
* Opt this page out of the header's Ask AI trigger. Defaults to whether Ask
|
|
@@ -75,7 +77,24 @@ interface Props {
|
|
|
75
77
|
ogEnabled?: boolean;
|
|
76
78
|
/** SEO overrides; a marketing page often sets its own canonical/og image. */
|
|
77
79
|
ogImage?: string | null;
|
|
80
|
+
/**
|
|
81
|
+
* Alt text for a user-supplied `ogImage`, emitted as `og:image:alt` /
|
|
82
|
+
* `twitter:image:alt`. The generated card derives its own from the title.
|
|
83
|
+
*/
|
|
84
|
+
ogImageAlt?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Pixel size of a user-supplied `ogImage`, emitted as `og:image:width` /
|
|
87
|
+
* `og:image:height` so crawlers can lay the card out before fetching it.
|
|
88
|
+
* The generated card declares its known size automatically.
|
|
89
|
+
*/
|
|
90
|
+
ogImageSize?: { height: number; width: number };
|
|
78
91
|
canonical?: string | null;
|
|
92
|
+
/**
|
|
93
|
+
* Emit schema.org JSON-LD for this page (`data.config.structuredData`) — a
|
|
94
|
+
* WebSite node, plus an article node on non-root routes. Defaults to on,
|
|
95
|
+
* matching RootLayout.
|
|
96
|
+
*/
|
|
97
|
+
structuredDataEnabled?: boolean;
|
|
79
98
|
/**
|
|
80
99
|
* X (Twitter) attribution (`data.config.x`): the site's account and an author
|
|
81
100
|
* handle, emitted as `twitter:site`/`twitter:creator`.
|
|
@@ -113,7 +132,10 @@ const {
|
|
|
113
132
|
siteUrl,
|
|
114
133
|
ogEnabled,
|
|
115
134
|
ogImage,
|
|
135
|
+
ogImageAlt,
|
|
136
|
+
ogImageSize,
|
|
116
137
|
canonical,
|
|
138
|
+
structuredDataEnabled,
|
|
117
139
|
x,
|
|
118
140
|
noindex,
|
|
119
141
|
locale = "en",
|
|
@@ -153,11 +175,12 @@ const basedRoute = withBase(route);
|
|
|
153
175
|
// don't come out double-slashed — the catch-all strips it the same way.
|
|
154
176
|
const siteBase = siteUrl ? siteUrl.replace(/\/$/u, "") : null;
|
|
155
177
|
// The route-derived part is percent-encoded (the sitemap convention) so a
|
|
156
|
-
// Unicode route slug yields a legal URI that byte-matches the sitemap <loc
|
|
178
|
+
// Unicode route slug yields a legal URI that byte-matches the sitemap <loc> —
|
|
179
|
+
// including the root, whose <loc> is `https://site/` with the slash.
|
|
157
180
|
const resolvedCanonical =
|
|
158
181
|
canonical ??
|
|
159
182
|
(siteBase
|
|
160
|
-
? `${siteBase}${basedRoute === "/" ? "" : encodeURI(basedRoute)}`
|
|
183
|
+
? `${siteBase}${basedRoute === "/" ? "/" : encodeURI(basedRoute)}`
|
|
161
184
|
: null);
|
|
162
185
|
// An explicit `ogImage` wins. A root-relative path (e.g. an image dropped in
|
|
163
186
|
// `public/`) is resolved against the site URL so crawlers get an absolute
|
|
@@ -180,6 +203,25 @@ const twitterCard = resolvedOgImage ? "summary_large_image" : "summary";
|
|
|
180
203
|
const xSite = normalizeXHandle(x?.handle);
|
|
181
204
|
const xCreator = normalizeXHandle(x?.creator);
|
|
182
205
|
|
|
206
|
+
// JSON-LD, mirroring RootLayout: skipped when disabled or noindexed. A custom
|
|
207
|
+
// page has no breadcrumb trail; the home route yields the WebSite node alone.
|
|
208
|
+
const structuredData =
|
|
209
|
+
structuredDataEnabled === false || noindex
|
|
210
|
+
? null
|
|
211
|
+
: buildStructuredData({
|
|
212
|
+
base: import.meta.env.BASE_URL,
|
|
213
|
+
breadcrumbs: [],
|
|
214
|
+
description,
|
|
215
|
+
locale,
|
|
216
|
+
route,
|
|
217
|
+
siteName: site.title,
|
|
218
|
+
siteUrl: siteUrl ?? null,
|
|
219
|
+
title: pageTitle,
|
|
220
|
+
});
|
|
221
|
+
const structuredDataJson = structuredData
|
|
222
|
+
? JSON.stringify(structuredData).replaceAll("<", "\\u003c")
|
|
223
|
+
: null;
|
|
224
|
+
|
|
183
225
|
const bannerKey = banner?.dismissible ? banner.key : null;
|
|
184
226
|
---
|
|
185
227
|
|
|
@@ -218,8 +260,26 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
218
260
|
<meta content={pageTitle} property="og:image:alt" />
|
|
219
261
|
</>
|
|
220
262
|
)}
|
|
263
|
+
{!ogGenerated && ogImageSize && (
|
|
264
|
+
<>
|
|
265
|
+
<meta
|
|
266
|
+
content={String(ogImageSize.width)}
|
|
267
|
+
property="og:image:width"
|
|
268
|
+
/>
|
|
269
|
+
<meta
|
|
270
|
+
content={String(ogImageSize.height)}
|
|
271
|
+
property="og:image:height"
|
|
272
|
+
/>
|
|
273
|
+
</>
|
|
274
|
+
)}
|
|
275
|
+
{!ogGenerated && ogImageAlt && (
|
|
276
|
+
<meta content={ogImageAlt} property="og:image:alt" />
|
|
277
|
+
)}
|
|
221
278
|
<meta content={resolvedOgImage} name="twitter:image" />
|
|
222
279
|
{ogGenerated && <meta content={pageTitle} name="twitter:image:alt" />}
|
|
280
|
+
{!ogGenerated && ogImageAlt && (
|
|
281
|
+
<meta content={ogImageAlt} name="twitter:image:alt" />
|
|
282
|
+
)}
|
|
223
283
|
</>
|
|
224
284
|
)
|
|
225
285
|
}
|
|
@@ -228,6 +288,15 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
228
288
|
{description && <meta content={description} name="twitter:description" />}
|
|
229
289
|
{xSite && <meta content={xSite} name="twitter:site" />}
|
|
230
290
|
{xCreator && <meta content={xCreator} name="twitter:creator" />}
|
|
291
|
+
{
|
|
292
|
+
structuredDataJson && (
|
|
293
|
+
<script
|
|
294
|
+
is:inline
|
|
295
|
+
set:html={structuredDataJson}
|
|
296
|
+
type="application/ld+json"
|
|
297
|
+
/>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
231
300
|
{
|
|
232
301
|
bannerKey && (
|
|
233
302
|
<script data-key={bannerKey} is:inline set:html={BANNER_INIT_SCRIPT} />
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import "blume:theme";
|
|
3
3
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
4
4
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
5
|
+
import type { FontHead } from "../../theme/fonts.ts";
|
|
5
6
|
import type { Navigation } from "../../core/types.ts";
|
|
6
7
|
import Analytics from "./Analytics.astro";
|
|
7
8
|
import WebMcp from "./WebMcp.astro";
|
|
@@ -51,7 +52,7 @@ interface Props {
|
|
|
51
52
|
navigation: Navigation;
|
|
52
53
|
route: string;
|
|
53
54
|
themeMode: "system" | "light" | "dark";
|
|
54
|
-
fontCssVars?: string[];
|
|
55
|
+
fontCssVars?: (string | FontHead)[];
|
|
55
56
|
searchEnabled: boolean;
|
|
56
57
|
pageTitle: string;
|
|
57
58
|
/** Keep the reference route out of crawler indexes. */
|
|
@@ -8,8 +8,10 @@ import type {
|
|
|
8
8
|
Heading,
|
|
9
9
|
LocaleSwitchOption,
|
|
10
10
|
Navigation,
|
|
11
|
+
NavSelector as NavSelectorType,
|
|
11
12
|
} from "../../core/types.ts";
|
|
12
13
|
import "blume:theme";
|
|
14
|
+
import type { FontHead } from "../../theme/fonts.ts";
|
|
13
15
|
import type { ComponentOverride } from "../../core/define-components.ts";
|
|
14
16
|
import {
|
|
15
17
|
OG_IMAGE_HEIGHT,
|
|
@@ -46,6 +48,7 @@ import PageActions from "./PageActions.astro";
|
|
|
46
48
|
import PageFeedback from "./PageFeedback.astro";
|
|
47
49
|
import Pagination from "./Pagination.astro";
|
|
48
50
|
import TableOfContents from "./TableOfContents.astro";
|
|
51
|
+
import VersionBanner from "./VersionBanner.astro";
|
|
49
52
|
|
|
50
53
|
interface Props {
|
|
51
54
|
site: { title: string; description?: string };
|
|
@@ -88,7 +91,7 @@ interface Props {
|
|
|
88
91
|
imageZoom?: boolean;
|
|
89
92
|
codeWrap?: boolean;
|
|
90
93
|
themeMode: "system" | "light" | "dark";
|
|
91
|
-
fontCssVars?: string[];
|
|
94
|
+
fontCssVars?: (string | FontHead)[];
|
|
92
95
|
searchEnabled: boolean;
|
|
93
96
|
indexable: boolean;
|
|
94
97
|
ogImage?: string | null;
|
|
@@ -149,6 +152,16 @@ interface Props {
|
|
|
149
152
|
xDefault?: string | null;
|
|
150
153
|
/** Language-switcher entries for the current page. */
|
|
151
154
|
localeSwitch?: LocaleSwitchOption[];
|
|
155
|
+
/** Auto-populated version switcher (`null` when versioning is off or a user selector owns it). */
|
|
156
|
+
versionSelector?: NavSelectorType | null;
|
|
157
|
+
/** Old-version notice for archived pages (`null` on current-docs pages). */
|
|
158
|
+
versionNotice?: {
|
|
159
|
+
message: string;
|
|
160
|
+
latestHref: string;
|
|
161
|
+
latestLabel: string;
|
|
162
|
+
} | null;
|
|
163
|
+
/** Viewed docs version for search filtering (`""` = current; `null`/absent = off). */
|
|
164
|
+
searchVersion?: string | null;
|
|
152
165
|
/**
|
|
153
166
|
* User layout-slot overrides from `components.ts` (`defineComponents`). Each
|
|
154
167
|
* key replaces the matching built-in; unknown keys are ignored. Wired slots:
|
|
@@ -220,6 +233,9 @@ const {
|
|
|
220
233
|
localeAlternates,
|
|
221
234
|
xDefault,
|
|
222
235
|
localeSwitch,
|
|
236
|
+
versionSelector,
|
|
237
|
+
versionNotice,
|
|
238
|
+
searchVersion = null,
|
|
223
239
|
layout = {},
|
|
224
240
|
clientData,
|
|
225
241
|
toc = { enabled: true, maxLevel: 3, minLevel: 2 },
|
|
@@ -499,16 +515,19 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
499
515
|
href="#blume-content">{strings.page.skipToContent}</a
|
|
500
516
|
>
|
|
501
517
|
<Banner banner={banner} strings={strings.banner} />
|
|
518
|
+
<VersionBanner notice={versionNotice ?? null} />
|
|
502
519
|
<HeaderSlot
|
|
503
520
|
askEnabled={askEnabled}
|
|
504
521
|
askStrings={strings.ask}
|
|
505
522
|
layout={layout}
|
|
506
523
|
localeSwitch={localeSwitch}
|
|
524
|
+
versionSelector={versionSelector}
|
|
507
525
|
logo={logo}
|
|
508
526
|
navigation={navigation}
|
|
509
527
|
route={page.route}
|
|
510
528
|
searchEnabled={searchEnabled}
|
|
511
529
|
searchLocale={searchLocale}
|
|
530
|
+
searchVersion={searchVersion}
|
|
512
531
|
navStrings={navStrings}
|
|
513
532
|
searchStrings={strings.search}
|
|
514
533
|
site={site}
|
|
@@ -19,10 +19,21 @@ interface Props {
|
|
|
19
19
|
strings?: UIStrings["search"];
|
|
20
20
|
/** Active locale to filter results to; omitted disables locale filtering. */
|
|
21
21
|
locale?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Docs version to filter results to (`""` = the current docs — a meaningful
|
|
24
|
+
* value, so `null`/omitted is what disables version filtering).
|
|
25
|
+
*/
|
|
26
|
+
version?: string | null;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
const {
|
|
25
|
-
|
|
29
|
+
const {
|
|
30
|
+
askEnabled = false,
|
|
31
|
+
navigation,
|
|
32
|
+
popularPages,
|
|
33
|
+
strings,
|
|
34
|
+
locale,
|
|
35
|
+
version = null,
|
|
36
|
+
} = Astro.props;
|
|
26
37
|
// Merge over the English baseline per key (rather than `strings ?? …`) so a
|
|
27
38
|
// partial — or empty `{}` — strings object still resolves every label to a
|
|
28
39
|
// default, matching the pattern PageActions uses for its own dictionary.
|
|
@@ -59,13 +70,15 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
59
70
|
data-i18n-popular={s.popular}
|
|
60
71
|
data-i18n-results={s.results}
|
|
61
72
|
data-locale={locale || undefined}
|
|
73
|
+
data-version={version ?? undefined}
|
|
74
|
+
data-versioned={version === null ? undefined : ""}
|
|
62
75
|
>
|
|
63
76
|
{/* The label and shortcut hint wait until `lg`: below it the hamburger and
|
|
64
77
|
inline tab bar share the header row, and a full-width search field would
|
|
65
78
|
press into the language switcher. */}
|
|
66
79
|
<button
|
|
67
80
|
aria-label={s.button}
|
|
68
|
-
class="inline-flex h-9 cursor-pointer items-center gap-2 rounded-full border border-border bg-background px-3 text-muted-foreground text-sm transition-colors hover:border-foreground hover:text-foreground lg:min-w-
|
|
81
|
+
class="inline-flex h-9 cursor-pointer items-center gap-2 rounded-full border border-border bg-background px-3 text-muted-foreground text-sm transition-colors hover:border-foreground hover:text-foreground lg:min-w-40"
|
|
69
82
|
data-blume-search-open
|
|
70
83
|
type="button"
|
|
71
84
|
>
|
|
@@ -135,15 +148,29 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
135
148
|
class="flex items-center justify-between gap-3 border-border border-t px-3 py-2 text-muted-foreground text-xs"
|
|
136
149
|
>
|
|
137
150
|
{
|
|
138
|
-
locale ? (
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
class="
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
locale || version !== null ? (
|
|
152
|
+
<span class="flex items-center gap-3">
|
|
153
|
+
{locale && (
|
|
154
|
+
<label class="flex cursor-pointer select-none items-center gap-1.5">
|
|
155
|
+
<input
|
|
156
|
+
class="size-3.5 accent-accent"
|
|
157
|
+
data-blume-search-all-locales
|
|
158
|
+
type="checkbox"
|
|
159
|
+
/>
|
|
160
|
+
{s.allLanguages}
|
|
161
|
+
</label>
|
|
162
|
+
)}
|
|
163
|
+
{version !== null && (
|
|
164
|
+
<label class="flex cursor-pointer select-none items-center gap-1.5">
|
|
165
|
+
<input
|
|
166
|
+
class="size-3.5 accent-accent"
|
|
167
|
+
data-blume-search-all-versions
|
|
168
|
+
type="checkbox"
|
|
169
|
+
/>
|
|
170
|
+
{s.allVersions}
|
|
171
|
+
</label>
|
|
172
|
+
)}
|
|
173
|
+
</span>
|
|
147
174
|
) : (
|
|
148
175
|
<span />
|
|
149
176
|
)
|
|
@@ -268,6 +295,11 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
268
295
|
// reader has opted to search across every language instead.
|
|
269
296
|
locale: string | null = null;
|
|
270
297
|
allLocales = false;
|
|
298
|
+
// The viewed docs version ("" = current; null when versioning is off),
|
|
299
|
+
// and the opt-in to search across every version.
|
|
300
|
+
versioned = false;
|
|
301
|
+
version = "";
|
|
302
|
+
allVersions = false;
|
|
271
303
|
|
|
272
304
|
connectedCallback() {
|
|
273
305
|
this.devOnlyMsg =
|
|
@@ -284,6 +316,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
284
316
|
this.resultsMsg =
|
|
285
317
|
this.getAttribute("data-i18n-results") || this.resultsMsg;
|
|
286
318
|
this.locale = this.getAttribute("data-locale");
|
|
319
|
+
// "" (the current docs) is a real version value, so a presence flag —
|
|
320
|
+
// not the attribute's truthiness — decides whether filtering is on.
|
|
321
|
+
this.versioned = this.hasAttribute("data-versioned");
|
|
322
|
+
this.version = this.getAttribute("data-version") ?? "";
|
|
287
323
|
this.dialog = this.querySelector("[data-blume-search-dialog]")!;
|
|
288
324
|
this.input = this.querySelector("[data-blume-search-input]")!;
|
|
289
325
|
this.grid = this.querySelector("[data-blume-search-grid]")!;
|
|
@@ -323,6 +359,25 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
323
359
|
});
|
|
324
360
|
}
|
|
325
361
|
|
|
362
|
+
// Per-version filtering mirrors the locale toggle: default to the
|
|
363
|
+
// viewed version, with a remembered opt-in to search every version.
|
|
364
|
+
const allVersionsToggle = this.querySelector<HTMLInputElement>(
|
|
365
|
+
"[data-blume-search-all-versions]"
|
|
366
|
+
);
|
|
367
|
+
if (allVersionsToggle) {
|
|
368
|
+
this.allVersions =
|
|
369
|
+
readStorage("blume-search-all-versions") === "1";
|
|
370
|
+
allVersionsToggle.checked = this.allVersions;
|
|
371
|
+
allVersionsToggle.addEventListener("change", () => {
|
|
372
|
+
this.allVersions = allVersionsToggle.checked;
|
|
373
|
+
writeStorage(
|
|
374
|
+
"blume-search-all-versions",
|
|
375
|
+
this.allVersions ? "1" : "0"
|
|
376
|
+
);
|
|
377
|
+
this.render();
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
326
381
|
// The handlers accept both ⌘ and Ctrl chords; show the right modifier
|
|
327
382
|
// per platform on the button hint and the footer's preview hint.
|
|
328
383
|
const isApple = /mac|iphone|ipad|ipod/iu.test(navigator.platform);
|
|
@@ -483,11 +538,14 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
483
538
|
|
|
484
539
|
const localeFilter =
|
|
485
540
|
this.locale && !this.allLocales ? this.locale : undefined;
|
|
541
|
+
const versionFilter =
|
|
542
|
+
this.versioned && !this.allVersions ? this.version : undefined;
|
|
486
543
|
let result: Awaited<ReturnType<SearchFn>>;
|
|
487
544
|
try {
|
|
488
545
|
result = await this.searchFn(query, {
|
|
489
546
|
locale: localeFilter,
|
|
490
547
|
section: this.activeSection ?? undefined,
|
|
548
|
+
version: versionFilter,
|
|
491
549
|
});
|
|
492
550
|
} catch {
|
|
493
551
|
// A hosted provider can reject (network error, outage); the results
|
|
@@ -636,10 +694,16 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
636
694
|
const excerpt = hit.excerpt
|
|
637
695
|
? `<span class="mt-0.5 line-clamp-2 text-muted-foreground text-xs">${hit.excerpt}</span>`
|
|
638
696
|
: "";
|
|
697
|
+
// A cross-version hit (all-versions search) names its version so the
|
|
698
|
+
// reader knows they're about to leave the docs they're viewing.
|
|
699
|
+
const versionTag =
|
|
700
|
+
this.versioned && hit.version !== undefined && hit.version !== this.version
|
|
701
|
+
? `<span class="ms-2 inline-block rounded-full bg-muted px-1.5 py-0.5 align-middle text-[0.65rem] text-muted-foreground">${escapeHtml(hit.version || "latest")}</span>`
|
|
702
|
+
: "";
|
|
639
703
|
el.innerHTML = `
|
|
640
704
|
<span class="mt-0.5 shrink-0 text-muted-foreground">${svg("file")}</span>
|
|
641
705
|
<span class="flex-1">
|
|
642
|
-
<span class="block truncate font-normal text-foreground text-sm">${hit.title}</span>
|
|
706
|
+
<span class="block truncate font-normal text-foreground text-sm">${hit.title}${versionTag}</span>
|
|
643
707
|
${excerpt}
|
|
644
708
|
</span>`;
|
|
645
709
|
const item: Selectable = { el, hit, kind: "link", url: href };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { withBase } from "../islands/base-path.ts";
|
|
3
|
+
import Icon from "../Icon.astro";
|
|
4
|
+
|
|
5
|
+
// The "you're viewing an old version" notice, shared by RootLayout and
|
|
6
|
+
// ReferenceLayout so every shell shows the same bar on archived pages. Unlike
|
|
7
|
+
// the announcement Banner it is never dismissible — the reader should know
|
|
8
|
+
// they're on frozen docs for as long as they are. The message and link label
|
|
9
|
+
// arrive fully resolved (localized, `{version}` substituted) from the
|
|
10
|
+
// catch-all, so this component carries no string fallbacks of its own.
|
|
11
|
+
interface Props {
|
|
12
|
+
notice: {
|
|
13
|
+
message: string;
|
|
14
|
+
/** The same page in the latest docs when it exists, else the docs root. */
|
|
15
|
+
latestHref: string;
|
|
16
|
+
latestLabel: string;
|
|
17
|
+
} | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { notice } = Astro.props;
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
notice && (
|
|
25
|
+
<div
|
|
26
|
+
class="flex items-center justify-center gap-x-2 gap-y-0.5 border-amber-300 border-b bg-amber-100 px-10 py-2.5 text-center text-amber-900 text-sm max-sm:flex-wrap dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200"
|
|
27
|
+
data-blume-version-banner
|
|
28
|
+
>
|
|
29
|
+
<span>{notice.message}</span>
|
|
30
|
+
<a
|
|
31
|
+
class="inline-flex items-center gap-1 font-medium underline underline-offset-2"
|
|
32
|
+
href={withBase(notice.latestHref)}
|
|
33
|
+
>
|
|
34
|
+
{notice.latestLabel}
|
|
35
|
+
<Icon name="arrow-right" size={14} />
|
|
36
|
+
</a>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -19,10 +19,15 @@ interface AnalyticsWindow {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export const track = (event: string, props: TrackProps): void => {
|
|
22
|
-
|
|
22
|
+
// Read through `globalThis` so an SSR/import-time call sees `undefined`
|
|
23
|
+
// instead of a bare-identifier ReferenceError.
|
|
24
|
+
const browserWindow = globalThis.window;
|
|
25
|
+
if (browserWindow === undefined) {
|
|
23
26
|
return;
|
|
24
27
|
}
|
|
25
|
-
|
|
28
|
+
// SAFETY: AnalyticsWindow only adds optional provider globals, so any window
|
|
29
|
+
// satisfies the intersection; each provider is feature-checked before use.
|
|
30
|
+
const w = browserWindow as typeof browserWindow & AnalyticsWindow;
|
|
26
31
|
|
|
27
32
|
// Vercel Web Analytics — self-gates to a no-op until `window.va` is set up.
|
|
28
33
|
vercelTrack(event, props);
|
|
@@ -32,7 +37,5 @@ export const track = (event: string, props: TrackProps): void => {
|
|
|
32
37
|
w.gtag?.("event", event, props);
|
|
33
38
|
w.plausible?.(event, { props });
|
|
34
39
|
// Universal hook for any other integration.
|
|
35
|
-
|
|
36
|
-
new CustomEvent("blume:track", { detail: { event, props } })
|
|
37
|
-
);
|
|
40
|
+
w.dispatchEvent(new CustomEvent("blume:track", { detail: { event, props } }));
|
|
38
41
|
};
|
|
@@ -13,7 +13,7 @@ const PATTERNS = [
|
|
|
13
13
|
/server rendered html/iu,
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
-
if (import.meta.env.DEV &&
|
|
16
|
+
if (import.meta.env.DEV && "window" in globalThis) {
|
|
17
17
|
const original = console.error.bind(console);
|
|
18
18
|
let shown = false;
|
|
19
19
|
console.error = (...args: unknown[]) => {
|
|
@@ -209,10 +209,7 @@ export const sidebarForRoute = (
|
|
|
209
209
|
};
|
|
210
210
|
|
|
211
211
|
/** Resolve previous/next pages around the current route. */
|
|
212
|
-
export const getPagination = (
|
|
213
|
-
flat: FlatPage[],
|
|
214
|
-
route: string
|
|
215
|
-
): { prev: FlatPage | null; next: FlatPage | null } => {
|
|
212
|
+
export const getPagination = (flat: FlatPage[], route: string) => {
|
|
216
213
|
const index = flat.findIndex((page) => page.route === route);
|
|
217
214
|
if (index === -1) {
|
|
218
215
|
return { next: null, prev: null };
|
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ComponentOverride,
|
|
3
|
+
IslandDescriptor,
|
|
4
|
+
} from "../../core/define-components.ts";
|
|
5
|
+
|
|
6
|
+
/** A leftover path string an override resolved to (see `resolveSlot`). */
|
|
7
|
+
const isPathString = (override: ComponentOverride): override is string =>
|
|
8
|
+
typeof override === "string";
|
|
9
|
+
|
|
10
|
+
/** An `IslandDescriptor` whose `component` is actually present. */
|
|
11
|
+
const isResolvedIsland = (
|
|
12
|
+
override: ComponentOverride
|
|
13
|
+
): override is IslandDescriptor =>
|
|
14
|
+
typeof override === "object" &&
|
|
15
|
+
override !== null &&
|
|
16
|
+
"component" in override &&
|
|
17
|
+
override.component !== undefined &&
|
|
18
|
+
override.component !== null;
|
|
2
19
|
|
|
3
20
|
/**
|
|
4
21
|
* Resolve a layout-slot override to the component Astro should render, falling
|
|
@@ -15,20 +32,16 @@ export const resolveSlot = <T>(
|
|
|
15
32
|
override: ComponentOverride | undefined,
|
|
16
33
|
fallback: T
|
|
17
34
|
): T => {
|
|
18
|
-
if (
|
|
19
|
-
override === undefined ||
|
|
20
|
-
override === null ||
|
|
21
|
-
typeof override === "string"
|
|
22
|
-
) {
|
|
35
|
+
if (override === undefined || override === null || isPathString(override)) {
|
|
23
36
|
return fallback;
|
|
24
37
|
}
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
override.component !== null
|
|
30
|
-
) {
|
|
38
|
+
if (isResolvedIsland(override)) {
|
|
39
|
+
// SAFETY: `ComponentReference` is untyped (`unknown`); the generated
|
|
40
|
+
// components map stores real components for this slot, so the descriptor's
|
|
41
|
+
// component is renderable as the slot's component type.
|
|
31
42
|
return override.component as T;
|
|
32
43
|
}
|
|
44
|
+
// SAFETY: same untyped `ComponentReference` — a bare value here is the
|
|
45
|
+
// imported component the config referenced for this slot.
|
|
33
46
|
return override as T;
|
|
34
47
|
};
|