1/// <reference types="@astrojs/starlight/locals" />
2import { defineRouteMiddleware } from "@astrojs/starlight/route-data";
3import type { StarlightRouteData } from "@astrojs/starlight/route-data";
4import { activeTab } from "./docs-tabs";
5
6type SidebarItem = StarlightRouteData["sidebar"][number];
7type SidebarLink = Extract<SidebarItem, { type: "link" }>;
8
9/**
10 * The config sidebar in `astro.config.mjs` is one top-level group per docs
11 * tab (see `docs-tabs.ts`). This middleware swaps the whole-site sidebar for
12 * just the active tab's entries so the sidebar only ever navigates within
13 * the current tab, and recomputes prev/next pagination so it never walks
14 * across a tab boundary.
15 *
16 * Blog routes are skipped — starlight-blog owns their sidebar (re-bucketed
17 * by `blog-sidebar.ts`).
18 */
19export const onRequest = defineRouteMiddleware(async (context, next) => {
20 await next();
21
22 const pathname = context.url.pathname;
23 if (pathname === "/blog" || pathname.startsWith("/blog/")) return;
24
25 const { starlightRoute } = context.locals;
26 const tab = activeTab(pathname);
27 const group = starlightRoute.sidebar.find(
28 (item): item is Extract<SidebarItem, { type: "group" }> =>
29 item.type === "group" && item.label === tab.label,
30 );
31 if (!group) return;
32
33 starlightRoute.sidebar = group.entries;
34
35 const links = flattenLinks(group.entries);
36 const index = links.findIndex((link) => link.isCurrent);
37 const { prev, next: nextLink } = starlightRoute.entry.data;
38 starlightRoute.pagination = {
39 prev: override(prev, index > 0 ? links[index - 1] : undefined),
40 next: override(nextLink, index !== -1 ? links[index + 1] : undefined),
41 };
42});
43
44/**
45 * Apply a page's `prev` / `next` frontmatter to the sidebar-derived link, the
46 * same way Starlight's own pagination does: `false` removes the link, a
47 * string relabels it, and `{ link, label }` replaces it outright.
48 */
49function override(
50 config: StarlightRouteData["entry"]["data"]["prev"],
51 link: SidebarLink | undefined,
52): SidebarLink | undefined {
53 if (config === undefined || config === true) return link;
54 if (config === false) return undefined;
55 if (typeof config === "string") {
56 return link ? { ...link, label: config } : undefined;
57 }
58 const href = config.link ?? link?.href;
59 const label = config.label ?? link?.label;
60 if (!href || !label) return link;
61 return {
62 type: "link",
63 label,
64 href,
65 isCurrent: false,
66 badge: undefined,
67 attrs: {},
68 };
69}
70
71function flattenLinks(items: SidebarItem[]): SidebarLink[] {
72 const links: SidebarLink[] = [];
73 for (const item of items) {
74 if (item.type === "link") links.push(item);
75 else links.push(...flattenLinks(item.entries));
76 }
77 return links;
78}