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
Section titled “Starlight - Scroll Current Page into View”Note: Use Starlight v0.26.0 or later instead.
-
Create
Sidebar
componentsrc/components/Sidebar.astro ---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
Section titled “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
Section titled “Alphabetical list”---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
Section titled “Articles grouped by date”---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
Section titled “Articles grouped by tags”---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
Section titled “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>
- 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
.