This doc explains every way we've extended or patched Starlight. There are six distinct layers, ordered roughly from most to least invasive.
Trigger: postinstall in package.json runs npx patch-package after every npm install.
Patch files live in patches/. These are the nuclear option — used when Starlight gives us no other extension point.
Three hunks against three different files:
components/SidebarSublist.astro — Sidebar icons
Starlight has no native API for per-item icons in the sidebar. We inject ~/components/SidebarIcon.astro (Lottie animations) before each sidebar label:
-<span>{entry.label}</span>
+<span>{entry.icon && <SidebarIcon {...entry.icon} />}{entry.label}</span>The icon property comes from sidebar entry attrs set in our sidebar generation logic.
integrations/remark-rehype-utils.ts — Remark/rehype pipeline scope
Starlight's remark/rehype processing originally bails out for any file not inside the docs collection. We widen the guard to also process partials/ and changelog/:
-if (!normalizePath(file.path).startsWith(docsCollectionPath)) return false;
+if (
+ !normalizePath(file.path).startsWith(docsCollectionPath) &&
+ !normalizePath(file.path).includes("/src/content/partials/") &&
+ !normalizePath(file.path).includes("/src/content/changelog/")
+) return false;Without this, heading transforms, tab components, etc. don't run inside partials or changelogs.
user-components/Tabs.astro — Injectable icon component
Makes the Icon component inside <Tabs> replaceable via prop, so callers (e.g. TypeScriptExample) can swap in a custom icon renderer:
+IconComponent?: typeof Icon;
-const { syncKey } = Astro.props;
+const { syncKey, IconComponent = Icon } = Astro.props;
...
-{icon && <Icon name={icon} />}
+{icon && <IconComponent name={icon} />}DocSearch.astro — Keyboard shortcut hint UI
We disable the / shortcut in DocSearch (re-routing it to the sidebar search input instead). The original button always rendered a / slash icon. The patch makes the hint adaptive:
- Default: renders styled
<kbd>-style key badges (Ctrl K) - Only shows the slash SVG icon when
/is enabled ANDCtrl/Cmd+Kis disabled
The slash icon CSS is moved from a static <style> block into a JS conditional that runs after docsearch() initializes and reads options.keyboardShortcuts.
index.ts — Schema extension
Adds keyboardShortcuts to the DocSearch Zod config schema so the above is type-safe:
keyboardShortcuts: z.object({
"Ctrl/Cmd+K": z.boolean().optional(),
"/": z.boolean().optional(),
}).optional();Upgrade note: Both patches are pinned to exact package versions (
starlight@0.36.0,starlight-docsearch@0.6.0). Upgrading either package requires regenerating the patch withnpx patch-package @astrojs/starlightafter manually re-applying the changes.
Location: astro.config.ts → vite.resolve.alias
Starlight doesn't expose Page.astro in its official components: override map. We shim it at the Vite bundler level:
vite: {
resolve: {
alias: {
"./Page.astro": fileURLToPath(new URL("./src/components/overrides/Page.astro", import.meta.url)),
"../components/Page.astro": fileURLToPath(new URL("./src/components/overrides/Page.astro", import.meta.url)),
},
},
},Both alias paths are needed because different internal Starlight files import Page.astro using different relative depths.
What src/components/overrides/Page.astro does:
It wraps Starlight's original Page.astro and mutates the starlightRoute locals before passing them through:
- ToC regeneration — calls
generateTableOfContents(html)against the fully-rendered HTML instead of using Starlight's AST-derived ToC. This means our custom heading transforms (slugs, shift-headings) are reflected correctly. - Sidebar replacement — calls
getSidebar(Astro)to build a product-specific sidebar, replacing Starlight's static autogenerated one. - Pagination — flattens the custom sidebar and computes prev/next from it, respecting
data-group-labelattrs for section labels. - Description generation — if
frontmatter.descriptionis set, renders it through Markdown. If not, extracts a description from the rendered HTML. lastUpdatedsuppression — setsdata.lastUpdated = undefinedwhen arevieweddate is present, so only one recency signal shows in the UI.- Full-width layout — injects
--sl-content-width: 67.5remon pages without a ToC.
Registered in astro.config.ts under starlight({ components: { ... } }).
All files are in src/components/overrides/.
These are overrides that use the official Starlight components: API.
See STARLIGHT_OVERRIDES.md.
Registered in astro.config.ts under starlight({ plugins: [...] }).
| Plugin | When active | Notes |
|---|---|---|
starlightLinksValidator |
RUN_LINK_CHECK=true only |
Validates internal links at build time. Long exclude list for dynamic routes, API paths, wildcard patterns. errorOnInvalidHashes and errorOnLocalLinks both disabled. |
starlightDocSearch |
Always | Algolia DocSearch via clientOptionsModule: "./src/plugins/docsearch/index.ts" — sets app ID/key, rewrites result URLs to current origin, adds "View all results" footer, disables the / keyboard shortcut. |
starlightImageZoom |
Always | Enables the zoom functionality consumed by MarkdownContent.astro. |
starlightScrollToTop |
Always | Custom double-chevron SVG path, progress ring (white), tooltip "Back to top", hidden on homepage. |
File: src/plugins/starlight/route-data.ts
Registered: astro.config.ts → starlight({ routeMiddleware: "..." })
Runs on every route via defineRouteMiddleware. Validates and normalizes the tags frontmatter array:
- For each tag, searches the
allowedTagsallowlist (fromsrc/schemas/tags.ts) for a case-insensitive match againstlabelor anyvariants. - If found, replaces the raw tag string with the canonical
label. - If not found, throws a build error with a link to the style guide.
This is why you can write javascript or JavaScript in frontmatter and it gets normalized to the canonical casing before it hits the UI or search index.
File: ec.config.mjs
Starlight's code block rendering runs through Expressive Code. We register six plugins:
| Plugin | File | Purpose |
|---|---|---|
pluginWorkersPlayground |
src/plugins/expressive-code/workers-playground.js |
Adds "Open in Workers Playground" button to eligible code blocks |
pluginOutputFrame |
src/plugins/expressive-code/output-frame.js |
Custom "output" frame type for terminal output blocks |
pluginDefaultTitles |
src/plugins/expressive-code/default-titles.js |
Auto-assigns language-based titles (e.g. "TypeScript") to blocks without an explicit title= |
pluginCollapsibleSections |
@expressive-code/plugin-collapsible-sections |
Upstream plugin enabling collapse ranges in code blocks |
pluginGraphqlApiExplorer |
src/plugins/expressive-code/graphql-api-explorer.js |
Adds "Open in GraphQL Explorer" button to graphql blocks |
pluginLineNumbers |
@expressive-code/plugin-line-numbers |
Upstream plugin; line numbers are off by default (showLineNumbers: false) |
Other config:
- Themes — Cloudflare's custom
solarflare-themedark and light VSCode themes curlalias —curlblocks are highlighted asshextractFileNameFromCode: false— disables auto-extracting titles from code comments- Style overrides — 1px border radius 0.25rem, adjusted text marker luminance for light/dark
These aren't Starlight-specific but affect how MDX content is processed. Registered in astro.config.ts → markdown:
| Plugin | File | Purpose |
|---|---|---|
remarkValidateImages |
src/plugins/remark/validate-images |
Build-time validation that image paths resolve |
rehypeMermaid |
src/plugins/rehype/mermaid.ts |
Converts mermaid code blocks to diagrams |
rehypeExternalLinks |
src/plugins/rehype/external-links.ts |
Adds target="_blank" rel="noopener" to external links |
rehypeHeadingSlugs |
src/plugins/rehype/heading-slugs.ts |
Custom slug generation for heading IDs |
rehypeAutolinkHeadings |
src/plugins/rehype/autolink-headings.ts |
Injects anchor links next to headings (used by MarkdownContent.astro CSS) |
rehypeTitleFigure |
rehype-title-figure (upstream) |
Wraps images with title attrs in <figure>/<figcaption> |
rehypeShiftHeadings |
src/plugins/rehype/shift-headings.ts |
Shifts heading levels (e.g. H1→H2) for content that starts at H1 |
These run via Starlight's markdown.headingLinks: false (disabled — we handle anchor links ourselves via rehypeAutolinkHeadings).