blume 1.5.0 → 1.5.2
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 +449 -135
- package/dist/cli/index.js.map +24 -23
- package/dist/types/ai/ask-context.d.ts +78 -0
- package/dist/types/core/config-input.d.ts +54 -2
- package/dist/types/core/data.d.ts +19 -2
- package/dist/types/core/open-in-chat.d.ts +9 -0
- package/dist/types/core/schema.d.ts +48 -1
- package/dist/types/core/types.d.ts +10 -3
- package/dist/types/openapi/references.d.ts +9 -0
- package/dist/types/search/orama-index.d.ts +70 -0
- package/dist/types/theme/fonts.d.ts +11 -2
- package/docs/advanced/api-reference.mdx +67 -5
- package/docs/advanced/custom-pages.mdx +5 -1
- package/docs/configuration/ai.mdx +35 -0
- package/docs/configuration/index.mdx +14 -2
- package/docs/configuration/search.mdx +4 -4
- package/docs/configuration/theming.mdx +4 -2
- package/docs/reference/cli.mdx +2 -2
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/ask-context.ts +51 -11
- package/src/ai/mcp/data.ts +3 -2
- package/src/ai/mcp/server.ts +3 -2
- package/src/assets/icon-dark.png +0 -0
- package/src/astro/generate.ts +172 -18
- package/src/astro/templates.ts +89 -15
- package/src/components/content/AccordionItem.astro +4 -0
- package/src/components/content/Update.astro +3 -0
- package/src/components/islands/AskAI.astro +6 -0
- package/src/components/islands/ask-ai.tsx +39 -9
- package/src/components/layout/Analytics.astro +9 -1
- package/src/components/layout/Favicon.astro +29 -8
- package/src/components/layout/Fonts.astro +23 -3
- package/src/components/layout/Header.astro +2 -2
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/PageActions.astro +120 -78
- package/src/components/layout/PageFeedback.astro +12 -3
- package/src/components/layout/PageLayout.astro +79 -5
- package/src/components/layout/ReferenceLayout.astro +12 -9
- package/src/components/layout/RootLayout.astro +153 -121
- package/src/components/layout/Search.astro +41 -26
- package/src/components/layout/drawer-inert.ts +10 -5
- package/src/components/layout/head-scripts.ts +34 -16
- package/src/components/layout/nav-utils.ts +34 -15
- package/src/components/layout/search/orama.ts +3 -2
- package/src/components/openapi/AsyncApiOperation.astro +22 -7
- package/src/components/openapi/MessageComposer.astro +238 -0
- package/src/components/openapi/Operation.astro +26 -12
- package/src/components/openapi/PanelTabs.astro +7 -0
- package/src/components/openapi/Playground.astro +320 -0
- package/src/components/openapi/RequestPanel.astro +1 -0
- package/src/components/openapi/async-snippets.ts +20 -7
- package/src/components/openapi/async.ts +13 -2
- package/src/components/openapi/message-composer.ts +242 -0
- package/src/components/openapi/message-model.ts +108 -0
- package/src/components/openapi/message.ts +153 -0
- package/src/components/openapi/operation-model.ts +260 -0
- package/src/components/openapi/playground-client.ts +486 -0
- package/src/components/openapi/playground-schema.ts +109 -0
- package/src/components/openapi/request.ts +287 -0
- package/src/components/openapi/security.ts +0 -56
- package/src/components/openapi/snippets.ts +23 -136
- package/src/components/openapi/validate-json.ts +144 -0
- package/src/components/openapi/ws-client.ts +194 -0
- package/src/core/config-input.ts +67 -1
- package/src/core/content-assets.ts +66 -15
- package/src/core/data.ts +16 -2
- package/src/core/last-modified.ts +76 -2
- package/src/core/links.ts +30 -4
- package/src/core/navigation.ts +26 -1
- package/src/core/open-in-chat.ts +17 -0
- package/src/core/project-graph.ts +11 -0
- package/src/core/schema.ts +60 -1
- package/src/core/server-features.ts +11 -0
- package/src/core/sources/normalize.ts +10 -2
- package/src/core/types.ts +10 -3
- package/src/deploy/vercel-negotiation.ts +34 -14
- package/src/og/card.ts +3 -1
- package/src/openapi/model.ts +7 -0
- package/src/openapi/proxy.ts +217 -0
- package/src/openapi/references.ts +8 -0
- package/src/openapi/source.ts +13 -0
- package/src/registry/eject.ts +4 -5
- package/src/search/orama-index.ts +109 -36
- package/src/theme/entry.ts +15 -2
- package/src/theme/fonts.ts +75 -3
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
3
3
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
4
|
+
import { openInChatProviders } from "../../core/open-in-chat.ts";
|
|
5
|
+
import type { OpenInChatProvider } from "../../core/open-in-chat.ts";
|
|
4
6
|
import { GITHUB_MARK } from "../github-mark.ts";
|
|
5
7
|
import Icon from "../Icon.astro";
|
|
6
8
|
|
|
@@ -15,12 +17,25 @@ interface Props {
|
|
|
15
17
|
mcpUrl?: string | null;
|
|
16
18
|
/** Display name of the MCP server (defaults to the site title). */
|
|
17
19
|
mcpName?: string | null;
|
|
20
|
+
/**
|
|
21
|
+
* "Open in chat" providers to list, in order; an empty list hides the
|
|
22
|
+
* action, and omitting the prop shows every provider.
|
|
23
|
+
*/
|
|
24
|
+
openInChat?: readonly OpenInChatProvider[];
|
|
18
25
|
/** Localized action labels. */
|
|
19
26
|
strings?: UIStrings["actions"];
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
const {
|
|
23
|
-
|
|
29
|
+
const {
|
|
30
|
+
route,
|
|
31
|
+
editUrl,
|
|
32
|
+
exportPdf,
|
|
33
|
+
exportEpub,
|
|
34
|
+
mcpUrl,
|
|
35
|
+
mcpName,
|
|
36
|
+
openInChat,
|
|
37
|
+
strings,
|
|
38
|
+
} = Astro.props;
|
|
24
39
|
// Merge over the English defaults so a label missing from a translation (or from
|
|
25
40
|
// a not-yet-regenerated snapshot) still renders instead of coming out blank.
|
|
26
41
|
const a = { ...EN_UI.actions, ...strings };
|
|
@@ -44,20 +59,28 @@ const LOGOS: Record<string, string> = {
|
|
|
44
59
|
v0: '<svg class="size-4 shrink-0" fill="currentColor" viewBox="0 0 147 70" xmlns="http://www.w3.org/2000/svg"><path d="M56 50.2031V14H70V60.1562C70 65.5928 65.5928 70 60.1562 70C57.5605 70 54.9982 68.9992 53.1562 67.1573L0 14H19.7969L56 50.2031Z"/><path d="M147 56H133V23.9531L100.953 56H133V70H96.6875C85.8144 70 77 61.1856 77 50.3125V14H91V46.1562L123.156 14H91V0H127.312C138.186 0 147 8.81439 147 19.6875V56Z"/></svg>',
|
|
45
60
|
};
|
|
46
61
|
|
|
62
|
+
/** Display names for the "Open in chat" providers. */
|
|
63
|
+
const PROVIDER_NAMES: Record<OpenInChatProvider, string> = {
|
|
64
|
+
chatgpt: "ChatGPT",
|
|
65
|
+
claude: "Claude",
|
|
66
|
+
cursor: "Cursor",
|
|
67
|
+
scira: "Scira",
|
|
68
|
+
t3: "T3 Chat",
|
|
69
|
+
v0: "v0",
|
|
70
|
+
};
|
|
71
|
+
|
|
47
72
|
// Brand names stay verbatim; the surrounding "Open in" phrase localizes via
|
|
48
|
-
// the `actions.openIn` template.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{ key: "t3", name: "T3 Chat" },
|
|
54
|
-
{ key: "scira", name: "Scira" },
|
|
55
|
-
{ key: "cursor", name: "Cursor" },
|
|
56
|
-
].map((provider) => ({
|
|
57
|
-
...provider,
|
|
58
|
-
label: a.openIn.replace("{name}", provider.name),
|
|
73
|
+
// the `actions.openIn` template. `ai.openInChat` picks and orders the list;
|
|
74
|
+
// an empty list hides the action entirely.
|
|
75
|
+
const providers = (openInChat ?? openInChatProviders).map((key) => ({
|
|
76
|
+
key,
|
|
77
|
+
label: a.openIn.replace("{name}", PROVIDER_NAMES[key]),
|
|
59
78
|
}));
|
|
60
79
|
|
|
80
|
+
// The default list opens with v0 and separates it from the chat assistants;
|
|
81
|
+
// keep that separator only while a configured list preserves v0 first.
|
|
82
|
+
const chatSeparator = providers.length > 1 && providers[0]?.key === "v0";
|
|
83
|
+
|
|
61
84
|
// Top-level actions match the ToC links: flat, no background/padding, color
|
|
62
85
|
// shift on hover only.
|
|
63
86
|
const rowClass =
|
|
@@ -134,41 +157,45 @@ const menuRowClass =
|
|
|
134
157
|
)
|
|
135
158
|
}
|
|
136
159
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
class=
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
160
|
+
{
|
|
161
|
+
providers.length > 0 && (
|
|
162
|
+
<details class="group relative">
|
|
163
|
+
<summary
|
|
164
|
+
class={`${rowClass} cursor-pointer list-none [&::-webkit-details-marker]:hidden`}
|
|
165
|
+
>
|
|
166
|
+
<Icon name="external-link" size={16} />
|
|
167
|
+
{a.openInChat}
|
|
168
|
+
<Icon
|
|
169
|
+
class="ms-auto transition-transform group-open:rotate-180"
|
|
170
|
+
name="chevron-down"
|
|
171
|
+
size={14}
|
|
172
|
+
/>
|
|
173
|
+
</summary>
|
|
174
|
+
<div
|
|
175
|
+
class="absolute top-full right-2 z-50 mt-1 w-max min-w-[14rem] max-w-[22rem] rounded-blume border border-border bg-background p-1 shadow-xl"
|
|
176
|
+
data-blume-menu
|
|
177
|
+
>
|
|
178
|
+
{providers.map((provider, index) => (
|
|
179
|
+
<>
|
|
180
|
+
{index === 1 && chatSeparator && (
|
|
181
|
+
<hr class="my-1 border-border border-t" />
|
|
182
|
+
)}
|
|
183
|
+
<a
|
|
184
|
+
class={menuRowClass}
|
|
185
|
+
data-open-in={provider.key}
|
|
186
|
+
rel="noreferrer"
|
|
187
|
+
target="_blank"
|
|
188
|
+
>
|
|
189
|
+
<span set:html={LOGOS[provider.key]} />
|
|
190
|
+
<span class="flex-1">{provider.label}</span>
|
|
191
|
+
<Icon name="external-link" size={13} />
|
|
192
|
+
</a>
|
|
193
|
+
</>
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
</details>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
172
199
|
|
|
173
200
|
{
|
|
174
201
|
mcpUrl && (
|
|
@@ -285,28 +312,51 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
285
312
|
return clone.innerHTML;
|
|
286
313
|
};
|
|
287
314
|
|
|
288
|
-
const
|
|
289
|
-
|
|
315
|
+
const placeMenu = (details: HTMLDetailsElement) => {
|
|
316
|
+
const menu = details.querySelector<HTMLElement>("[data-blume-menu]");
|
|
317
|
+
const summary = details.querySelector("summary");
|
|
318
|
+
if (!(menu && summary)) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const rect = summary.getBoundingClientRect();
|
|
322
|
+
const margin = 8;
|
|
323
|
+
const flipUp =
|
|
324
|
+
rect.bottom + menu.offsetHeight + margin > window.innerHeight &&
|
|
325
|
+
rect.top - menu.offsetHeight - margin > 0;
|
|
326
|
+
menu.classList.toggle("top-full", !flipUp);
|
|
327
|
+
menu.classList.toggle("mt-1", !flipUp);
|
|
328
|
+
menu.classList.toggle("bottom-full", flipUp);
|
|
329
|
+
menu.classList.toggle("mb-1", flipUp);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// rAF-coalesced so a live resize drag re-reads layout once per frame, not
|
|
333
|
+
// once per event, while the open menu still tracks the viewport. Registered
|
|
334
|
+
// once at module scope (this bundle runs once per real page load) and
|
|
335
|
+
// querying live, because client-router swaps replace the menu elements.
|
|
336
|
+
window.addEventListener(
|
|
337
|
+
"resize",
|
|
338
|
+
rafThrottle(() => {
|
|
339
|
+
const open = document.querySelector<HTMLDetailsElement>(
|
|
340
|
+
"[data-blume-page-actions] details[open]"
|
|
341
|
+
);
|
|
342
|
+
if (open) {
|
|
343
|
+
placeMenu(open);
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// Per-page setup, run on the initial load and again after every
|
|
349
|
+
// client-router swap: the swap rebuilds the actions menu from
|
|
350
|
+
// server-rendered markup, so every handler below binds to fresh elements.
|
|
351
|
+
const initPageActions = () => {
|
|
352
|
+
const root = document.querySelector("[data-blume-page-actions]");
|
|
353
|
+
if (!root) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
290
356
|
// The action dropdowns are native <details>. Make them behave like a proper
|
|
291
357
|
// menu: only one open at a time, and flip above the trigger when opening
|
|
292
358
|
// downward would run past the viewport bottom.
|
|
293
359
|
const dropdowns = [...root.querySelectorAll<HTMLDetailsElement>("details")];
|
|
294
|
-
const placeMenu = (details: HTMLDetailsElement) => {
|
|
295
|
-
const menu = details.querySelector<HTMLElement>("[data-blume-menu]");
|
|
296
|
-
const summary = details.querySelector("summary");
|
|
297
|
-
if (!(menu && summary)) {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
const rect = summary.getBoundingClientRect();
|
|
301
|
-
const margin = 8;
|
|
302
|
-
const flipUp =
|
|
303
|
-
rect.bottom + menu.offsetHeight + margin > window.innerHeight &&
|
|
304
|
-
rect.top - menu.offsetHeight - margin > 0;
|
|
305
|
-
menu.classList.toggle("top-full", !flipUp);
|
|
306
|
-
menu.classList.toggle("mt-1", !flipUp);
|
|
307
|
-
menu.classList.toggle("bottom-full", flipUp);
|
|
308
|
-
menu.classList.toggle("mb-1", flipUp);
|
|
309
|
-
};
|
|
310
360
|
for (const details of dropdowns) {
|
|
311
361
|
details.addEventListener("toggle", () => {
|
|
312
362
|
if (!details.open) {
|
|
@@ -320,17 +370,6 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
320
370
|
placeMenu(details);
|
|
321
371
|
});
|
|
322
372
|
}
|
|
323
|
-
// rAF-coalesced so a live resize drag re-reads layout once per frame,
|
|
324
|
-
// not once per event, while the open menu still tracks the viewport.
|
|
325
|
-
window.addEventListener(
|
|
326
|
-
"resize",
|
|
327
|
-
rafThrottle(() => {
|
|
328
|
-
const open = dropdowns.find((details) => details.open);
|
|
329
|
-
if (open) {
|
|
330
|
-
placeMenu(open);
|
|
331
|
-
}
|
|
332
|
-
})
|
|
333
|
-
);
|
|
334
373
|
|
|
335
374
|
// `data-md` is the base-less logical route; the raw-markdown endpoint is a
|
|
336
375
|
// generated page route, so it's served under the deployment base like any
|
|
@@ -491,5 +530,8 @@ hr { border: 0; border-top: 1px solid #ddd; margin: 2em 0; }`;
|
|
|
491
530
|
}
|
|
492
531
|
}
|
|
493
532
|
});
|
|
494
|
-
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
initPageActions();
|
|
536
|
+
document.addEventListener("astro:after-swap", initPageActions);
|
|
495
537
|
</script>
|
|
@@ -42,8 +42,14 @@ const buttonClass =
|
|
|
42
42
|
<script>
|
|
43
43
|
import { track } from "./analytics-client.ts";
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Run on the initial load and again after every client-router swap, which
|
|
46
|
+
// rebuilds the rating widget from server-rendered markup — the handlers
|
|
47
|
+
// below always bind to the freshly swapped-in elements.
|
|
48
|
+
const initFeedback = () => {
|
|
49
|
+
const root = document.querySelector("[data-blume-page-feedback]");
|
|
50
|
+
if (!root) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
47
53
|
const actions = root.querySelector("[data-feedback-actions]");
|
|
48
54
|
const thanks = root.querySelector("[data-feedback-thanks]");
|
|
49
55
|
for (const button of root.querySelectorAll("[data-feedback-value]")) {
|
|
@@ -57,5 +63,8 @@ const buttonClass =
|
|
|
57
63
|
thanks?.classList.remove("hidden");
|
|
58
64
|
});
|
|
59
65
|
}
|
|
60
|
-
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
initFeedback();
|
|
69
|
+
document.addEventListener("astro:after-swap", initFeedback);
|
|
61
70
|
</script>
|
|
@@ -24,22 +24,25 @@ 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";
|
|
38
|
+
import { ClientRouter } from "astro:transitions";
|
|
36
39
|
import Analytics from "./Analytics.astro";
|
|
37
40
|
import Banner from "./Banner.astro";
|
|
38
41
|
import Favicon from "./Favicon.astro";
|
|
39
42
|
import Fonts from "./Fonts.astro";
|
|
40
43
|
import { BANNER_INIT_SCRIPT, THEME_INIT_SCRIPT } from "./head-scripts.ts";
|
|
41
44
|
import Header from "./Header.astro";
|
|
42
|
-
import {
|
|
45
|
+
import { currentTabForRoute } from "./nav-utils.ts";
|
|
43
46
|
import WebMcp from "./WebMcp.astro";
|
|
44
47
|
|
|
45
48
|
interface Props {
|
|
@@ -57,7 +60,7 @@ interface Props {
|
|
|
57
60
|
*/
|
|
58
61
|
page?: { title?: string; description?: string; route?: string };
|
|
59
62
|
themeMode: "system" | "light" | "dark";
|
|
60
|
-
fontCssVars?: string[];
|
|
63
|
+
fontCssVars?: (string | FontHead)[];
|
|
61
64
|
searchEnabled: boolean;
|
|
62
65
|
/**
|
|
63
66
|
* Opt this page out of the header's Ask AI trigger. Defaults to whether Ask
|
|
@@ -75,7 +78,24 @@ interface Props {
|
|
|
75
78
|
ogEnabled?: boolean;
|
|
76
79
|
/** SEO overrides; a marketing page often sets its own canonical/og image. */
|
|
77
80
|
ogImage?: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Alt text for a user-supplied `ogImage`, emitted as `og:image:alt` /
|
|
83
|
+
* `twitter:image:alt`. The generated card derives its own from the title.
|
|
84
|
+
*/
|
|
85
|
+
ogImageAlt?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Pixel size of a user-supplied `ogImage`, emitted as `og:image:width` /
|
|
88
|
+
* `og:image:height` so crawlers can lay the card out before fetching it.
|
|
89
|
+
* The generated card declares its known size automatically.
|
|
90
|
+
*/
|
|
91
|
+
ogImageSize?: { height: number; width: number };
|
|
78
92
|
canonical?: string | null;
|
|
93
|
+
/**
|
|
94
|
+
* Emit schema.org JSON-LD for this page (`data.config.structuredData`) — a
|
|
95
|
+
* WebSite node, plus an article node on non-root routes. Defaults to on,
|
|
96
|
+
* matching RootLayout.
|
|
97
|
+
*/
|
|
98
|
+
structuredDataEnabled?: boolean;
|
|
79
99
|
/**
|
|
80
100
|
* X (Twitter) attribution (`data.config.x`): the site's account and an author
|
|
81
101
|
* handle, emitted as `twitter:site`/`twitter:creator`.
|
|
@@ -113,7 +133,10 @@ const {
|
|
|
113
133
|
siteUrl,
|
|
114
134
|
ogEnabled,
|
|
115
135
|
ogImage,
|
|
136
|
+
ogImageAlt,
|
|
137
|
+
ogImageSize,
|
|
116
138
|
canonical,
|
|
139
|
+
structuredDataEnabled,
|
|
117
140
|
x,
|
|
118
141
|
noindex,
|
|
119
142
|
locale = "en",
|
|
@@ -138,7 +161,7 @@ const searchLocale =
|
|
|
138
161
|
const pageTitle = page?.title ?? site.title;
|
|
139
162
|
const description = page?.description ?? site.description;
|
|
140
163
|
const route = page?.route ?? "/";
|
|
141
|
-
const activeTab =
|
|
164
|
+
const activeTab = currentTabForRoute(navigation.tabs, route, navigation.root);
|
|
142
165
|
|
|
143
166
|
// Derive canonical + og:image from the site URL the same way the catch-all does
|
|
144
167
|
// for content pages, so a custom page gets both for free. The matching OG card
|
|
@@ -153,11 +176,12 @@ const basedRoute = withBase(route);
|
|
|
153
176
|
// don't come out double-slashed — the catch-all strips it the same way.
|
|
154
177
|
const siteBase = siteUrl ? siteUrl.replace(/\/$/u, "") : null;
|
|
155
178
|
// 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
|
|
179
|
+
// Unicode route slug yields a legal URI that byte-matches the sitemap <loc> —
|
|
180
|
+
// including the root, whose <loc> is `https://site/` with the slash.
|
|
157
181
|
const resolvedCanonical =
|
|
158
182
|
canonical ??
|
|
159
183
|
(siteBase
|
|
160
|
-
? `${siteBase}${basedRoute === "/" ? "" : encodeURI(basedRoute)}`
|
|
184
|
+
? `${siteBase}${basedRoute === "/" ? "/" : encodeURI(basedRoute)}`
|
|
161
185
|
: null);
|
|
162
186
|
// An explicit `ogImage` wins. A root-relative path (e.g. an image dropped in
|
|
163
187
|
// `public/`) is resolved against the site URL so crawlers get an absolute
|
|
@@ -180,6 +204,25 @@ const twitterCard = resolvedOgImage ? "summary_large_image" : "summary";
|
|
|
180
204
|
const xSite = normalizeXHandle(x?.handle);
|
|
181
205
|
const xCreator = normalizeXHandle(x?.creator);
|
|
182
206
|
|
|
207
|
+
// JSON-LD, mirroring RootLayout: skipped when disabled or noindexed. A custom
|
|
208
|
+
// page has no breadcrumb trail; the home route yields the WebSite node alone.
|
|
209
|
+
const structuredData =
|
|
210
|
+
structuredDataEnabled === false || noindex
|
|
211
|
+
? null
|
|
212
|
+
: buildStructuredData({
|
|
213
|
+
base: import.meta.env.BASE_URL,
|
|
214
|
+
breadcrumbs: [],
|
|
215
|
+
description,
|
|
216
|
+
locale,
|
|
217
|
+
route,
|
|
218
|
+
siteName: site.title,
|
|
219
|
+
siteUrl: siteUrl ?? null,
|
|
220
|
+
title: pageTitle,
|
|
221
|
+
});
|
|
222
|
+
const structuredDataJson = structuredData
|
|
223
|
+
? JSON.stringify(structuredData).replaceAll("<", "\\u003c")
|
|
224
|
+
: null;
|
|
225
|
+
|
|
183
226
|
const bannerKey = banner?.dismissible ? banner.key : null;
|
|
184
227
|
---
|
|
185
228
|
|
|
@@ -188,6 +231,10 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
188
231
|
<head>
|
|
189
232
|
<meta charset="utf-8" />
|
|
190
233
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
234
|
+
{/* Client-side navigation, matching RootLayout: same-origin link clicks
|
|
235
|
+
swap the DOM in place instead of tearing the document down, so no
|
|
236
|
+
browser paints a blank frame between pages. */}
|
|
237
|
+
<ClientRouter />
|
|
191
238
|
<title>{pageTitle}</title>
|
|
192
239
|
<Favicon appleIcon={appleIcon} favicon={favicon} />
|
|
193
240
|
<Fonts cssVars={fontCssVars ?? []} />
|
|
@@ -218,8 +265,26 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
218
265
|
<meta content={pageTitle} property="og:image:alt" />
|
|
219
266
|
</>
|
|
220
267
|
)}
|
|
268
|
+
{!ogGenerated && ogImageSize && (
|
|
269
|
+
<>
|
|
270
|
+
<meta
|
|
271
|
+
content={String(ogImageSize.width)}
|
|
272
|
+
property="og:image:width"
|
|
273
|
+
/>
|
|
274
|
+
<meta
|
|
275
|
+
content={String(ogImageSize.height)}
|
|
276
|
+
property="og:image:height"
|
|
277
|
+
/>
|
|
278
|
+
</>
|
|
279
|
+
)}
|
|
280
|
+
{!ogGenerated && ogImageAlt && (
|
|
281
|
+
<meta content={ogImageAlt} property="og:image:alt" />
|
|
282
|
+
)}
|
|
221
283
|
<meta content={resolvedOgImage} name="twitter:image" />
|
|
222
284
|
{ogGenerated && <meta content={pageTitle} name="twitter:image:alt" />}
|
|
285
|
+
{!ogGenerated && ogImageAlt && (
|
|
286
|
+
<meta content={ogImageAlt} name="twitter:image:alt" />
|
|
287
|
+
)}
|
|
223
288
|
</>
|
|
224
289
|
)
|
|
225
290
|
}
|
|
@@ -228,6 +293,15 @@ const bannerKey = banner?.dismissible ? banner.key : null;
|
|
|
228
293
|
{description && <meta content={description} name="twitter:description" />}
|
|
229
294
|
{xSite && <meta content={xSite} name="twitter:site" />}
|
|
230
295
|
{xCreator && <meta content={xCreator} name="twitter:creator" />}
|
|
296
|
+
{
|
|
297
|
+
structuredDataJson && (
|
|
298
|
+
<script
|
|
299
|
+
is:inline
|
|
300
|
+
set:html={structuredDataJson}
|
|
301
|
+
type="application/ld+json"
|
|
302
|
+
/>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
231
305
|
{
|
|
232
306
|
bannerKey && (
|
|
233
307
|
<script data-key={bannerKey} is:inline set:html={BANNER_INIT_SCRIPT} />
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
import "blume:theme";
|
|
3
|
+
import type { BlumeFavicon } from "../../core/data.ts";
|
|
3
4
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
4
5
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
6
|
+
import type { FontHead } from "../../theme/fonts.ts";
|
|
5
7
|
import type { Navigation } from "../../core/types.ts";
|
|
6
8
|
import Analytics from "./Analytics.astro";
|
|
7
9
|
import WebMcp from "./WebMcp.astro";
|
|
@@ -14,6 +16,13 @@ import Header from "./Header.astro";
|
|
|
14
16
|
// A minimal shell for the Scalar API/AsyncAPI reference: Blume's banner + navbar
|
|
15
17
|
// on top, then a full-height region the reference mounts into. Unlike RootLayout
|
|
16
18
|
// it has no docs sidebar/TOC/prose column, so the reference renders edge to edge.
|
|
19
|
+
//
|
|
20
|
+
// Deliberately NOT wired into the client router (`<ClientRouter />`): Scalar is
|
|
21
|
+
// a SPA whose mount script runs once per real page load, so a client-side swap
|
|
22
|
+
// back onto this page would leave the mount region empty (Astro never re-runs a
|
|
23
|
+
// script it has already executed). Without the router's marker meta, the docs
|
|
24
|
+
// pages' router falls back to a normal full load when navigating here — exactly
|
|
25
|
+
// what a fresh Scalar mount needs.
|
|
17
26
|
interface Props {
|
|
18
27
|
site: { title: string; description?: string };
|
|
19
28
|
logo?: {
|
|
@@ -24,14 +33,8 @@ interface Props {
|
|
|
24
33
|
href: string;
|
|
25
34
|
text?: string;
|
|
26
35
|
} | null;
|
|
27
|
-
favicon?:
|
|
28
|
-
|
|
29
|
-
type?: string;
|
|
30
|
-
} | null;
|
|
31
|
-
appleIcon?: {
|
|
32
|
-
href: string;
|
|
33
|
-
type?: string;
|
|
34
|
-
} | null;
|
|
36
|
+
favicon?: BlumeFavicon | null;
|
|
37
|
+
appleIcon?: BlumeFavicon | null;
|
|
35
38
|
banner?: {
|
|
36
39
|
content: string;
|
|
37
40
|
link?: { text: string; href: string };
|
|
@@ -51,7 +54,7 @@ interface Props {
|
|
|
51
54
|
navigation: Navigation;
|
|
52
55
|
route: string;
|
|
53
56
|
themeMode: "system" | "light" | "dark";
|
|
54
|
-
fontCssVars?: string[];
|
|
57
|
+
fontCssVars?: (string | FontHead)[];
|
|
55
58
|
searchEnabled: boolean;
|
|
56
59
|
pageTitle: string;
|
|
57
60
|
/** Keep the reference route out of crawler indexes. */
|