summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Swithinbank2023-10-06 18:53:53 +0200
committerGitHub2023-10-06 18:53:53 +0200
commit140e729a8bf12f805ae0b7e2b5ad959cf68d8e22 (patch)
tree903b20b9de85d386f04938b7f839710000c5f8f4
parent903a57942ceb99b68672c3fa54622b39cc5d76f8 (diff)
downloadIT.starlight-140e729a8bf12f805ae0b7e2b5ad959cf68d8e22.tar.gz
IT.starlight-140e729a8bf12f805ae0b7e2b5ad959cf68d8e22.tar.bz2
IT.starlight-140e729a8bf12f805ae0b7e2b5ad959cf68d8e22.zip
Support component customisation (#709)
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com>
-rw-r--r--.changeset/plenty-donkeys-lay.md21
-rw-r--r--docs/src/content/docs/guides/overriding-components.md133
-rw-r--r--docs/src/content/docs/guides/sidebar.mdx2
-rw-r--r--docs/src/content/docs/index.mdx2
-rw-r--r--docs/src/content/docs/reference/configuration.md16
-rw-r--r--docs/src/content/docs/reference/overrides.md374
-rw-r--r--packages/starlight/404.astro13
-rw-r--r--packages/starlight/__tests__/basics/route-data.test.ts87
-rw-r--r--packages/starlight/__tests__/basics/toc.test.ts2
-rw-r--r--packages/starlight/__tests__/edit-url/edit-url.test.ts48
-rw-r--r--packages/starlight/__tests__/edit-url/vitest.config.ts8
-rw-r--r--packages/starlight/__tests__/test-config.ts8
-rw-r--r--packages/starlight/components/Banner.astro8
-rw-r--r--packages/starlight/components/ContentPanel.astro4
-rw-r--r--packages/starlight/components/EditLink.astro29
-rw-r--r--packages/starlight/components/FallbackContentNotice.astro7
-rw-r--r--packages/starlight/components/Footer.astro37
-rw-r--r--packages/starlight/components/Head.astro (renamed from packages/starlight/components/HeadSEO.astro)17
-rw-r--r--packages/starlight/components/Header.astro42
-rw-r--r--packages/starlight/components/Hero.astro15
-rw-r--r--packages/starlight/components/LanguageSelect.astro5
-rw-r--r--packages/starlight/components/LastUpdated.astro29
-rw-r--r--packages/starlight/components/MarkdownContent.astro4
-rw-r--r--packages/starlight/components/MobileMenuFooter.astro17
-rw-r--r--packages/starlight/components/MobileMenuToggle.astro6
-rw-r--r--packages/starlight/components/MobileTableOfContents.astro (renamed from packages/starlight/components/TableOfContents/MobileTableOfContents.astro)55
-rw-r--r--packages/starlight/components/Page.astro120
-rw-r--r--packages/starlight/components/PageFrame.astro (renamed from packages/starlight/layout/PageFrame.astro)24
-rw-r--r--packages/starlight/components/PageSidebar.astro (renamed from packages/starlight/components/RightSidebarPanel.astro)28
-rw-r--r--packages/starlight/components/PageTitle.astro16
-rw-r--r--packages/starlight/components/Pagination.astro (renamed from packages/starlight/components/PrevNextLinks.astro)12
-rw-r--r--packages/starlight/components/RightSidebar.astro36
-rw-r--r--packages/starlight/components/Search.astro8
-rw-r--r--packages/starlight/components/Sidebar.astro43
-rw-r--r--packages/starlight/components/SiteTitle.astro21
-rw-r--r--packages/starlight/components/SkipLink.astro8
-rw-r--r--packages/starlight/components/SocialIcons.astro41
-rw-r--r--packages/starlight/components/TableOfContents.astro27
-rw-r--r--packages/starlight/components/TableOfContents/TableOfContentsList.astro2
-rw-r--r--packages/starlight/components/TableOfContents/starlight-toc.ts4
-rw-r--r--packages/starlight/components/ThemeProvider.astro1
-rw-r--r--packages/starlight/components/ThemeSelect.astro5
-rw-r--r--packages/starlight/components/TwoColumnContent.astro (renamed from packages/starlight/layout/TwoColumnContent.astro)6
-rw-r--r--packages/starlight/constants.ts4
-rw-r--r--packages/starlight/index.astro6
-rw-r--r--packages/starlight/integrations/virtual-user-config.ts3
-rw-r--r--packages/starlight/layout/Page.astro132
-rw-r--r--packages/starlight/package.json129
-rw-r--r--packages/starlight/props.ts1
-rw-r--r--packages/starlight/schemas/components.ts256
-rw-r--r--packages/starlight/schemas/social.ts65
-rw-r--r--packages/starlight/utils/generateToC.ts (renamed from packages/starlight/components/TableOfContents/generateToC.ts)3
-rw-r--r--packages/starlight/utils/navigation.ts2
-rw-r--r--packages/starlight/utils/route-data.ts97
-rw-r--r--packages/starlight/utils/routing.ts10
-rw-r--r--packages/starlight/utils/user-config.ts40
-rw-r--r--packages/starlight/utils/validateLogoImports.ts21
-rw-r--r--packages/starlight/virtual.d.ts37
-rw-r--r--packages/starlight/vitest.config.ts13
59 files changed, 1683 insertions, 527 deletions
diff --git a/.changeset/plenty-donkeys-lay.md b/.changeset/plenty-donkeys-lay.md
new file mode 100644
index 00000000..cb041fbd
--- /dev/null
+++ b/.changeset/plenty-donkeys-lay.md
@@ -0,0 +1,21 @@
+---
+'@astrojs/starlight': minor
+---
+
+Add support for overriding Starlight’s built-in components
+
+⚠️ **BREAKING CHANGE** — The page footer is now included on pages with `template: splash` in their frontmatter. Previously, this was not the case. If you are using `template: splash` and want to continue to hide footer elements, disable them in your frontmatter:
+
+```md
+---
+title: Landing page
+template: splash
+# Disable unwanted footer elements as needed
+editUrl: false
+lastUpdated: false
+prev: false
+next: false
+---
+```
+
+⚠️ **BREAKING CHANGE** — This change involved refactoring the structure of some of Starlight’s built-in components slightly. If you were previously overriding these using other techniques, you may need to adjust your code. \ No newline at end of file
diff --git a/docs/src/content/docs/guides/overriding-components.md b/docs/src/content/docs/guides/overriding-components.md
new file mode 100644
index 00000000..71ea7e1d
--- /dev/null
+++ b/docs/src/content/docs/guides/overriding-components.md
@@ -0,0 +1,133 @@
+---
+title: Overriding Components
+description: Learn how to override Starlight’s built-in components to add custom elements to your documentation site’s UI.
+sidebar:
+ badge: New
+---
+
+Starlight’s default UI and configuration options are designed to be flexible and work for a range of content. Much of Starlight's default appearance can be customized with [CSS](/guides/css-and-tailwind/) and [configuration options](/guides/customization/).
+
+When you need more than what’s possible out of the box, Starlight supports building your own custom components to extend or override (completely replace) its default components.
+
+## When to override
+
+Overriding Starlight’s default components can be useful when:
+
+- You want to change how a part of Starlight’s UI looks in a way not possible with [custom CSS](/css-and-tailwind/).
+- You want to change how a part of Starlight’s UI behaves.
+- You want to add some additional UI alongside Starlight’s existing UI.
+
+## How to override
+
+1. Choose the Starlight component you want to override.
+ You can find a full list of components in the [Overrides Reference](/reference/overrides/).
+
+ This example will override Starlight’s [`SocialIcons`](/reference/overrides/#socialicons) component in the page nav bar.
+
+2. Create an Astro component to replace the Starlight component with.
+ This example renders a contact link.
+
+ ```astro
+ ---
+ // src/components/EmailLink.astro
+ import type { Props } from '@astrojs/starlight/props';
+ ---
+
+ <a href="mailto:houston@example.com">E-mail Me</a>
+ ```
+
+3. Tell Starlight to use your custom component in the [`components`](/reference/configuration/#components) configuration option in `astro.config.mjs`:
+
+ ```js {9-12}
+ // astro.config.mjs
+ import { defineConfig } from 'astro/config';
+ import starlight from '@astrojs/starlight';
+
+ export default defineConfig({
+ integrations: [
+ starlight({
+ title: 'My Docs with Overrides',
+ components: {
+ // Override the default `SocialLinks` component.
+ SocialIcons: './src/components/EmailLink.astro',
+ },
+ }),
+ ],
+ });
+ ```
+
+## Reuse a built-in component
+
+You can build with Starlight’s default UI components just as you would with your own: importing and rendering them in your own custom components. This allows you to keep all of Starlight's basic UI within your design, while adding extra UI alongside them.
+
+The example below shows a custom component that renders an e-mail link along with the default `SocialLinks` component:
+
+```astro {4,8}
+---
+// src/components/EmailLink.astro
+import type { Props } from '@astrojs/starlight/props';
+import Default from '@astrojs/starlight/SocialIcons.astro';
+---
+
+<a href="mailto:houston@example.com">E-mail Me</a>
+<Default {...Astro.props}><slot /></Default>
+```
+
+When rendering a built-in component inside a custom component:
+
+- Spread `Astro.props` into it. This makes sure that it receives all the data it needs to render.
+- Add a [`<slot />`](https://docs.astro.build/en/core-concepts/astro-components/#slots) inside the default component. This makes sure that if the component is passed any child elements, Astro knows where to render them.
+
+## Use page data
+
+When overriding a Starlight component, your custom implementation receives a standard `Astro.props` object containing all the data for the current page.
+This allows you to use these values to control how your component template renders.
+
+For example, you can read the page’s frontmatter values as `Astro.props.entry.data`. In the following example, a replacement [`PageTitle`](/reference/overrides/#pagetitle) component uses this to display the current page’s title:
+
+```astro {5} "{title}"
+---
+// src/components/Title.astro
+import type { Props } from '@astrojs/starlight/props';
+
+const { title } = Astro.props.entry.data;
+---
+
+<h1 id="_top">{title}</h1>
+
+<style>
+ h1 {
+ font-family: 'Comic Sans';
+ }
+</style>
+```
+
+Learn more about all the available props in the [Overrides Reference](/reference/overrides/#prop-types).
+
+### Only override on specific pages
+
+Component overrides apply to all pages. However, you can conditionally render using values from `Astro.props` to determine when to show your custom UI, when to show Starlight’s default UI, or even when to show something entirely different.
+
+In the following example, a component overriding Starlight's [`Footer`](/reference/overrides/#footer-1) displays "Built with Starlight 🌟" on the homepage only, and otherwise shows the default footer on all other pages:
+
+```astro
+---
+// src/components/ConditionalFooter.astro
+import type { Props } from '@astrojs/starlight/props';
+import Default from '@astrojs/starlight/Footer.astro';
+
+const isHomepage = Astro.props.slug === '';
+---
+
+{
+ isHomepage ? (
+ <footer>Built with Starlight 🌟</footer>
+ ) : (
+ <Default {...Astro.props}>
+ <slot />
+ </Default>
+ )
+}
+```
+
+Learn more about conditional rendering in [Astro’s Template Syntax guide](https://docs.astro.build/en/core-concepts/astro-syntax/#dynamic-html).
diff --git a/docs/src/content/docs/guides/sidebar.mdx b/docs/src/content/docs/guides/sidebar.mdx
index 6a732e6d..33256366 100644
--- a/docs/src/content/docs/guides/sidebar.mdx
+++ b/docs/src/content/docs/guides/sidebar.mdx
@@ -1,8 +1,6 @@
---
title: Sidebar Navigation
description: Learn how to set up and customize your Starlight site’s sidebar navigation links.
-sidebar:
- badge: New
---
import FileTree from '../../../components/file-tree.astro';
diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx
index 6836d865..6cef6afa 100644
--- a/docs/src/content/docs/index.mdx
+++ b/docs/src/content/docs/index.mdx
@@ -5,6 +5,8 @@ head:
content: Starlight 🌟 Build documentation sites with Astro
description: Starlight helps you build beautiful, high-performance documentation websites with Astro.
template: splash
+editUrl: false
+lastUpdated: false
banner:
content: |
<span class="ph-banner">
diff --git a/docs/src/content/docs/reference/configuration.md b/docs/src/content/docs/reference/configuration.md
index ad2d8591..b392f7f5 100644
--- a/docs/src/content/docs/reference/configuration.md
+++ b/docs/src/content/docs/reference/configuration.md
@@ -436,3 +436,19 @@ Sets the delimiter between page title and site title in the page’s `<title>` t
By default, every page has a `<title>` of `Page Title | Site Title`.
For example, this page is titled “Configuration Reference” and this site is titled “Starlight”, so the `<title>` for this page is “Configuration Reference | Starlight”.
+
+### `components`
+
+**type:** `Record<string, string>`
+
+Provide the paths to components to override Starlight’s default implementations.
+
+```js
+starlight({
+ components: {
+ SocialLinks: './src/components/MySocialLinks.astro',
+ },
+});
+```
+
+See the [Overrides Reference](/reference/overrides/) for details of all the components that you can override.
diff --git a/docs/src/content/docs/reference/overrides.md b/docs/src/content/docs/reference/overrides.md
new file mode 100644
index 00000000..6b2ea2c4
--- /dev/null
+++ b/docs/src/content/docs/reference/overrides.md
@@ -0,0 +1,374 @@
+---
+title: Overrides Reference
+description: An overview of the components and component props supported by Starlight overrides.
+tableOfContents:
+ maxHeadingLevel: 4
+---
+
+You can override Starlight’s built-in components by providing paths to replacement components in Starlight’s [`components`](/reference/configuration#components) configuration option.
+This page lists all components available to override and links to their default implementations on GitHub.
+
+Learn more in the [Guide to Overriding Components](/guides/overriding-components/).
+
+## Component props
+
+All components can access a standard `Astro.props` object that contains information about the current page.
+
+To type your custom components, import the `Props` type from Starlight:
+
+```astro
+---
+import type { Props } from '@astrojs/starlight/props';
+
+const { hasSidebar } = Astro.props;
+// ^ type: boolean
+---
+```
+
+This will give you autocomplete and types when accessing `Astro.props`.
+
+### Props
+
+Starlight will pass the following props to your custom components.
+
+#### `dir`
+
+**Type:** `'ltr' | 'rtl'`
+
+Page writing direction.
+
+#### `lang`
+
+**Type:** `string`
+
+BCP-47 language tag for this page’s locale, e.g. `en`, `zh`, or `pt-BR`.
+
+#### `locale`
+
+**Type:** `string | undefined`
+
+The base path at which a language is served. `undefined` for root locale slugs.
+
+#### `slug`
+
+**Type:** `string`
+
+The slug for this page generated from the content filename.
+
+#### `id`
+
+**Type:** `string`
+
+The unique ID for this page based on the content filename.
+
+#### `isFallback`
+
+**Type:** `true | undefined`
+
+`true` if this page is untranslated in the current language and using fallback content from the default locale.
+Only used in multilingual sites.
+
+#### `entryMeta`
+
+**Type:** `{ dir: 'ltr' | 'rtl'; lang: string }`
+
+Locale metadata for the page content. Can be different from top-level locale values when a page is using fallback content.
+
+#### `entry`
+
+The Astro content collection entry for the current page.
+Includes frontmatter values for the current page at `entry.data`.
+
+```ts
+entry: {
+ data: {
+ title: string;
+ description: string | undefined;
+ // etc.
+ }
+}
+```
+
+Learn more about the shape of this object in [Astro’s Collection Entry Type](https://docs.astro.build/en/reference/api-reference/#collection-entry-type) reference.
+
+#### `sidebar`
+
+**Type:** `SidebarEntry[]`
+
+Site navigation sidebar entries for this page.
+
+#### `hasSidebar`
+
+**Type:** `boolean`
+
+Whether or not the sidebar should be displayed on this page.
+
+#### `pagination`
+
+**Type:** `{ prev?: Link; next?: Link }`
+
+Links to the previous and next page in the sidebar if enabled.
+
+#### `toc`
+
+**Type:** `{ minHeadingLevel: number; maxHeadingLevel: number; items: TocItem[] } | undefined`
+
+Table of contents for this page if enabled.
+
+#### `headings`
+
+**Type:** `{ depth: number; slug: string; text: string }[]`
+
+Array of all Markdown headings extracted from the current page.
+Use [`toc`](#toc) instead if you want to build a table of contents that respects Starlight’s configuration options.
+
+#### `lastUpdated`
+
+**Type:** `Date | undefined`
+
+JavaScript `Date` object representing when this page was last updated if enabled.
+
+#### `editUrl`
+
+**Type:** `URL | undefined`
+
+`URL` object for the address where this page can be edited if enabled.
+
+---
+
+## Components
+
+### Head
+
+These components are rendered inside each page’s `<head>` element.
+They should only include [elements permitted inside `<head>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head#see_also).
+
+#### `Head`
+
+**Default component:** [`Head.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Head.astro)
+
+Component rendered inside each page’s `<head>`.
+Includes important tags including `<title>`, and `<meta charset="utf-8">`.
+
+Override this component as a last resort.
+Prefer the [`head`](/reference/configuration#head) option Starlight config if possible.
+
+#### `ThemeProvider`
+
+**Default component:** [`ThemeProvider.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/ThemeProvider.astro)
+
+Component rendered inside `<head>` that sets up dark/light theme support.
+The default implementation includes an inline script and a `<template>` used by the script in [`<ThemeSelect />`](#themeselect).
+
+---
+
+### Accessibility
+
+#### `SkipLink`
+
+**Default component:** [`SkipLink.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/SkipLink.astro)
+
+Component rendered as the first element inside `<body>` which links to the main page content for accessibility.
+The default implementation is hidden until a user focuses it by tabbing with their keyboard.
+
+---
+
+### Layout
+
+These components are responsible for laying out Starlight’s components and managing views across different breakpoints.
+Overriding these comes with significant complexity.
+When possible, prefer overriding a lower-level component.
+
+#### `PageFrame`
+
+**Default component:** [`PageFrame.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/PageFrame.astro)
+
+Layout component wrapped around most of the page content.
+The default implementation sets up the header–sidebar–main layout and includes `header` and `sidebar` named slots along with a default slot for the main content.
+It also renders [`<MobileMenuToggle />`](#mobilemenutoggle) to support toggling the sidebar navigation on small (mobile) viewports.
+
+#### `MobileMenuToggle`
+
+**Default component:** [`MobileMenuToggle.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/MobileMenuToggle.astro)
+
+Component rendered inside [`<PageFrame>`](#pageframe) that is responsible for toggling the sidebar navigation on small (mobile) viewports.
+
+#### `TwoColumnContent`
+
+**Default component:** [`TwoColumnContent.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/TwoColumnContent.astro)
+
+Layout component wrapped around the main content column and right sidebar (table of contents).
+The default implementation handles the switch between a single-column, small-viewport layout and a two-column, larger-viewport layout.
+
+---
+
+### Header
+
+These components render Starlight’s top navigation bar.
+
+#### `Header`
+
+**Default component:** [`Header.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Header.astro)
+
+Header component displayed at the top of every page.
+The default implementation displays [`<SiteTitle />`](#sitetitle), [`<Search />`](#search), [`<SocialIcons />`](#socialicons), [`<ThemeSelect />`](#themeselect), and [`<LanguageSelect />`](#languageselect).
+
+#### `SiteTitle`
+
+**Default component:** [`SiteTitle.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/SiteTitle.astro)
+
+Component rendered at the start of the site header to render the site title.
+The default implementation includes logic for rendering logos defined in Starlight config.
+
+#### `Search`
+
+**Default component:** [`Search.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Search.astro)
+
+Component used to render Starlight’s search UI.
+The default implementation includes the button in the header and the code for displaying a search modal when it is clicked and loading [Pagefind’s UI](https://pagefind.app/).
+
+#### `SocialIcons`
+
+**Default component:** [`SocialIcons.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/SocialIcons.astro)
+
+Component rendered in the site header including social icon links.
+The default implementation uses the [`social`](/reference/configuration#social) option in Starlight config to render icons and links.
+
+#### `ThemeSelect`
+
+**Default component:** [`ThemeSelect.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/ThemeSelect.astro)
+
+Component rendered in the site header that allows users to select their preferred color scheme.
+
+#### `LanguageSelect`
+
+**Default component:** [`LanguageSelect.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/LanguageSelect.astro)
+
+Component rendered in the site header that allows users to switch to a different language.
+
+---
+
+### Global Sidebar
+
+Starlight’s global sidebar includes the main site navigation.
+On narrow viewports this is hidden behind a drop-down menu.
+
+#### `Sidebar`
+
+**Default component:** [`Sidebar.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Sidebar.astro)
+
+Component rendered before page content that contains global navigation.
+The default implementation displays as a sidebar on wide enough viewports and inside a drop-down menu on small (mobile) viewports.
+It also renders [`<MobileMenuFooter />`](#mobilemenufooter) to show additional items inside the mobile menu.
+
+#### `MobileMenuFooter`
+
+**Default component:** [`MobileMenuFooter.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/MobileMenuFooter.astro)
+
+Component rendered at the bottom of the mobile drop-down menu.
+The default implementation renders [`<ThemeSelect />`](#themeselect) and [`<LanguageSelect />`](#languageselect).
+
+---
+
+### Page Sidebar
+
+Starlight’s page sidebar is responsible for displaying a table of contents outlining the current page’s subheadings.
+On narrow viewports this collapse into a sticky, drop-down menu.
+
+#### `PageSidebar`
+
+**Default component:** [`PageSidebar.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/PageSidebar.astro)
+
+Component rendered before the main page’s content to display a table of contents.
+The default implementation renders [`<TableOfContents />`](#tableofcontents) and [`<MobileTableOfContents />`](#mobiletableofcontents).
+
+#### `TableOfContents`
+
+**Default component:** [`TableOfContents.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/TableOfContents.astro)
+
+Component that renders the current page’s table of contents on wider viewports.
+
+#### `MobileTableOfContents`
+
+**Default component:** [`MobileTableOfContents.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/MobileTableOfContents.astro)
+
+Component that renders the current page’s table of contents on small (mobile) viewports.
+
+---
+
+### Content
+
+These components are rendered in the main column of page content.
+
+#### `Banner`
+
+**Default component:** [`Banner.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Banner.astro)
+
+Banner component rendered at the top of each page.
+The default implementation uses the page’s [`banner`](/reference/frontmatter#banner) frontmatter value to decide whether or not to render.
+
+#### `ContentPanel`
+
+**Default component:** [`ContentPanel.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/ContentPanel.astro)
+
+Layout component used to wrap sections of the main content column.
+
+#### `PageTitle`
+
+**Default component:** [`PageTitle.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/PageTitle.astro)
+
+Component containing the `<h1>` element for the current page.
+
+Implementations should ensure they set `id="_top"` on the `<h1>` element as in the default implementation.
+
+#### `FallbackContentNotice`
+
+**Default component:** [`FallbackContentNotice.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/FallbackContentNotice.astro)
+
+Notice displayed to users on pages where a translation for the current language is not available.
+Only used on multilingual sites.
+
+#### `Hero`
+
+**Default component:** [`Hero.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Hero.astro)
+
+Component rendered at the top of the page when [`hero`](/reference/frontmatter#hero) is set in frontmatter.
+The default implementation shows a large title, tagline, and call-to-action links alongside an optional image.
+
+#### `MarkdownContent`
+
+**Default component:** [`MarkdownContent.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/MarkdownContent.astro)
+
+Component rendered around each page’s main content.
+The default implementation sets up basic styles to apply to Markdown content.
+
+---
+
+### Footer
+
+These components are rendered at the bottom of the main column of page content.
+
+#### `Footer`
+
+**Default component:** [`Footer.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Footer.astro)
+
+Footer component displayed at the bottom of each page.
+The default implementation displays [`<LastUpdated />`](#lastupdated), [`<Pagination />`](#pagination), and [`<EditLink />`](#editlink).
+
+#### `LastUpdated`
+
+**Default component:** [`LastUpdated.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/LastUpdated.astro)
+
+Component rendered in the page footer to display the last-updated date.
+
+#### `EditLink`
+
+**Default component:** [`EditLink.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/EditLink.astro)
+
+Component rendered in the page footer to display a link to where the page can be edited.
+
+#### `Pagination`
+
+**Default component:** [`Pagination.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Pagination.astro)
+
+Component rendered in the page footer to display navigation arrows between previous/next pages.
diff --git a/packages/starlight/404.astro b/packages/starlight/404.astro
index 3b10bf45..913c5143 100644
--- a/packages/starlight/404.astro
+++ b/packages/starlight/404.astro
@@ -2,7 +2,8 @@
import { getEntry } from 'astro:content';
import config from 'virtual:starlight/user-config';
import EmptyContent from './components/EmptyMarkdown.md';
-import Page from './layout/Page.astro';
+import Page from './components/Page.astro';
+import { generateRouteData } from './utils/route-data';
import type { StarlightDocsEntry } from './utils/routing';
import { useTranslations } from './utils/translations';
@@ -24,6 +25,8 @@ const fallbackEntry: StarlightDocsEntry = {
editUrl: false,
head: [],
hero: { tagline: t('404.text'), actions: [] },
+ pagefind: false,
+ sidebar: { hidden: false },
},
render: async () => ({
Content: EmptyContent,
@@ -35,8 +38,10 @@ const fallbackEntry: StarlightDocsEntry = {
const userEntry = await getEntry('docs', '404');
const entry = userEntry || fallbackEntry;
const { Content, headings } = await entry.render();
+const route = generateRouteData({
+ props: { ...entryMeta, entryMeta, headings, entry, id: entry.id, slug: entry.slug },
+ url: Astro.url,
+});
---
-<Page {headings} entry={entry} id={entry.id} slug={entry.slug} {...entryMeta} {entryMeta}>
- <Content />
-</Page>
+<Page {...route}><Content /></Page>
diff --git a/packages/starlight/__tests__/basics/route-data.test.ts b/packages/starlight/__tests__/basics/route-data.test.ts
new file mode 100644
index 00000000..13522d02
--- /dev/null
+++ b/packages/starlight/__tests__/basics/route-data.test.ts
@@ -0,0 +1,87 @@
+import { expect, test, vi } from 'vitest';
+import { generateRouteData } from '../../utils/route-data';
+import { routes } from '../../utils/routing';
+
+vi.mock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ docs: [
+ ['index.mdx', { title: 'Home Page' }],
+ ['getting-started.mdx', { title: 'Splash', template: 'splash' }],
+ ['showcase.mdx', { title: 'ToC Disabled', tableOfContents: false }],
+ ['environmental-impact.md', { title: 'Explicit update date', lastUpdated: new Date() }],
+ ],
+ })
+);
+
+test('adds data to route shape', () => {
+ const route = routes[0]!;
+ const data = generateRouteData({
+ props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
+ url: new URL('https://example.com'),
+ });
+ expect(data.hasSidebar).toBe(true);
+ expect(data).toHaveProperty('lastUpdated');
+ expect(data.toc).toMatchInlineSnapshot(`
+ {
+ "items": [
+ {
+ "children": [],
+ "depth": 2,
+ "slug": "_top",
+ "text": "Overview",
+ },
+ ],
+ "maxHeadingLevel": 3,
+ "minHeadingLevel": 2,
+ }
+ `);
+ expect(data.pagination).toMatchInlineSnapshot(`
+ {
+ "next": {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/environmental-impact/",
+ "isCurrent": false,
+ "label": "Explicit update date",
+ "type": "link",
+ },
+ "prev": undefined,
+ }
+ `);
+ expect(data.sidebar.map((entry) => entry.label)).toMatchInlineSnapshot(`
+ [
+ "Home Page",
+ "Explicit update date",
+ "Splash",
+ "ToC Disabled",
+ ]
+ `);
+});
+
+test('disables table of contents for splash template', () => {
+ const route = routes[1]!;
+ const data = generateRouteData({
+ props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
+ url: new URL('https://example.com/getting-started/'),
+ });
+ expect(data.toc).toBeUndefined();
+});
+
+test('disables table of contents if frontmatter includes `tableOfContents: false`', () => {
+ const route = routes[2]!;
+ const data = generateRouteData({
+ props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
+ url: new URL('https://example.com/showcase/'),
+ });
+ expect(data.toc).toBeUndefined();
+});
+
+test('uses explicit last updated date from frontmatter', () => {
+ const route = routes[3]!;
+ const data = generateRouteData({
+ props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
+ url: new URL('https://example.com/showcase/'),
+ });
+ expect(data.lastUpdated).toBeInstanceOf(Date);
+ expect(data.lastUpdated).toEqual(route.entry.data.lastUpdated);
+});
diff --git a/packages/starlight/__tests__/basics/toc.test.ts b/packages/starlight/__tests__/basics/toc.test.ts
index 51745d7b..ae1136f9 100644
--- a/packages/starlight/__tests__/basics/toc.test.ts
+++ b/packages/starlight/__tests__/basics/toc.test.ts
@@ -1,5 +1,5 @@
import { expect, test } from 'vitest';
-import { generateToC } from '../../components/TableOfContents/generateToC';
+import { generateToC } from '../../utils/generateToC';
const defaultOpts = { minHeadingLevel: 2, maxHeadingLevel: 3, title: 'Overview' };
diff --git a/packages/starlight/__tests__/edit-url/edit-url.test.ts b/packages/starlight/__tests__/edit-url/edit-url.test.ts
new file mode 100644
index 00000000..25ab98aa
--- /dev/null
+++ b/packages/starlight/__tests__/edit-url/edit-url.test.ts
@@ -0,0 +1,48 @@
+import { expect, test, vi } from 'vitest';
+import { generateRouteData } from '../../utils/route-data';
+import { routes } from '../../utils/routing';
+
+vi.mock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ docs: [
+ ['index.mdx', { title: 'Home Page' }],
+ ['getting-started.mdx', { title: 'Getting Started' }],
+ [
+ 'showcase.mdx',
+ { title: 'Custom edit link', editUrl: 'https://example.com/custom-edit?link' },
+ ],
+ ],
+ })
+);
+
+test('synthesizes edit URL using file location and `editLink.baseUrl`', () => {
+ {
+ const route = routes[0]!;
+ const data = generateRouteData({
+ props: { ...route, headings: [] },
+ url: new URL('https://example.com'),
+ });
+ expect(data.editUrl?.href).toBe(
+ 'https://github.com/withastro/starlight/edit/main/docs/src/content/docs/index.mdx'
+ );
+ }
+ {
+ const route = routes[1]!;
+ const data = generateRouteData({
+ props: { ...route, headings: [] },
+ url: new URL('https://example.com'),
+ });
+ expect(data.editUrl?.href).toBe(
+ 'https://github.com/withastro/starlight/edit/main/docs/src/content/docs/getting-started.mdx'
+ );
+ }
+});
+
+test('uses frontmatter `editUrl` if defined', () => {
+ const route = routes[2]!;
+ const data = generateRouteData({
+ props: { ...route, headings: [] },
+ url: new URL('https://example.com'),
+ });
+ expect(data.editUrl?.href).toBe('https://example.com/custom-edit?link');
+});
diff --git a/packages/starlight/__tests__/edit-url/vitest.config.ts b/packages/starlight/__tests__/edit-url/vitest.config.ts
new file mode 100644
index 00000000..4035c234
--- /dev/null
+++ b/packages/starlight/__tests__/edit-url/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineVitestConfig } from '../test-config';
+
+export default defineVitestConfig({
+ title: 'Docs With Edit Links',
+ editLink: {
+ baseUrl: 'https://github.com/withastro/starlight/edit/main/docs/',
+ },
+});
diff --git a/packages/starlight/__tests__/test-config.ts b/packages/starlight/__tests__/test-config.ts
index 9d06a693..8d0c42e6 100644
--- a/packages/starlight/__tests__/test-config.ts
+++ b/packages/starlight/__tests__/test-config.ts
@@ -6,11 +6,9 @@ import { vitePluginStarlightUserConfig } from '../integrations/virtual-user-conf
import { StarlightConfigSchema } from '../utils/user-config';
export function defineVitestConfig(config: z.input<typeof StarlightConfigSchema>) {
+ const root = new URL('./', import.meta.url);
+ const srcDir = new URL('./src/', root);
return getViteConfig({
- plugins: [
- vitePluginStarlightUserConfig(StarlightConfigSchema.parse(config), {
- root: new URL(import.meta.url),
- }),
- ],
+ plugins: [vitePluginStarlightUserConfig(StarlightConfigSchema.parse(config), { root, srcDir })],
});
}
diff --git a/packages/starlight/components/Banner.astro b/packages/starlight/components/Banner.astro
index e6307bec..107baa70 100644
--- a/packages/starlight/components/Banner.astro
+++ b/packages/starlight/components/Banner.astro
@@ -1,10 +1,10 @@
---
-interface Props {
- content: string;
-}
+import type { Props } from '../props';
+
+const { banner } = Astro.props.entry.data;
---
-<div class="sl-banner" set:html={Astro.props.content} />
+{banner && <div class="sl-banner" set:html={banner.content} />}
<style>
.sl-banner {
diff --git a/packages/starlight/components/ContentPanel.astro b/packages/starlight/components/ContentPanel.astro
index d4089408..3f23fcd0 100644
--- a/packages/starlight/components/ContentPanel.astro
+++ b/packages/starlight/components/ContentPanel.astro
@@ -1,3 +1,7 @@
+---
+import type { Props } from '../props';
+---
+
<div class="content-panel">
<div class="sl-container"><slot /></div>
</div>
diff --git a/packages/starlight/components/EditLink.astro b/packages/starlight/components/EditLink.astro
index 4de01348..f5f5f65b 100644
--- a/packages/starlight/components/EditLink.astro
+++ b/packages/starlight/components/EditLink.astro
@@ -1,34 +1,15 @@
---
-import type { CollectionEntry } from 'astro:content';
-import config from 'virtual:starlight/user-config';
-import project from 'virtual:starlight/project-context';
-import { useTranslations } from '../utils/translations';
import Icon from '../user-components/Icon.astro';
-
-interface Props {
- data: CollectionEntry<'docs'>['data'];
- id: CollectionEntry<'docs'>['id'];
- locale: string | undefined;
-}
+import type { Props } from '../props';
+import { useTranslations } from '../utils/translations';
const t = useTranslations(Astro.props.locale);
-const { editUrl } = Astro.props.data;
-const srcPath = project.srcDir.replace(project.root, '');
-
-let { baseUrl } = config.editLink;
-if (baseUrl && baseUrl.at(-1) !== '/') baseUrl += '/';
-
-const url =
- typeof editUrl === 'string'
- ? editUrl
- : baseUrl
- ? baseUrl + srcPath + 'content/docs/' + Astro.props.id
- : undefined;
+const { editUrl } = Astro.props;
---
{
- editUrl !== false && url && (
- <a href={url} class="sl-flex">
+ editUrl && (
+ <a href={editUrl} class="sl-flex">
<Icon name="pencil" size="1.2em" />
{t('page.editLink')}
</a>
diff --git a/packages/starlight/components/FallbackContentNotice.astro b/packages/starlight/components/FallbackContentNotice.astro
index 9b7ca623..171eeb15 100644
--- a/packages/starlight/components/FallbackContentNotice.astro
+++ b/packages/starlight/components/FallbackContentNotice.astro
@@ -1,10 +1,7 @@
---
-import { useTranslations } from '../utils/translations';
import Icon from '../user-components/Icon.astro';
-
-interface Props {
- locale: string | undefined;
-}
+import type { Props } from '../props';
+import { useTranslations } from '../utils/translations';
const t = useTranslations(Astro.props.locale);
---
diff --git a/packages/starlight/components/Footer.astro b/packages/starlight/components/Footer.astro
index 2d14dee7..f5936f01 100644
--- a/packages/starlight/components/Footer.astro
+++ b/packages/starlight/components/Footer.astro
@@ -1,42 +1,15 @@
---
-import config from 'virtual:starlight/user-config';
-import { type SidebarEntry, getPrevNextLinks } from '../utils/navigation';
-import type { StarlightDocsEntry } from '../utils/routing';
-import type { LocaleData } from '../utils/slugs';
+import type { Props } from '../props';
-import LastUpdated from '../components/LastUpdated.astro';
-import PrevNextLinks from '../components/PrevNextLinks.astro';
-import EditLink from './EditLink.astro';
-
-interface Props extends LocaleData {
- entry: StarlightDocsEntry;
- sidebar: SidebarEntry[];
-}
-
-const { entry, dir, lang, locale, sidebar } = Astro.props;
-const prevNextLinks = getPrevNextLinks(sidebar, config.pagination, {
- prev: entry.data.prev,
- next: entry.data.next,
-});
+import { EditLink, LastUpdated, Pagination } from 'virtual:starlight/components';
---
<footer>
<div class="meta sl-flex">
- {config.editLink.baseUrl && <EditLink data={entry.data} id={entry.id} {locale} />}
- {
- (entry.data.lastUpdated ?? config.lastUpdated) && (
- <LastUpdated
- id={entry.id}
- {lang}
- lastUpdated={
- typeof entry.data.lastUpdated !== 'boolean' ? entry.data.lastUpdated : undefined
- }
- {locale}
- />
- )
- }
+ <EditLink {...Astro.props} />
+ <LastUpdated {...Astro.props} />
</div>
- <PrevNextLinks {...prevNextLinks} {dir} {locale} />
+ <Pagination {...Astro.props} />
</footer>
<style>
diff --git a/packages/starlight/components/HeadSEO.astro b/packages/starlight/components/Head.astro
index e744103c..ec2431fc 100644
--- a/packages/starlight/components/HeadSEO.astro
+++ b/packages/starlight/components/Head.astro
@@ -1,18 +1,15 @@
---
-import type { CollectionEntry, z } from 'astro:content';
+import type { z } from 'astro/zod';
import config from 'virtual:starlight/user-config';
+import { version } from '../package.json';
import type { HeadConfigSchema } from '../schemas/head';
+import { fileWithBase } from '../utils/base';
import { createHead } from '../utils/head';
import { localizedUrl } from '../utils/localizedUrl';
-import { fileWithBase } from '../utils/base';
-import { version } from '../package.json';
-
-interface Props {
- data: CollectionEntry<'docs'>['data'];
- lang: string;
-}
+import type { Props } from '../props';
-const { data, lang } = Astro.props;
+const { entry, lang } = Astro.props;
+const { data } = entry;
const canonical = Astro.site ? new URL(Astro.url.pathname, Astro.site) : undefined;
const description = data.description || config.description;
@@ -94,7 +91,7 @@ if (config.social?.twitter) {
tag: 'meta',
attrs: {
name: 'twitter:site',
- content: new URL(config.social.twitter).pathname,
+ content: new URL(config.social.twitter.url).pathname,
},
});
}
diff --git a/packages/starlight/components/Header.astro b/packages/starlight/components/Header.astro
index 6ed8aa1c..51bf9a08 100644
--- a/packages/starlight/components/Header.astro
+++ b/packages/starlight/components/Header.astro
@@ -1,24 +1,28 @@
---
-import LanguageSelect from './LanguageSelect.astro';
-import Search from './Search.astro';
-import SiteTitle from './SiteTitle.astro';
-import SocialIcons from './SocialIcons.astro';
-import ThemeSelect from './ThemeSelect.astro';
+import type { Props } from '../props';
-interface Props {
- locale: string | undefined;
-}
-
-const { locale } = Astro.props;
+import {
+ LanguageSelect,
+ Search,
+ SiteTitle,
+ SocialIcons,
+ ThemeSelect,
+} from 'virtual:starlight/components';
---
<div class="header sl-flex">
- <SiteTitle {locale} />
- <Search {locale} />
+ <div class="sl-flex">
+ <SiteTitle {...Astro.props} />
+ </div>
+ <div class="sl-flex">
+ <Search {...Astro.props} />
+ </div>
<div class="sl-hidden md:sl-flex right-group">
- <SocialIcons />
- <ThemeSelect {locale} />
- <LanguageSelect {locale} />
+ <div class="sl-flex social-icons">
+ <SocialIcons {...Astro.props} />
+ </div>
+ <ThemeSelect {...Astro.props} />
+ <LanguageSelect {...Astro.props} />
</div>
</div>
@@ -30,10 +34,16 @@ const { locale } = Astro.props;
height: 100%;
}
- .right-group {
+ .right-group,
+ .social-icons {
gap: 1rem;
align-items: center;
}
+ .social-icons::after {
+ content: '';
+ height: 2rem;
+ border-inline-end: 1px solid var(--sl-color-gray-5);
+ }
@media (min-width: 50rem) {
:global(:root[data-has-sidebar]) {
diff --git a/packages/starlight/components/Hero.astro b/packages/starlight/components/Hero.astro
index fbc3692c..bd9e8202 100644
--- a/packages/starlight/components/Hero.astro
+++ b/packages/starlight/components/Hero.astro
@@ -1,21 +1,18 @@
---
-import type { CollectionEntry } from 'astro:content';
import { Image } from 'astro:assets';
+import { PAGE_TITLE_ID } from '../constants';
+import type { Props } from '../props';
import CallToAction from './CallToAction.astro';
-interface Props {
- fallbackTitle: string;
- hero: NonNullable<CollectionEntry<'docs'>['data']['hero']>;
-}
-
-const { title = Astro.props.fallbackTitle, tagline, image, actions } = Astro.props.hero;
+const { data } = Astro.props.entry;
+const { title = data.title, tagline, image, actions = [] } = data.hero || {};
const imageAttrs = {
loading: 'eager' as const,
decoding: 'async' as const,
width: 400,
height: 400,
- alt: image?.alt,
+ alt: image?.alt || '',
};
---
@@ -33,7 +30,7 @@ const imageAttrs = {
}
<div class="sl-flex stack">
<div class="sl-flex copy">
- <h1 id="_top" data-page-title set:html={title} />
+ <h1 id={PAGE_TITLE_ID} data-page-title set:html={title} />
{tagline && <div class="tagline" set:html={tagline} />}
</div>
{
diff --git a/packages/starlight/components/LanguageSelect.astro b/packages/starlight/components/LanguageSelect.astro
index d59df1c1..077935b3 100644
--- a/packages/starlight/components/LanguageSelect.astro
+++ b/packages/starlight/components/LanguageSelect.astro
@@ -3,10 +3,7 @@ import config from 'virtual:starlight/user-config';
import { localizedUrl } from '../utils/localizedUrl';
import { useTranslations } from '../utils/translations';
import Select from './Select.astro';
-
-interface Props {
- locale: string | undefined;
-}
+import type { Props } from '../props';
/**
* Get the equivalent of the current page path for the passed locale.
diff --git a/packages/starlight/components/LastUpdated.astro b/packages/starlight/components/LastUpdated.astro
index 278057e9..73a6c2d0 100644
--- a/packages/starlight/components/LastUpdated.astro
+++ b/packages/starlight/components/LastUpdated.astro
@@ -1,36 +1,17 @@
---
-import type { CollectionEntry } from 'astro:content';
-import { fileURLToPath } from 'node:url';
-import project from 'virtual:starlight/project-context';
-import { getFileCommitDate } from '../utils/git';
+import type { Props } from '../props';
import { useTranslations } from '../utils/translations';
-interface Props {
- id: CollectionEntry<'docs'>['id'];
- lang: string;
- lastUpdated: Date | undefined;
- locale: string | undefined;
-}
-
-const { id, lang, lastUpdated, locale } = Astro.props;
+const { lang, lastUpdated, locale } = Astro.props;
const t = useTranslations(locale);
-
-const currentFilePath = fileURLToPath(new URL('src/content/docs/' + id, project.root));
-
-let date = lastUpdated;
-try {
- if (!date) {
- ({ date } = getFileCommitDate(currentFilePath, 'newest'));
- }
-} catch {}
---
{
- date && (
+ lastUpdated && (
<p>
{t('page.lastUpdated')}{' '}
- <time datetime={date.toISOString()}>
- {date.toLocaleDateString(lang, { dateStyle: 'medium' })}
+ <time datetime={lastUpdated.toISOString()}>
+ {lastUpdated.toLocaleDateString(lang, { dateStyle: 'medium' })}
</time>
</p>
)
diff --git a/packages/starlight/components/MarkdownContent.astro b/packages/starlight/components/MarkdownContent.astro
index ea44bd6c..5e866a1a 100644
--- a/packages/starlight/components/MarkdownContent.astro
+++ b/packages/starlight/components/MarkdownContent.astro
@@ -1,3 +1,7 @@
+---
+import type { Props } from '../props';
+---
+
<div class="content"><slot /></div>
<style>
diff --git a/packages/starlight/components/MobileMenuFooter.astro b/packages/starlight/components/MobileMenuFooter.astro
new file mode 100644
index 00000000..a5258da1
--- /dev/null
+++ b/packages/starlight/components/MobileMenuFooter.astro
@@ -0,0 +1,17 @@
+---
+import { LanguageSelect, ThemeSelect } from 'virtual:starlight/components';
+import type { Props } from '../props';
+---
+
+<div class="mobile-preferences sl-flex">
+ <ThemeSelect {...Astro.props} />
+ <LanguageSelect {...Astro.props} />
+</div>
+
+<style>
+ .mobile-preferences {
+ justify-content: space-between;
+ border-top: 1px solid var(--sl-color-gray-6);
+ padding: 0.5rem 0;
+ }
+</style>
diff --git a/packages/starlight/components/MobileMenuToggle.astro b/packages/starlight/components/MobileMenuToggle.astro
index a6e620b0..e6619bbb 100644
--- a/packages/starlight/components/MobileMenuToggle.astro
+++ b/packages/starlight/components/MobileMenuToggle.astro
@@ -1,10 +1,8 @@
---
-import Icon from '../user-components/Icon.astro';
+import type { Props } from '../props';
import { useTranslations } from '../utils/translations';
-interface Props {
- locale: string | undefined;
-}
+import Icon from '../user-components/Icon.astro';
const t = useTranslations(Astro.props.locale);
---
diff --git a/packages/starlight/components/TableOfContents/MobileTableOfContents.astro b/packages/starlight/components/MobileTableOfContents.astro
index 34fe1d5e..d5ec3cc6 100644
--- a/packages/starlight/components/TableOfContents/MobileTableOfContents.astro
+++ b/packages/starlight/components/MobileTableOfContents.astro
@@ -1,36 +1,33 @@
---
-import { useTranslations } from '../../utils/translations';
-import Icon from '../../user-components/Icon.astro';
-import TableOfContentsList from './TableOfContentsList.astro';
-import type { TocItem } from './generateToC';
+import { useTranslations } from '../utils/translations';
+import Icon from '../user-components/Icon.astro';
+import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
+import type { Props } from '../props';
-interface Props {
- toc: TocItem[];
- locale: string | undefined;
- maxHeadingLevel: number;
- minHeadingLevel: number;
-}
-
-const { locale, toc, maxHeadingLevel, minHeadingLevel } = Astro.props;
+const { locale, toc } = Astro.props;
const t = useTranslations(locale);
---
-<mobile-starlight-toc data-min-h={minHeadingLevel} data-max-h={maxHeadingLevel}>
- <nav aria-labelledby="starlight__on-this-page--mobile" class="lg:sl-hidden">
- <details id="starlight__mobile-toc">
- <summary id="starlight__on-this-page--mobile" class="sl-flex">
- <div class="toggle sl-flex">
- {t('tableOfContents.onThisPage')}
- <Icon name={'right-caret'} class="caret" size="1rem" />
- </div>
- <span class="display-current">{toc[0]?.text}</span>
- </summary>
- <div class="dropdown">
- <TableOfContentsList toc={toc} isMobile />
- </div>
- </details>
- </nav>
-</mobile-starlight-toc>
+{
+ toc && (
+ <mobile-starlight-toc data-min-h={toc.minHeadingLevel} data-max-h={toc.maxHeadingLevel}>
+ <nav aria-labelledby="starlight__on-this-page--mobile">
+ <details id="starlight__mobile-toc">
+ <summary id="starlight__on-this-page--mobile" class="sl-flex">
+ <div class="toggle sl-flex">
+ {t('tableOfContents.onThisPage')}
+ <Icon name={'right-caret'} class="caret" size="1rem" />
+ </div>
+ <span class="display-current" />
+ </summary>
+ <div class="dropdown">
+ <TableOfContentsList toc={toc.items} isMobile />
+ </div>
+ </details>
+ </nav>
+ </mobile-starlight-toc>
+ )
+}
<style>
nav {
@@ -112,7 +109,7 @@ const t = useTranslations(locale);
</style>
<script>
- import { StarlightTOC } from './starlight-toc';
+ import { StarlightTOC } from './TableOfContents/starlight-toc';
class MobileStarlightTOC extends StarlightTOC {
override set current(link: HTMLAnchorElement) {
diff --git a/packages/starlight/components/Page.astro b/packages/starlight/components/Page.astro
new file mode 100644
index 00000000..306a0ed2
--- /dev/null
+++ b/packages/starlight/components/Page.astro
@@ -0,0 +1,120 @@
+---
+import type { Props } from '../props';
+
+// Built-in CSS styles.
+import '../style/props.css';
+import '../style/reset.css';
+import '../style/shiki.css';
+import '../style/util.css';
+
+// Components — can override built-in CSS, but not user CSS.
+import {
+ Banner,
+ ContentPanel,
+ PageTitle,
+ FallbackContentNotice,
+ Footer,
+ Header,
+ Head,
+ Hero,
+ MarkdownContent,
+ PageSidebar,
+ Sidebar,
+ SkipLink,
+ ThemeProvider,
+ PageFrame,
+ TwoColumnContent,
+} from 'virtual:starlight/components';
+
+// Remark component CSS (needs to override `MarkdownContent.astro`)
+import '../style/asides.css';
+
+// Important that this is the last import so it can override built-in styles.
+import 'virtual:starlight/user-css';
+
+const pagefindEnabled =
+ Astro.props.entry.slug !== '404' &&
+ !Astro.props.entry.slug.endsWith('/404') &&
+ Astro.props.entry.data.pagefind !== false;
+---
+
+<html
+ lang={Astro.props.lang}
+ dir={Astro.props.dir}
+ data-has-toc={Boolean(Astro.props.toc)}
+ data-has-sidebar={Astro.props.hasSidebar}
+ data-has-hero={Boolean(Astro.props.entry.data.hero)}
+>
+ <head>
+ <Head {...Astro.props} />
+ <style>
+ html:not([data-has-toc]) {
+ --sl-mobile-toc-height: 0rem;
+ }
+ html:not([data-has-sidebar]) {
+ --sl-content-width: 67.5rem;
+ }
+ /* Add scroll padding to ensure anchor headings aren't obscured by nav */
+ html {
+ /* Additional padding is needed to account for the mobile TOC */
+ scroll-padding-top: calc(1.5rem + var(--sl-nav-height) + var(--sl-mobile-toc-height));
+ }
+ main {
+ padding-bottom: 3vh;
+ }
+ @media (min-width: 50em) {
+ [data-has-sidebar] {
+ --sl-content-inline-start: var(--sl-sidebar-width);
+ }
+ }
+ @media (min-width: 72em) {
+ html {
+ scroll-padding-top: calc(1.5rem + var(--sl-nav-height));
+ }
+ }
+ </style>
+ <ThemeProvider {...Astro.props} />
+ </head>
+ <body>
+ <SkipLink {...Astro.props} />
+ <PageFrame {...Astro.props}>
+ <Header slot="header" {...Astro.props} />
+ {Astro.props.hasSidebar && <Sidebar slot="sidebar" {...Astro.props} />}
+ <TwoColumnContent {...Astro.props}>
+ <PageSidebar slot="right-sidebar" {...Astro.props} />
+ <main
+ data-pagefind-body={pagefindEnabled}
+ lang={Astro.props.entryMeta.lang}
+ dir={Astro.props.entryMeta.dir}
+ >
+ {/* TODO: Revisit how this logic flows. */}
+ <Banner {...Astro.props} />
+ {
+ Astro.props.entry.data.hero ? (
+ <ContentPanel {...Astro.props}>
+ <Hero {...Astro.props} />
+ <MarkdownContent {...Astro.props}>
+ <slot />
+ </MarkdownContent>
+ <Footer {...Astro.props} />
+ </ContentPanel>
+ ) : (
+ <>
+ <ContentPanel {...Astro.props}>
+ <PageTitle {...Astro.props} />
+ {Astro.props.isFallback && <FallbackContentNotice {...Astro.props} />}
+ </ContentPanel>
+ <ContentPanel {...Astro.props}>
+ <MarkdownContent {...Astro.props}>
+ <slot />
+ </MarkdownContent>
+ <Footer {...Astro.props} />
+ </ContentPanel>
+ </>
+ )
+ }
+ </main>
+ </TwoColumnContent>
+ </PageFrame>
+ </body>
+</html>
diff --git a/packages/starlight/layout/PageFrame.astro b/packages/starlight/components/PageFrame.astro
index f0f1f7c9..86e98725 100644
--- a/packages/starlight/layout/PageFrame.astro
+++ b/packages/starlight/components/PageFrame.astro
@@ -1,11 +1,8 @@
---
-import MobileMenuToggle from '../components/MobileMenuToggle.astro';
+import type { Props } from '../props';
import { useTranslations } from '../utils/translations';
-interface Props {
- hasSidebar: boolean;
- locale: string | undefined;
-}
+import { MobileMenuToggle } from 'virtual:starlight/components';
const { hasSidebar, locale } = Astro.props;
const t = useTranslations(locale);
@@ -16,9 +13,9 @@ const t = useTranslations(locale);
{
hasSidebar && (
<nav class="sidebar" aria-label={t('sidebarNav.accessibleLabel')}>
- <MobileMenuToggle {locale} />
+ <MobileMenuToggle {...Astro.props} />
<div id="starlight__sidebar" class="sidebar-pane">
- <div class="sidebar-content">
+ <div class="sidebar-content sl-flex">
<slot name="sidebar" />
</div>
</div>
@@ -60,6 +57,7 @@ const t = useTranslations(locale);
padding-top: var(--sl-nav-height);
width: 100%;
background-color: var(--sl-color-black);
+ overflow-y: auto;
}
:global([aria-expanded='true']) ~ .sidebar-pane {
@@ -68,7 +66,17 @@ const t = useTranslations(locale);
.sidebar-content {
height: 100%;
- overflow-y: auto;
+ min-height: max-content;
+ padding: 1rem var(--sl-sidebar-pad-x) 0;
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ @media (min-width: 50rem) {
+ .sidebar-content::after {
+ content: '';
+ padding-bottom: 1px;
+ }
}
.main-frame {
diff --git a/packages/starlight/components/RightSidebarPanel.astro b/packages/starlight/components/PageSidebar.astro
index bc4244f2..eab3d391 100644
--- a/packages/starlight/components/RightSidebarPanel.astro
+++ b/packages/starlight/components/PageSidebar.astro
@@ -1,16 +1,28 @@
-<div class="right-sidebar-panel sl-hidden lg:sl-block">
- <div class="sl-container">
- <slot />
- </div>
-</div>
+---
+import type { Props } from '../props';
+
+import { TableOfContents, MobileTableOfContents } from 'virtual:starlight/components';
+---
+
+{
+ Astro.props.toc && (
+ <>
+ <div class="lg:sl-hidden">
+ <MobileTableOfContents {...Astro.props} />
+ </div>
+ <div class="right-sidebar-panel sl-hidden lg:sl-block">
+ <div class="sl-container">
+ <TableOfContents {...Astro.props} />
+ </div>
+ </div>
+ </>
+ )
+}
<style>
.right-sidebar-panel {
padding: 1rem var(--sl-sidebar-pad-x);
}
- .right-sidebar-panel + .right-sidebar-panel {
- border-top: 1px solid var(--sl-color-hairline);
- }
.sl-container {
width: calc(var(--sl-sidebar-width) - 2 * var(--sl-sidebar-pad-x));
}
diff --git a/packages/starlight/components/PageTitle.astro b/packages/starlight/components/PageTitle.astro
new file mode 100644
index 00000000..8c6d932b
--- /dev/null
+++ b/packages/starlight/components/PageTitle.astro
@@ -0,0 +1,16 @@
+---
+import { PAGE_TITLE_ID } from '../constants';
+import type { Props } from '../props';
+---
+
+<h1 id={PAGE_TITLE_ID}>{Astro.props.entry.data.title}</h1>
+
+<style>
+ h1 {
+ margin-top: 1rem;
+ font-size: var(--sl-text-h1);
+ line-height: var(--sl-line-height-headings);
+ font-weight: 600;
+ color: var(--sl-color-white);
+ }
+</style>
diff --git a/packages/starlight/components/PrevNextLinks.astro b/packages/starlight/components/Pagination.astro
index d18f1557..2b66ced5 100644
--- a/packages/starlight/components/PrevNextLinks.astro
+++ b/packages/starlight/components/Pagination.astro
@@ -1,16 +1,10 @@
---
-import type { Link } from '../utils/navigation';
import { useTranslations } from '../utils/translations';
import Icon from '../user-components/Icon.astro';
+import type { Props } from '../props';
-interface Props {
- prev: Link | undefined;
- next: Link | undefined;
- dir: 'ltr' | 'rtl';
- locale: string | undefined;
-}
-
-const { prev, next, dir, locale } = Astro.props;
+const { dir, locale, pagination } = Astro.props;
+const { prev, next } = pagination;
const isRtl = dir === 'rtl';
const t = useTranslations(locale);
---
diff --git a/packages/starlight/components/RightSidebar.astro b/packages/starlight/components/RightSidebar.astro
deleted file mode 100644
index 142a7f0f..00000000
--- a/packages/starlight/components/RightSidebar.astro
+++ /dev/null
@@ -1,36 +0,0 @@
----
-import type { MarkdownHeading } from 'astro';
-import RightSidebarPanel from './RightSidebarPanel.astro';
-import MobileTableOfContents from './TableOfContents/MobileTableOfContents.astro';
-import TableOfContents from './TableOfContents.astro';
-import { generateToC } from './TableOfContents/generateToC';
-import { useTranslations } from '../utils/translations';
-
-interface Props {
- headings: MarkdownHeading[];
- locale: string | undefined;
- tocConfig: { maxHeadingLevel: number; minHeadingLevel: number } | false;
-}
-
-const { headings, locale, tocConfig } = Astro.props;
-const t = useTranslations(locale);
-const tocProps = tocConfig && {
- ...tocConfig,
- locale,
- toc: generateToC(headings, {
- ...tocConfig,
- title: t('tableOfContents.overview'),
- }),
-};
----
-
-{
- tocProps && (
- <>
- <MobileTableOfContents {...tocProps} />
- <RightSidebarPanel>
- <TableOfContents {...tocProps} />
- </RightSidebarPanel>
- </>
- )
-}
diff --git a/packages/starlight/components/Search.astro b/packages/starlight/components/Search.astro
index f5417b60..e88e5986 100644
--- a/packages/starlight/components/Search.astro
+++ b/packages/starlight/components/Search.astro
@@ -2,10 +2,7 @@
import '@pagefind/default-ui/css/ui.css';
import { useTranslations } from '../utils/translations';
import Icon from '../user-components/Icon.astro';
-
-interface Props {
- locale: string | undefined;
-}
+import type { Props } from '../props';
const t = useTranslations(Astro.props.locale);
const pagefindTranslations = {
@@ -129,6 +126,9 @@ const pagefindTranslations = {
</script>
<style>
+ site-search {
+ display: contents;
+ }
button[data-open-modal] {
display: flex;
align-items: center;
diff --git a/packages/starlight/components/Sidebar.astro b/packages/starlight/components/Sidebar.astro
index 7b898e91..00374ee0 100644
--- a/packages/starlight/components/Sidebar.astro
+++ b/packages/starlight/components/Sidebar.astro
@@ -1,42 +1,13 @@
---
-import type { getSidebar } from '../utils/navigation';
-import LanguageSelect from './LanguageSelect.astro';
-import SidebarSublist from './SidebarSublist.astro';
-import ThemeSelect from './ThemeSelect.astro';
+import type { Props } from '../props';
-interface Props {
- sidebar: ReturnType<typeof getSidebar>;
- locale: string | undefined;
-}
+import { MobileMenuFooter } from 'virtual:starlight/components';
+import SidebarSublist from './SidebarSublist.astro';
-const { sidebar, locale } = Astro.props;
+const { sidebar } = Astro.props;
---
-<div class="sidebar sl-flex">
- <SidebarSublist sublist={sidebar} />
- <div class="mobile-preferences sl-flex md:sl-hidden">
- <ThemeSelect {locale} />
- <LanguageSelect {locale} />
- </div>
+<SidebarSublist sublist={sidebar} />
+<div class="md:sl-hidden">
+ <MobileMenuFooter {...Astro.props} />
</div>
-
-<style>
- .sidebar {
- height: 100%;
- padding: 1rem var(--sl-sidebar-pad-x);
- flex-direction: column;
- gap: 1rem;
- }
-
- .mobile-preferences {
- justify-content: space-between;
- border-top: 1px solid var(--sl-color-gray-6);
- padding: 0.5rem 0;
- }
-
- @media (min-width: 50rem) {
- .sidebar > :global(:nth-last-child(2)) {
- padding-bottom: 1rem;
- }
- }
-</style>
diff --git a/packages/starlight/components/SiteTitle.astro b/packages/starlight/components/SiteTitle.astro
index b4cf4c90..bb1c2038 100644
--- a/packages/starlight/components/SiteTitle.astro
+++ b/packages/starlight/components/SiteTitle.astro
@@ -2,26 +2,7 @@
import { logos } from 'virtual:starlight/user-images';
import config from 'virtual:starlight/user-config';
import { pathWithBase } from '../utils/base';
-
-interface Props {
- locale: string | undefined;
-}
-
-if (config.logo) {
- let err: string | undefined;
- if ('src' in config.logo) {
- if (!logos.dark || !logos.light) {
- err = `Could not resolve logo import for "${config.logo.src}" (logo.src)`;
- }
- } else {
- if (!logos.dark) {
- err = `Could not resolve logo import for "${config.logo.dark}" (logo.dark)`;
- } else if (!logos.light) {
- err = `Could not resolve logo import for "${config.logo.light}" (logo.light)`;
- }
- }
- if (err) throw new Error(err);
-}
+import type { Props } from '../props';
const href = pathWithBase(Astro.props.locale || '/');
---
diff --git a/packages/starlight/components/SkipLink.astro b/packages/starlight/components/SkipLink.astro
index 3363dbe8..79de547c 100644
--- a/packages/starlight/components/SkipLink.astro
+++ b/packages/starlight/components/SkipLink.astro
@@ -1,14 +1,12 @@
---
+import { PAGE_TITLE_ID } from '../constants';
import { useTranslations } from '../utils/translations';
-
-interface Props {
- locale: string | undefined;
-}
+import type { Props } from '../props';
const t = useTranslations(Astro.props.locale);
---
-<a href="#_top">{t('skipLink.label')}</a>
+<a href={`#${PAGE_TITLE_ID}`}>{t('skipLink.label')}</a>
<style>
a {
diff --git a/packages/starlight/components/SocialIcons.astro b/packages/starlight/components/SocialIcons.astro
index 4f5803a3..75625255 100644
--- a/packages/starlight/components/SocialIcons.astro
+++ b/packages/starlight/components/SocialIcons.astro
@@ -1,49 +1,22 @@
---
import config from 'virtual:starlight/user-config';
import Icon from '../user-components/Icon.astro';
+import type { Props } from '../props';
type Platform = keyof NonNullable<typeof config.social>;
-
-const labels: Record<Platform, string> = {
- github: 'GitHub',
- gitlab: 'GitLab',
- bitbucket: 'Bitbucket',
- discord: 'Discord',
- gitter: 'Gitter',
- twitter: 'Twitter',
- mastodon: 'Mastodon',
- codeberg: 'Codeberg',
- codePen: 'CodePen',
- youtube: 'YouTube',
- threads: 'Threads',
- linkedin: 'LinkedIn',
- twitch: 'Twitch',
- microsoftTeams: 'Microsoft Teams',
- instagram: 'Instagram',
- stackOverflow: 'Stack Overflow',
- 'x.com': 'X',
- telegram: 'Telegram',
- rss: 'RSS',
- facebook: 'Facebook',
- email: 'Email',
-};
-
-const links = Object.entries(config.social || {}).filter(([, url]) => Boolean(url)) as [
- platform: Platform,
- url: string,
-][];
+type SocialConfig = NonNullable<NonNullable<typeof config.social>[Platform]>;
+const links = Object.entries(config.social || {}) as [Platform, SocialConfig][];
---
{
links.length > 0 && (
<>
- {links.map(([platform, url]) => (
+ {links.map(([platform, { label, url }]) => (
<a href={url} rel="me" class="sl-flex">
- <span class="sr-only">{labels[platform]}</span>
+ <span class="sr-only">{label}</span>
<Icon name={platform} />
</a>
))}
- <div class="divider" />
</>
)
}
@@ -55,8 +28,4 @@ const links = Object.entries(config.social || {}).filter(([, url]) => Boolean(ur
a:hover {
opacity: 0.66;
}
- .divider {
- height: 2rem;
- border-inline-end: 1px solid var(--sl-color-gray-5);
- }
</style>
diff --git a/packages/starlight/components/TableOfContents.astro b/packages/starlight/components/TableOfContents.astro
index 570305af..4b0f1d11 100644
--- a/packages/starlight/components/TableOfContents.astro
+++ b/packages/starlight/components/TableOfContents.astro
@@ -1,24 +1,21 @@
---
import { useTranslations } from '../utils/translations';
import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
-import type { TocItem } from './TableOfContents/generateToC';
+import type { Props } from '../props';
-interface Props {
- toc: TocItem[];
- locale: string | undefined;
- maxHeadingLevel: number;
- minHeadingLevel: number;
-}
-
-const { locale, toc, maxHeadingLevel, minHeadingLevel } = Astro.props;
+const { locale, toc } = Astro.props;
const t = useTranslations(locale);
---
-<starlight-toc data-min-h={minHeadingLevel} data-max-h={maxHeadingLevel}>
- <nav aria-labelledby="starlight__on-this-page">
- <h2 id="starlight__on-this-page">{t('tableOfContents.onThisPage')}</h2>
- <TableOfContentsList {toc} />
- </nav>
-</starlight-toc>
+{
+ toc && (
+ <starlight-toc data-min-h={toc.minHeadingLevel} data-max-h={toc.maxHeadingLevel}>
+ <nav aria-labelledby="starlight__on-this-page">
+ <h2 id="starlight__on-this-page">{t('tableOfContents.onThisPage')}</h2>
+ <TableOfContentsList toc={toc.items} />
+ </nav>
+ </starlight-toc>
+ )
+}
<script src="./TableOfContents/starlight-toc"></script>
diff --git a/packages/starlight/components/TableOfContents/TableOfContentsList.astro b/packages/starlight/components/TableOfContents/TableOfContentsList.astro
index 7c290a1e..db370b61 100644
--- a/packages/starlight/components/TableOfContents/TableOfContentsList.astro
+++ b/packages/starlight/components/TableOfContents/TableOfContentsList.astro
@@ -1,5 +1,5 @@
---
-import type { TocItem } from './generateToC';
+import type { TocItem } from '../../utils/generateToC';
interface Props {
toc: TocItem[];
diff --git a/packages/starlight/components/TableOfContents/starlight-toc.ts b/packages/starlight/components/TableOfContents/starlight-toc.ts
index 4283f361..a5de15bd 100644
--- a/packages/starlight/components/TableOfContents/starlight-toc.ts
+++ b/packages/starlight/components/TableOfContents/starlight-toc.ts
@@ -1,3 +1,5 @@
+import { PAGE_TITLE_ID } from '../../constants';
+
export class StarlightTOC extends HTMLElement {
private _current = this.querySelector('a[aria-current="true"]') as HTMLAnchorElement | null;
private minH = parseInt(this.dataset.minH || '2', 10);
@@ -20,7 +22,7 @@ export class StarlightTOC extends HTMLElement {
const isHeading = (el: Element): el is HTMLHeadingElement => {
if (el instanceof HTMLHeadingElement) {
// Special case for page title h1
- if ('pageTitle' in el.dataset) return true;
+ if (el.id === PAGE_TITLE_ID) return true;
// Check the heading level is within the user-configured limits for the ToC
const level = el.tagName[1];
if (level) {
diff --git a/packages/starlight/components/ThemeProvider.astro b/packages/starlight/components/ThemeProvider.astro
index 8751721f..759ebc5f 100644
--- a/packages/starlight/components/ThemeProvider.astro
+++ b/packages/starlight/components/ThemeProvider.astro
@@ -1,4 +1,5 @@
---
+import type { Props } from '../props';
import Icon from '../user-components/Icon.astro';
---
diff --git a/packages/starlight/components/ThemeSelect.astro b/packages/starlight/components/ThemeSelect.astro
index 0146e09e..7d8e3fd4 100644
--- a/packages/starlight/components/ThemeSelect.astro
+++ b/packages/starlight/components/ThemeSelect.astro
@@ -1,10 +1,7 @@
---
import { useTranslations } from '../utils/translations';
import Select from './Select.astro';
-
-interface Props {
- locale: string | undefined;
-}
+import type { Props } from '../props';
const t = useTranslations(Astro.props.locale);
---
diff --git a/packages/starlight/layout/TwoColumnContent.astro b/packages/starlight/components/TwoColumnContent.astro
index 381632fb..5eefb3e7 100644
--- a/packages/starlight/layout/TwoColumnContent.astro
+++ b/packages/starlight/components/TwoColumnContent.astro
@@ -1,12 +1,10 @@
---
-interface Props {
- hasToC: boolean;
-}
+import type { Props } from '../props';
---
<div class="lg:sl-flex">
{
- Astro.props.hasToC && (
+ Astro.props.toc && (
<aside class="right-sidebar-container">
<div class="right-sidebar">
<slot name="right-sidebar" />
diff --git a/packages/starlight/constants.ts b/packages/starlight/constants.ts
new file mode 100644
index 00000000..f679b6ae
--- /dev/null
+++ b/packages/starlight/constants.ts
@@ -0,0 +1,4 @@
+// N.B. THIS FILE IS IMPORTED IN BOTH SERVER- AND CLIENT-SIDE CODE.
+// THINK TWICE BEFORE ADDING STUFF AS IT WILL GET SHIPPED TO THE CLIENT.
+
+export const PAGE_TITLE_ID = '_top';
diff --git a/packages/starlight/index.astro b/packages/starlight/index.astro
index d480f420..65128ae8 100644
--- a/packages/starlight/index.astro
+++ b/packages/starlight/index.astro
@@ -1,8 +1,9 @@
---
import type { InferGetStaticPropsType } from 'astro';
+import { generateRouteData } from './utils/route-data';
import { paths } from './utils/routing';
-import Page from './layout/Page.astro';
+import Page from './components/Page.astro';
export async function getStaticPaths() {
return paths;
@@ -10,6 +11,7 @@ export async function getStaticPaths() {
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
const { Content, headings } = await Astro.props.entry.render();
+const route = generateRouteData({ props: { ...Astro.props, headings }, url: Astro.url });
---
-<Page {...Astro.props} {headings}><Content /></Page>
+<Page {...route}><Content /></Page>
diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts
index 86c2b2a4..f8b8cd21 100644
--- a/packages/starlight/integrations/virtual-user-config.ts
+++ b/packages/starlight/integrations/virtual-user-config.ts
@@ -29,6 +29,9 @@ export function vitePluginStarlightUserConfig(
opts.logo.light
)}; export const logos = { dark, light };`
: 'export const logos = {};',
+ 'virtual:starlight/components': Object.entries(opts.components)
+ .map(([name, path]) => `export { default as ${name} } from ${resolveId(path)};`)
+ .join(''),
} satisfies Record<string, string>;
/** Mapping names prefixed with `\0` to their original form. */
diff --git a/packages/starlight/layout/Page.astro b/packages/starlight/layout/Page.astro
deleted file mode 100644
index 0999122d..00000000
--- a/packages/starlight/layout/Page.astro
+++ /dev/null
@@ -1,132 +0,0 @@
----
-import config from 'virtual:starlight/user-config';
-import type { MarkdownHeading } from 'astro';
-import { getSidebar } from '../utils/navigation';
-import type { Route } from '../utils/routing';
-
-// Built-in CSS styles.
-import '../style/props.css';
-import '../style/reset.css';
-import '../style/shiki.css';
-import '../style/util.css';
-
-// Components — can override built-in CSS, but not user CSS.
-import ContentPanel from '../components/ContentPanel.astro';
-import FallbackContentNotice from '../components/FallbackContentNotice.astro';
-import Footer from '../components/Footer.astro';
-import HeadSEO from '../components/HeadSEO.astro';
-import Header from '../components/Header.astro';
-import Hero from '../components/Hero.astro';
-import MarkdownContent from '../components/MarkdownContent.astro';
-import RightSidebar from '../components/RightSidebar.astro';
-import Sidebar from '../components/Sidebar.astro';
-import SkipLink from '../components/SkipLink.astro';
-import ThemeProvider from '../components/ThemeProvider.astro';
-import PageFrame from '../layout/PageFrame.astro';
-import TwoColumnContent from '../layout/TwoColumnContent.astro';
-import Banner from '../components/Banner.astro';
-
-// Remark component CSS (needs to override `MarkdownContent.astro`)
-import '../style/asides.css';
-
-// Important that this is the last import so it can override built-in styles.
-import 'virtual:starlight/user-css';
-
-type Props = Route & { headings: MarkdownHeading[] };
-
-const { dir, entry, entryMeta, headings, isFallback, lang, locale } = Astro.props;
-const sidebar = getSidebar(Astro.url.pathname, locale);
-
-const hasSidebar = entry.data.template !== 'splash';
-const tocConfig = !hasSidebar
- ? false
- : entry.data.tableOfContents !== undefined
- ? entry.data.tableOfContents
- : config.tableOfContents;
-const hasToC = Boolean(tocConfig);
-const hasHero = Boolean(entry.data.hero);
-const pagefindEnabled =
- entry.slug !== '404' && !entry.slug.endsWith('/404') && entry.data.pagefind !== false;
----
-
-<html
- lang={lang}
- dir={dir}
- data-has-toc={hasToC}
- data-has-sidebar={hasSidebar}
- data-has-hero={hasHero}
->
- <head>
- <HeadSEO data={entry.data} lang={lang} />
- <style>
- html:not([data-has-toc]) {
- --sl-mobile-toc-height: 0rem;
- }
- html:not([data-has-sidebar]) {
- --sl-content-width: 67.5rem;
- }
- /* Add scroll padding to ensure anchor headings aren't obscured by nav */
- html {
- /* Additional padding is needed to account for the mobile TOC */
- scroll-padding-top: calc(1.5rem + var(--sl-nav-height) + var(--sl-mobile-toc-height));
- }
- main {
- padding-bottom: 3vh;
- }
- @media (min-width: 50em) {
- [data-has-sidebar] {
- --sl-content-inline-start: var(--sl-sidebar-width);
- }
- }
- @media (min-width: 72em) {
- html {
- scroll-padding-top: calc(1.5rem + var(--sl-nav-height));
- }
- }
- </style>
- </head>
- <body>
- <ThemeProvider />
- <SkipLink {locale} />
- <PageFrame {locale} {hasSidebar}>
- <Header slot="header" {locale} />
- {hasSidebar && <Sidebar slot="sidebar" {sidebar} {locale} />}
- <TwoColumnContent {hasToC}>
- <RightSidebar slot="right-sidebar" {headings} {locale} {tocConfig} />
- <main data-pagefind-body={pagefindEnabled} lang={entryMeta.lang} dir={entryMeta.dir}>
- {/* TODO: Revisit how this logic flows. */}
- {entry.data.banner && <Banner {...entry.data.banner} />}
- {
- entry.data.hero ? (
- <ContentPanel>
- <Hero hero={entry.data.hero} fallbackTitle={entry.data.title} />
- <MarkdownContent>
- <slot />
- </MarkdownContent>
- </ContentPanel>
- ) : (
- <>
- <ContentPanel>
- <h1
- id="_top"
- data-page-title
- style="font-size: var(--sl-text-h1); line-height: var(--sl-line-height-headings); font-weight: 600; color: var(--sl-color-white); margin-top: 1rem;"
- >
- {entry.data.title}
- </h1>
- {isFallback && <FallbackContentNotice {locale} />}
- </ContentPanel>
- <ContentPanel>
- <MarkdownContent>
- <slot />
- </MarkdownContent>
- <Footer {...{ entry, dir, lang, locale, sidebar }} />
- </ContentPanel>
- </>
- )
- }
- </main>
- </TwoColumnContent>
- </PageFrame>
- </body>
-</html>
diff --git a/packages/starlight/package.json b/packages/starlight/package.json
index dc9f928a..f301e36f 100644
--- a/packages/starlight/package.json
+++ b/packages/starlight/package.json
@@ -26,6 +26,135 @@
"exports": {
".": "./index.ts",
"./components": "./components.ts",
+ "./components/Badge.astro": {
+ "types": "./components/Badge.astro.tsx",
+ "import": "./components/Badge.astro"
+ },
+ "./components/LanguageSelect.astro": {
+ "types": "./components/LanguageSelect.astro.tsx",
+ "import": "./components/LanguageSelect.astro"
+ },
+ "./components/Select.astro": {
+ "types": "./components/Select.astro.tsx",
+ "import": "./components/Select.astro"
+ },
+ "./components/Banner.astro": {
+ "types": "./components/Banner.astro.tsx",
+ "import": "./components/Banner.astro"
+ },
+ "./components/LastUpdated.astro": {
+ "types": "./components/LastUpdated.astro.tsx",
+ "import": "./components/LastUpdated.astro"
+ },
+ "./components/Sidebar.astro": {
+ "types": "./components/Sidebar.astro.tsx",
+ "import": "./components/Sidebar.astro"
+ },
+ "./components/CallToAction.astro": {
+ "types": "./components/CallToAction.astro.tsx",
+ "import": "./components/CallToAction.astro"
+ },
+ "./components/MarkdownContent.astro": {
+ "types": "./components/MarkdownContent.astro.tsx",
+ "import": "./components/MarkdownContent.astro"
+ },
+ "./components/SidebarSublist.astro": {
+ "types": "./components/SidebarSublist.astro.tsx",
+ "import": "./components/SidebarSublist.astro"
+ },
+ "./components/ContentPanel.astro": {
+ "types": "./components/ContentPanel.astro.tsx",
+ "import": "./components/ContentPanel.astro"
+ },
+ "./components/MobileMenuFooter.astro": {
+ "types": "./components/MobileMenuFooter.astro.tsx",
+ "import": "./components/MobileMenuFooter.astro"
+ },
+ "./components/SiteTitle.astro": {
+ "types": "./components/SiteTitle.astro.tsx",
+ "import": "./components/SiteTitle.astro"
+ },
+ "./components/EditLink.astro": {
+ "types": "./components/EditLink.astro.tsx",
+ "import": "./components/EditLink.astro"
+ },
+ "./components/MobileMenuToggle.astro": {
+ "types": "./components/MobileMenuToggle.astro.tsx",
+ "import": "./components/MobileMenuToggle.astro"
+ },
+ "./components/SkipLink.astro": {
+ "types": "./components/SkipLink.astro.tsx",
+ "import": "./components/SkipLink.astro"
+ },
+ "./components/MobileTableOfContents.astro": {
+ "types": "./components/MobileTableOfContents.astro.tsx",
+ "import": "./components/MobileTableOfContents.astro"
+ },
+ "./components/SocialIcons.astro": {
+ "types": "./components/SocialIcons.astro.tsx",
+ "import": "./components/SocialIcons.astro"
+ },
+ "./components/FallbackContentNotice.astro": {
+ "types": "./components/FallbackContentNotice.astro.tsx",
+ "import": "./components/FallbackContentNotice.astro"
+ },
+ "./components/Page.astro": {
+ "types": "./components/Page.astro.tsx",
+ "import": "./components/Page.astro"
+ },
+ "./components/Footer.astro": {
+ "types": "./components/Footer.astro.tsx",
+ "import": "./components/Footer.astro"
+ },
+ "./components/PageFrame.astro": {
+ "types": "./components/PageFrame.astro.tsx",
+ "import": "./components/PageFrame.astro"
+ },
+ "./components/TableOfContents.astro": {
+ "types": "./components/TableOfContents.astro.tsx",
+ "import": "./components/TableOfContents.astro"
+ },
+ "./components/Head.astro": {
+ "types": "./components/Head.astro.tsx",
+ "import": "./components/Head.astro"
+ },
+ "./components/PageSidebar.astro": {
+ "types": "./components/PageSidebar.astro.tsx",
+ "import": "./components/PageSidebar.astro"
+ },
+ "./components/ThemeProvider.astro": {
+ "types": "./components/ThemeProvider.astro.tsx",
+ "import": "./components/ThemeProvider.astro"
+ },
+ "./components/Header.astro": {
+ "types": "./components/Header.astro.tsx",
+ "import": "./components/Header.astro"
+ },
+ "./components/PageTitle.astro": {
+ "types": "./components/PageTitle.astro.tsx",
+ "import": "./components/PageTitle.astro"
+ },
+ "./components/ThemeSelect.astro": {
+ "types": "./components/ThemeSelect.astro.tsx",
+ "import": "./components/ThemeSelect.astro"
+ },
+ "./components/Hero.astro": {
+ "types": "./components/Hero.astro.tsx",
+ "import": "./components/Hero.astro"
+ },
+ "./components/Pagination.astro": {
+ "types": "./components/Pagination.astro.tsx",
+ "import": "./components/Pagination.astro"
+ },
+ "./components/TwoColumnContent.astro": {
+ "types": "./components/TwoColumnContent.astro.tsx",
+ "import": "./components/TwoColumnContent.astro"
+ },
+ "./components/Search.astro": {
+ "types": "./components/Search.astro.tsx",
+ "import": "./components/Search.astro"
+ },
+ "./props": "./props.ts",
"./schema": "./schema.ts",
"./types": "./types.ts",
"./index.astro": "./index.astro",
diff --git a/packages/starlight/props.ts b/packages/starlight/props.ts
new file mode 100644
index 00000000..76c119e4
--- /dev/null
+++ b/packages/starlight/props.ts
@@ -0,0 +1 @@
+export type { StarlightRouteData as Props } from './utils/route-data';
diff --git a/packages/starlight/schemas/components.ts b/packages/starlight/schemas/components.ts
new file mode 100644
index 00000000..26253031
--- /dev/null
+++ b/packages/starlight/schemas/components.ts
@@ -0,0 +1,256 @@
+import { z } from 'astro/zod';
+
+export function ComponentConfigSchema() {
+ return z
+ .object({
+ /*
+ HEAD ----------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Component rendered inside each page’s `<head>`.
+ * Includes important tags including `<title>`, and `<meta charset="utf-8">`.
+ *
+ * Override this component as a last resort. Prefer the `head` option Starlight config if possible.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Head.astro `Head` default implementation}
+ */
+ Head: z.string().default('@astrojs/starlight/components/Head.astro'),
+
+ /**
+ * Component rendered inside `<head>` that sets up dark/light theme support.
+ * The default implementation includes an inline script and a `<template>` used by the
+ * script in `ThemeSelect.astro`.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/ThemeProvider.astro `ThemeProvider` default implementation}
+ */
+ ThemeProvider: z.string().default('@astrojs/starlight/components/ThemeProvider.astro'),
+
+ /*
+ BODY ----------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Component rendered as the first element inside `<body>` which links to the main page
+ * content for accessibility. The default implementation is hidden until a user focuses it
+ * by tabbing with their keyboard.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/SkipLink.astro `SkipLink` default implementation}
+ */
+ SkipLink: z.string().default('@astrojs/starlight/components/SkipLink.astro'),
+
+ /*
+ LAYOUT --------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Layout component wrapped around most of the page content.
+ * The default implementation sets up the header–sidebar–main layout and includes
+ * `header` and `sidebar` named slots along with a default slot for the main content.
+ * It also renders `<MobileMenuToggle />` to support toggling the sidebar navigation
+ * on small (mobile) viewports.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/PageFrame.astro `PageFrame` default implementation}
+ */
+ PageFrame: z.string().default('@astrojs/starlight/components/PageFrame.astro'),
+ /**
+ * Component rendered inside `<PageFrame>` that is responsible for toggling the
+ * sidebar navigation on small (mobile) viewports.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/MobileMenuToggle.astro `MobileMenuToggle` default implementation}
+ */
+ MobileMenuToggle: z.string().default('@astrojs/starlight/components/MobileMenuToggle.astro'),
+
+ /**
+ * Layout component wrapped around the main content column and right sidebar (table of contents).
+ * The default implementation handles the switch between a single-column, small-viewport layout
+ * and a two-column, larger-viewport layout.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/TwoColumnContent.astro `TwoColumnContent` default implementation}
+ */
+ TwoColumnContent: z.string().default('@astrojs/starlight/components/TwoColumnContent.astro'),
+
+ /*
+ HEADER --------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Header component displayed at the top of every page.
+ * The default implementation displays `<SiteTitle />`, `<Search />`, `<SocialIcons />`,
+ * `<ThemeSelect />`, and `<LanguageSelect />`.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Header.astro `Header` default implementation}
+ */
+ Header: z.string().default('@astrojs/starlight/components/Header.astro'),
+ /**
+ * Component rendered at the start of the site header to render the site title.
+ * The default implementation includes logic for rendering logos defined in Starlight config.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/SiteTitle.astro `SiteTitle` default implementation}
+ */
+ SiteTitle: z.string().default('@astrojs/starlight/components/SiteTitle.astro'),
+ /**
+ * Component used to render Starlight’s search UI. The default implementation includes the
+ * button in the header and the code for displaying a search modal when it is clicked.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Search.astro `Search` default implementation}
+ */
+ Search: z.string().default('@astrojs/starlight/components/Search.astro'),
+ /**
+ * Component rendered in the site header including social icon links. The default
+ * implementation uses the `social` option in Starlight config to render icons and links.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/SocialIcons.astro `SocialIcons` default implementation}
+ */
+ SocialIcons: z.string().default('@astrojs/starlight/components/SocialIcons.astro'),
+ /**
+ * Component rendered in the site header that allows users to select their preferred color scheme.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/ThemeSelect.astro `ThemeSelect` default implementation}
+ */
+ ThemeSelect: z.string().default('@astrojs/starlight/components/ThemeSelect.astro'),
+ /**
+ * Component rendered in the site header that allows users to switch to a different language.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/LanguageSelect.astro `LanguageSelect` default implementation}
+ */
+ LanguageSelect: z.string().default('@astrojs/starlight/components/LanguageSelect.astro'),
+
+ /*
+ SIDEBAR -------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Component rendered before page content that contains global navigation.
+ * The default implementation displays as a sidebar on wide enough viewports and inside a
+ * drop-down menu on small (mobile) viewports. It also renders `<MobileMenuFooter />` to
+ * show additional items inside the mobile menu.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Sidebar.astro `Sidebar` default implementation}
+ */
+ Sidebar: z.string().default('@astrojs/starlight/components/Sidebar.astro'),
+ /**
+ * Component rendered at the bottom of the mobile drop-down menu.
+ * The default implementation renders `<ThemeSelect />` and `<LanguageSelect />`.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/MobileMenuFooter.astro `MobileMenuFooter` default implementation}
+ */
+ MobileMenuFooter: z.string().default('@astrojs/starlight/components/MobileMenuFooter.astro'),
+
+ /*
+ TOC -----------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Component rendered before the main page’s content to display a table of contents.
+ * The default implementation renders `<TableOfContents />` and `<MobileTableOfContents />`.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/PageSidebar.astro `PageSidebar` default implementation}
+ */
+ PageSidebar: z.string().default('@astrojs/starlight/components/PageSidebar.astro'),
+ /**
+ * Component that renders the current page’s table of contents on wider viewports.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/TableOfContents.astro `TableOfContents` default implementation}
+ */
+ TableOfContents: z.string().default('@astrojs/starlight/components/TableOfContents.astro'),
+ /**
+ * Component that renders the current page’s table of contents on small (mobile) viewports.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/MobileTableOfContents.astro `MobileTableOfContents` default implementation}
+ */
+ MobileTableOfContents: z
+ .string()
+ .default('@astrojs/starlight/components/MobileTableOfContents.astro'),
+
+ /*
+ CONTENT HEADER ------------------------------------------------------------------------------
+ */
+
+ /**
+ * Banner component rendered at the top of each page. The default implementation uses the
+ * page’s `banner` frontmatter value to decide whether or not to render.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Banner.astro `Banner` default implementation}
+ */
+ Banner: z.string().default('@astrojs/starlight/components/Banner.astro'),
+
+ /**
+ * Layout component used to wrap sections of the main content column.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/ContentPanel.astro `ContentPanel` default implementation}
+ */
+ ContentPanel: z.string().default('@astrojs/starlight/components/ContentPanel.astro'),
+
+ /**
+ * Component containing the `<h1>` element for the current page.
+ *
+ * Implementations should ensure they set `id="_top"` on the `<h1>` element as in the default
+ * implementation.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/PageTitle.astro `PageTitle` default implementation}
+ */
+ PageTitle: z.string().default('@astrojs/starlight/components/PageTitle.astro'),
+
+ /**
+ * Notice displayed to users on pages where a translation for the current language is not
+ * available. Only used on multilingual sites.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/FallbackContentNotice.astro `FallbackContentNotice` default implementation}
+ */
+ FallbackContentNotice: z
+ .string()
+ .default('@astrojs/starlight/components/FallbackContentNotice.astro'),
+
+ /**
+ * Component rendered at the top of the page when `hero` is set in frontmatter. The default
+ * implementation shows a large title, tagline, and call-to-action links alongside an optional image.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Hero.astro `Hero` default implementation}
+ */
+ Hero: z.string().default('@astrojs/starlight/components/Hero.astro'),
+
+ /*
+ CONTENT -------------------------------------------------------------------------------------
+ */
+
+ /**
+ * Component rendered around each page’s main content.
+ * The default implementation sets up basic styles to apply to Markdown content.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/MarkdownContent.astro `MarkdownContent` default implementation}
+ */
+ MarkdownContent: z.string().default('@astrojs/starlight/components/MarkdownContent.astro'),
+
+ /*
+ CONTENT FOOTER ------------------------------------------------------------------------------
+ */
+
+ /**
+ * Footer component displayed at the bottom of each documentation page.
+ * The default implementation displays `<LastUpdated />`, `<Pagination />`, and `<EditLink />`.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Footer.astro `Footer` default implementation}
+ */
+ Footer: z.string().default('@astrojs/starlight/components/Footer.astro'),
+ /**
+ * Component rendered in the page footer to display the last-updated date.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/LastUpdated.astro `LastUpdated` default implementation}
+ */
+ LastUpdated: z.string().default('@astrojs/starlight/components/LastUpdated.astro'),
+ /**
+ * Component rendered in the page footer to display navigation arrows between previous/next pages.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Pagination.astro `Pagination` default implementation}
+ */
+ Pagination: z.string().default('@astrojs/starlight/components/Pagination.astro'),
+ /**
+ * Component rendered in the page footer to display a link to where the page can be edited.
+ *
+ * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/EditLink.astro `EditLink` default implementation}
+ */
+ EditLink: z.string().default('@astrojs/starlight/components/EditLink.astro'),
+ })
+ .default({});
+}
diff --git a/packages/starlight/schemas/social.ts b/packages/starlight/schemas/social.ts
new file mode 100644
index 00000000..f199e766
--- /dev/null
+++ b/packages/starlight/schemas/social.ts
@@ -0,0 +1,65 @@
+import { z } from 'astro/zod';
+
+export const SocialLinksSchema = () =>
+ z
+ .record(
+ z.enum([
+ 'twitter',
+ 'mastodon',
+ 'github',
+ 'gitlab',
+ 'bitbucket',
+ 'discord',
+ 'gitter',
+ 'codeberg',
+ 'codePen',
+ 'youtube',
+ 'threads',
+ 'linkedin',
+ 'twitch',
+ 'microsoftTeams',
+ 'instagram',
+ 'stackOverflow',
+ 'x.com',
+ 'telegram',
+ 'rss',
+ 'facebook',
+ 'email',
+ ]),
+ // Link to the respective social profile for this site
+ z.string().url()
+ )
+ .transform((links) => {
+ const labelledLinks: Partial<Record<keyof typeof links, { label: string; url: string }>> = {};
+ for (const _k in links) {
+ const key = _k as keyof typeof links;
+ const url = links[key];
+ if (!url) continue;
+ const label = {
+ github: 'GitHub',
+ gitlab: 'GitLab',
+ bitbucket: 'Bitbucket',
+ discord: 'Discord',
+ gitter: 'Gitter',
+ twitter: 'Twitter',
+ mastodon: 'Mastodon',
+ codeberg: 'Codeberg',
+ codePen: 'CodePen',
+ youtube: 'YouTube',
+ threads: 'Threads',
+ linkedin: 'LinkedIn',
+ twitch: 'Twitch',
+ microsoftTeams: 'Microsoft Teams',
+ instagram: 'Instagram',
+ stackOverflow: 'Stack Overflow',
+ 'x.com': 'X',
+ telegram: 'Telegram',
+ rss: 'RSS',
+ facebook: 'Facebook',
+ email: 'Email',
+ }[key];
+ labelledLinks[key] = { label, url };
+ }
+ return labelledLinks;
+ })
+ .optional();
diff --git a/packages/starlight/components/TableOfContents/generateToC.ts b/packages/starlight/utils/generateToC.ts
index 73c08e3d..b961ad54 100644
--- a/packages/starlight/components/TableOfContents/generateToC.ts
+++ b/packages/starlight/utils/generateToC.ts
@@ -1,4 +1,5 @@
import type { MarkdownHeading } from 'astro';
+import { PAGE_TITLE_ID } from '../constants';
export interface TocItem extends MarkdownHeading {
children: TocItem[];
@@ -16,7 +17,7 @@ export function generateToC(
{ minHeadingLevel, maxHeadingLevel, title }: TocOpts
) {
headings = headings.filter(({ depth }) => depth >= minHeadingLevel && depth <= maxHeadingLevel);
- const toc: Array<TocItem> = [{ depth: 2, slug: '_top', text: title, children: [] }];
+ const toc: Array<TocItem> = [{ depth: 2, slug: PAGE_TITLE_ID, text: title, children: [] }];
for (const heading of headings) injectChild(toc, { ...heading, children: [] });
return toc;
}
diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts
index a8af46b5..c8a45b4a 100644
--- a/packages/starlight/utils/navigation.ts
+++ b/packages/starlight/utils/navigation.ts
@@ -287,7 +287,9 @@ export function getPrevNextLinks(
next?: PrevNextLinkConfig;
}
): {
+ /** Link to previous page in the sidebar. */
prev: Link | undefined;
+ /** Link to next page in the sidebar. */
next: Link | undefined;
} {
const entries = flattenSidebar(sidebar);
diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts
new file mode 100644
index 00000000..ae204f80
--- /dev/null
+++ b/packages/starlight/utils/route-data.ts
@@ -0,0 +1,97 @@
+import type { MarkdownHeading } from 'astro';
+import { fileURLToPath } from 'node:url';
+import project from 'virtual:starlight/project-context';
+import config from 'virtual:starlight/user-config';
+import { generateToC, type TocItem } from './generateToC';
+import { getFileCommitDate } from './git';
+import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation';
+import type { Route } from './routing';
+import { useTranslations } from './translations';
+import { ensureTrailingSlash } from './path';
+
+interface PageProps extends Route {
+ headings: MarkdownHeading[];
+}
+
+export interface StarlightRouteData extends Route {
+ /** Array of Markdown headings extracted from the current page. */
+ headings: MarkdownHeading[];
+ /** Site navigation sidebar entries for this page. */
+ sidebar: SidebarEntry[];
+ /** Whether or not the sidebar should be displayed on this page. */
+ hasSidebar: boolean;
+ /** Links to the previous and next page in the sidebar if enabled. */
+ pagination: ReturnType<typeof getPrevNextLinks>;
+ /** Table of contents for this page if enabled. */
+ toc: { minHeadingLevel: number; maxHeadingLevel: number; items: TocItem[] } | undefined;
+ /** JS Date object representing when this page was last updated if enabled. */
+ lastUpdated: Date | undefined;
+ /** URL object for the address where this page can be edited if enabled. */
+ editUrl: URL | undefined;
+}
+
+export function generateRouteData({
+ props,
+ url,
+}: {
+ props: PageProps;
+ url: URL;
+}): StarlightRouteData {
+ const { entry, locale } = props;
+ const sidebar = getSidebar(url.pathname, locale);
+ return {
+ ...props,
+ sidebar,
+ hasSidebar: entry.data.template !== 'splash',
+ pagination: getPrevNextLinks(sidebar, config.pagination, entry.data),
+ toc: getToC(props),
+ lastUpdated: getLastUpdated(props),
+ editUrl: getEditUrl(props),
+ };
+}
+
+function getToC({ entry, locale, headings }: PageProps) {
+ const tocConfig =
+ entry.data.template === 'splash'
+ ? false
+ : entry.data.tableOfContents !== undefined
+ ? entry.data.tableOfContents
+ : config.tableOfContents;
+ if (!tocConfig) return;
+ const t = useTranslations(locale);
+ return {
+ ...tocConfig,
+ items: generateToC(headings, { ...tocConfig, title: t('tableOfContents.overview') }),
+ };
+}
+
+function getLastUpdated({ entry, id }: PageProps): Date | undefined {
+ if (entry.data.lastUpdated ?? config.lastUpdated) {
+ const currentFilePath = fileURLToPath(new URL('src/content/docs/' + id, project.root));
+ let date = typeof entry.data.lastUpdated !== 'boolean' ? entry.data.lastUpdated : undefined;
+ if (!date) {
+ try {
+ ({ date } = getFileCommitDate(currentFilePath, 'newest'));
+ } catch {}
+ }
+ return date;
+ }
+ return;
+}
+
+function getEditUrl({ entry, id }: PageProps): URL | undefined {
+ const { editUrl } = entry.data;
+ // If frontmatter value is false, editing is disabled for this page.
+ if (editUrl === false) return;
+
+ let url: string | undefined;
+ if (typeof editUrl === 'string') {
+ // If a URL was provided in frontmatter, use that.
+ url = editUrl;
+ } else if (config.editLink.baseUrl) {
+ const srcPath = project.srcDir.replace(project.root, '');
+ // If a base URL was added in Starlight config, synthesize the edit URL from it.
+ url = ensureTrailingSlash(config.editLink.baseUrl) + srcPath + 'content/docs/' + id;
+ }
+ return url ? new URL(url) : undefined;
+}
diff --git a/packages/starlight/utils/routing.ts b/packages/starlight/utils/routing.ts
index 496578ce..5b0b7091 100644
--- a/packages/starlight/utils/routing.ts
+++ b/packages/starlight/utils/routing.ts
@@ -8,16 +8,26 @@ import {
slugToLocaleData,
slugToParam,
} from './slugs';
+import { validateLogoImports } from './validateLogoImports';
+
+// Validate any user-provided logos imported correctly.
+// We do this here so all pages trigger it and at the top level so it runs just once.
+validateLogoImports();
export type StarlightDocsEntry = Omit<CollectionEntry<'docs'>, 'slug'> & {
slug: string;
};
export interface Route extends LocaleData {
+ /** Content collection entry for the current page. Includes frontmatter at `data`. */
entry: StarlightDocsEntry;
+ /** Locale metadata for the page content. Can be different from top-level locale values when a page is using fallback content. */
entryMeta: LocaleData;
+ /** The slug, a.k.a. permalink, for this page. */
slug: string;
+ /** The unique ID for this page. */
id: string;
+ /** True if this page is untranslated in the current language and using fallback content from the default locale. */
isFallback?: true;
[key: string]: unknown;
}
diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts
index 4cb01293..03e991f6 100644
--- a/packages/starlight/utils/user-config.ts
+++ b/packages/starlight/utils/user-config.ts
@@ -1,10 +1,13 @@
import { z } from 'astro/zod';
import { parse as bcpParse, stringify as bcpStringify } from 'bcp-47';
+import { BadgeConfigSchema } from '../schemas/badge';
+import { ComponentConfigSchema } from '../schemas/components';
+import { FaviconSchema } from '../schemas/favicon';
import { HeadConfigSchema } from '../schemas/head';
import { LogoConfigSchema } from '../schemas/logo';
-import { TableOfContentsSchema } from '../schemas/tableOfContents';
-import { FaviconSchema } from '../schemas/favicon';
import { SidebarItemSchema } from '../schemas/sidebar';
+import { SocialLinksSchema } from '../schemas/social';
+import { TableOfContentsSchema } from '../schemas/tableOfContents';
const LocaleSchema = z.object({
/** The label for this language to show in UI, e.g. `"English"`, `"العربية"`, or `"简体中文"`. */
@@ -60,35 +63,7 @@ const UserConfigSchema = z.object({
* youtube: 'https://youtube.com/@astrodotbuild',
* }
*/
- social: z
- .record(
- z.enum([
- 'twitter',
- 'mastodon',
- 'github',
- 'gitlab',
- 'bitbucket',
- 'discord',
- 'gitter',
- 'codeberg',
- 'codePen',
- 'youtube',
- 'threads',
- 'linkedin',
- 'twitch',
- 'microsoftTeams',
- 'instagram',
- 'stackOverflow',
- 'x.com',
- 'telegram',
- 'rss',
- 'facebook',
- 'email',
- ]),
- // Link to the respective social profile for this site
- z.string().url()
- )
- .optional(),
+ social: SocialLinksSchema(),
/** The tagline for your website. */
tagline: z.string().optional().describe('The tagline for your website.'),
@@ -209,6 +184,9 @@ const UserConfigSchema = z.object({
/** The default favicon for your site which should be a path to an image in the `public/` directory. */
favicon: FaviconSchema(),
+ /** Specify paths to components that should override Starlight’s default components */
+ components: ComponentConfigSchema(),
+
/** Will be used as title delimiter in the generated `<title>` tag. */
titleDelimiter: z
.string()
diff --git a/packages/starlight/utils/validateLogoImports.ts b/packages/starlight/utils/validateLogoImports.ts
new file mode 100644
index 00000000..773fae98
--- /dev/null
+++ b/packages/starlight/utils/validateLogoImports.ts
@@ -0,0 +1,21 @@
+import config from 'virtual:starlight/user-config';
+import { logos } from 'virtual:starlight/user-images';
+
+/** Check user-imported logo images have resolved correctly. */
+export function validateLogoImports(): void {
+ if (config.logo) {
+ let err: string | undefined;
+ if ('src' in config.logo) {
+ if (!logos.dark || !logos.light) {
+ err = `Could not resolve logo import for "${config.logo.src}" (logo.src)`;
+ }
+ } else {
+ if (!logos.dark) {
+ err = `Could not resolve logo import for "${config.logo.dark}" (logo.dark)`;
+ } else if (!logos.light) {
+ err = `Could not resolve logo import for "${config.logo.light}" (logo.light)`;
+ }
+ }
+ if (err) throw new Error(err);
+ }
+}
diff --git a/packages/starlight/virtual.d.ts b/packages/starlight/virtual.d.ts
index 1230c460..f448489c 100644
--- a/packages/starlight/virtual.d.ts
+++ b/packages/starlight/virtual.d.ts
@@ -15,3 +15,40 @@ declare module 'virtual:starlight/user-images' {
light?: ImageMetadata;
};
}
+
+declare module 'virtual:starlight/components' {
+ export const Banner: typeof import('./components/Banner.astro').default;
+ export const ContentPanel: typeof import('./components/ContentPanel.astro').default;
+ export const PageTitle: typeof import('./components/PageTitle.astro').default;
+ export const FallbackContentNotice: typeof import('./components/FallbackContentNotice.astro').default;
+
+ export const Footer: typeof import('./components/Footer.astro').default;
+ export const LastUpdated: typeof import('./components/LastUpdated.astro').default;
+ export const Pagination: typeof import('./components/Pagination.astro').default;
+ export const EditLink: typeof import('./components/EditLink.astro').default;
+
+ export const Header: typeof import('./components/Header.astro').default;
+ export const LanguageSelect: typeof import('./components/LanguageSelect.astro').default;
+ export const Search: typeof import('./components/Search.astro').default;
+ export const SiteTitle: typeof import('./components/SiteTitle.astro').default;
+ export const SocialIcons: typeof import('./components/SocialIcons.astro').default;
+ export const ThemeSelect: typeof import('./components/ThemeSelect.astro').default;
+
+ export const Head: typeof import('./components/Head.astro').default;
+ export const Hero: typeof import('./components/Hero.astro').default;
+ export const MarkdownContent: typeof import('./components/MarkdownContent.astro').default;
+
+ export const PageSidebar: typeof import('./components/PageSidebar.astro').default;
+ export const TableOfContents: typeof import('./components/TableOfContents.astro').default;
+ export const MobileTableOfContents: typeof import('./components/MobileTableOfContents.astro').default;
+
+ export const Sidebar: typeof import('./components/Sidebar.astro').default;
+ export const SkipLink: typeof import('./components/SkipLink.astro').default;
+ export const ThemeProvider: typeof import('./components/ThemeProvider.astro').default;
+
+ export const PageFrame: typeof import('./components/PageFrame.astro').default;
+ export const MobileMenuToggle: typeof import('./components/MobileMenuToggle.astro').default;
+ export const MobileMenuFooter: typeof import('./components/MobileMenuFooter.astro').default;
+
+ export const TwoColumnContent: typeof import('./components/TwoColumnContent.astro').default;
+}
diff --git a/packages/starlight/vitest.config.ts b/packages/starlight/vitest.config.ts
index ab9b2f4f..fe6acc49 100644
--- a/packages/starlight/vitest.config.ts
+++ b/packages/starlight/vitest.config.ts
@@ -20,7 +20,18 @@ export default defineConfig({
coverage: {
all: true,
reportsDirectory: './__coverage__',
- exclude: [...defaultCoverageExcludes, '**/vitest.*', 'components.ts', 'types.ts'],
+ exclude: [
+ ...defaultCoverageExcludes,
+ '**/vitest.*',
+ 'components.ts',
+ 'types.ts',
+ // We use this to set up test environments so it isn‘t picked up, but we are testing it downstream.
+ 'integrations/virtual-user-config.ts',
+ // Types-only export.
+ 'props.ts',
+ // Main integration entrypoint — don’t think we’re able to test this directly currently.
+ 'index.ts',
+ ],
thresholdAutoUpdate: true,
lines: 69.21,
functions: 90.24,