blume 1.7.1 → 1.7.3
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 +18 -0
- package/dist/cli/chunk-0qymqwzz.js +164 -0
- package/dist/cli/chunk-0qymqwzz.js.map +15 -0
- package/dist/cli/{chunk-8gnpdsn1.js → chunk-0xjyb285.js} +2 -2
- package/dist/cli/{chunk-12dzsn9b.js → chunk-1jefwnfs.js} +82 -81
- package/dist/cli/{chunk-12dzsn9b.js.map → chunk-1jefwnfs.js.map} +3 -3
- package/dist/cli/{chunk-27gtm2ym.js → chunk-2mzebbbz.js} +1 -1
- package/dist/cli/{chunk-s5e5jt53.js → chunk-2z47ypj8.js} +1 -1
- package/dist/cli/{chunk-jtb45atp.js → chunk-3r45185y.js} +10 -12
- package/dist/cli/{chunk-jtb45atp.js.map → chunk-3r45185y.js.map} +2 -2
- package/dist/cli/{chunk-6mq7qkve.js → chunk-4x36ddpw.js} +18 -24
- package/dist/cli/{chunk-6mq7qkve.js.map → chunk-4x36ddpw.js.map} +2 -2
- package/dist/cli/{chunk-he2zfgah.js → chunk-5093q3n7.js} +22 -30
- package/dist/cli/{chunk-he2zfgah.js.map → chunk-5093q3n7.js.map} +2 -2
- package/dist/cli/{chunk-k0v1f8bb.js → chunk-5g0w1e2c.js} +21 -28
- package/dist/cli/{chunk-k0v1f8bb.js.map → chunk-5g0w1e2c.js.map} +2 -2
- package/dist/cli/{chunk-5gfw0q4j.js → chunk-5qk08vmp.js} +26 -34
- package/dist/cli/{chunk-5gfw0q4j.js.map → chunk-5qk08vmp.js.map} +2 -2
- package/dist/cli/{chunk-r99hynxh.js → chunk-7s8hm3b6.js} +41 -9
- package/dist/cli/{chunk-r99hynxh.js.map → chunk-7s8hm3b6.js.map} +3 -3
- package/dist/cli/{chunk-vyqj481z.js → chunk-8cjtbafj.js} +68 -66
- package/dist/cli/chunk-8cjtbafj.js.map +13 -0
- package/dist/cli/{chunk-aqjvpd03.js → chunk-97r59kpr.js} +27 -33
- package/dist/cli/{chunk-aqjvpd03.js.map → chunk-97r59kpr.js.map} +2 -2
- package/dist/cli/{chunk-np8dmfb0.js → chunk-ahnw3kxw.js} +26 -33
- package/dist/cli/{chunk-np8dmfb0.js.map → chunk-ahnw3kxw.js.map} +2 -2
- package/dist/cli/{chunk-j5f2wrj5.js → chunk-b27xqwn9.js} +10 -15
- package/dist/cli/{chunk-j5f2wrj5.js.map → chunk-b27xqwn9.js.map} +2 -2
- package/dist/cli/{chunk-kmx2mydj.js → chunk-bf6bt1xt.js} +8 -8
- package/dist/cli/{chunk-kmx2mydj.js.map → chunk-bf6bt1xt.js.map} +1 -1
- package/dist/cli/{chunk-90pdhkpm.js → chunk-bvwwhd84.js} +23 -32
- package/dist/cli/{chunk-90pdhkpm.js.map → chunk-bvwwhd84.js.map} +2 -2
- package/dist/cli/{chunk-mfm4sjwx.js → chunk-cjtn640a.js} +32 -43
- package/dist/cli/{chunk-mfm4sjwx.js.map → chunk-cjtn640a.js.map} +2 -2
- package/dist/cli/{chunk-pxj10x8y.js → chunk-ct47dqpx.js} +14 -3
- package/dist/cli/{chunk-pxj10x8y.js.map → chunk-ct47dqpx.js.map} +4 -3
- package/dist/cli/{chunk-x66c5yjn.js → chunk-dwgcp5sm.js} +2 -2
- package/dist/cli/{chunk-4trphnvy.js → chunk-e7f42gdj.js} +10 -13
- package/dist/cli/{chunk-4trphnvy.js.map → chunk-e7f42gdj.js.map} +2 -2
- package/dist/cli/{chunk-82atea4k.js → chunk-esphfr8p.js} +14 -18
- package/dist/cli/{chunk-82atea4k.js.map → chunk-esphfr8p.js.map} +2 -2
- package/dist/cli/{chunk-q56730e0.js → chunk-ex56aa81.js} +53 -53
- package/dist/cli/chunk-ex56aa81.js.map +13 -0
- package/dist/cli/{chunk-ywn7t0pb.js → chunk-garjf5z9.js} +3 -3
- package/dist/cli/{chunk-ev67ycx0.js → chunk-jq5n4avg.js} +1 -1
- package/dist/cli/{chunk-ka5k7cz9.js → chunk-js7saxwm.js} +35 -39
- package/dist/cli/{chunk-ka5k7cz9.js.map → chunk-js7saxwm.js.map} +4 -6
- package/dist/cli/{chunk-x1wvw7a8.js → chunk-k79xp7av.js} +168 -208
- package/dist/cli/chunk-k79xp7av.js.map +39 -0
- package/dist/cli/{chunk-3r94j3tc.js → chunk-nn13znc2.js} +2 -2
- package/dist/cli/{chunk-4ae4f395.js → chunk-ps4m1xh4.js} +60 -35
- package/dist/cli/chunk-ps4m1xh4.js.map +15 -0
- package/dist/cli/{chunk-wd27zjcz.js → chunk-q4rae3bg.js} +1 -1
- package/dist/cli/{chunk-pdwg3q9g.js → chunk-rqy0s5wh.js} +21 -30
- package/dist/cli/{chunk-pdwg3q9g.js.map → chunk-rqy0s5wh.js.map} +2 -2
- package/dist/cli/{chunk-52cwcqvp.js → chunk-rz9jmfhz.js} +15 -24
- package/dist/cli/{chunk-52cwcqvp.js.map → chunk-rz9jmfhz.js.map} +2 -2
- package/dist/cli/{chunk-8p3xe5jv.js → chunk-vacwm2hv.js} +3 -3
- package/dist/cli/{chunk-cbjnx4s8.js → chunk-vh9w1sgp.js} +1 -1
- package/dist/cli/{chunk-sbdqrjbb.js → chunk-vrfp10qk.js} +1 -1
- package/dist/cli/{chunk-h9ekmtz7.js → chunk-yg63d42r.js} +28 -35
- package/dist/cli/{chunk-h9ekmtz7.js.map → chunk-yg63d42r.js.map} +2 -2
- package/dist/cli/{chunk-5hs6gb7n.js → chunk-yzhm0j9q.js} +1 -1
- package/dist/cli/index.js +397 -34
- package/dist/cli/index.js.map +12 -4
- package/dist/types/core/config-input.d.ts +59 -0
- package/dist/types/core/data.d.ts +2 -0
- package/dist/types/core/schema.d.ts +47 -3
- package/dist/types/core/types.d.ts +5 -0
- package/docs/configuration/ask-ai.mdx +61 -0
- package/docs/configuration/index.mdx +3 -1
- package/docs/content/navigation.mdx +3 -0
- package/docs/content/syntax.mdx +10 -0
- package/docs/discoverability/agent-discovery.mdx +82 -1
- package/docs/discoverability/index.mdx +1 -1
- package/docs/discoverability/llms-txt.mdx +1 -1
- package/docs/reference/frontmatter.mdx +2 -0
- package/package.json +1 -1
- package/src/ai/agent-readability.ts +5 -0
- package/src/ai/ai-catalog.ts +241 -0
- package/src/ai/cors.ts +87 -0
- package/src/ai/link-headers.ts +12 -0
- package/src/ai/llms.ts +6 -0
- package/src/ai/mcp/discovery.ts +1 -1
- package/src/astro/generate.ts +4 -0
- package/src/astro/templates.ts +88 -23
- package/src/cli/commands/build.ts +3 -1
- package/src/components/islands/hooks.ts +50 -1
- package/src/components/layout/NavTree.astro +75 -66
- package/src/components/layout/RootLayout.astro +28 -2
- package/src/components/layout/analytics-client.ts +36 -7
- package/src/components/layout/nav-utils.ts +17 -3
- package/src/core/adapter.ts +61 -0
- package/src/core/config-input.ts +60 -0
- package/src/core/data.ts +2 -0
- package/src/core/navigation.ts +22 -3
- package/src/core/schema.ts +88 -0
- package/src/core/types.ts +5 -0
- package/src/deploy/artifacts.ts +12 -1
- package/src/deploy/headers.ts +6 -0
- package/src/deploy/vercel-negotiation.ts +25 -2
- package/src/registry/eject.ts +2 -0
- package/src/search/build.ts +25 -3
- package/src/theme/entry.ts +23 -0
- package/dist/cli/chunk-2aj8ddew.js +0 -72
- package/dist/cli/chunk-2aj8ddew.js.map +0 -10
- package/dist/cli/chunk-4ae4f395.js.map +0 -15
- package/dist/cli/chunk-4xyggvgf.js +0 -21
- package/dist/cli/chunk-4xyggvgf.js.map +0 -10
- package/dist/cli/chunk-6kzzpsx8.js +0 -26
- package/dist/cli/chunk-6kzzpsx8.js.map +0 -10
- package/dist/cli/chunk-bcy492zc.js +0 -16
- package/dist/cli/chunk-bcy492zc.js.map +0 -10
- package/dist/cli/chunk-btfr9yvw.js +0 -41
- package/dist/cli/chunk-btfr9yvw.js.map +0 -10
- package/dist/cli/chunk-ey89bjj1.js +0 -209
- package/dist/cli/chunk-ey89bjj1.js.map +0 -11
- package/dist/cli/chunk-q56730e0.js.map +0 -13
- package/dist/cli/chunk-qvvpnwaz.js +0 -69
- package/dist/cli/chunk-qvvpnwaz.js.map +0 -11
- package/dist/cli/chunk-vt8fgygt.js +0 -23
- package/dist/cli/chunk-vt8fgygt.js.map +0 -10
- package/dist/cli/chunk-vxv4x1n8.js +0 -17
- package/dist/cli/chunk-vxv4x1n8.js.map +0 -10
- package/dist/cli/chunk-vyqj481z.js.map +0 -13
- package/dist/cli/chunk-x1wvw7a8.js.map +0 -40
- /package/dist/cli/{chunk-8gnpdsn1.js.map → chunk-0xjyb285.js.map} +0 -0
- /package/dist/cli/{chunk-27gtm2ym.js.map → chunk-2mzebbbz.js.map} +0 -0
- /package/dist/cli/{chunk-s5e5jt53.js.map → chunk-2z47ypj8.js.map} +0 -0
- /package/dist/cli/{chunk-x66c5yjn.js.map → chunk-dwgcp5sm.js.map} +0 -0
- /package/dist/cli/{chunk-ywn7t0pb.js.map → chunk-garjf5z9.js.map} +0 -0
- /package/dist/cli/{chunk-ev67ycx0.js.map → chunk-jq5n4avg.js.map} +0 -0
- /package/dist/cli/{chunk-3r94j3tc.js.map → chunk-nn13znc2.js.map} +0 -0
- /package/dist/cli/{chunk-wd27zjcz.js.map → chunk-q4rae3bg.js.map} +0 -0
- /package/dist/cli/{chunk-8p3xe5jv.js.map → chunk-vacwm2hv.js.map} +0 -0
- /package/dist/cli/{chunk-cbjnx4s8.js.map → chunk-vh9w1sgp.js.map} +0 -0
- /package/dist/cli/{chunk-sbdqrjbb.js.map → chunk-vrfp10qk.js.map} +0 -0
- /package/dist/cli/{chunk-5hs6gb7n.js.map → chunk-yzhm0j9q.js.map} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import type { BlumeClientData } from "../../core/data.ts";
|
|
4
|
+
import { track } from "../layout/analytics-client.ts";
|
|
4
5
|
import type { SearchFn, SearchResult } from "../layout/search/types.ts";
|
|
5
6
|
import { joinBase, stripBase } from "./base-path.ts";
|
|
6
7
|
|
|
@@ -196,6 +197,42 @@ export const useAskAI = (options: UseAskAIOptions = {}): UseAskAI => {
|
|
|
196
197
|
const controller = new AbortController();
|
|
197
198
|
abortRef.current = controller;
|
|
198
199
|
const live = () => current === generation.current;
|
|
200
|
+
const path = currentPath();
|
|
201
|
+
// Usage reaches the configured analytics providers the same way page
|
|
202
|
+
// feedback does: the question now, its outcome once the stream settles.
|
|
203
|
+
// A reset mid-answer revokes the outcome along with the UI update.
|
|
204
|
+
// Analytics keys on the raw pathname, like page feedback and the
|
|
205
|
+
// providers' own pageviews, so the events join under a `base`; the
|
|
206
|
+
// endpoint gets the base-stripped route for grounding. Providers receive
|
|
207
|
+
// the question's length only: its text is free-form reader input (pasted
|
|
208
|
+
// keys, error logs) that would breach their PII terms and their
|
|
209
|
+
// per-value size caps, so it rides the `blume:track` event alone for a
|
|
210
|
+
// site to bridge on its own terms.
|
|
211
|
+
const { pathname } = window.location;
|
|
212
|
+
const report = (
|
|
213
|
+
event: "ask" | "ask_answer" | "ask_error",
|
|
214
|
+
props: Record<string, number>
|
|
215
|
+
) =>
|
|
216
|
+
track(
|
|
217
|
+
event,
|
|
218
|
+
{ ...props, path: pathname, questionChars: trimmed.length },
|
|
219
|
+
{ question: trimmed }
|
|
220
|
+
);
|
|
221
|
+
report("ask", {});
|
|
222
|
+
// A monotonic clock: the wall clock can jump mid-stream (NTP, sleep).
|
|
223
|
+
const startedAt = performance.now();
|
|
224
|
+
const outcome = (
|
|
225
|
+
event: "ask_answer" | "ask_error",
|
|
226
|
+
props: Record<string, number>
|
|
227
|
+
) =>
|
|
228
|
+
report(event, {
|
|
229
|
+
...props,
|
|
230
|
+
ms: Math.round(performance.now() - startedAt),
|
|
231
|
+
});
|
|
232
|
+
// The HTTP status once a response exists. `streamText` defers provider
|
|
233
|
+
// errors to stream consumption, so a 200 can still break mid-flight;
|
|
234
|
+
// that reports as a 200 error, not as "no response".
|
|
235
|
+
let status = 0;
|
|
199
236
|
const history: AskMessage[] = [
|
|
200
237
|
...messages,
|
|
201
238
|
{ content: trimmed, role: "user" },
|
|
@@ -207,16 +244,18 @@ export const useAskAI = (options: UseAskAIOptions = {}): UseAskAI => {
|
|
|
207
244
|
const response = await fetch(endpoint, {
|
|
208
245
|
body: JSON.stringify({
|
|
209
246
|
messages: history,
|
|
210
|
-
page: { path
|
|
247
|
+
page: { path },
|
|
211
248
|
}),
|
|
212
249
|
headers: { "content-type": "application/json" },
|
|
213
250
|
method: "POST",
|
|
214
251
|
signal: controller.signal,
|
|
215
252
|
});
|
|
253
|
+
({ status } = response);
|
|
216
254
|
if (!response.ok) {
|
|
217
255
|
// An error body (JSON, HTML error page) must not stream in as the
|
|
218
256
|
// assistant's answer.
|
|
219
257
|
if (live()) {
|
|
258
|
+
outcome("ask_error", { status });
|
|
220
259
|
assistant.content = errorMessage;
|
|
221
260
|
setMessages([...history, { ...assistant }]);
|
|
222
261
|
}
|
|
@@ -243,11 +282,21 @@ export const useAskAI = (options: UseAskAIOptions = {}): UseAskAI => {
|
|
|
243
282
|
}
|
|
244
283
|
}
|
|
245
284
|
}
|
|
285
|
+
if (live()) {
|
|
286
|
+
// A 200 with nothing in it (no body, an empty stream) leaves the
|
|
287
|
+
// reader a blank bubble — that is not an answer.
|
|
288
|
+
if (assistant.content) {
|
|
289
|
+
outcome("ask_answer", { chars: assistant.content.length });
|
|
290
|
+
} else {
|
|
291
|
+
outcome("ask_error", { status });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
246
294
|
} catch {
|
|
247
295
|
// A thrown fetch (offline, DNS failure, CORS) must not strand the
|
|
248
296
|
// pre-appended empty assistant message as a stuck placeholder. A
|
|
249
297
|
// reset's abort lands here too — the guard keeps it silent.
|
|
250
298
|
if (live()) {
|
|
299
|
+
outcome("ask_error", { status });
|
|
251
300
|
assistant.content = errorMessage;
|
|
252
301
|
setMessages([...history, { ...assistant }]);
|
|
253
302
|
}
|
|
@@ -35,7 +35,10 @@ interface Props {
|
|
|
35
35
|
/**
|
|
36
36
|
* Stable ids for the full sidebar's groups (`navGroupIds`), so panel and
|
|
37
37
|
* fragment ids match across the scoped views the layout renders. Absent,
|
|
38
|
-
* ids fall back to positions within this render.
|
|
38
|
+
* ids fall back to positions within this render. A group missing from the
|
|
39
|
+
* map exists only in this render — a container the tab scoping rebuilt
|
|
40
|
+
* without a nested tab section — so no fragment can serve it: it renders
|
|
41
|
+
* in full instead of being deferred.
|
|
39
42
|
*/
|
|
40
43
|
ids?: Map<NavNode, string>;
|
|
41
44
|
/**
|
|
@@ -63,9 +66,12 @@ const {
|
|
|
63
66
|
const idOf = (node: NavNode, positional: string): string =>
|
|
64
67
|
ids?.get(node) ?? positional;
|
|
65
68
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The fragment a group's section loads from, when sections are deferred and
|
|
71
|
+
* the group is one the fragments know (in `ids`, or `ids` is absent).
|
|
72
|
+
*/
|
|
73
|
+
const fragmentFor = (node: NavNode, id: string): string | undefined =>
|
|
74
|
+
fragmentBase && (ids?.has(node) ?? true) ? `${fragmentBase}/${id}` : undefined;
|
|
69
75
|
|
|
70
76
|
// Merge over the English defaults so a label missing from a translation (or
|
|
71
77
|
// from a not-yet-regenerated snapshot) still renders instead of coming out
|
|
@@ -150,66 +156,56 @@ const initialId =
|
|
|
150
156
|
strings={n}
|
|
151
157
|
/>
|
|
152
158
|
</div>
|
|
153
|
-
{panels.map((panel) =>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
{panels.map((panel) => {
|
|
160
|
+
const src = panel.active ? undefined : fragmentFor(panel.node, panel.id);
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
data-nav-depth={panel.depth}
|
|
164
|
+
data-nav-panel={panel.id}
|
|
165
|
+
data-nav-src={src}
|
|
166
|
+
hidden={panel.id !== initialId}
|
|
167
|
+
>
|
|
168
|
+
{/* The title is the only sidebar link to the section's own page, so
|
|
169
|
+
a routed panel keeps it as a link; without a route the whole row
|
|
170
|
+
becomes the back button. Either way every part of the row is
|
|
171
|
+
interactive. */}
|
|
172
|
+
{panel.route ? (
|
|
173
|
+
<div class="mb-3 flex items-center gap-0.5">
|
|
174
|
+
<button
|
|
175
|
+
aria-label={n.back}
|
|
176
|
+
class="-ml-1 flex shrink-0 items-center justify-center self-stretch rounded-[0.65rem] px-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
177
|
+
data-nav-back={panel.parentId}
|
|
178
|
+
type="button"
|
|
179
|
+
>
|
|
180
|
+
<Icon class="rtl:-scale-x-100" name="arrow-left" size={16} />
|
|
181
|
+
</button>
|
|
182
|
+
<a
|
|
183
|
+
aria-current={panel.route === currentRoute ? "page" : undefined}
|
|
184
|
+
class="flex-1 truncate rounded-[0.65rem] px-1 py-1 font-semibold text-foreground text-sm transition-colors hover:bg-muted"
|
|
185
|
+
href={withBase(panel.route)}
|
|
186
|
+
>
|
|
187
|
+
{panel.label}
|
|
188
|
+
</a>
|
|
189
|
+
</div>
|
|
190
|
+
) : (
|
|
166
191
|
<button
|
|
167
|
-
aria-label={n.back}
|
|
168
|
-
class="-ml-1 flex
|
|
192
|
+
aria-label={`${n.back}: ${panel.label}`}
|
|
193
|
+
class="-ml-1 mb-3 flex w-full items-center gap-1.5 rounded-[0.65rem] p-1 text-left font-semibold text-foreground text-sm transition-colors hover:bg-muted"
|
|
169
194
|
data-nav-back={panel.parentId}
|
|
170
195
|
type="button"
|
|
171
196
|
>
|
|
172
|
-
<Icon
|
|
197
|
+
<Icon
|
|
198
|
+
class="shrink-0 text-muted-foreground rtl:-scale-x-100"
|
|
199
|
+
name="arrow-left"
|
|
200
|
+
size={16}
|
|
201
|
+
/>
|
|
202
|
+
<span class="flex-1 truncate">{panel.label}</span>
|
|
173
203
|
</button>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{panel.label}
|
|
180
|
-
</a>
|
|
181
|
-
</div>
|
|
182
|
-
) : (
|
|
183
|
-
<button
|
|
184
|
-
aria-label={`${n.back}: ${panel.label}`}
|
|
185
|
-
class="-ml-1 mb-3 flex w-full items-center gap-1.5 rounded-[0.65rem] p-1 text-left font-semibold text-foreground text-sm transition-colors hover:bg-muted"
|
|
186
|
-
data-nav-back={panel.parentId}
|
|
187
|
-
type="button"
|
|
188
|
-
>
|
|
189
|
-
<Icon
|
|
190
|
-
class="shrink-0 text-muted-foreground rtl:-scale-x-100"
|
|
191
|
-
name="arrow-left"
|
|
192
|
-
size={16}
|
|
193
|
-
/>
|
|
194
|
-
<span class="flex-1 truncate">{panel.label}</span>
|
|
195
|
-
</button>
|
|
196
|
-
)}
|
|
197
|
-
{/* An inactive panel's contents are deferred (fetched into this
|
|
198
|
-
element on drill-in) when fragments are on; otherwise the
|
|
199
|
-
build-time cache renders them once. */}
|
|
200
|
-
{panel.active ? (
|
|
201
|
-
<Self
|
|
202
|
-
currentRoute={currentRoute}
|
|
203
|
-
depth={1}
|
|
204
|
-
fragmentBase={fragmentBase}
|
|
205
|
-
idPrefix={panel.id}
|
|
206
|
-
ids={ids}
|
|
207
|
-
items={panel.children}
|
|
208
|
-
root={false}
|
|
209
|
-
strings={n}
|
|
210
|
-
/>
|
|
211
|
-
) : fragmentBase ? null : (
|
|
212
|
-
<NavTreeCache node={panel.node} variant={`${panel.id}|${n.back}|${n.deprecated}`}>
|
|
204
|
+
)}
|
|
205
|
+
{/* An inactive panel's contents are deferred (fetched into this
|
|
206
|
+
element on drill-in) when it has a fragment; otherwise the
|
|
207
|
+
build-time cache renders them once. */}
|
|
208
|
+
{panel.active ? (
|
|
213
209
|
<Self
|
|
214
210
|
currentRoute={currentRoute}
|
|
215
211
|
depth={1}
|
|
@@ -220,10 +216,23 @@ const initialId =
|
|
|
220
216
|
root={false}
|
|
221
217
|
strings={n}
|
|
222
218
|
/>
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
219
|
+
) : src ? null : (
|
|
220
|
+
<NavTreeCache node={panel.node} variant={`${panel.id}|${n.back}|${n.deprecated}`}>
|
|
221
|
+
<Self
|
|
222
|
+
currentRoute={currentRoute}
|
|
223
|
+
depth={1}
|
|
224
|
+
fragmentBase={fragmentBase}
|
|
225
|
+
idPrefix={panel.id}
|
|
226
|
+
ids={ids}
|
|
227
|
+
items={panel.children}
|
|
228
|
+
root={false}
|
|
229
|
+
strings={n}
|
|
230
|
+
/>
|
|
231
|
+
</NavTreeCache>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
})}
|
|
227
236
|
</blume-nav>
|
|
228
237
|
) : (
|
|
229
238
|
<ul class="m-0 list-none space-y-px p-0">
|
|
@@ -362,11 +371,11 @@ const initialId =
|
|
|
362
371
|
</summary>
|
|
363
372
|
<div
|
|
364
373
|
class="space-y-0.5 border-border border-l pl-3"
|
|
365
|
-
data-nav-src={open ? undefined : fragmentFor(id)}
|
|
374
|
+
data-nav-src={open ? undefined : fragmentFor(item, id)}
|
|
366
375
|
>
|
|
367
376
|
{/* A closed group's children are deferred (fetched into this
|
|
368
|
-
element on first open) when
|
|
369
|
-
{!open &&
|
|
377
|
+
element on first open) when it has a fragment. */}
|
|
378
|
+
{!open && fragmentFor(item, id) ? null : active ? (
|
|
370
379
|
<Self
|
|
371
380
|
currentRoute={currentRoute}
|
|
372
381
|
depth={depth + 1}
|
|
@@ -146,7 +146,11 @@ interface Props {
|
|
|
146
146
|
* the site root. The HTML counterpart of the homepage-only HTTP `Link`
|
|
147
147
|
* header (see `ai/link-headers.ts`).
|
|
148
148
|
*/
|
|
149
|
-
discovery?: {
|
|
149
|
+
discovery?: {
|
|
150
|
+
agentReadability: boolean;
|
|
151
|
+
aiCatalog: boolean;
|
|
152
|
+
llmsTxt: boolean;
|
|
153
|
+
} | null;
|
|
150
154
|
siteUrl?: string | null;
|
|
151
155
|
pageType?: string;
|
|
152
156
|
published?: string | Date | null;
|
|
@@ -407,7 +411,10 @@ const sidebar = sidebarForRoute(
|
|
|
407
411
|
navigation.root
|
|
408
412
|
);
|
|
409
413
|
// Stable group ids over the full tree, so the scoped view above names its
|
|
410
|
-
// panels and deferred fragments the same way every other page does.
|
|
414
|
+
// panels and deferred fragments the same way every other page does. A
|
|
415
|
+
// container the scoping rebuilt is not in the map, and NavTree renders it in
|
|
416
|
+
// full rather than deferring it to a fragment that would render the full
|
|
417
|
+
// tree's version.
|
|
411
418
|
const navIds = navGroupIds(navigation.sidebar);
|
|
412
419
|
const activeTab = currentTabForRoute(
|
|
413
420
|
navigation.tabs,
|
|
@@ -545,6 +552,25 @@ createIconSprite(Astro.locals);
|
|
|
545
552
|
<link href={withBase("/llms.txt")} rel="describedby" type="text/plain" />
|
|
546
553
|
)
|
|
547
554
|
}
|
|
555
|
+
{/* The AI Catalog / ARD manifest under both relations its two specs
|
|
556
|
+
define: `ai-catalog` (ai-catalog spec) and `ard` (ARD v0.91), each
|
|
557
|
+
pointing at that spec's own well-known path. */}
|
|
558
|
+
{
|
|
559
|
+
discovery?.aiCatalog && (
|
|
560
|
+
<>
|
|
561
|
+
<link
|
|
562
|
+
href={withBase("/.well-known/ai-catalog.json")}
|
|
563
|
+
rel="ai-catalog"
|
|
564
|
+
type="application/ai-catalog+json"
|
|
565
|
+
/>
|
|
566
|
+
<link
|
|
567
|
+
href={withBase("/.well-known/ard.json")}
|
|
568
|
+
rel="ard"
|
|
569
|
+
type="application/json"
|
|
570
|
+
/>
|
|
571
|
+
</>
|
|
572
|
+
)
|
|
573
|
+
}
|
|
548
574
|
{
|
|
549
575
|
markdownMirror && (
|
|
550
576
|
<link href={markdownMirror} rel="alternate" type="text/markdown" />
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* `analytics.scripts` is reached via best-effort global detection or the
|
|
7
7
|
* `blume:track` CustomEvent, which fires unconditionally so a project can bridge
|
|
8
8
|
* the event to anything. Every call no-ops cleanly when a provider isn't present
|
|
9
|
-
* — for example during `blume dev`, where `Analytics.astro` injects nothing
|
|
9
|
+
* — for example during `blume dev`, where `Analytics.astro` injects nothing —
|
|
10
|
+
* and a provider that throws (a consent shim that stubs `gtag` with a raise, a
|
|
11
|
+
* broken snippet) is isolated so it neither starves the providers after it nor
|
|
12
|
+
* surfaces in the feature that reported the event.
|
|
10
13
|
*/
|
|
11
14
|
import { track as vercelTrack } from "@vercel/analytics";
|
|
12
15
|
|
|
@@ -19,7 +22,27 @@ interface AnalyticsWindow {
|
|
|
19
22
|
posthog?: { capture?: (event: string, props?: TrackProps) => void };
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
/** Run one provider call; its failure must not reach the others or the caller. */
|
|
26
|
+
const attempt = (send: () => void): void => {
|
|
27
|
+
try {
|
|
28
|
+
send();
|
|
29
|
+
} catch {
|
|
30
|
+
// Analytics never breaks the feature that reported the event.
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param event The event name.
|
|
36
|
+
* @param props Properties every provider receives.
|
|
37
|
+
* @param local Properties only the `blume:track` CustomEvent carries — free
|
|
38
|
+
* text a site may bridge to a provider on its own terms, but that must not
|
|
39
|
+
* reach third parties unasked (a reader's Ask AI question, for instance).
|
|
40
|
+
*/
|
|
41
|
+
export const track = (
|
|
42
|
+
event: string,
|
|
43
|
+
props: TrackProps,
|
|
44
|
+
local: TrackProps = {}
|
|
45
|
+
): void => {
|
|
23
46
|
// Read through `globalThis` so an SSR/import-time call sees `undefined`
|
|
24
47
|
// instead of a bare-identifier ReferenceError.
|
|
25
48
|
const browserWindow = globalThis.window;
|
|
@@ -31,12 +54,18 @@ export const track = (event: string, props: TrackProps): void => {
|
|
|
31
54
|
const w = browserWindow as typeof browserWindow & AnalyticsWindow;
|
|
32
55
|
|
|
33
56
|
// Vercel Web Analytics — self-gates to a no-op until `window.va` is set up.
|
|
34
|
-
vercelTrack(event, props);
|
|
57
|
+
attempt(() => vercelTrack(event, props));
|
|
35
58
|
// PostHog — the injected array.js stub queues calls until the lib loads.
|
|
36
|
-
w.posthog?.capture?.(event, props);
|
|
59
|
+
attempt(() => w.posthog?.capture?.(event, props));
|
|
37
60
|
// Popular providers wired through `analytics.scripts` (GA4/GTM, Plausible).
|
|
38
|
-
w.gtag?.("event", event, props);
|
|
39
|
-
w.plausible?.(event, { props });
|
|
61
|
+
attempt(() => w.gtag?.("event", event, props));
|
|
62
|
+
attempt(() => w.plausible?.(event, { props }));
|
|
40
63
|
// Universal hook for any other integration.
|
|
41
|
-
|
|
64
|
+
attempt(() =>
|
|
65
|
+
w.dispatchEvent(
|
|
66
|
+
new CustomEvent("blume:track", {
|
|
67
|
+
detail: { event, props: { ...props, ...local } },
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
);
|
|
42
71
|
};
|
|
@@ -151,6 +151,15 @@ const isTabSection = (node: NavNode, tabPaths: Set<string>): boolean => {
|
|
|
151
151
|
* instead of duplicating each tab as a sidebar group. A container left empty by
|
|
152
152
|
* this pruning is dropped too, so no bare heading is stranded. The root tab
|
|
153
153
|
* spans everything, so it never removes anything.
|
|
154
|
+
*
|
|
155
|
+
* A group with no tab section anywhere beneath it is kept as the same object:
|
|
156
|
+
* the stable group ids (`navGroupIds`) and the build-time subtree cache are
|
|
157
|
+
* both keyed by node identity, so a copy would lose its id — its collapsed
|
|
158
|
+
* fragment was then requested by a positional name no route serves — and
|
|
159
|
+
* re-render on every page. A container that did lose a section is rebuilt,
|
|
160
|
+
* so it has no id: the deferred fragments render from the full tree, which
|
|
161
|
+
* would put the section back, so `NavTree` renders such a container in full
|
|
162
|
+
* instead (its untouched children still defer by their own ids).
|
|
154
163
|
*/
|
|
155
164
|
const withoutTabSections = (
|
|
156
165
|
nodes: NavNode[],
|
|
@@ -173,10 +182,15 @@ const withoutTabSections = (
|
|
|
173
182
|
continue;
|
|
174
183
|
}
|
|
175
184
|
if (item.kind === "group") {
|
|
176
|
-
// A container left empty by pruning is dropped, so no bare heading is
|
|
177
|
-
// stranded.
|
|
178
185
|
const children = prune(item.children);
|
|
179
|
-
if (
|
|
186
|
+
if (
|
|
187
|
+
children.length === item.children.length &&
|
|
188
|
+
children.every((child, index) => child === item.children[index])
|
|
189
|
+
) {
|
|
190
|
+
kept.push(item);
|
|
191
|
+
} else if (children.length > 0) {
|
|
192
|
+
// A container left empty by pruning is dropped, so no bare heading
|
|
193
|
+
// is stranded.
|
|
180
194
|
kept.push({ ...item, children });
|
|
181
195
|
}
|
|
182
196
|
} else {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The serializable descriptor an integration factory returns — `posthog({ key })`,
|
|
5
|
+
* `vercel()`, `script({ src })` and their siblings. A descriptor is plain data:
|
|
6
|
+
* the CLI evaluates `blume.config.ts` once, validates what came back, and
|
|
7
|
+
* writes it into the generated project's data snapshot as a JSON literal. That
|
|
8
|
+
* is where the runtime reads it — nothing generated imports the config at
|
|
9
|
+
* request time — so a descriptor can't carry functions, class instances, or
|
|
10
|
+
* anything else JSON drops.
|
|
11
|
+
*
|
|
12
|
+
* Every consumer reads the descriptor instead of switching on a provider name:
|
|
13
|
+
* the head emitter and `blume doctor` branch on `kind`, the generated
|
|
14
|
+
* `.blume/package.json` declares `runtimeDeps`, and the secrets check warns
|
|
15
|
+
* when an entry of `requiredSecrets` is unset.
|
|
16
|
+
*
|
|
17
|
+
* An adapter's verbatim option passthrough is typed as {@link JsonValue} and
|
|
18
|
+
* validated with `z.json()`, so a function, `undefined`, a bigint, or a
|
|
19
|
+
* non-finite number fails config validation with a path instead of vanishing
|
|
20
|
+
* (or throwing) when the snapshot is serialized.
|
|
21
|
+
*/
|
|
22
|
+
export interface AdapterDescriptor<Kind extends string, Options> {
|
|
23
|
+
/** Which integration this is. */
|
|
24
|
+
kind: Kind;
|
|
25
|
+
/** The options the factory was called with, verbatim. */
|
|
26
|
+
options: Options;
|
|
27
|
+
/** Env vars the integration reads at runtime; `blume dev`/`build` warn when one is unset. */
|
|
28
|
+
requiredSecrets: string[];
|
|
29
|
+
/** Extra packages the generated `.blume/package.json` must declare. */
|
|
30
|
+
runtimeDeps: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A value JSON can carry unchanged — what an adapter's passthrough options are
|
|
35
|
+
* typed as. Structurally identical to what `z.json()` accepts.
|
|
36
|
+
*/
|
|
37
|
+
export type JsonValue =
|
|
38
|
+
| string
|
|
39
|
+
| number
|
|
40
|
+
| boolean
|
|
41
|
+
| null
|
|
42
|
+
| JsonValue[]
|
|
43
|
+
| { [key: string]: JsonValue };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The schema for one descriptor `kind`, validating its `options` with the
|
|
47
|
+
* adapter's own option schema. Members of a `z.discriminatedUnion("kind", …)`.
|
|
48
|
+
*/
|
|
49
|
+
export const adapterDescriptorSchema = <
|
|
50
|
+
Kind extends string,
|
|
51
|
+
Options extends z.ZodType,
|
|
52
|
+
>(
|
|
53
|
+
kind: Kind,
|
|
54
|
+
options: Options
|
|
55
|
+
) =>
|
|
56
|
+
z.strictObject({
|
|
57
|
+
kind: z.literal(kind),
|
|
58
|
+
options,
|
|
59
|
+
requiredSecrets: z.array(z.string()),
|
|
60
|
+
runtimeDeps: z.array(z.string()),
|
|
61
|
+
});
|
package/src/core/config-input.ts
CHANGED
|
@@ -681,6 +681,8 @@ export interface AskSuggestion {
|
|
|
681
681
|
/** Backends that can route an Ask AI request. */
|
|
682
682
|
type AskProviderGateway = "gateway" | "openrouter" | "llmgateway";
|
|
683
683
|
type AskProvider = AskProviderGateway | "inkeep" | "openai-compatible";
|
|
684
|
+
/** How much the model reasons before answering (`ai.ask.reasoning`). */
|
|
685
|
+
type AskReasoning = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
684
686
|
|
|
685
687
|
/** How much retrieved documentation each Ask AI question carries. */
|
|
686
688
|
export interface AskRetrievalConfig {
|
|
@@ -715,6 +717,18 @@ export interface AskConfig {
|
|
|
715
717
|
* overrides the built-in preset.
|
|
716
718
|
*/
|
|
717
719
|
baseUrl?: string;
|
|
720
|
+
/**
|
|
721
|
+
* Origins allowed to call the generated endpoint from another site — a
|
|
722
|
+
* marketing page that embeds an ask box, for example — or `"*"` to allow
|
|
723
|
+
* every origin. The route answers preflight requests and names a listed
|
|
724
|
+
* origin on every response, errors included; every other origin stays
|
|
725
|
+
* subject to the browser's same-origin rule. Callers must send the body as
|
|
726
|
+
* JSON with a `content-type: application/json` header, or Astro's cross-site
|
|
727
|
+
* request check rejects the `POST` before the route runs. Only the generated
|
|
728
|
+
* route reads this; an external `endpoint` handles its own CORS and can't be
|
|
729
|
+
* combined with it.
|
|
730
|
+
*/
|
|
731
|
+
cors?: string[];
|
|
718
732
|
/** Turn Ask AI on. Defaults to `false`. */
|
|
719
733
|
enabled?: boolean;
|
|
720
734
|
/**
|
|
@@ -740,6 +754,18 @@ export interface AskConfig {
|
|
|
740
754
|
model?: string;
|
|
741
755
|
/** Which backend routes the request. Defaults to `gateway`. */
|
|
742
756
|
provider?: AskProvider;
|
|
757
|
+
/**
|
|
758
|
+
* How much the model reasons before answering, from `"none"` to `"xhigh"`.
|
|
759
|
+
* Sent as the backend's own reasoning-effort control: the AI SDK's
|
|
760
|
+
* `reasoning` option on the gateway, `reasoning.effort` on OpenRouter, and
|
|
761
|
+
* `reasoning_effort` on OpenAI-compatible endpoints. The model has to
|
|
762
|
+
* support the level — OpenAI rejects one a model doesn't offer — and the
|
|
763
|
+
* endpoint has to accept the parameter; Inkeep has no reasoning control,
|
|
764
|
+
* so the field is rejected there. Omitted keeps the model's default.
|
|
765
|
+
* `"none"` is the fastest and cheapest for grounded docs Q&A, where the
|
|
766
|
+
* retrieved excerpts carry the answer.
|
|
767
|
+
*/
|
|
768
|
+
reasoning?: AskReasoning;
|
|
743
769
|
/**
|
|
744
770
|
* How much documentation each question carries into the model's prompt.
|
|
745
771
|
* Lower values cut time-to-first-token — which dominates on a self-hosted
|
|
@@ -750,6 +776,31 @@ export interface AskConfig {
|
|
|
750
776
|
suggestions?: AskSuggestion[];
|
|
751
777
|
}
|
|
752
778
|
|
|
779
|
+
/** What the AI Catalog (ARD) manifest carries. */
|
|
780
|
+
export interface AiCatalogConfig {
|
|
781
|
+
/** Emit `/.well-known/ai-catalog.json` and `/.well-known/ard.json`. Defaults to `true`. */
|
|
782
|
+
enabled?: boolean;
|
|
783
|
+
/**
|
|
784
|
+
* Representative queries per entry, keyed by the entry's `<namespace>:<name>`
|
|
785
|
+
* — its identifier minus the `urn:air:<host>:` prefix (`mcp:docs`,
|
|
786
|
+
* `skill:blume`, `api:docs`, `reference:<slug>`, `docs:llms-txt`). Each
|
|
787
|
+
* list replaces the generated defaults for that entry: 2–5 short
|
|
788
|
+
* natural-language questions the resource can answer, which agent
|
|
789
|
+
* registries embed for semantic search.
|
|
790
|
+
*
|
|
791
|
+
* ```ts
|
|
792
|
+
* ai: {
|
|
793
|
+
* catalog: {
|
|
794
|
+
* queries: {
|
|
795
|
+
* "mcp:acme": ["how do I install Acme", "search the Acme docs"],
|
|
796
|
+
* },
|
|
797
|
+
* },
|
|
798
|
+
* }
|
|
799
|
+
* ```
|
|
800
|
+
*/
|
|
801
|
+
queries?: Record<string, string[]>;
|
|
802
|
+
}
|
|
803
|
+
|
|
753
804
|
/** What the `llms.txt`/`llms-full.txt` files include. */
|
|
754
805
|
export interface LlmsTxtConfig {
|
|
755
806
|
/**
|
|
@@ -806,6 +857,15 @@ export interface AiConfig {
|
|
|
806
857
|
api?: boolean;
|
|
807
858
|
/** The Ask AI chat assistant. */
|
|
808
859
|
ask?: AskConfig;
|
|
860
|
+
/**
|
|
861
|
+
* The AI Catalog / ARD manifest (`/.well-known/ai-catalog.json`, mirrored
|
|
862
|
+
* at `/.well-known/ard.json`): a domain-level index of the agent-facing
|
|
863
|
+
* resources the site publishes — MCP server, agent skills, the JSON docs
|
|
864
|
+
* API, API references, llms.txt — for agent registries. Needs a
|
|
865
|
+
* `deployment.site`. Defaults to `true`; the object form overrides the
|
|
866
|
+
* generated representative queries per entry.
|
|
867
|
+
*/
|
|
868
|
+
catalog?: boolean | AiCatalogConfig;
|
|
809
869
|
/**
|
|
810
870
|
* Emit `llms.txt` (an index of the docs for LLMs). Defaults to `true`.
|
|
811
871
|
* The object form adds knobs for what the files include.
|
package/src/core/data.ts
CHANGED
|
@@ -140,6 +140,8 @@ export interface BlumeDataConfig {
|
|
|
140
140
|
*/
|
|
141
141
|
discovery: {
|
|
142
142
|
agentReadability: boolean;
|
|
143
|
+
/** Whether the AI Catalog / ARD manifest is published (`ai.catalog`). */
|
|
144
|
+
aiCatalog: boolean;
|
|
143
145
|
/** Whether the JSON docs API and its `/openapi.json` are published. */
|
|
144
146
|
api: boolean;
|
|
145
147
|
llmsTxt: boolean;
|
package/src/core/navigation.ts
CHANGED
|
@@ -112,6 +112,8 @@ interface MutableGroup {
|
|
|
112
112
|
path: string;
|
|
113
113
|
/** The group's URL path (folder route prefix); set as pages are inserted. */
|
|
114
114
|
routePath?: string;
|
|
115
|
+
/** The folder's index page route, when it has one; the group row's link. */
|
|
116
|
+
route?: string;
|
|
115
117
|
label: string;
|
|
116
118
|
icon?: string;
|
|
117
119
|
collapsed?: boolean;
|
|
@@ -214,8 +216,14 @@ const applyFolderMeta = (
|
|
|
214
216
|
sharedMeta: Map<string, FolderMeta>,
|
|
215
217
|
metaPrefix: string,
|
|
216
218
|
sharedMetaPrefix: string,
|
|
217
|
-
indexDisplay: Map<string, SidebarDisplay
|
|
219
|
+
indexDisplay: Map<string, SidebarDisplay>,
|
|
220
|
+
indexRoute: Map<string, string>
|
|
218
221
|
): void => {
|
|
222
|
+
// A folder with an index page links its group row to it — the same shape
|
|
223
|
+
// as an explicit-config group's `root`, and the only sidebar link to the
|
|
224
|
+
// section's own page once the index row is hidden. Index-less folders keep
|
|
225
|
+
// no link: their row would 404.
|
|
226
|
+
group.route = indexRoute.get(group.path);
|
|
219
227
|
// Locale-specific meta wins; a shared `meta.$.*` (keyed by the locale-stripped
|
|
220
228
|
// group path — version-prefixed inside a snapshot) applies to every locale
|
|
221
229
|
// otherwise.
|
|
@@ -254,7 +262,8 @@ const applyFolderMeta = (
|
|
|
254
262
|
sharedMeta,
|
|
255
263
|
metaPrefix,
|
|
256
264
|
sharedMetaPrefix,
|
|
257
|
-
indexDisplay
|
|
265
|
+
indexDisplay,
|
|
266
|
+
indexRoute
|
|
258
267
|
);
|
|
259
268
|
}
|
|
260
269
|
}
|
|
@@ -510,6 +519,7 @@ const toNavNode = (node: MutableNode, display: SidebarDisplay): NavNode => {
|
|
|
510
519
|
kind: "group",
|
|
511
520
|
label: node.label,
|
|
512
521
|
path: node.routePath,
|
|
522
|
+
route: node.route,
|
|
513
523
|
};
|
|
514
524
|
};
|
|
515
525
|
|
|
@@ -529,6 +539,11 @@ const buildFileSystemSidebar = (
|
|
|
529
539
|
// Collected before the hidden filter (like the title check): hiding the index
|
|
530
540
|
// row from the panel shouldn't stop it configuring its group.
|
|
531
541
|
const indexDisplay = new Map<string, SidebarDisplay>();
|
|
542
|
+
// Folder path -> that folder's index page route, for the group row's link.
|
|
543
|
+
// Also collected before the hidden filter: hiding the index row is how a
|
|
544
|
+
// site drops the duplicate label under a linked header, so the link must
|
|
545
|
+
// survive it. The content root is not a group, so its index is skipped.
|
|
546
|
+
const indexRoute = new Map<string, string>();
|
|
532
547
|
|
|
533
548
|
for (const page of pages) {
|
|
534
549
|
// Group by the locale-stripped path so the locale dir is not a nav group.
|
|
@@ -558,6 +573,9 @@ const buildFileSystemSidebar = (
|
|
|
558
573
|
if (page.meta.sidebar.display && !page.fallback) {
|
|
559
574
|
indexDisplay.set(dirs.join("/"), page.meta.sidebar.display);
|
|
560
575
|
}
|
|
576
|
+
if (dirs.length > 0) {
|
|
577
|
+
indexRoute.set(dirs.join("/"), page.route);
|
|
578
|
+
}
|
|
561
579
|
}
|
|
562
580
|
|
|
563
581
|
if (page.meta.sidebar.hidden) {
|
|
@@ -611,7 +629,8 @@ const buildFileSystemSidebar = (
|
|
|
611
629
|
sharedMeta,
|
|
612
630
|
metaPrefix,
|
|
613
631
|
sharedMetaPrefix,
|
|
614
|
-
indexDisplay
|
|
632
|
+
indexDisplay,
|
|
633
|
+
indexRoute
|
|
615
634
|
);
|
|
616
635
|
sortNodes(root.children, diagnostics);
|
|
617
636
|
hoistPages(root.children, display, true);
|