diff --git a/.changeset/plenty-donkeys-lay.md b/.changeset/plenty-donkeys-lay.md
new file mode 100644
index 0000000000..cb041fbdeb
--- /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 0000000000..71ea7e1dc4
--- /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';
+ ---
+
+ E-mail Me
+ ```
+
+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';
+---
+
+E-mail Me
+
+```
+
+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 [``](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;
+---
+
+
{title}
+
+
+```
+
+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 ? (
+
+ ) : (
+
+
+
+ )
+}
+```
+
+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 6a732e6dca..332563667d 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 6836d86546..6cef6afac5 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: |
diff --git a/docs/src/content/docs/reference/configuration.md b/docs/src/content/docs/reference/configuration.md
index ad2d859109..b392f7f514 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 `` t
By default, every page has a `` of `Page Title | Site Title`.
For example, this page is titled “Configuration Reference” and this site is titled “Starlight”, so the `` for this page is “Configuration Reference | Starlight”.
+
+### `components`
+
+**type:** `Record`
+
+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 0000000000..6b2ea2c42b
--- /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 `` element.
+They should only include [elements permitted inside ``](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 ``.
+Includes important tags including ``, and ``.
+
+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 `` that sets up dark/light theme support.
+The default implementation includes an inline script and a `` used by the script in [``](#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 `` 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) 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) 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), [``](#search), [``](#socialicons), [``](#themeselect), and [``](#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) 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) and [``](#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) and [``](#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 `
` element for the current page.
+
+Implementations should ensure they set `id="_top"` on the `
` 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), [``](#pagination), and [``](#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 3b10bf4574..913c5143f6 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,
+});
---
-
-
-
+
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 0000000000..13522d0224
--- /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 51745d7b81..ae1136f969 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 0000000000..25ab98aaeb
--- /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 0000000000..4035c23406
--- /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 9d06a69312..8d0c42e64a 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) {
+ 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 e6307bec18..107baa707d 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;
---
-
+{banner && }
diff --git a/packages/starlight/components/MobileMenuToggle.astro b/packages/starlight/components/MobileMenuToggle.astro
index a6e620b05d..e6619bbbaa 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
similarity index 74%
rename from packages/starlight/components/TableOfContents/MobileTableOfContents.astro
rename to packages/starlight/components/MobileTableOfContents.astro
index 34fe1d5e19..d5ec3cc672 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);
---
-
-
-
+{
+ toc && (
+
+
+
+ )
+}
diff --git a/packages/starlight/components/SiteTitle.astro b/packages/starlight/components/SiteTitle.astro
index b4cf4c9064..bb1c20384e 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 3363dbe894..79de547c57 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);
---
-{t('skipLink.label')}
+{t('skipLink.label')}
diff --git a/packages/starlight/components/TableOfContents.astro b/packages/starlight/components/TableOfContents.astro
index 570305af93..4b0f1d118b 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);
---
-
-
-
+{
+ toc && (
+
+
+
+ )
+}
diff --git a/packages/starlight/components/TableOfContents/TableOfContentsList.astro b/packages/starlight/components/TableOfContents/TableOfContentsList.astro
index 7c290a1ef2..db370b6126 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 4283f36168..a5de15bd33 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 8751721fa2..759ebc5f42 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 0146e09e8a..7d8e3fd425 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
similarity index 93%
rename from packages/starlight/layout/TwoColumnContent.astro
rename to packages/starlight/components/TwoColumnContent.astro
index 381632fb85..5eefb3e79a 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';
---