Sidebar
Starlight provides a sidebar, but there are some issues:
It doesnβt preserve state (scroll, open/closed submenus) on page switch(fixed in v0.26.0)- Itβs not possible to have a root page for submenus
There are some workarounds:
Starlight - Scroll Current Page into View
Note: Use Starlight v0.26.0 or later instead.
-
Create
Sidebar
componentsrc/components/Sidebar.astro ---import type { Props } from "@astrojs/starlight/props";import Default from "@astrojs/starlight/components/Sidebar.astro";---<Default {...Astro.props}><slot /></Default><script>function isElementVisibleInContainer(element: Element, container: Element) {const elementRect = element.getBoundingClientRect();const containerRect = container.getBoundingClientRect();return (elementRect.top >= containerRect.top &&elementRect.left >= containerRect.left &&elementRect.bottom <= containerRect.bottom &&elementRect.right <= containerRect.right);}// Function to persist scroll position on page changefunction setScrollPosition() {const sidebar = document.querySelector("#starlight__sidebar");const selectedMenuItem = document.querySelector("#starlight__sidebar a[aria-current='page']");if (sidebar &&selectedMenuItem &&!isElementVisibleInContainer(selectedMenuItem, sidebar)) {selectedMenuItem.scrollIntoView();}}setScrollPosition();</script> -
Configure Astro
astro.config.mjs export default defineConfig({integrations: [starlight({components: {Sidebar: "./src/components/Sidebar.astro",},}),],});
Alternative sidebars for Starlight
Starlight provides configurable sidebar, which is good enough for documentation. For digital garden we may want to use different approach, for example:
Alphabetical list
---import type { Props } from "@astrojs/starlight/props";import Default from "@astrojs/starlight/components/Sidebar.astro";
import { getBrainDb } from "@braindb/astro";import { isContent } from "@lib/braindb.mjs";import type { Document } from "@braindb/core";
const firstChar = (str: string) => String.fromCodePoint(str.codePointAt(0)!);
const docsByChar = new Map<string, Document[]>();(await getBrainDb().documents()).forEach((doc) => { if (isContent(doc)) { const char = firstChar(doc.title()).toUpperCase(); docsByChar.set(char, docsByChar.get(char) || []); docsByChar.get(char)?.push(doc); }});
const comparator = new Intl.Collator("en");const sidebarAlpha = [...docsByChar.keys()] .sort(comparator.compare) .map((char) => { let collapsed = true; return { type: "group", label: char, entries: docsByChar.get(char)?.map((doc) => { const isCurrent = doc.path() === `/${Astro.props.id}`; if (isCurrent) collapsed = false; return { type: "link", href: doc.url(), isCurrent, // @ts-ignore label: doc.frontmatter()?.["sidebar"]?.["label"] || doc.title(), // @ts-ignore badge: doc.frontmatter()?.["sidebar"]?.["badge"], // @ts-ignore attrs: doc.frontmatter()?.["sidebar"]?.["attrs"] || {}, }; }), collapsed, badge: undefined, }; }) as Props["sidebar"];
sidebarAlpha.unshift(Astro.props.sidebar[0]);---
<Default {...Astro.props} sidebar={sidebarAlpha}><slot /></Default>
Articles grouped by date
---import type { Props } from "@astrojs/starlight/props";import Default from "@astrojs/starlight/components/Sidebar.astro";
import { getBrainDb } from "@braindb/astro";import { isContent } from "@lib/braindb.mjs";import type { Document } from "@braindb/core";
const docsByDate = new Map<string, Document[]>();(await getBrainDb().documents({ sort: ["updated_at", "desc"] })).forEach((doc) => { if (isContent(doc)) { const date = doc.updatedAt().toISOString().split("T")[0]; docsByDate.set(date, docsByDate.get(date) || []); docsByDate.get(date)?.push(doc); }});
const sidebarDates = Array.from(docsByDate.keys()) .map((d) => [d, d.split("-").reverse().join(".")]) .map(([date, label]) => { let collapsed = true; return { type: "group", label, entries: docsByDate.get(date)?.map((doc) => { const isCurrent = doc.path() === `/${Astro.props.id}`; if (isCurrent) collapsed = false; return { type: "link", href: doc.url(), isCurrent, // @ts-ignore label: doc.frontmatter()?.["sidebar"]?.["label"] || doc.title(), // @ts-ignore badge: doc.frontmatter()?.["sidebar"]?.["badge"], // @ts-ignore attrs: doc.frontmatter()?.["sidebar"]?.["attrs"] || {}, }; }), collapsed, badge: undefined, }; }) as Props["sidebar"];
sidebarDates.unshift(Astro.props.sidebar[0]);---
<Default {...Astro.props} sidebar={sidebarDates}><slot /></Default>
Articles grouped by tags
---import type { Props } from "@astrojs/starlight/props";import Default from "@astrojs/starlight/components/Sidebar.astro";
import { getBrainDb } from "@braindb/astro";import { isContent } from "@lib/braindb.mjs";import type { Document } from "@braindb/core";
const docsByTags = new Map<string, Document[]>();(await getBrainDb().documents()).forEach((doc) => { if (isContent(doc)) { // @ts-expect-error doc.frontmatter().tags.forEach((tag: string) => { docsByTags.set(tag, docsByTags.get(tag) || []); docsByTags.get(tag)?.push(doc); }); }});
const comparator = new Intl.Collator("en");const sidebarTags = [...docsByTags.keys()] .sort(comparator.compare) .map((tag) => { let collapsed = true; return { type: "group", label: `#${tag}`, entries: docsByTags.get(tag)?.map((doc) => { const isCurrent = doc.path() === `/${Astro.props.id}`; if (isCurrent) collapsed = false; return { type: "link", href: doc.url(), isCurrent, // @ts-ignore label: doc.frontmatter()?.["sidebar"]?.["label"] || doc.title(), // @ts-ignore badge: doc.frontmatter()?.["sidebar"]?.["badge"], // @ts-ignore attrs: doc.frontmatter()?.["sidebar"]?.["attrs"] || {}, }; }), collapsed, badge: undefined, }; }) as Props["sidebar"];
sidebarTags.unshift(Astro.props.sidebar[0]);---
<Default {...Astro.props} sidebar={sidebarTags}><slot /></Default>
Multiple sidebars experiment
---import { Tabs, TabItem } from "@astrojs/starlight/components";// ... other code see above---
<Tabs> <TabItem label="Default"> <Default {...Astro.props}><slot /></Default> </TabItem> <TabItem label="A-Z"> <Default {...Astro.props} sidebar={sidebarAlpha}><slot /></Default> </TabItem> <TabItem label="Latest"> <Default {...Astro.props} sidebar={sidebarDates}><slot /></Default> </TabItem></Tabs>
TODO
- Render as partials and fetch from the server instead of prerendering, because there are too many DOM nodes.
- Tabs should preserve state across navigations.
- The scroll area should be inside the
TabItem
.