blume 1.4.3 → 1.5.0
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 +17 -0
- package/dist/cli/index.js +1621 -576
- package/dist/cli/index.js.map +109 -104
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +79 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- 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/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- 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 +1 -0
- 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 +54 -29
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +152 -33
- 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/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/RootLayout.astro +18 -0
- 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 +88 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- 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/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 +15 -3
- package/src/core/schema.ts +213 -67
- 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 +11 -4
- 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 +30 -11
- 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/fonts.ts +8 -4
- 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
|
@@ -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()];
|
|
@@ -8,6 +8,7 @@ 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";
|
|
13
14
|
import type { ComponentOverride } from "../../core/define-components.ts";
|
|
@@ -46,6 +47,7 @@ import PageActions from "./PageActions.astro";
|
|
|
46
47
|
import PageFeedback from "./PageFeedback.astro";
|
|
47
48
|
import Pagination from "./Pagination.astro";
|
|
48
49
|
import TableOfContents from "./TableOfContents.astro";
|
|
50
|
+
import VersionBanner from "./VersionBanner.astro";
|
|
49
51
|
|
|
50
52
|
interface Props {
|
|
51
53
|
site: { title: string; description?: string };
|
|
@@ -149,6 +151,16 @@ interface Props {
|
|
|
149
151
|
xDefault?: string | null;
|
|
150
152
|
/** Language-switcher entries for the current page. */
|
|
151
153
|
localeSwitch?: LocaleSwitchOption[];
|
|
154
|
+
/** Auto-populated version switcher (`null` when versioning is off or a user selector owns it). */
|
|
155
|
+
versionSelector?: NavSelectorType | null;
|
|
156
|
+
/** Old-version notice for archived pages (`null` on current-docs pages). */
|
|
157
|
+
versionNotice?: {
|
|
158
|
+
message: string;
|
|
159
|
+
latestHref: string;
|
|
160
|
+
latestLabel: string;
|
|
161
|
+
} | null;
|
|
162
|
+
/** Viewed docs version for search filtering (`""` = current; `null`/absent = off). */
|
|
163
|
+
searchVersion?: string | null;
|
|
152
164
|
/**
|
|
153
165
|
* User layout-slot overrides from `components.ts` (`defineComponents`). Each
|
|
154
166
|
* key replaces the matching built-in; unknown keys are ignored. Wired slots:
|
|
@@ -220,6 +232,9 @@ const {
|
|
|
220
232
|
localeAlternates,
|
|
221
233
|
xDefault,
|
|
222
234
|
localeSwitch,
|
|
235
|
+
versionSelector,
|
|
236
|
+
versionNotice,
|
|
237
|
+
searchVersion = null,
|
|
223
238
|
layout = {},
|
|
224
239
|
clientData,
|
|
225
240
|
toc = { enabled: true, maxLevel: 3, minLevel: 2 },
|
|
@@ -499,16 +514,19 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
499
514
|
href="#blume-content">{strings.page.skipToContent}</a
|
|
500
515
|
>
|
|
501
516
|
<Banner banner={banner} strings={strings.banner} />
|
|
517
|
+
<VersionBanner notice={versionNotice ?? null} />
|
|
502
518
|
<HeaderSlot
|
|
503
519
|
askEnabled={askEnabled}
|
|
504
520
|
askStrings={strings.ask}
|
|
505
521
|
layout={layout}
|
|
506
522
|
localeSwitch={localeSwitch}
|
|
523
|
+
versionSelector={versionSelector}
|
|
507
524
|
logo={logo}
|
|
508
525
|
navigation={navigation}
|
|
509
526
|
route={page.route}
|
|
510
527
|
searchEnabled={searchEnabled}
|
|
511
528
|
searchLocale={searchLocale}
|
|
529
|
+
searchVersion={searchVersion}
|
|
512
530
|
navStrings={navStrings}
|
|
513
531
|
searchStrings={strings.search}
|
|
514
532
|
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
|
};
|
|
@@ -8,6 +8,7 @@ interface AlgoliaRecord {
|
|
|
8
8
|
title: string;
|
|
9
9
|
description?: string;
|
|
10
10
|
content?: string;
|
|
11
|
+
version?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -27,15 +28,24 @@ export const createSearch = (opts: {
|
|
|
27
28
|
hitsPerPage: SEARCH_LIMIT,
|
|
28
29
|
indexName: opts.indexName,
|
|
29
30
|
query,
|
|
30
|
-
// The sync uploads `locale` on every record so
|
|
31
|
-
// scope hosted results to the active language
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
// The sync uploads `locale` and `version` on every record so a
|
|
32
|
+
// site can scope hosted results to the active language and the
|
|
33
|
+
// viewed docs version (the current docs upload as "current").
|
|
34
|
+
...(() => {
|
|
35
|
+
const facetFilters = [
|
|
36
|
+
...(options?.locale ? [`locale:${options.locale}`] : []),
|
|
37
|
+
...(options?.version === undefined
|
|
38
|
+
? []
|
|
39
|
+
: [`version:${options.version || "current"}`]),
|
|
40
|
+
];
|
|
41
|
+
return facetFilters.length > 0 ? { facetFilters } : {};
|
|
42
|
+
})(),
|
|
35
43
|
},
|
|
36
44
|
],
|
|
37
45
|
});
|
|
38
46
|
const [first] = results;
|
|
47
|
+
// SAFETY: the build-time sync uploads every record in the AlgoliaRecord
|
|
48
|
+
// shape, so hits returned by that index carry those fields.
|
|
39
49
|
const records =
|
|
40
50
|
first && "hits" in first ? (first.hits as AlgoliaRecord[]) : [];
|
|
41
51
|
const hits = records.map((record) => ({
|
|
@@ -46,6 +56,9 @@ export const createSearch = (opts: {
|
|
|
46
56
|
),
|
|
47
57
|
title: highlight(record.title, query),
|
|
48
58
|
url: record.url,
|
|
59
|
+
// Records store the current docs' version as "current" (hosted backends
|
|
60
|
+
// treat empty facet values unreliably); the hit contract uses "".
|
|
61
|
+
version: record.version === "current" ? "" : record.version,
|
|
49
62
|
}));
|
|
50
63
|
return { hits, sections: [] };
|
|
51
64
|
};
|
|
@@ -18,6 +18,9 @@ export const createSearch =
|
|
|
18
18
|
if (!response.ok) {
|
|
19
19
|
return { hits: [], sections: [] };
|
|
20
20
|
}
|
|
21
|
+
// SAFETY: the endpoint is Blume-generated (`search-endpoint` template) and
|
|
22
|
+
// responds with the SearchHit list it built; title/excerpt are still
|
|
23
|
+
// escaped below before the dialog injects them as HTML.
|
|
21
24
|
const records = (await response.json()) as SearchHit[];
|
|
22
25
|
const hits = records.slice(0, SEARCH_LIMIT).map((hit) => ({
|
|
23
26
|
...hit,
|
|
@@ -3,6 +3,16 @@ import { Document } from "flexsearch";
|
|
|
3
3
|
import { buildResult, RESULT_POOL } from "./types.ts";
|
|
4
4
|
import type { IndexedDocument, SearchFn } from "./types.ts";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The fields FlexSearch indexes. An anonymous alias of the indexed fields:
|
|
8
|
+
* unlike the `IndexedDocument` interface it satisfies FlexSearch's
|
|
9
|
+
* index-signature `DocumentData` constraint structurally.
|
|
10
|
+
*/
|
|
11
|
+
type SearchDocument = Pick<
|
|
12
|
+
IndexedDocument,
|
|
13
|
+
"content" | "description" | "route" | "title"
|
|
14
|
+
>;
|
|
15
|
+
|
|
6
16
|
/**
|
|
7
17
|
* FlexSearch: reuse the same static `blume-search.json` Orama ships, but build
|
|
8
18
|
* a FlexSearch document index in the browser. Keyless; works in dev and build.
|
|
@@ -13,17 +23,17 @@ export const createSearch = async (opts: {
|
|
|
13
23
|
indexUrl: string;
|
|
14
24
|
}): Promise<SearchFn> => {
|
|
15
25
|
const response = await fetch(opts.indexUrl);
|
|
26
|
+
// SAFETY: `blume-search.json` is generated by our own search indexer, which
|
|
27
|
+
// writes exactly this document shape.
|
|
16
28
|
const documents = (await response.json()) as IndexedDocument[];
|
|
17
29
|
const byRoute = new Map(documents.map((doc) => [doc.route, doc]));
|
|
18
30
|
|
|
19
|
-
const index = new Document({
|
|
31
|
+
const index = new Document<SearchDocument>({
|
|
20
32
|
document: { id: "route", index: ["title", "description", "content"] },
|
|
21
33
|
tokenize: "forward",
|
|
22
34
|
});
|
|
23
35
|
for (const doc of documents) {
|
|
24
|
-
|
|
25
|
-
// record satisfies it structurally but TS needs the cast.
|
|
26
|
-
index.add(doc as unknown as Record<string, string>);
|
|
36
|
+
index.add(doc);
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
// FlexSearch's in-memory search is synchronous, so the SearchFn resolves
|
|
@@ -40,9 +50,15 @@ export const createSearch = async (opts: {
|
|
|
40
50
|
}
|
|
41
51
|
seen.add(route);
|
|
42
52
|
const doc = byRoute.get(route);
|
|
43
|
-
// Filter to the active locale (when
|
|
44
|
-
// so section counts and results stay within
|
|
45
|
-
|
|
53
|
+
// Filter to the active locale and version (when requested) before
|
|
54
|
+
// shaping, so section counts and results stay within scope. `""` is
|
|
55
|
+
// the current version, so version presence is tested explicitly.
|
|
56
|
+
if (
|
|
57
|
+
doc &&
|
|
58
|
+
(!options?.locale || doc.locale === options.locale) &&
|
|
59
|
+
(options?.version === undefined ||
|
|
60
|
+
(doc.version ?? "") === options.version)
|
|
61
|
+
) {
|
|
46
62
|
matched.push(doc);
|
|
47
63
|
}
|
|
48
64
|
}
|
|
@@ -31,7 +31,7 @@ export const createSearch = (opts: {
|
|
|
31
31
|
...(options?.locale && { where: { locale: options.locale } }),
|
|
32
32
|
});
|
|
33
33
|
const hits = (results?.hits ?? []).map((hit) => {
|
|
34
|
-
const doc = hit.document
|
|
34
|
+
const doc: OramaCloudRecord = hit.document;
|
|
35
35
|
return {
|
|
36
36
|
content: doc.content ?? "",
|
|
37
37
|
excerpt: highlight(
|
|
@@ -19,13 +19,16 @@ export const createSearch = async (opts: {
|
|
|
19
19
|
locale?: string;
|
|
20
20
|
}): Promise<SearchFn> => {
|
|
21
21
|
const response = await fetch(opts.indexUrl);
|
|
22
|
+
// SAFETY: `blume-search.json` is generated by our own indexer
|
|
23
|
+
// (`buildSearchDocuments`), which writes exactly this document shape.
|
|
22
24
|
const documents = (await response.json()) as IndexedDocument[];
|
|
23
25
|
const db = await buildOramaIndex(documents, opts.locale);
|
|
24
26
|
|
|
25
27
|
return async (query, options) => {
|
|
26
28
|
const docs = await queryOramaIndex(db, query, RESULT_POOL, {
|
|
27
29
|
locale: options?.locale,
|
|
30
|
+
version: options?.version,
|
|
28
31
|
});
|
|
29
|
-
return buildResult(docs
|
|
32
|
+
return buildResult(docs, query, options?.section);
|
|
30
33
|
};
|
|
31
34
|
};
|
|
@@ -23,6 +23,8 @@ export const createSearch = async (opts: {
|
|
|
23
23
|
}): Promise<SearchFn> => {
|
|
24
24
|
// The pagefind bundle lives in the built site (not node_modules) and is
|
|
25
25
|
// resolved at runtime by URL — it can't be a static, code-splittable path.
|
|
26
|
+
// SAFETY: the URL points at the `pagefind.js` module our own build emitted,
|
|
27
|
+
// whose export contract (`search()`) is fixed by pagefind.
|
|
26
28
|
// oxlint-disable-next-line react-doctor/no-dynamic-import-path
|
|
27
29
|
const pagefind = (await import(
|
|
28
30
|
/* @vite-ignore */
|
|
@@ -13,6 +13,11 @@ export interface SearchHit {
|
|
|
13
13
|
section?: string;
|
|
14
14
|
/** Plain-text page content, used to render the preview pane. */
|
|
15
15
|
content?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Docs version the hit belongs to (`""` = current). Local indexes and the
|
|
18
|
+
* hosted Algolia/Typesense adapters set it; other providers leave it unset.
|
|
19
|
+
*/
|
|
20
|
+
version?: string;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
/** A category pill with its result count. */
|
|
@@ -30,7 +35,12 @@ export interface SearchResult {
|
|
|
30
35
|
/** A configured query function — the common contract every provider returns. */
|
|
31
36
|
export type SearchFn = (
|
|
32
37
|
query: string,
|
|
33
|
-
options?: {
|
|
38
|
+
options?: {
|
|
39
|
+
section?: string;
|
|
40
|
+
locale?: string;
|
|
41
|
+
/** Docs version to scope to (`""` = current); omitted disables it. */
|
|
42
|
+
version?: string;
|
|
43
|
+
}
|
|
34
44
|
) => Promise<SearchResult>;
|
|
35
45
|
|
|
36
46
|
/** A document in the client-loaded `blume-search.json` index. */
|
|
@@ -42,6 +52,7 @@ export interface IndexedDocument {
|
|
|
42
52
|
breadcrumb?: string[];
|
|
43
53
|
section?: string;
|
|
44
54
|
locale?: string;
|
|
55
|
+
version?: string;
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
/** Max results surfaced in the dialog. */
|
|
@@ -193,6 +204,7 @@ export const buildResult = (
|
|
|
193
204
|
section: doc.section ?? "",
|
|
194
205
|
title: highlight(doc.title, query),
|
|
195
206
|
url: doc.route,
|
|
207
|
+
version: doc.version,
|
|
196
208
|
}));
|
|
197
209
|
return { hits, sections };
|
|
198
210
|
};
|
|
@@ -8,6 +8,7 @@ interface TypesenseRecord extends Record<string, unknown> {
|
|
|
8
8
|
title: string;
|
|
9
9
|
description?: string;
|
|
10
10
|
content?: string;
|
|
11
|
+
version?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -40,9 +41,20 @@ export const createSearch = (opts: {
|
|
|
40
41
|
per_page: SEARCH_LIMIT,
|
|
41
42
|
q: query,
|
|
42
43
|
query_by: "title,description,content",
|
|
43
|
-
// The sync marks `locale` as
|
|
44
|
-
//
|
|
45
|
-
|
|
44
|
+
// The sync marks `locale` and `version` as facets so hosted results
|
|
45
|
+
// scope to the active language and the viewed docs version (the
|
|
46
|
+
// current docs upload as "current").
|
|
47
|
+
...(() => {
|
|
48
|
+
const clauses = [
|
|
49
|
+
...(options?.locale ? [`locale:=${options.locale}`] : []),
|
|
50
|
+
...(options?.version === undefined
|
|
51
|
+
? []
|
|
52
|
+
: [`version:=${options.version || "current"}`]),
|
|
53
|
+
];
|
|
54
|
+
return clauses.length > 0
|
|
55
|
+
? { filter_by: clauses.join(" && ") }
|
|
56
|
+
: {};
|
|
57
|
+
})(),
|
|
46
58
|
},
|
|
47
59
|
{}
|
|
48
60
|
);
|
|
@@ -56,6 +68,10 @@ export const createSearch = (opts: {
|
|
|
56
68
|
),
|
|
57
69
|
title: highlight(doc.title, query),
|
|
58
70
|
url: doc.url,
|
|
71
|
+
// Records store the current docs' version as "current" (hosted
|
|
72
|
+
// backends treat empty facet values unreliably); the hit contract
|
|
73
|
+
// uses "".
|
|
74
|
+
version: doc.version === "current" ? "" : doc.version,
|
|
59
75
|
};
|
|
60
76
|
});
|
|
61
77
|
return { hits, sections: [] };
|