summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Swithinbank2025-02-15 11:32:41 +0100
committerGitHub2025-02-15 11:32:41 +0100
commitf493361d7b64a3279980e0f046c3a52196ab94e0 (patch)
tree1d32c59068cfebdc73d2fd6f3484426bcaf443dc
parentf895f75b17f36c826cc871ba1826e5ae1dff44ca (diff)
downloadIT.starlight-f493361d7b64a3279980e0f046c3a52196ab94e0.tar.gz
IT.starlight-f493361d7b64a3279980e0f046c3a52196ab94e0.tar.bz2
IT.starlight-f493361d7b64a3279980e0f046c3a52196ab94e0.zip
Move route data to `Astro.locals` (#2390)
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com> Co-authored-by: trueberryless <99918022+trueberryless@users.noreply.github.com>
-rw-r--r--.changeset/chilled-bees-pump.md36
-rw-r--r--docs/src/components/sidebar-preview.astro2
-rw-r--r--docs/src/content/docs/guides/overriding-components.mdx38
-rw-r--r--docs/src/content/docs/guides/route-data.mdx139
-rw-r--r--docs/src/content/docs/guides/sidebar.mdx2
-rw-r--r--docs/src/content/docs/hi/guides/overriding-components.md2
-rw-r--r--docs/src/content/docs/hi/guides/sidebar.mdx2
-rw-r--r--docs/src/content/docs/id/guides/overriding-components.md2
-rw-r--r--docs/src/content/docs/id/guides/sidebar.mdx2
-rw-r--r--docs/src/content/docs/reference/overrides.md145
-rw-r--r--docs/src/content/docs/reference/plugins.md37
-rw-r--r--docs/src/content/docs/reference/route-data.mdx197
-rw-r--r--packages/docsearch/DocSearch.astro1
-rw-r--r--packages/starlight/__tests__/basics/config-errors.test.ts1
-rw-r--r--packages/starlight/__tests__/basics/route-data.test.ts25
-rw-r--r--packages/starlight/__tests__/basics/routing.test.ts3
-rw-r--r--packages/starlight/__tests__/basics/starlight-page-route-data.test.ts16
-rw-r--r--packages/starlight/__tests__/edit-url/edit-url.test.ts2
-rw-r--r--packages/starlight/__tests__/i18n-root-locale/routing.test.ts2
-rw-r--r--packages/starlight/__tests__/middleware/middleware.test.ts30
-rw-r--r--packages/starlight/__tests__/middleware/vitest.config.ts3
-rw-r--r--packages/starlight/__tests__/plugins/config.test.ts25
-rw-r--r--packages/starlight/__tests__/plugins/route-middleware.test.ts46
-rw-r--r--packages/starlight/__tests__/test-utils.ts2
-rw-r--r--packages/starlight/components/Banner.astro4
-rw-r--r--packages/starlight/components/ContentPanel.astro4
-rw-r--r--packages/starlight/components/DraftContentNotice.astro1
-rw-r--r--packages/starlight/components/EditLink.astro3
-rw-r--r--packages/starlight/components/FallbackContentNotice.astro1
-rw-r--r--packages/starlight/components/Footer.astro8
-rw-r--r--packages/starlight/components/Head.astro3
-rw-r--r--packages/starlight/components/Header.astro11
-rw-r--r--packages/starlight/components/Hero.astro3
-rw-r--r--packages/starlight/components/LanguageSelect.astro5
-rw-r--r--packages/starlight/components/LastUpdated.astro4
-rw-r--r--packages/starlight/components/MarkdownContent.astro1
-rw-r--r--packages/starlight/components/MobileMenuFooter.astro7
-rw-r--r--packages/starlight/components/MobileMenuToggle.astro1
-rw-r--r--packages/starlight/components/MobileTableOfContents.astro3
-rw-r--r--packages/starlight/components/Page.astro64
-rw-r--r--packages/starlight/components/PageFrame.astro5
-rw-r--r--packages/starlight/components/PageSidebar.astro8
-rw-r--r--packages/starlight/components/PageTitle.astro3
-rw-r--r--packages/starlight/components/Pagination.astro3
-rw-r--r--packages/starlight/components/Search.astro1
-rw-r--r--packages/starlight/components/Sidebar.astro8
-rw-r--r--packages/starlight/components/SidebarPersister.astro3
-rw-r--r--packages/starlight/components/SidebarSublist.astro3
-rw-r--r--packages/starlight/components/SiteTitle.astro3
-rw-r--r--packages/starlight/components/SkipLink.astro1
-rw-r--r--packages/starlight/components/SocialIcons.astro1
-rw-r--r--packages/starlight/components/StarlightPage.astro10
-rw-r--r--packages/starlight/components/TableOfContents.astro3
-rw-r--r--packages/starlight/components/ThemeProvider.astro1
-rw-r--r--packages/starlight/components/ThemeSelect.astro1
-rw-r--r--packages/starlight/components/TwoColumnContent.astro6
-rw-r--r--packages/starlight/integrations/virtual-user-config.ts27
-rw-r--r--packages/starlight/locals.d.ts26
-rw-r--r--packages/starlight/locals.ts38
-rw-r--r--packages/starlight/package.json2
-rw-r--r--packages/starlight/props.ts14
-rw-r--r--packages/starlight/route-data.ts11
-rw-r--r--packages/starlight/routes/common.astro16
-rw-r--r--packages/starlight/routes/ssr/index.astro2
-rw-r--r--packages/starlight/routes/static/404.astro42
-rw-r--r--packages/starlight/routes/static/index.astro5
-rw-r--r--packages/starlight/utils/i18n.ts20
-rw-r--r--packages/starlight/utils/navigation.ts55
-rw-r--r--packages/starlight/utils/plugins.ts69
-rw-r--r--packages/starlight/utils/routing/data.ts (renamed from packages/starlight/utils/route-data.ts)86
-rw-r--r--packages/starlight/utils/routing/index.ts (renamed from packages/starlight/utils/routing.ts)50
-rw-r--r--packages/starlight/utils/routing/middleware.ts81
-rw-r--r--packages/starlight/utils/routing/types.ts96
-rw-r--r--packages/starlight/utils/slugs.ts12
-rw-r--r--packages/starlight/utils/starlight-page.ts12
-rw-r--r--packages/starlight/utils/user-config.ts8
-rw-r--r--packages/starlight/virtual-internal.d.ts4
-rw-r--r--pnpm-lock.yaml8
78 files changed, 1087 insertions, 540 deletions
diff --git a/.changeset/chilled-bees-pump.md b/.changeset/chilled-bees-pump.md
new file mode 100644
index 00000000..bbcf9bea
--- /dev/null
+++ b/.changeset/chilled-bees-pump.md
@@ -0,0 +1,36 @@
+---
+'@astrojs/starlight': minor
+---
+
+Moves route data to `Astro.locals` instead of passing it down via component props
+
+⚠️ **Breaking change:**
+Previously, all of Starlight’s templating components, including user or plugin overrides, had access to a data object for the current route via `Astro.props`.
+This data is now available as `Astro.locals.starlightRoute` instead.
+
+To update, refactor any component overrides you have:
+
+- Remove imports of `@astrojs/starlight/props`, which is now deprecated.
+- Update code that accesses `Astro.props` to use `Astro.locals.starlightRoute` instead.
+- Remove any spreading of `{...Astro.props}` into child components, which is no longer required.
+
+In the following example, a custom override for Starlight’s `LastUpdated` component is updated for the new style:
+
+```diff
+---
+import Default from '@astrojs/starlight/components/LastUpdated.astro';
+- import type { Props } from '@astrojs/starlight/props';
+
+- const { lastUpdated } = Astro.props;
++ const { lastUpdated } = Astro.locals.starlightRoute;
+
+const updatedThisYear = lastUpdated?.getFullYear() === new Date().getFullYear();
+---
+
+{updatedThisYear && (
+- <Default {...Astro.props}><slot /></Default>
++ <Default><slot /></Default>
+)}
+```
+
+_Community Starlight plugins may also need to be manually updated to work with Starlight 0.32. If you encounter any issues, please reach out to the plugin author to see if it is a known issue or if an updated version is being worked on._
diff --git a/docs/src/components/sidebar-preview.astro b/docs/src/components/sidebar-preview.astro
index d983ad5d..2ecdd9d3 100644
--- a/docs/src/components/sidebar-preview.astro
+++ b/docs/src/components/sidebar-preview.astro
@@ -6,7 +6,7 @@ import type {
} from '../../../packages/starlight/schemas/sidebar';
import SidebarSublist from '../../../packages/starlight/components/SidebarSublist.astro';
import type { Badge } from '../../../packages/starlight/schemas/badge';
-import type { SidebarEntry } from '../../../packages/starlight/utils/navigation';
+import type { SidebarEntry } from '../../../packages/starlight/utils/routing/types';
interface Props {
config: SidebarConfig;
diff --git a/docs/src/content/docs/guides/overriding-components.mdx b/docs/src/content/docs/guides/overriding-components.mdx
index dc465589..c2fbfec0 100644
--- a/docs/src/content/docs/guides/overriding-components.mdx
+++ b/docs/src/content/docs/guides/overriding-components.mdx
@@ -36,10 +36,11 @@ Overriding Starlight’s default components can be useful when:
```astro
---
// src/components/EmailLink.astro
- import type { Props } from '@astrojs/starlight/props';
+
+ const email = 'houston@example.com';
---
- <a href="mailto:houston@example.com">E-mail Me</a>
+ <a href=`mailto:${email}`>E-mail Me</a>
```
3. Tell Starlight to use your custom component in the [`components`](/reference/configuration/#components) configuration option in `astro.config.mjs`:
@@ -70,34 +71,29 @@ You can build with Starlight’s default UI components just as you would with yo
The example below shows a custom component that renders an e-mail link along with the default `SocialIcons` component:
-```astro {4,8}
+```astro {3,7}
---
// src/components/EmailLink.astro
-import type { Props } from '@astrojs/starlight/props';
import Default from '@astrojs/starlight/components/SocialIcons.astro';
---
<a href="mailto:houston@example.com">E-mail Me</a>
-<Default {...Astro.props}><slot /></Default>
+<Default><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/basics/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.
+When rendering a built-in component inside a custom component add a [`<slot />`](https://docs.astro.build/en/basics/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.
If you are reusing the [`PageFrame`](/reference/overrides/#pageframe) or [`TwoColumnContent`](/reference/overrides/#twocolumncontent) components which contain [named slots](https://docs.astro.build/en/basics/astro-components/#named-slots), you also need to [transfer](https://docs.astro.build/en/basics/astro-components/#transferring-slots) these slots as well.
The example below shows a custom component that reuses the `TwoColumnContent` component which contains an additional `right-sidebar` named slot that needs to be transferred:
-```astro {9}
+```astro {8}
---
// src/components/CustomContent.astro
-import type { Props } from '@astrojs/starlight/props';
import Default from '@astrojs/starlight/components/TwoColumnContent.astro';
---
-<Default {...Astro.props}>
+<Default>
<slot />
<slot name="right-sidebar" slot="right-sidebar" />
</Default>
@@ -105,17 +101,16 @@ import Default from '@astrojs/starlight/components/TwoColumnContent.astro';
## 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.
+When overriding a Starlight component, you can access the global [`starlightRoute` object](/guides/route-data/) 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:
+In the following example, a replacement [`PageTitle`](/reference/overrides/#pagetitle) component displays the current page’s title as set in the content’s frontmatter:
-```astro {5} "{title}"
+```astro {4} "{title}"
---
// src/components/Title.astro
-import type { Props } from '@astrojs/starlight/props';
-const { title } = Astro.props.entry.data;
+const { title } = Astro.locals.starlightRoute.entry.data;
---
<h1 id="_top">{title}</h1>
@@ -127,28 +122,27 @@ const { title } = Astro.props.entry.data;
</style>
```
-Learn more about all the available props in the [Overrides Reference](/reference/overrides/#component-props).
+Learn more about all the available properties in the [Route Data Reference](/reference/route-data/).
### 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.
+Component overrides apply to all pages. However, you can conditionally render using values from `starlightRoute` 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/components/Footer.astro';
-const isHomepage = Astro.props.id === '';
+const isHomepage = Astro.locals.starlightRoute.id === '';
---
{
isHomepage ? (
<footer>Built with Starlight 🌟</footer>
) : (
- <Default {...Astro.props}>
+ <Default>
<slot />
</Default>
)
diff --git a/docs/src/content/docs/guides/route-data.mdx b/docs/src/content/docs/guides/route-data.mdx
new file mode 100644
index 00000000..5214b61f
--- /dev/null
+++ b/docs/src/content/docs/guides/route-data.mdx
@@ -0,0 +1,139 @@
+---
+title: Route Data
+description: Learn how Starlight’s page data model is used to render your pages and how you can customize it.
+---
+
+import { Steps } from '@astrojs/starlight/components';
+
+When Starlight renders a page in your documentation, it first creates a route data object to represent what is on that page.
+This guide explains how route data is generated, how to use it, and how you can customize it to modify Starlight’s default behavior.
+
+See the [“Route Data Reference”](/reference/route-data/) for a full list of the available properties.
+
+## What is route data?
+
+Starlight route data is an object containing all the information required to render a single page.
+It includes information for the current page as well as data generated from your Starlight configuration.
+
+## Using route data
+
+All of Starlight’s components use route data to decide what to render for each page.
+For example, the [`siteTitle`](/reference/route-data/#sitetitle) string is used to display the site title and the [`sidebar`](/reference/route-data/#sidebar) array is used to render the global sidebar navigation.
+
+You can access this data from the `Astro.locals.starlightRoute` global in Astro components:
+
+```astro title="example.astro" {2}
+---
+const { siteTitle } = Astro.locals.starlightRoute;
+---
+
+<p>The title of this site is “{siteTitle}”</p>
+```
+
+This can be useful for example when building [component overrides](/guides/overriding-components/) to customize what you display.
+
+## Customizing route data
+
+Starlight’s route data works out of the box and does not require any configuration.
+However, for advanced use cases, you may want to customize route data for some or all pages to modify how your site displays.
+
+This is a similar concept to [component overrides](/guides/overriding-components/), but instead of modifying how Starlight renders your data, you modify the data Starlight renders.
+
+### When to customize route data
+
+Customizing route data can be useful when you want to modify how Starlight processes your data in a way not possible with existing configuration options.
+
+For example, you may want to filter sidebar items or customize titles for specific pages.
+Changes like this do not require modifying Starlight’s default components, only the data passed to those components.
+
+### How to customize route data
+
+You can customize route data using a special form of “middleware”.
+This is a function that is called every time Starlight renders a page and can modify values in the route data object.
+
+<Steps>
+
+1. Create a new file exporting an `onRequest` function using Starlight’s `defineRouteMiddleware()` utility:
+
+ ```ts
+ // src/routeData.ts
+ import { defineRouteMiddleware } from '@astrojs/starlight/route-data';
+
+ export const onRequest = defineRouteMiddleware(() => {});
+ ```
+
+2. Tell Starlight where your route data middleware file is located in `astro.config.mjs`:
+
+ ```js ins={9}
+ // astro.config.mjs
+ import { defineConfig } from 'astro/config';
+ import starlight from '@astrojs/starlight';
+
+ export default defineConfig({
+ integrations: [
+ starlight({
+ title: 'My delightful docs site',
+ routeMiddleware: './src/routeData.ts',
+ }),
+ ],
+ });
+ ```
+
+3. Update your `onRequest` function to modify route data.
+
+ The first argument your middleware will receive is [Astro’s `context` object](https://docs.astro.build/en/reference/api-reference/).
+ This contains full information about the current page render, including the current URL and `locals`.
+
+ In this example, we are going to make our docs more exciting by adding an exclamation mark to the end of every page’s title.
+
+ ```ts
+ // src/routeData.ts
+ import { defineRouteMiddleware } from '@astrojs/starlight/route-data';
+
+ export const onRequest = defineRouteMiddleware((context) => {
+ // Get the content collection entry for this page.
+ const { entry } = context.locals.starlightRoute;
+ // Update the title to add an exclamation mark.
+ entry.data.title = entry.data.title + '!';
+ });
+ ```
+
+</Steps>
+
+#### Multiple route middleware
+
+Starlight also supports providing multiple middleware.
+Set `routeMiddleware` to an array of paths to add more than one middleware handler:
+
+```js {9}
+// astro.config.mjs
+import { defineConfig } from 'astro/config';
+import starlight from '@astrojs/starlight';
+
+export default defineConfig({
+ integrations: [
+ starlight({
+ title: 'My site with multiple middleware',
+ routeMiddleware: ['./src/middleware-one.ts', './src/middleware-two.ts'],
+ }),
+ ],
+});
+```
+
+#### Waiting for later route middleware
+
+To wait for middleware later in the stack to run before executing your code, you can await the `next()` callback passed as the second argument to your middleware function.
+This can be useful to wait for a plugin’s middleware to run before making changes for example.
+
+```ts "next" "await next();"
+// src/routeData.ts
+import { defineRouteMiddleware } from '@astrojs/starlight/route-data';
+
+export const onRequest = defineRouteMiddleware(async (context, next) => {
+ // Wait for later middleware to run.
+ await next();
+ // Modify route data.
+ const { entry } = context.locals.starlightRoute;
+ entry.data.title = entry.data.title + '!';
+});
+```
diff --git a/docs/src/content/docs/guides/sidebar.mdx b/docs/src/content/docs/guides/sidebar.mdx
index 35500284..63e26b16 100644
--- a/docs/src/content/docs/guides/sidebar.mdx
+++ b/docs/src/content/docs/guides/sidebar.mdx
@@ -191,7 +191,7 @@ The configuration above generates the following sidebar:
Starlight can automatically generate a group in your sidebar based on a directory of your docs.
This is helpful when you do not want to manually enter each sidebar item in a group.
-By default, pages are sorted in alphabetical order according to the file [`slug`](/reference/overrides/#slug).
+By default, pages are sorted in alphabetical order according to the file [`slug`](/reference/route-data/#slug).
Add an autogenerated group using an object with `label` and `autogenerate` properties. Your `autogenerate` configuration must specify the `directory` to use for sidebar entries. For example, with the following configuration:
diff --git a/docs/src/content/docs/hi/guides/overriding-components.md b/docs/src/content/docs/hi/guides/overriding-components.md
index bfa74d1d..1c7a8502 100644
--- a/docs/src/content/docs/hi/guides/overriding-components.md
+++ b/docs/src/content/docs/hi/guides/overriding-components.md
@@ -100,7 +100,7 @@ const { title } = Astro.props.entry.data;
</style>
```
-[ओवरराइड्स संदर्भ](/hi/reference/overrides/#component-props) में सभी उपलब्ध प्रॉप्स के बारे में और जानें।
+[ओवरराइड्स संदर्भ](/hi/reference/route-data/) में सभी उपलब्ध प्रॉप्स के बारे में और जानें।
### केवल विशिष्ट पृष्ठों पर ही ओवरराइड करें
diff --git a/docs/src/content/docs/hi/guides/sidebar.mdx b/docs/src/content/docs/hi/guides/sidebar.mdx
index d42c3249..4d510256 100644
--- a/docs/src/content/docs/hi/guides/sidebar.mdx
+++ b/docs/src/content/docs/hi/guides/sidebar.mdx
@@ -139,7 +139,7 @@ starlight({
Starlight स्वचालित रूप से आपके दस्तावेज़ों की निर्देशिका के आधार पर आपके साइडबार में एक समूह उत्पन्न कर सकता है।
यह तब सहायक होता है जब आप किसी समूह में प्रत्येक साइडबार आइटम को मैन्युअल रूप से दर्ज नहीं करना चाहते हैं।
-डिफ़ॉल्ट रूप से, पेजों को फ़ाइल [`slug`](/hi/reference/overrides/#slug) के अनुसार वर्णानुक्रम में क्रमबद्ध किया जाता है।
+डिफ़ॉल्ट रूप से, पेजों को फ़ाइल [`slug`](/hi/reference/route-data/#slug) के अनुसार वर्णानुक्रम में क्रमबद्ध किया जाता है।
`label` और `autogenerate` गुणों वाले ऑब्जेक्ट का उपयोग करके एक स्वतः निर्मित समूह जोड़ें। साइडबार प्रविष्टियों के लिए उपयोग करने के लिए आपके `autogenerate` कॉन्फ़िगरेशन को `directory` निर्दिष्ट करना होगा। उदाहरण के लिए, निम्नलिखित कॉन्फ़िगरेशन के साथ:
diff --git a/docs/src/content/docs/id/guides/overriding-components.md b/docs/src/content/docs/id/guides/overriding-components.md
index e1ccc5f3..326c3ab3 100644
--- a/docs/src/content/docs/id/guides/overriding-components.md
+++ b/docs/src/content/docs/id/guides/overriding-components.md
@@ -101,7 +101,7 @@ const { title } = Astro.props.entry.data;
</style>
```
-Pelajari lebih lanjut tentang semua prop yang tersedia di [Referensi Penggantian](/id/reference/overrides/#component-props).
+Pelajari lebih lanjut tentang semua prop yang tersedia di [Referensi Penggantian](/id/reference/route-data/).
### Mengganti komponen hanya pada halaman tertentu
diff --git a/docs/src/content/docs/id/guides/sidebar.mdx b/docs/src/content/docs/id/guides/sidebar.mdx
index 3dc713db..8fa0a11f 100644
--- a/docs/src/content/docs/id/guides/sidebar.mdx
+++ b/docs/src/content/docs/id/guides/sidebar.mdx
@@ -191,7 +191,7 @@ Starlight dapat secara otomatis membuat grup di sidebar Anda berdasarkan direkto
Ini berguna ketika Anda tidak ingin memasukkan setiap item sidebar secara manual ke dalam grup.
Halaman akan diurutkan secara alfabetis berdasarkan nama file secara default.
-Secara default, halaman diurutkan berdasarkan abjad menurut file [`slug`](/id/reference/overrides/#slug).
+Secara default, halaman diurutkan berdasarkan abjad menurut file [`slug`](/id/reference/route-data/#slug).
Tambahkan grup yang dihasilkan secara otomatis menggunakan objek dengan properti `label` dan `autogenerate`. Konfigurasi `autogenerate` Anda harus menentukan `directory` yang akan digunakan untuk entri sidebar. Sebagai contoh, dengan konfigurasi berikut:
diff --git a/docs/src/content/docs/reference/overrides.md b/docs/src/content/docs/reference/overrides.md
index f5773f86..ff5e14e1 100644
--- a/docs/src/content/docs/reference/overrides.md
+++ b/docs/src/content/docs/reference/overrides.md
@@ -10,149 +10,6 @@ This page lists all components available to override and links to their default
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
----
-// src/components/Custom.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-CN`, or `pt-BR`.
-
-#### `locale`
-
-**Type:** `string | undefined`
-
-The base path at which a language is served. `undefined` for root locale slugs.
-
-#### `siteTitle`
-
-**Type:** `string`
-
-The site title for this page’s locale.
-
-#### `siteTitleHref`
-
-**Type:** `string`
-
-The value for the site title’s `href` attribute, linking back to the homepage, e.g. `/`.
-For multilingual sites this will include the current locale, e.g. `/en/` or `/zh-cn/`.
-
-#### `slug`
-
-**Type:** `string`
-
-The slug for this page generated from the content filename.
-
-This property is deprecated and will be removed in a future version of Starlight.
-Migrate to the new Content Layer API by using [Starlight’s `docsLoader`](/manual-setup/#configure-content-collections) and use the [`id`](#id) property instead.
-
-#### `id`
-
-**Type:** `string`
-
-The slug for this page or the unique ID for this page based on the content filename if using the [`legacy.collections`](https://docs.astro.build/en/reference/legacy-flags/#collections) flag.
-
-#### `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/modules/astro-content/#collectionentry) 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 component 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
@@ -230,7 +87,7 @@ These components render Starlight’s top navigation bar.
**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-1), [`<Search />`](#search), [`<SocialIcons />`](#socialicons), [`<ThemeSelect />`](#themeselect), and [`<LanguageSelect />`](#languageselect).
+The default implementation displays [`<SiteTitle />`](#sitetitle), [`<Search />`](#search), [`<SocialIcons />`](#socialicons), [`<ThemeSelect />`](#themeselect), and [`<LanguageSelect />`](#languageselect).
#### `SiteTitle`
diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md
index ab9028f4..04e9ab4c 100644
--- a/docs/src/content/docs/reference/plugins.md
+++ b/docs/src/content/docs/reference/plugins.md
@@ -15,6 +15,7 @@ Learn more about using a Starlight plugin in the [Configuration Reference](/refe
A Starlight plugin has the following shape.
See below for details of the different properties and hook parameters.
+<!-- prettier-ignore-start -->
```ts
interface StarlightPlugin {
name: string;
@@ -28,6 +29,7 @@ interface StarlightPlugin {
config: StarlightUserConfig;
updateConfig: (newConfig: StarlightUserConfig) => void;
addIntegration: (integration: AstroIntegration) => void;
+ addRouteMiddleware: (config: { entrypoint: string; order?: 'pre' | 'post' | 'default' }) => void;
astroConfig: AstroConfig;
command: 'dev' | 'build' | 'preview';
isRestart: boolean;
@@ -38,6 +40,7 @@ interface StarlightPlugin {
};
}
```
+<!-- prettier-ignore-end -->
## `name`
@@ -208,6 +211,40 @@ export default {
};
```
+#### `addRouteMiddleware`
+
+**type:** `(config: { entrypoint: string; order?: 'pre' | 'post' | 'default' }) => void`
+
+A callback function to add a [route middleware handler](/guides/route-data/) to the site.
+
+The `entrypoint` property must be a module specifier for your plugin’s middleware file that exports an `onRequest` handler.
+
+In the following example, a plugin published as `@example/starlight-plugin` adds a route middleware using an npm module specifier:
+
+```js {6-9}
+// plugin.ts
+export default {
+ name: '@example/starlight-plugin',
+ hooks: {
+ setup({ addRouteMiddleware }) {
+ addRouteMiddleware({
+ entrypoint: '@example/starlight-plugin/route-middleware',
+ });
+ },
+ },
+};
+```
+
+##### Controlling execution order
+
+By default, plugin middleware runs in the order the plugins are added.
+
+Use the optional `order` property if you need more control over when your middleware runs.
+Set `order: "pre"` to run before a user’s middleware.
+Set `order: "post"` to run after all other middleware.
+
+If two plugins add middleware with the same `order` value, the plugin added first will run first.
+
#### `astroConfig`
**type:** `AstroConfig`
diff --git a/docs/src/content/docs/reference/route-data.mdx b/docs/src/content/docs/reference/route-data.mdx
new file mode 100644
index 00000000..c0fb08e0
--- /dev/null
+++ b/docs/src/content/docs/reference/route-data.mdx
@@ -0,0 +1,197 @@
+---
+title: Route Data Reference
+description: The full reference documentation for Starlight’s route data object.
+---
+
+Starlight’s route data object contains information about the current page.
+Learn more about how Starlight’s data model works in the [“Route Data” guide](/guides/route-data/).
+
+In Astro components, access route data from `Astro.locals.starlightRoute`:
+
+```astro {4}
+---
+// src/components/Custom.astro
+
+const { hasSidebar } = Astro.locals.starlightRoute;
+---
+```
+
+In [route middleware](/guides/route-data/#customizing-route-data), access route data from the context object passed to your middleware function:
+
+```ts {5}
+// src/routeData.ts
+import { defineRouteMiddleware } from '@astrojs/starlight/route-data';
+
+export const onRequest = defineRouteMiddleware((context) => {
+ const { hasSidebar } = context.locals.starlightRoute;
+});
+```
+
+## `starlightRoute`
+
+The `starlightRoute` object has the following properties:
+
+### `dir`
+
+**Type:** `'ltr' | 'rtl'`
+
+Page writing direction.
+
+### `lang`
+
+**Type:** `string`
+
+BCP-47 language tag for this page’s locale, e.g. `en`, `zh-CN`, or `pt-BR`.
+
+### `locale`
+
+**Type:** `string | undefined`
+
+The base path at which a language is served. `undefined` for root locale slugs.
+
+### `siteTitle`
+
+**Type:** `string`
+
+The site title for this page’s locale.
+
+### `siteTitleHref`
+
+**Type:** `string`
+
+The value for the site title’s `href` attribute, linking back to the homepage, e.g. `/`.
+For multilingual sites this will include the current locale, e.g. `/en/` or `/zh-cn/`.
+
+### `slug`
+
+**Type:** `string`
+
+The slug for this page generated from the content filename.
+
+This property is deprecated and will be removed in a future version of Starlight.
+Migrate to the new Content Layer API by using [Starlight’s `docsLoader`](/manual-setup/#configure-content-collections) and use the [`id`](#id) property instead.
+
+### `id`
+
+**Type:** `string`
+
+The slug for this page or the unique ID for this page based on the content filename if using the [`legacy.collections`](https://docs.astro.build/en/reference/legacy-flags/#collections) flag.
+
+### `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/modules/astro-content/#collectionentry) 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 component 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.
+
+## Utilities
+
+### `defineRouteMiddleware()`
+
+Use the `defineRouteMiddleware()` utility to help type your route middleware module:
+
+```ts "defineRouteMiddleware"
+// src/routeData.ts
+import { defineRouteMiddleware } from '@astrojs/starlight/route-data';
+
+export const onRequest = defineRouteMiddleware((context) => {
+ // ...
+});
+```
+
+### `StarlightRouteData` type
+
+If you are writing code that needs to work with Starlight’s route data, you can import the `StarlightRouteData` type to match the shape of `Astro.locals.starlightRoute`.
+
+In the following example, a `usePageTitleInTOC()` function updates route data to use the current page’s title as the label for the first item in the table of contents, replacing the default “Overview” label.
+The `StarlightRouteData` type allows you to check whether the route data changes are valid.
+
+```ts "StarlightRouteData"
+// src/route-utils.ts
+import type { StarlightRouteData } from '@astrojs/starlight/route-data';
+
+export function usePageTitleInTOC(starlightRoute: StarlightRouteData) {
+ const overviewLink = starlightRoute.toc?.items[0];
+ if (overviewLink) {
+ overviewLink.text = starlightRoute.entry.data.title;
+ }
+}
+```
+
+This function can then be called from a route middleware:
+
+```ts {3,6}
+// src/route-middleware.ts
+import { defineRouteMiddleware } from '@astrojs/starlight/route-data';
+import { usePageTitleInTOC } from './route-utils';
+
+export const onRequest = defineRouteMiddleware((context) => {
+ usePageTitleInTOC(context.locals.starlightRoute);
+});
+```
diff --git a/packages/docsearch/DocSearch.astro b/packages/docsearch/DocSearch.astro
index f89d3196..f50c208d 100644
--- a/packages/docsearch/DocSearch.astro
+++ b/packages/docsearch/DocSearch.astro
@@ -1,5 +1,4 @@
---
-import type { Props } from '@astrojs/starlight/props';
import '@docsearch/css/dist/modal.css';
import type docsearch from '@docsearch/js';
import './variables.css';
diff --git a/packages/starlight/__tests__/basics/config-errors.test.ts b/packages/starlight/__tests__/basics/config-errors.test.ts
index 426f5ae2..f3426263 100644
--- a/packages/starlight/__tests__/basics/config-errors.test.ts
+++ b/packages/starlight/__tests__/basics/config-errors.test.ts
@@ -73,6 +73,7 @@ test('parses valid config successfully', () => {
},
"pagination": true,
"prerender": true,
+ "routeMiddleware": [],
"tableOfContents": {
"maxHeadingLevel": 3,
"minHeadingLevel": 2,
diff --git a/packages/starlight/__tests__/basics/route-data.test.ts b/packages/starlight/__tests__/basics/route-data.test.ts
index 39d19fab..18c457f2 100644
--- a/packages/starlight/__tests__/basics/route-data.test.ts
+++ b/packages/starlight/__tests__/basics/route-data.test.ts
@@ -1,7 +1,6 @@
import { expect, test, vi } from 'vitest';
-import { generateRouteData } from '../../utils/route-data';
+import { generateRouteData } from '../../utils/routing/data';
import { routes } from '../../utils/routing';
-import pkg from '../../package.json';
vi.mock('astro:content', async () =>
(await import('../test-utils')).mockedAstroContent({
@@ -86,25 +85,3 @@ test('uses explicit last updated date from frontmatter', () => {
expect(data.lastUpdated).toBeInstanceOf(Date);
expect(data.lastUpdated).toEqual(route.entry.data.lastUpdated);
});
-
-test('throws when accessing a label using the deprecated `labels` prop in pre v1 versions', () => {
- const isPreV1 = pkg.version[0] === '0';
-
- const route = routes[0]!;
- const data = generateRouteData({
- props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
- url: new URL('https://example.com'),
- });
-
- if (isPreV1) {
- expect(() => data.labels['any']).toThrowErrorMatchingInlineSnapshot(`
- "[AstroUserError]:
- The \`labels\` prop in component overrides has been removed.
- Hint:
- Replace \`Astro.props.labels["any"]\` with \`Astro.locals.t("any")\` instead.
- For more information see https://starlight.astro.build/guides/i18n/#using-ui-translations"
- `);
- } else {
- expect(() => data.labels['any']).not.toThrow();
- }
-});
diff --git a/packages/starlight/__tests__/basics/routing.test.ts b/packages/starlight/__tests__/basics/routing.test.ts
index 60f98256..8d1ecac5 100644
--- a/packages/starlight/__tests__/basics/routing.test.ts
+++ b/packages/starlight/__tests__/basics/routing.test.ts
@@ -3,8 +3,9 @@ import { getCollection } from 'astro:content';
import config from 'virtual:starlight/user-config';
import project from 'virtual:starlight/project-context';
import { expect, test, vi } from 'vitest';
-import { routes, paths, getRouteBySlugParam, type Route } from '../../utils/routing';
+import { routes, paths, getRouteBySlugParam } from '../../utils/routing';
import { slugToParam } from '../../utils/slugs';
+import type { Route } from '../../utils/routing/types';
vi.mock('astro:content', async () =>
(await import('../test-utils')).mockedAstroContent({
diff --git a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
index 7f55bd22..b9a23232 100644
--- a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
+++ b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
@@ -1,5 +1,5 @@
import { expect, test, vi } from 'vitest';
-import { generateRouteData } from '../../utils/route-data';
+import { generateRouteData } from '../../utils/routing/data';
import { routes } from '../../utils/routing';
import {
generateStarlightPageRouteData,
@@ -469,20 +469,6 @@ test('hides the sidebar if the `hasSidebar` option is not specified and the spla
expect(data.hasSidebar).toBe(false);
});
-test('throws when accessing a label using the deprecated `labels` prop', async () => {
- const data = await generateStarlightPageRouteData({
- props: starlightPageProps,
- url: starlightPageUrl,
- });
- expect(() => data.labels['any']).toThrowErrorMatchingInlineSnapshot(`
- "[AstroUserError]:
- The \`labels\` prop in component overrides has been removed.
- Hint:
- Replace \`Astro.props.labels["any"]\` with \`Astro.locals.t("any")\` instead.
- For more information see https://starlight.astro.build/guides/i18n/#using-ui-translations"
- `);
-});
-
test('uses provided edit URL if any', async () => {
const editUrl = 'https://example.com/edit';
const data = await generateStarlightPageRouteData({
diff --git a/packages/starlight/__tests__/edit-url/edit-url.test.ts b/packages/starlight/__tests__/edit-url/edit-url.test.ts
index 25ab98aa..ea1477b5 100644
--- a/packages/starlight/__tests__/edit-url/edit-url.test.ts
+++ b/packages/starlight/__tests__/edit-url/edit-url.test.ts
@@ -1,5 +1,5 @@
import { expect, test, vi } from 'vitest';
-import { generateRouteData } from '../../utils/route-data';
+import { generateRouteData } from '../../utils/routing/data';
import { routes } from '../../utils/routing';
vi.mock('astro:content', async () =>
diff --git a/packages/starlight/__tests__/i18n-root-locale/routing.test.ts b/packages/starlight/__tests__/i18n-root-locale/routing.test.ts
index f23be463..d01c4204 100644
--- a/packages/starlight/__tests__/i18n-root-locale/routing.test.ts
+++ b/packages/starlight/__tests__/i18n-root-locale/routing.test.ts
@@ -2,7 +2,7 @@ import project from 'virtual:starlight/project-context';
import config from 'virtual:starlight/user-config';
import { assert, expect, test, vi } from 'vitest';
import { routes } from '../../utils/routing';
-import { generateRouteData } from '../../utils/route-data';
+import { generateRouteData } from '../../utils/routing/data';
import * as git from 'virtual:starlight/git-info';
vi.mock('astro:content', async () =>
diff --git a/packages/starlight/__tests__/middleware/middleware.test.ts b/packages/starlight/__tests__/middleware/middleware.test.ts
new file mode 100644
index 00000000..e459d512
--- /dev/null
+++ b/packages/starlight/__tests__/middleware/middleware.test.ts
@@ -0,0 +1,30 @@
+import type { APIContext } from 'astro';
+import { expect, test } from 'vitest';
+import { onRequest } from '../../locals';
+
+test('starlightRoute throws when accessed outside of a Starlight page', async () => {
+ const context = { locals: {}, currentLocale: 'en' } as APIContext;
+ await onRequest(context, async () => new Response());
+ expect(() => {
+ context.locals.starlightRoute;
+ }).toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ \`locals.starlightRoute\` is not defined
+ Hint:
+ This usually means a component that accesses \`locals.starlightRoute\` is being rendered outside of a Starlight page, which is not supported.
+
+ If this is a component you authored, you can do one of the following:
+
+ 1. Avoid using this component in non-Starlight pages.
+ 2. Wrap the code that reads \`locals.starlightRoute\` in a \`try/catch\` block and handle the cases where \`starlightRoute\` is not available.
+
+ If this is a Starlight built-in or third-party component, you may need to report a bug or avoid this use of the component."
+ `);
+});
+
+test('starlightRoute returns as expected if it has been set', async () => {
+ const context = { locals: {}, currentLocale: 'en' } as APIContext;
+ await onRequest(context, async () => new Response());
+ context.locals.starlightRoute = { siteTitle: 'Test title' } as any;
+ expect(context.locals.starlightRoute.siteTitle).toBe('Test title');
+});
diff --git a/packages/starlight/__tests__/middleware/vitest.config.ts b/packages/starlight/__tests__/middleware/vitest.config.ts
new file mode 100644
index 00000000..9050e41d
--- /dev/null
+++ b/packages/starlight/__tests__/middleware/vitest.config.ts
@@ -0,0 +1,3 @@
+import { defineVitestConfig } from '../test-config';
+
+export default defineVitestConfig({ title: 'Middleware' });
diff --git a/packages/starlight/__tests__/plugins/config.test.ts b/packages/starlight/__tests__/plugins/config.test.ts
index 91b44120..a206206f 100644
--- a/packages/starlight/__tests__/plugins/config.test.ts
+++ b/packages/starlight/__tests__/plugins/config.test.ts
@@ -95,7 +95,30 @@ describe('validation', () => {
createTestPluginContext()
)
).rejects.toThrowError(
- /The 'test-plugin' plugin tried to update the 'plugins' config key which is not supported./
+ /The `test-plugin` plugin tried to update the `plugins` config key which is not supported./
+ );
+ });
+
+ test('validates configuration updates from plugins do not update the `routeMiddleware` config key', async () => {
+ await expect(
+ async () =>
+ await runPlugins(
+ { title: 'Test Docs' },
+ [
+ {
+ name: 'test-plugin',
+ hooks: {
+ setup: ({ updateConfig }) => {
+ // @ts-expect-error - plugins cannot update the `routeMiddleware` config key.
+ updateConfig({ routeMiddleware: './test-middleware.ts' });
+ },
+ },
+ },
+ ],
+ createTestPluginContext()
+ )
+ ).rejects.toThrowError(
+ /The `test-plugin` plugin tried to update the `routeMiddleware` config key which is not supported./
);
});
diff --git a/packages/starlight/__tests__/plugins/route-middleware.test.ts b/packages/starlight/__tests__/plugins/route-middleware.test.ts
new file mode 100644
index 00000000..2eb08fe1
--- /dev/null
+++ b/packages/starlight/__tests__/plugins/route-middleware.test.ts
@@ -0,0 +1,46 @@
+import { expect, test } from 'vitest';
+import { runPlugins } from '../../utils/plugins';
+import { createTestPluginContext } from '../test-plugin-utils';
+
+test('adds route middleware entrypoints added by plugins respecting order', async () => {
+ const { starlightConfig } = await runPlugins(
+ { title: 'Test Docs', routeMiddleware: 'user' },
+ [
+ {
+ name: 'test-plugin-1',
+ hooks: {
+ setup({ addRouteMiddleware }) {
+ addRouteMiddleware({ entrypoint: 'one' });
+ },
+ },
+ },
+ {
+ name: 'test-plugin-2',
+ hooks: {
+ setup({ addRouteMiddleware }) {
+ addRouteMiddleware({ entrypoint: 'two', order: 'pre' });
+ },
+ },
+ },
+ {
+ name: 'test-plugin-3',
+ hooks: {
+ setup({ addRouteMiddleware }) {
+ addRouteMiddleware({ entrypoint: 'three', order: 'post' });
+ },
+ },
+ },
+ {
+ name: 'test-plugin-4',
+ hooks: {
+ setup({ addRouteMiddleware }) {
+ addRouteMiddleware({ entrypoint: 'four' });
+ },
+ },
+ },
+ ],
+ createTestPluginContext()
+ );
+
+ expect(starlightConfig.routeMiddleware).toMatchObject(['two', 'user', 'one', 'four', 'three']);
+});
diff --git a/packages/starlight/__tests__/test-utils.ts b/packages/starlight/__tests__/test-utils.ts
index 12ebc804..1198ae47 100644
--- a/packages/starlight/__tests__/test-utils.ts
+++ b/packages/starlight/__tests__/test-utils.ts
@@ -1,7 +1,7 @@
import { z } from 'astro/zod';
import project from 'virtual:starlight/project-context';
import { docsSchema, i18nSchema } from '../schema';
-import type { StarlightDocsCollectionEntry } from '../utils/routing';
+import type { StarlightDocsCollectionEntry } from '../utils/routing/types';
import { vi } from 'vitest';
const frontmatterSchema = docsSchema()({
diff --git a/packages/starlight/components/Banner.astro b/packages/starlight/components/Banner.astro
index 107baa70..40c2122e 100644
--- a/packages/starlight/components/Banner.astro
+++ b/packages/starlight/components/Banner.astro
@@ -1,7 +1,5 @@
---
-import type { Props } from '../props';
-
-const { banner } = Astro.props.entry.data;
+const { banner } = Astro.locals.starlightRoute.entry.data;
---
{banner && <div class="sl-banner" set:html={banner.content} />}
diff --git a/packages/starlight/components/ContentPanel.astro b/packages/starlight/components/ContentPanel.astro
index 3f23fcd0..d4089408 100644
--- a/packages/starlight/components/ContentPanel.astro
+++ b/packages/starlight/components/ContentPanel.astro
@@ -1,7 +1,3 @@
----
-import type { Props } from '../props';
----
-
<div class="content-panel">
<div class="sl-container"><slot /></div>
</div>
diff --git a/packages/starlight/components/DraftContentNotice.astro b/packages/starlight/components/DraftContentNotice.astro
index 70d0d4fa..8c365153 100644
--- a/packages/starlight/components/DraftContentNotice.astro
+++ b/packages/starlight/components/DraftContentNotice.astro
@@ -1,6 +1,5 @@
---
import ContentNotice from './ContentNotice.astro';
-import type { Props } from '../props';
---
<ContentNotice icon="warning" label={Astro.locals.t('page.draft')} />
diff --git a/packages/starlight/components/EditLink.astro b/packages/starlight/components/EditLink.astro
index 2e7542f8..024f38b7 100644
--- a/packages/starlight/components/EditLink.astro
+++ b/packages/starlight/components/EditLink.astro
@@ -1,8 +1,7 @@
---
import Icon from '../user-components/Icon.astro';
-import type { Props } from '../props';
-const { editUrl } = Astro.props;
+const { editUrl } = Astro.locals.starlightRoute;
---
{
diff --git a/packages/starlight/components/FallbackContentNotice.astro b/packages/starlight/components/FallbackContentNotice.astro
index 616d06fe..efed300f 100644
--- a/packages/starlight/components/FallbackContentNotice.astro
+++ b/packages/starlight/components/FallbackContentNotice.astro
@@ -1,6 +1,5 @@
---
import ContentNotice from './ContentNotice.astro';
-import type { Props } from '../props';
---
<ContentNotice icon="warning" label={Astro.locals.t('i18n.untranslatedContent')} />
diff --git a/packages/starlight/components/Footer.astro b/packages/starlight/components/Footer.astro
index 0bba6825..4c88eae5 100644
--- a/packages/starlight/components/Footer.astro
+++ b/packages/starlight/components/Footer.astro
@@ -1,6 +1,4 @@
---
-import type { Props } from '../props';
-
import EditLink from 'virtual:starlight/components/EditLink';
import LastUpdated from 'virtual:starlight/components/LastUpdated';
import Pagination from 'virtual:starlight/components/Pagination';
@@ -10,10 +8,10 @@ import { Icon } from '../components';
<footer class="sl-flex">
<div class="meta sl-flex">
- <EditLink {...Astro.props} />
- <LastUpdated {...Astro.props} />
+ <EditLink />
+ <LastUpdated />
</div>
- <Pagination {...Astro.props} />
+ <Pagination />
{
config.credits && (
diff --git a/packages/starlight/components/Head.astro b/packages/starlight/components/Head.astro
index 3a66e7c5..924b9968 100644
--- a/packages/starlight/components/Head.astro
+++ b/packages/starlight/components/Head.astro
@@ -7,9 +7,8 @@ import type { HeadConfigSchema } from '../schemas/head';
import { fileWithBase } from '../utils/base';
import { createHead } from '../utils/head';
import { localizedUrl } from '../utils/localizedUrl';
-import type { Props } from '../props';
-const { entry, lang, siteTitle } = Astro.props;
+const { entry, lang, siteTitle } = Astro.locals.starlightRoute;
const { data } = entry;
const canonical = Astro.site ? new URL(Astro.url.pathname, Astro.site) : undefined;
diff --git a/packages/starlight/components/Header.astro b/packages/starlight/components/Header.astro
index 804b7980..cb7f6501 100644
--- a/packages/starlight/components/Header.astro
+++ b/packages/starlight/components/Header.astro
@@ -1,6 +1,5 @@
---
import config from 'virtual:starlight/user-config';
-import type { Props } from '../props';
import LanguageSelect from 'virtual:starlight/components/LanguageSelect';
import Search from 'virtual:starlight/components/Search';
@@ -17,17 +16,17 @@ const shouldRenderSearch =
<div class="header sl-flex">
<div class="title-wrapper sl-flex">
- <SiteTitle {...Astro.props} />
+ <SiteTitle />
</div>
<div class="sl-flex print:hidden">
- {shouldRenderSearch && <Search {...Astro.props} />}
+ {shouldRenderSearch && <Search />}
</div>
<div class="sl-hidden md:sl-flex print:hidden right-group">
<div class="sl-flex social-icons">
- <SocialIcons {...Astro.props} />
+ <SocialIcons />
</div>
- <ThemeSelect {...Astro.props} />
- <LanguageSelect {...Astro.props} />
+ <ThemeSelect />
+ <LanguageSelect />
</div>
</div>
diff --git a/packages/starlight/components/Hero.astro b/packages/starlight/components/Hero.astro
index 0d184d40..4e3d42d9 100644
--- a/packages/starlight/components/Hero.astro
+++ b/packages/starlight/components/Hero.astro
@@ -1,10 +1,9 @@
---
import { Image } from 'astro:assets';
import { PAGE_TITLE_ID } from '../constants';
-import type { Props } from '../props';
import LinkButton from '../user-components/LinkButton.astro';
-const { data } = Astro.props.entry;
+const { data } = Astro.locals.starlightRoute.entry;
const { title = data.title, tagline, image, actions = [] } = data.hero || {};
const imageAttrs = {
diff --git a/packages/starlight/components/LanguageSelect.astro b/packages/starlight/components/LanguageSelect.astro
index 92a88b8a..b2bd2639 100644
--- a/packages/starlight/components/LanguageSelect.astro
+++ b/packages/starlight/components/LanguageSelect.astro
@@ -3,7 +3,6 @@ import context from 'virtual:starlight/project-context';
import config from 'virtual:starlight/user-config';
import { localizedUrl } from '../utils/localizedUrl';
import Select from './Select.astro';
-import type { Props } from '../props';
/**
* Get the equivalent of the current page path for the passed locale.
@@ -19,10 +18,10 @@ function localizedPathname(locale: string | undefined): string {
<Select
icon="translate"
label={Astro.locals.t('languageSelect.accessibleLabel')}
- value={localizedPathname(Astro.props.locale)}
+ value={localizedPathname(Astro.locals.starlightRoute.locale)}
options={Object.entries(config.locales).map(([code, locale]) => ({
value: localizedPathname(code),
- selected: code === Astro.props.locale,
+ selected: code === Astro.locals.starlightRoute.locale,
label: locale!.label,
}))}
width="7em"
diff --git a/packages/starlight/components/LastUpdated.astro b/packages/starlight/components/LastUpdated.astro
index 74c28210..9a29831a 100644
--- a/packages/starlight/components/LastUpdated.astro
+++ b/packages/starlight/components/LastUpdated.astro
@@ -1,7 +1,5 @@
---
-import type { Props } from '../props';
-
-const { lang, lastUpdated } = Astro.props;
+const { lang, lastUpdated } = Astro.locals.starlightRoute;
---
{
diff --git a/packages/starlight/components/MarkdownContent.astro b/packages/starlight/components/MarkdownContent.astro
index 0d40cd47..90fe3d88 100644
--- a/packages/starlight/components/MarkdownContent.astro
+++ b/packages/starlight/components/MarkdownContent.astro
@@ -1,5 +1,4 @@
---
-import type { Props } from '../props';
import '../style/markdown.css';
---
diff --git a/packages/starlight/components/MobileMenuFooter.astro b/packages/starlight/components/MobileMenuFooter.astro
index 62eb23e6..d74b71e7 100644
--- a/packages/starlight/components/MobileMenuFooter.astro
+++ b/packages/starlight/components/MobileMenuFooter.astro
@@ -2,15 +2,14 @@
import LanguageSelect from 'virtual:starlight/components/LanguageSelect';
import SocialIcons from 'virtual:starlight/components/SocialIcons';
import ThemeSelect from 'virtual:starlight/components/ThemeSelect';
-import type { Props } from '../props';
---
<div class="mobile-preferences sl-flex">
<div class="sl-flex social-icons">
- <SocialIcons {...Astro.props} />
+ <SocialIcons />
</div>
- <ThemeSelect {...Astro.props} />
- <LanguageSelect {...Astro.props} />
+ <ThemeSelect />
+ <LanguageSelect />
</div>
<style>
diff --git a/packages/starlight/components/MobileMenuToggle.astro b/packages/starlight/components/MobileMenuToggle.astro
index f66789f1..6e65710b 100644
--- a/packages/starlight/components/MobileMenuToggle.astro
+++ b/packages/starlight/components/MobileMenuToggle.astro
@@ -1,5 +1,4 @@
---
-import type { Props } from '../props';
import Icon from '../user-components/Icon.astro';
---
diff --git a/packages/starlight/components/MobileTableOfContents.astro b/packages/starlight/components/MobileTableOfContents.astro
index 506ee20b..d724eaf8 100644
--- a/packages/starlight/components/MobileTableOfContents.astro
+++ b/packages/starlight/components/MobileTableOfContents.astro
@@ -1,9 +1,8 @@
---
import Icon from '../user-components/Icon.astro';
import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
-import type { Props } from '../props';
-const { toc } = Astro.props;
+const { toc } = Astro.locals.starlightRoute;
---
{
diff --git a/packages/starlight/components/Page.astro b/packages/starlight/components/Page.astro
index ade23f5c..caebe091 100644
--- a/packages/starlight/components/Page.astro
+++ b/packages/starlight/components/Page.astro
@@ -1,6 +1,4 @@
---
-import type { Props } from '../props';
-
// Built-in CSS styles.
import '../style/props.css';
import '../style/reset.css';
@@ -33,23 +31,25 @@ import 'virtual:starlight/user-css';
import printHref from '../style/print.css?url&no-inline';
+const { starlightRoute } = Astro.locals;
+
const pagefindEnabled =
- Astro.props.entry.slug !== '404' &&
- !Astro.props.entry.slug.endsWith('/404') &&
- Astro.props.entry.data.pagefind !== false;
+ starlightRoute.entry.slug !== '404' &&
+ !starlightRoute.entry.slug.endsWith('/404') &&
+ starlightRoute.entry.data.pagefind !== false;
const htmlDataAttributes: DOMStringMap = { 'data-theme': 'dark' };
-if (Boolean(Astro.props.toc)) htmlDataAttributes['data-has-toc'] = '';
-if (Astro.props.hasSidebar) htmlDataAttributes['data-has-sidebar'] = '';
-if (Boolean(Astro.props.entry.data.hero)) htmlDataAttributes['data-has-hero'] = '';
+if (Boolean(starlightRoute.toc)) htmlDataAttributes['data-has-toc'] = '';
+if (starlightRoute.hasSidebar) htmlDataAttributes['data-has-sidebar'] = '';
+if (Boolean(starlightRoute.entry.data.hero)) htmlDataAttributes['data-has-hero'] = '';
const mainDataAttributes: DOMStringMap = {};
if (pagefindEnabled) mainDataAttributes['data-pagefind-body'] = '';
---
-<html lang={Astro.props.lang} dir={Astro.props.dir} {...htmlDataAttributes}>
+<html lang={starlightRoute.lang} dir={starlightRoute.dir} {...htmlDataAttributes}>
<head>
- <Head {...Astro.props} />
+ <Head />
<style>
html:not([data-has-toc]) {
--sl-mobile-toc-height: 0rem;
@@ -76,45 +76,45 @@ if (pagefindEnabled) mainDataAttributes['data-pagefind-body'] = '';
}
}
</style>
- <ThemeProvider {...Astro.props} />
+ <ThemeProvider />
<link rel="stylesheet" href={printHref} media="print" />
</head>
<body>
- <SkipLink {...Astro.props} />
- <PageFrame {...Astro.props}>
- <Header slot="header" {...Astro.props} />
- {Astro.props.hasSidebar && <Sidebar slot="sidebar" {...Astro.props} />}
+ <SkipLink />
+ <PageFrame>
+ <Header slot="header" />
+ {starlightRoute.hasSidebar && <Sidebar slot="sidebar" />}
<script src="./SidebarPersistState"></script>
- <TwoColumnContent {...Astro.props}>
- <PageSidebar slot="right-sidebar" {...Astro.props} />
+ <TwoColumnContent>
+ <PageSidebar slot="right-sidebar" />
<main
{...mainDataAttributes}
- lang={Astro.props.entryMeta.lang}
- dir={Astro.props.entryMeta.dir}
+ lang={starlightRoute.entryMeta.lang}
+ dir={starlightRoute.entryMeta.dir}
>
{/* TODO: Revisit how this logic flows. */}
- <Banner {...Astro.props} />
+ <Banner />
{
- Astro.props.entry.data.hero ? (
- <ContentPanel {...Astro.props}>
- <Hero {...Astro.props} />
- <MarkdownContent {...Astro.props}>
+ starlightRoute.entry.data.hero ? (
+ <ContentPanel>
+ <Hero />
+ <MarkdownContent>
<slot />
</MarkdownContent>
- <Footer {...Astro.props} />
+ <Footer />
</ContentPanel>
) : (
<>
- <ContentPanel {...Astro.props}>
- <PageTitle {...Astro.props} />
- {Astro.props.entry.data.draft && <DraftContentNotice {...Astro.props} />}
- {Astro.props.isFallback && <FallbackContentNotice {...Astro.props} />}
+ <ContentPanel>
+ <PageTitle />
+ {starlightRoute.entry.data.draft && <DraftContentNotice />}
+ {starlightRoute.isFallback && <FallbackContentNotice />}
</ContentPanel>
- <ContentPanel {...Astro.props}>
- <MarkdownContent {...Astro.props}>
+ <ContentPanel>
+ <MarkdownContent>
<slot />
</MarkdownContent>
- <Footer {...Astro.props} />
+ <Footer />
</ContentPanel>
</>
)
diff --git a/packages/starlight/components/PageFrame.astro b/packages/starlight/components/PageFrame.astro
index 9bc10f25..7c76228a 100644
--- a/packages/starlight/components/PageFrame.astro
+++ b/packages/starlight/components/PageFrame.astro
@@ -1,8 +1,7 @@
---
import MobileMenuToggle from 'virtual:starlight/components/MobileMenuToggle';
-import type { Props } from '../props';
-const { hasSidebar } = Astro.props;
+const { hasSidebar } = Astro.locals.starlightRoute;
---
<div class="page sl-flex">
@@ -10,7 +9,7 @@ const { hasSidebar } = Astro.props;
{
hasSidebar && (
<nav class="sidebar print:hidden" aria-label={Astro.locals.t('sidebarNav.accessibleLabel')}>
- <MobileMenuToggle {...Astro.props} />
+ <MobileMenuToggle />
<div id="starlight__sidebar" class="sidebar-pane">
<div class="sidebar-content sl-flex">
<slot name="sidebar" />
diff --git a/packages/starlight/components/PageSidebar.astro b/packages/starlight/components/PageSidebar.astro
index 97caff9f..b87e4702 100644
--- a/packages/starlight/components/PageSidebar.astro
+++ b/packages/starlight/components/PageSidebar.astro
@@ -1,19 +1,17 @@
---
-import type { Props } from '../props';
-
import MobileTableOfContents from 'virtual:starlight/components/MobileTableOfContents';
import TableOfContents from 'virtual:starlight/components/TableOfContents';
---
{
- Astro.props.toc && (
+ Astro.locals.starlightRoute.toc && (
<>
<div class="lg:sl-hidden">
- <MobileTableOfContents {...Astro.props} />
+ <MobileTableOfContents />
</div>
<div class="right-sidebar-panel sl-hidden lg:sl-block">
<div class="sl-container">
- <TableOfContents {...Astro.props} />
+ <TableOfContents />
</div>
</div>
</>
diff --git a/packages/starlight/components/PageTitle.astro b/packages/starlight/components/PageTitle.astro
index 8c6d932b..8d9da54a 100644
--- a/packages/starlight/components/PageTitle.astro
+++ b/packages/starlight/components/PageTitle.astro
@@ -1,9 +1,8 @@
---
import { PAGE_TITLE_ID } from '../constants';
-import type { Props } from '../props';
---
-<h1 id={PAGE_TITLE_ID}>{Astro.props.entry.data.title}</h1>
+<h1 id={PAGE_TITLE_ID}>{Astro.locals.starlightRoute.entry.data.title}</h1>
<style>
h1 {
diff --git a/packages/starlight/components/Pagination.astro b/packages/starlight/components/Pagination.astro
index 5a6f68bb..d5723ab4 100644
--- a/packages/starlight/components/Pagination.astro
+++ b/packages/starlight/components/Pagination.astro
@@ -1,8 +1,7 @@
---
import Icon from '../user-components/Icon.astro';
-import type { Props } from '../props';
-const { dir, pagination } = Astro.props;
+const { dir, pagination } = Astro.locals.starlightRoute;
const { prev, next } = pagination;
const isRtl = dir === 'rtl';
---
diff --git a/packages/starlight/components/Search.astro b/packages/starlight/components/Search.astro
index 70a6cd70..6ce176bb 100644
--- a/packages/starlight/components/Search.astro
+++ b/packages/starlight/components/Search.astro
@@ -2,7 +2,6 @@
import '@pagefind/default-ui/css/ui.css';
import Icon from '../user-components/Icon.astro';
import project from 'virtual:starlight/project-context';
-import type { Props } from '../props';
const pagefindTranslations = {
placeholder: Astro.locals.t('search.label'),
diff --git a/packages/starlight/components/Sidebar.astro b/packages/starlight/components/Sidebar.astro
index 1dd4c047..570c5827 100644
--- a/packages/starlight/components/Sidebar.astro
+++ b/packages/starlight/components/Sidebar.astro
@@ -1,17 +1,15 @@
---
-import type { Props } from '../props';
-
import MobileMenuFooter from 'virtual:starlight/components/MobileMenuFooter';
import SidebarPersister from './SidebarPersister.astro';
import SidebarSublist from './SidebarSublist.astro';
-const { sidebar } = Astro.props;
+const { sidebar } = Astro.locals.starlightRoute;
---
-<SidebarPersister {...Astro.props}>
+<SidebarPersister>
<SidebarSublist sublist={sidebar} />
</SidebarPersister>
<div class="md:sl-hidden">
- <MobileMenuFooter {...Astro.props} />
+ <MobileMenuFooter />
</div>
diff --git a/packages/starlight/components/SidebarPersister.astro b/packages/starlight/components/SidebarPersister.astro
index be3b7d63..d83b02c0 100644
--- a/packages/starlight/components/SidebarPersister.astro
+++ b/packages/starlight/components/SidebarPersister.astro
@@ -19,10 +19,9 @@
@see https://github.com/withastro/starlight/pull/2633
*/
-import type { Props } from '../props';
import { getSidebarHash } from '../utils/navigation';
-const hash = getSidebarHash(Astro.props.sidebar);
+const hash = getSidebarHash(Astro.locals.starlightRoute.sidebar);
declare global {
interface Window {
diff --git a/packages/starlight/components/SidebarSublist.astro b/packages/starlight/components/SidebarSublist.astro
index b521ba13..a11898c7 100644
--- a/packages/starlight/components/SidebarSublist.astro
+++ b/packages/starlight/components/SidebarSublist.astro
@@ -1,5 +1,6 @@
---
-import { flattenSidebar, type SidebarEntry } from '../utils/navigation';
+import { flattenSidebar } from '../utils/navigation';
+import type { SidebarEntry } from '../utils/routing/types';
import Icon from '../user-components/Icon.astro';
import Badge from '../user-components/Badge.astro';
import SidebarRestorePoint from './SidebarRestorePoint.astro';
diff --git a/packages/starlight/components/SiteTitle.astro b/packages/starlight/components/SiteTitle.astro
index c516ebee..fd2fa031 100644
--- a/packages/starlight/components/SiteTitle.astro
+++ b/packages/starlight/components/SiteTitle.astro
@@ -1,8 +1,7 @@
---
import { logos } from 'virtual:starlight/user-images';
import config from 'virtual:starlight/user-config';
-import type { Props } from '../props';
-const { siteTitle, siteTitleHref } = Astro.props;
+const { siteTitle, siteTitleHref } = Astro.locals.starlightRoute;
---
<a href={siteTitleHref} class="site-title sl-flex">
diff --git a/packages/starlight/components/SkipLink.astro b/packages/starlight/components/SkipLink.astro
index 72d18c17..213c7bf2 100644
--- a/packages/starlight/components/SkipLink.astro
+++ b/packages/starlight/components/SkipLink.astro
@@ -1,6 +1,5 @@
---
import { PAGE_TITLE_ID } from '../constants';
-import type { Props } from '../props';
---
<a href={`#${PAGE_TITLE_ID}`}>{Astro.locals.t('skipLink.label')}</a>
diff --git a/packages/starlight/components/SocialIcons.astro b/packages/starlight/components/SocialIcons.astro
index 59557068..ad9056ff 100644
--- a/packages/starlight/components/SocialIcons.astro
+++ b/packages/starlight/components/SocialIcons.astro
@@ -1,7 +1,6 @@
---
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>;
type SocialConfig = NonNullable<NonNullable<typeof config.social>[Platform]>;
diff --git a/packages/starlight/components/StarlightPage.astro b/packages/starlight/components/StarlightPage.astro
index 52e5c58e..532e36ff 100644
--- a/packages/starlight/components/StarlightPage.astro
+++ b/packages/starlight/components/StarlightPage.astro
@@ -1,4 +1,5 @@
---
+import { attachRouteDataAndRunMiddleware } from '../utils/routing/middleware';
import {
generateStarlightPageRouteData,
type StarlightPageProps as Props,
@@ -6,8 +7,11 @@ import {
import Page from './Page.astro';
export type StarlightPageProps = Props;
+
+await attachRouteDataAndRunMiddleware(
+ Astro,
+ await generateStarlightPageRouteData({ props: Astro.props, url: Astro.url })
+);
---
-<Page {...await generateStarlightPageRouteData({ props: Astro.props, url: Astro.url })}>
- <slot />
-</Page>
+<Page><slot /></Page>
diff --git a/packages/starlight/components/TableOfContents.astro b/packages/starlight/components/TableOfContents.astro
index 3d8d9e37..0d507a70 100644
--- a/packages/starlight/components/TableOfContents.astro
+++ b/packages/starlight/components/TableOfContents.astro
@@ -1,8 +1,7 @@
---
import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
-import type { Props } from '../props';
-const { toc } = Astro.props;
+const { toc } = Astro.locals.starlightRoute;
---
{
diff --git a/packages/starlight/components/ThemeProvider.astro b/packages/starlight/components/ThemeProvider.astro
index 759ebc5f..8751721f 100644
--- a/packages/starlight/components/ThemeProvider.astro
+++ b/packages/starlight/components/ThemeProvider.astro
@@ -1,5 +1,4 @@
---
-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 7db1aff7..ff95f702 100644
--- a/packages/starlight/components/ThemeSelect.astro
+++ b/packages/starlight/components/ThemeSelect.astro
@@ -1,6 +1,5 @@
---
import Select from './Select.astro';
-import type { Props } from '../props';
---
<starlight-theme-select>
diff --git a/packages/starlight/components/TwoColumnContent.astro b/packages/starlight/components/TwoColumnContent.astro
index af2c9cec..2b229a50 100644
--- a/packages/starlight/components/TwoColumnContent.astro
+++ b/packages/starlight/components/TwoColumnContent.astro
@@ -1,10 +1,6 @@
----
-import type { Props } from '../props';
----
-
<div class="lg:sl-flex">
{
- Astro.props.toc && (
+ Astro.locals.starlightRoute.toc && (
<aside class="right-sidebar-container print:hidden">
<div class="right-sidebar">
<slot name="right-sidebar" />
diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts
index cef33dde..22c86ccc 100644
--- a/packages/starlight/integrations/virtual-user-config.ts
+++ b/packages/starlight/integrations/virtual-user-config.ts
@@ -100,6 +100,33 @@ export function vitePluginStarlightUserConfig(
} catch {}
export const collections = userCollections;`,
'virtual:starlight/plugin-translations': `export default ${JSON.stringify(pluginTranslations)}`,
+ /**
+ * Exports an array of route middleware functions.
+ * For example, might generate a module that looks like:
+ *
+ * ```js
+ * import { onRequest as routeMiddleware0 } from "/users/houston/docs/src/middleware";
+ * import { onRequest as routeMiddleware1 } from "@houston-inc/plugin/middleware";
+ *
+ * export const routeMiddleware = [
+ * routeMiddleware0,
+ * routeMiddleware1,
+ * ];
+ * ```
+ */
+ 'virtual:starlight/route-middleware':
+ opts.routeMiddleware
+ .reduce(
+ ([imports, entries], id, index) => {
+ const importName = `routeMiddleware${index}`;
+ imports += `import { onRequest as ${importName} } from ${resolveId(id)};\n`;
+ entries += `\t${importName},\n`;
+ return [imports, entries] as [string, string];
+ },
+ ['', 'export const routeMiddleware = [\n'] as [string, string]
+ )
+ .join('\n') + '];',
+ /** Map of modules exporting Starlight’s templating components. */
'virtual:starlight/pagefind-config': `export const pagefindUserConfig = ${JSON.stringify(opts.pagefind || {})}`,
...virtualComponentModules,
} satisfies Record<string, string>;
diff --git a/packages/starlight/locals.d.ts b/packages/starlight/locals.d.ts
index 160b5eae..12ae5fad 100644
--- a/packages/starlight/locals.d.ts
+++ b/packages/starlight/locals.d.ts
@@ -12,6 +12,32 @@ declare namespace StarlightApp {
*/
declare namespace App {
interface Locals {
+ /**
+ * Starlight’s localization API, powered by i18next.
+ *
+ * @see https://starlight.astro.build/guides/i18n/#using-ui-translations
+ *
+ * @example
+ * // Render a UI string for the current locale.
+ * <p>{Astro.locals.t('404.text')}</p>
+ */
t: import('./utils/createTranslationSystem').I18nT;
+
+ /**
+ * Starlight’s data for the current route.
+ *
+ * @see https://starlight.astro.build/guides/route-data/
+ *
+ * @throws Will throw an error if accessed on non-Starlight routes.
+ *
+ * @example
+ * // Render the title for the current page
+ * <h1>{Astro.locals.starlightRoute.entry.data.title}</h1>
+ *
+ * @example
+ * // Check if the current page should render the sidebar
+ * const { hasSidebar } = Astro.locals.starlightRoute;
+ */
+ starlightRoute: import('./utils/routing/types').StarlightRouteData;
}
}
diff --git a/packages/starlight/locals.ts b/packages/starlight/locals.ts
index 481a8e01..5337f586 100644
--- a/packages/starlight/locals.ts
+++ b/packages/starlight/locals.ts
@@ -1,8 +1,42 @@
+import type { APIContext } from 'astro';
+import { AstroError } from 'astro/errors';
import { defineMiddleware } from 'astro:middleware';
+import type { StarlightRouteData } from './route-data';
import { useTranslations } from './utils/translations';
-export const onRequest = defineMiddleware((context, next) => {
+export const onRequest = defineMiddleware(async (context, next) => {
context.locals.t = useTranslations(context.currentLocale);
-
+ initializeStarlightRoute(context);
return next();
});
+
+/**
+ * Sets up a `starlightRoute` property on locals. Initially, this will throw an error if accessed.
+ * When rendering, Starlight’s routes set `starlightRoute` with the resolved route data object for
+ * the current page.
+ *
+ * This ensures Starlight components can easily access `starlightRoute` without needing type guards,
+ * we can throw a helpful message if `starlightRoute` is accessed on non-Starlight pages, and we
+ * avoid generating route data in this middleware which also runs for non-Starlight route.
+ */
+export function initializeStarlightRoute(context: APIContext) {
+ const state: { routeData: StarlightRouteData | undefined } = { routeData: undefined };
+ Object.defineProperty(context.locals, 'starlightRoute', {
+ get() {
+ if (!state.routeData) {
+ throw new AstroError(
+ '`locals.starlightRoute` is not defined',
+ 'This usually means a component that accesses `locals.starlightRoute` is being rendered outside of a Starlight page, which is not supported.\n\n' +
+ 'If this is a component you authored, you can do one of the following:\n\n' +
+ '1. Avoid using this component in non-Starlight pages.\n' +
+ '2. Wrap the code that reads `locals.starlightRoute` in a `try/catch` block and handle the cases where `starlightRoute` is not available.\n\n' +
+ 'If this is a Starlight built-in or third-party component, you may need to report a bug or avoid this use of the component.'
+ );
+ }
+ return state.routeData;
+ },
+ set(routeData: StarlightRouteData) {
+ state.routeData = routeData;
+ },
+ });
+}
diff --git a/packages/starlight/package.json b/packages/starlight/package.json
index ecaffe16..78011e23 100644
--- a/packages/starlight/package.json
+++ b/packages/starlight/package.json
@@ -165,6 +165,7 @@
"./props": "./props.ts",
"./schema": "./schema.ts",
"./loaders": "./loaders.ts",
+ "./route-data": "./route-data.ts",
"./types": "./types.ts",
"./expressive-code": {
"types": "./expressive-code.d.ts",
@@ -204,6 +205,7 @@
"hastscript": "^9.0.0",
"i18next": "^23.11.5",
"js-yaml": "^4.1.0",
+ "klona": "^2.0.6",
"mdast-util-directive": "^3.0.0",
"mdast-util-to-markdown": "^2.1.0",
"mdast-util-to-string": "^4.0.0",
diff --git a/packages/starlight/props.ts b/packages/starlight/props.ts
index 910d5b00..c94d5080 100644
--- a/packages/starlight/props.ts
+++ b/packages/starlight/props.ts
@@ -1,2 +1,14 @@
-export type { StarlightRouteData as Props } from './utils/route-data';
+import type { StarlightRouteData } from './utils/routing/types';
export type { StarlightPageProps } from './utils/starlight-page';
+
+/**
+ * @deprecated The `Props` type is deprecated. If updating an override to use
+ * `Astro.locals.starlightRoute` instead of `Astro.props`, import the new `StarlightRouteData`
+ * type instead:
+ * ```astro
+ * ---
+ * import type { StarlightRouteData } from '@astrojs/starlight/route-data';
+ * ---
+ * ```
+ */
+export type Props = StarlightRouteData;
diff --git a/packages/starlight/route-data.ts b/packages/starlight/route-data.ts
new file mode 100644
index 00000000..b54bbd84
--- /dev/null
+++ b/packages/starlight/route-data.ts
@@ -0,0 +1,11 @@
+import type { APIContext } from 'astro';
+export type { StarlightRouteData } from './utils/routing/types';
+
+export type RouteMiddlewareHandler = (
+ context: APIContext,
+ next: () => Promise<void>
+) => void | Promise<void>;
+
+export function defineRouteMiddleware(fn: RouteMiddlewareHandler) {
+ return fn;
+}
diff --git a/packages/starlight/routes/common.astro b/packages/starlight/routes/common.astro
index ed3b92d1..b96ef3fb 100644
--- a/packages/starlight/routes/common.astro
+++ b/packages/starlight/routes/common.astro
@@ -1,17 +1,11 @@
---
-import { render } from 'astro:content';
-import { generateRouteData } from '../utils/route-data';
-import type { Route } from '../utils/routing';
import Page from '../components/Page.astro';
+import { useRouteData } from '../utils/routing/data';
+import { attachRouteDataAndRunMiddleware } from '../utils/routing/middleware';
-export type Props = {
- route: Route;
-};
+await attachRouteDataAndRunMiddleware(Astro, await useRouteData(Astro));
-const { route } = Astro.props;
-
-const { Content, headings } = await render(route.entry);
-const routeData = generateRouteData({ props: { ...route, headings }, url: Astro.url });
+const { Content, entry } = Astro.locals.starlightRoute;
---
-<Page {...routeData}><Content frontmatter={route.entry.data} /></Page>
+<Page>{Content && <Content frontmatter={entry.data} />}</Page>
diff --git a/packages/starlight/routes/ssr/index.astro b/packages/starlight/routes/ssr/index.astro
index 9625bd57..8698373f 100644
--- a/packages/starlight/routes/ssr/index.astro
+++ b/packages/starlight/routes/ssr/index.astro
@@ -11,4 +11,4 @@ if (route === undefined) {
}
---
-<CommonPage route={route} />
+<CommonPage />
diff --git a/packages/starlight/routes/static/404.astro b/packages/starlight/routes/static/404.astro
index 06ab643a..aa90d9cb 100644
--- a/packages/starlight/routes/static/404.astro
+++ b/packages/starlight/routes/static/404.astro
@@ -1,47 +1,7 @@
---
-import { getEntry } from 'astro:content';
-import project from 'virtual:starlight/project-context';
-import config from 'virtual:starlight/user-config';
-import { getCollectionPathFromRoot } from '../../utils/collection';
-import {
- normalizeCollectionEntry,
- type Route,
- type StarlightDocsCollectionEntry,
- type StarlightDocsEntry,
-} from '../../utils/routing';
-import { BuiltInDefaultLocale } from '../../utils/i18n';
import CommonPage from '../common.astro';
export const prerender = true;
-
-const { lang = BuiltInDefaultLocale.lang, dir = BuiltInDefaultLocale.dir } =
- config.defaultLocale || {};
-let locale = config.defaultLocale?.locale;
-if (locale === 'root') locale = undefined;
-
-const entryMeta = { dir, lang, locale };
-
-const fallbackEntry: StarlightDocsEntry = {
- slug: '404',
- id: '404',
- body: '',
- collection: 'docs',
- data: {
- title: '404',
- template: 'splash',
- editUrl: false,
- head: [],
- hero: { tagline: Astro.locals.t('404.text'), actions: [] },
- pagefind: false,
- sidebar: { hidden: false, attrs: {} },
- draft: false,
- },
- filePath: `${getCollectionPathFromRoot('docs', project)}/404.md`,
-};
-
-const userEntry = (await getEntry('docs', '404')) as StarlightDocsCollectionEntry;
-const entry = userEntry ? normalizeCollectionEntry(userEntry) : fallbackEntry;
-const route: Route = { ...entryMeta, entryMeta, entry, id: entry.id, slug: entry.slug };
---
-<CommonPage {route} />
+<CommonPage />
diff --git a/packages/starlight/routes/static/index.astro b/packages/starlight/routes/static/index.astro
index dfa6f259..559645c5 100644
--- a/packages/starlight/routes/static/index.astro
+++ b/packages/starlight/routes/static/index.astro
@@ -1,5 +1,4 @@
---
-import type { InferGetStaticPropsType } from 'astro';
import { paths } from '../../utils/routing';
import CommonPage from '../common.astro';
@@ -8,8 +7,6 @@ export const prerender = true;
export async function getStaticPaths() {
return paths;
}
-
-type Props = InferGetStaticPropsType<typeof getStaticPaths>;
---
-<CommonPage route={Astro.props} />
+<CommonPage />
diff --git a/packages/starlight/utils/i18n.ts b/packages/starlight/utils/i18n.ts
index a7d5be97..3919e9bd 100644
--- a/packages/starlight/utils/i18n.ts
+++ b/packages/starlight/utils/i18n.ts
@@ -3,26 +3,6 @@ import { AstroError } from 'astro/errors';
import type { StarlightConfig } from './user-config';
/**
- * A proxy object that throws an error when a user tries to access the deprecated `labels` prop in
- * a component override.
- *
- * @todo Remove in a future release once people have updated — no later than v1.
- */
-export const DeprecatedLabelsPropProxy = new Proxy<Record<string, never>>(
- {},
- {
- get(_, key) {
- const label = String(key);
- throw new AstroError(
- `The \`labels\` prop in component overrides has been removed.`,
- `Replace \`Astro.props.labels["${label}"]\` with \`Astro.locals.t("${label}")\` instead.\n` +
- 'For more information see https://starlight.astro.build/guides/i18n/#using-ui-translations'
- );
- },
- }
-);
-
-/**
* A list of well-known right-to-left languages used as a fallback when determining the text
* direction of a locale is not supported by the `Intl.Locale` API in the current environment.
*
diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts
index 37184627..cfcd2e83 100644
--- a/packages/starlight/utils/navigation.ts
+++ b/packages/starlight/utils/navigation.ts
@@ -1,6 +1,6 @@
import { AstroError } from 'astro/errors';
-import config from 'virtual:starlight/user-config';
import project from 'virtual:starlight/project-context';
+import config from 'virtual:starlight/user-config';
import type { Badge, I18nBadge, I18nBadgeConfig } from '../schemas/badge';
import type { PrevNextLinkConfig } from '../schemas/prevNextLink';
import type {
@@ -10,6 +10,7 @@ import type {
SidebarItem,
SidebarLinkItem,
} from '../schemas/sidebar';
+import { getCollectionPathFromRoot } from './collection';
import { createPathFormatter } from './createPathFormatter';
import { formatPath } from './format-path';
import { BuiltInDefaultLocale, pickLang } from './i18n';
@@ -19,10 +20,16 @@ import {
stripExtension,
stripLeadingAndTrailingSlashes,
} from './path';
-import { getLocaleRoutes, routes, type Route } from './routing';
+import { getLocaleRoutes, routes } from './routing';
+import type {
+ SidebarGroup,
+ SidebarLink,
+ PaginationLinks,
+ Route,
+ SidebarEntry,
+} from './routing/types';
import { localeToLang, localizedId, slugToPathname } from './slugs';
import type { StarlightConfig } from './user-config';
-import { getCollectionPathFromRoot } from './collection';
const DirKey = Symbol('DirKey');
const SlugKey = Symbol('SlugKey');
@@ -31,25 +38,6 @@ const neverPathFormatter = createPathFormatter({ trailingSlash: 'never' });
const docsCollectionPathFromRoot = getCollectionPathFromRoot('docs', project);
-export interface Link {
- type: 'link';
- label: string;
- href: string;
- isCurrent: boolean;
- badge: Badge | undefined;
- attrs: LinkHTMLAttributes;
-}
-
-interface Group {
- type: 'group';
- label: string;
- entries: (Link | Group)[];
- collapsed: boolean;
- badge: Badge | undefined;
-}
-
-export type SidebarEntry = Link | Group;
-
/**
* A representation of the route structure. For each object entry:
* if it’s a folder, the key is the directory name, and value is the directory
@@ -107,7 +95,7 @@ function groupFromAutogenerateConfig(
locale: string | undefined,
routes: Route[],
currentPathname: string
-): Group {
+): SidebarGroup {
const { collapsed: subgroupCollapsed, directory } = item.autogenerate;
const localeDir = locale ? locale + '/' + directory : directory;
const dirDocs = routes.filter((doc) => {
@@ -186,7 +174,7 @@ function makeSidebarLink(
label: string,
badge?: Badge,
attrs?: LinkHTMLAttributes
-): Link {
+): SidebarLink {
if (!isAbsolute(href)) {
href = formatPath(href);
}
@@ -203,7 +191,7 @@ function makeLink({
href: string;
badge?: Badge | undefined;
attrs?: LinkHTMLAttributes | undefined;
-}): Link {
+}): SidebarLink {
return { type: 'link', ...opts, badge, isCurrent: false, attrs };
}
@@ -275,7 +263,7 @@ function treeify(routes: Route[], locale: string | undefined, baseDir: string):
}
/** Create a link entry for a given content collection entry. */
-function linkFromRoute(route: Route): Link {
+function linkFromRoute(route: Route): SidebarLink {
return makeSidebarLink(
slugToPathname(route.slug),
route.entry.data.sidebar.label || route.entry.data.title,
@@ -315,7 +303,7 @@ function groupFromDir(
currentPathname: string,
locale: string | undefined,
collapsed: boolean
-): Group {
+): SidebarGroup {
const entries = sortDirEntries(Object.entries(dir)).map(([key, dirOrRoute]) =>
dirToItem(dirOrRoute, `${fullPath}/${key}`, key, currentPathname, locale, collapsed)
);
@@ -451,7 +439,7 @@ function recursivelyBuildSidebarIdentity(sidebar: SidebarEntry[]): string {
}
/** Turn the nested tree structure of a sidebar into a flat list of all the links. */
-export function flattenSidebar(sidebar: SidebarEntry[]): Link[] {
+export function flattenSidebar(sidebar: SidebarEntry[]): SidebarLink[] {
return sidebar.flatMap((entry) =>
entry.type === 'group' ? flattenSidebar(entry.entries) : entry
);
@@ -465,12 +453,7 @@ export function getPrevNextLinks(
prev?: PrevNextLinkConfig;
next?: PrevNextLinkConfig;
}
-): {
- /** Link to previous page in the sidebar. */
- prev: Link | undefined;
- /** Link to next page in the sidebar. */
- next: Link | undefined;
-} {
+): PaginationLinks {
const entries = flattenSidebar(sidebar);
const currentIndex = entries.findIndex((entry) => entry.isCurrent);
const prev = applyPrevNextLinkConfig(entries[currentIndex - 1], paginationEnabled, config.prev);
@@ -484,10 +467,10 @@ export function getPrevNextLinks(
/** Apply a prev/next link config to a navigation link. */
function applyPrevNextLinkConfig(
- link: Link | undefined,
+ link: SidebarLink | undefined,
paginationEnabled: boolean,
config: PrevNextLinkConfig | undefined
-): Link | undefined {
+): SidebarLink | undefined {
// Explicitly remove the link.
if (config === false) return undefined;
// Use the generated link if any.
diff --git a/packages/starlight/utils/plugins.ts b/packages/starlight/utils/plugins.ts
index b5dc7f15..a4912cb1 100644
--- a/packages/starlight/utils/plugins.ts
+++ b/packages/starlight/utils/plugins.ts
@@ -1,7 +1,12 @@
import type { AstroIntegration, HookParameters as AstroHookParameters } from 'astro';
+import { AstroError } from 'astro/errors';
import { z } from 'astro/zod';
-import { StarlightConfigSchema, type StarlightUserConfig } from '../utils/user-config';
import { parseWithFriendlyErrors } from '../utils/error-map';
+import {
+ StarlightConfigSchema,
+ type StarlightConfig,
+ type StarlightUserConfig,
+} from '../utils/user-config';
import type { UserI18nSchema } from './translations';
import { createTranslationSystemFromFs } from './translations-fs';
import { absolutePathToLang as getAbsolutePathFromLang } from '../integrations/shared/absolutePathToLang';
@@ -34,6 +39,8 @@ export async function runPlugins(
// A list of translations injected by the various plugins keyed by locale.
const pluginTranslations: PluginTranslations = {};
+ // A list of route middleware added by the various plugins.
+ const routeMiddlewareConfigs: Array<z.output<typeof routeMiddlewareConfigSchema>> = [];
for (const {
hooks: { 'i18n:setup': i18nSetup },
@@ -76,8 +83,15 @@ export async function runPlugins(
updateConfig(newConfig) {
// Ensure that plugins do not update the `plugins` config key.
if ('plugins' in newConfig) {
- throw new Error(
- `The '${name}' plugin tried to update the 'plugins' config key which is not supported.`
+ throw new AstroError(
+ `The \`${name}\` plugin tried to update the \`plugins\` config key which is not supported.`
+ );
+ }
+ if ('routeMiddleware' in newConfig) {
+ throw new AstroError(
+ `The \`${name}\` plugin tried to update the \`routeMiddleware\` config key which is not supported.`,
+ 'Use the `addRouteMiddleware()` utility instead.\n' +
+ 'See https://starlight.astro.build/reference/plugins/#addroutemiddleware for more details.'
);
}
@@ -97,6 +111,9 @@ export async function runPlugins(
// Collect any Astro integrations added by the plugin.
integrations.push(integration);
},
+ addRouteMiddleware(middlewareConfig) {
+ routeMiddlewareConfigs.push(middlewareConfig);
+ },
astroConfig: {
...context.config,
integrations: [...context.config.integrations, ...integrations],
@@ -109,9 +126,29 @@ export async function runPlugins(
});
}
+ applyPluginMiddleware(routeMiddlewareConfigs, starlightConfig);
+
return { integrations, starlightConfig, pluginTranslations, useTranslations, absolutePathToLang };
}
+/** Updates `routeMiddleware` in the Starlight config to add plugin middlewares in the correct order. */
+function applyPluginMiddleware(
+ routeMiddlewareConfigs: { entrypoint: string; order: 'default' | 'pre' | 'post' }[],
+ starlightConfig: StarlightConfig
+) {
+ const middlewareBuckets = routeMiddlewareConfigs.reduce<
+ Record<'pre' | 'default' | 'post', string[]>
+ >(
+ (buckets, { entrypoint, order = 'default' }) => {
+ buckets[order].push(entrypoint);
+ return buckets;
+ },
+ { pre: [], default: [], post: [] }
+ );
+ starlightConfig.routeMiddleware.unshift(...middlewareBuckets.pre);
+ starlightConfig.routeMiddleware.push(...middlewareBuckets.default, ...middlewareBuckets.post);
+}
+
export function injectPluginTranslationsTypes(
translations: PluginTranslations,
injectTypes: AstroHookParameters<'astro:config:done'>['injectTypes']
@@ -145,6 +182,11 @@ const astroIntegrationSchema = z.object({
hooks: z.object({}).passthrough().default({}),
}) as z.Schema<AstroIntegration>;
+const routeMiddlewareConfigSchema = z.object({
+ entrypoint: z.string(),
+ order: z.enum(['pre', 'post', 'default']).default('default'),
+});
+
const baseStarlightPluginSchema = z.object({
/** Name of the Starlight plugin. */
name: z.string(),
@@ -183,7 +225,9 @@ const configSetupHookSchema = z
* }
*/
updateConfig: z.function(
- z.tuple([z.record(z.any()) as z.Schema<Partial<StarlightUserConfig>>]),
+ z.tuple([
+ z.record(z.any()) as z.Schema<Partial<Omit<StarlightUserConfig, 'routeMiddleware'>>>,
+ ]),
z.void()
),
/**
@@ -210,6 +254,23 @@ const configSetupHookSchema = z
*/
addIntegration: z.function(z.tuple([astroIntegrationSchema]), z.void()),
/**
+ * A callback function to register additional route middleware handlers.
+ *
+ * If the order of execution is important, a plugin can use the `order` option to enforce
+ * running first or last.
+ *
+ * @example
+ * {
+ * name: 'My Starlight Plugin',
+ * hooks: {
+ * setup({ addRouteMiddleware }) {
+ * addRouteMiddleware({ entrypoint: '@me/my-plugin/route-middleware' });
+ * },
+ * },
+ * }
+ */
+ addRouteMiddleware: z.function(z.tuple([routeMiddlewareConfigSchema]), z.void()),
+ /**
* A read-only copy of the user-supplied Astro configuration.
*
* Note that this configuration is resolved before any other integrations have run.
diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/routing/data.ts
index 1f688020..302532d9 100644
--- a/packages/starlight/utils/route-data.ts
+++ b/packages/starlight/utils/routing/data.ts
@@ -1,39 +1,34 @@
-import type { MarkdownHeading } from 'astro';
+import type { APIContext, MarkdownHeading } from 'astro';
+import project from 'virtual:starlight/project-context';
import config from 'virtual:starlight/user-config';
-import { generateToC, type TocItem } from './generateToC';
+import { generateToC } from '../generateToC';
import { getNewestCommitDate } from 'virtual:starlight/git-info';
-import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation';
-import { ensureTrailingSlash } from './path';
-import type { Route } from './routing';
-import { formatPath } from './format-path';
-import { useTranslations } from './translations';
-import { DeprecatedLabelsPropProxy } from './i18n';
+import { getPrevNextLinks, getSidebar } from '../navigation';
+import { ensureTrailingSlash } from '../path';
+import { getRouteBySlugParam, normalizeCollectionEntry } from '../routing';
+import type {
+ Route,
+ StarlightDocsCollectionEntry,
+ StarlightDocsEntry,
+ StarlightRouteData,
+} from './types';
+import { formatPath } from '../format-path';
+import { useTranslations } from '../translations';
+import { BuiltInDefaultLocale } from '../i18n';
+import { getEntry, render } from 'astro:content';
+import { getCollectionPathFromRoot } from '../collection';
export interface PageProps extends Route {
headings: MarkdownHeading[];
}
-export interface StarlightRouteData extends Route {
- /** Title of the site. */
- siteTitle: string;
- /** URL or path used as the link when clicking on the site title. */
- siteTitleHref: string;
- /** 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;
- /** @deprecated Use `Astro.locals.t()` instead. */
- labels: Record<string, never>;
+export async function useRouteData(context: APIContext): Promise<StarlightRouteData> {
+ const route =
+ ('slug' in context.params && getRouteBySlugParam(context.params.slug)) ||
+ (await get404Route(context.locals));
+ const { Content, headings } = await render(route.entry);
+ const routeData = generateRouteData({ props: { ...route, headings }, url: context.url });
+ return { ...routeData, Content };
}
export function generateRouteData({
@@ -56,7 +51,6 @@ export function generateRouteData({
toc: getToC(props),
lastUpdated: getLastUpdated(props),
editUrl: getEditUrl(props),
- labels: DeprecatedLabelsPropProxy,
};
}
@@ -121,3 +115,35 @@ export function getSiteTitle(lang: string): string {
export function getSiteTitleHref(locale: string | undefined): string {
return formatPath(locale || '/');
}
+
+/** Generate a route object for Starlight’s 404 page. */
+async function get404Route(locals: App.Locals): Promise<Route> {
+ const { lang = BuiltInDefaultLocale.lang, dir = BuiltInDefaultLocale.dir } =
+ config.defaultLocale || {};
+ let locale = config.defaultLocale?.locale;
+ if (locale === 'root') locale = undefined;
+
+ const entryMeta = { dir, lang, locale };
+
+ const fallbackEntry: StarlightDocsEntry = {
+ slug: '404',
+ id: '404',
+ body: '',
+ collection: 'docs',
+ data: {
+ title: '404',
+ template: 'splash',
+ editUrl: false,
+ head: [],
+ hero: { tagline: locals.t('404.text'), actions: [] },
+ pagefind: false,
+ sidebar: { hidden: false, attrs: {} },
+ draft: false,
+ },
+ filePath: `${getCollectionPathFromRoot('docs', project)}/404.md`,
+ };
+
+ const userEntry = (await getEntry('docs', '404')) as StarlightDocsCollectionEntry;
+ const entry = userEntry ? normalizeCollectionEntry(userEntry) : fallbackEntry;
+ return { ...entryMeta, entryMeta, entry, id: entry.id, slug: entry.slug };
+}
diff --git a/packages/starlight/utils/routing.ts b/packages/starlight/utils/routing/index.ts
index b3ac686e..460d15a7 100644
--- a/packages/starlight/utils/routing.ts
+++ b/packages/starlight/utils/routing/index.ts
@@ -1,55 +1,17 @@
import type { GetStaticPathsItem } from 'astro';
-import { type CollectionEntry, getCollection } from 'astro:content';
+import { getCollection } from 'astro:content';
import config from 'virtual:starlight/user-config';
import project from 'virtual:starlight/project-context';
-import { getCollectionPathFromRoot } from './collection';
-import {
- type LocaleData,
- localizedId,
- localizedSlug,
- slugToLocaleData,
- slugToParam,
-} from './slugs';
-import { validateLogoImports } from './validateLogoImports';
-import { BuiltInDefaultLocale } from './i18n';
+import { getCollectionPathFromRoot } from '../collection';
+import { localizedId, localizedSlug, slugToLocaleData, slugToParam } from '../slugs';
+import { validateLogoImports } from '../validateLogoImports';
+import { BuiltInDefaultLocale } from '../i18n';
+import type { Route, StarlightDocsCollectionEntry, StarlightDocsEntry } from './types';
// 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();
-// The type returned from `CollectionEntry` is different for legacy collections and collections
-// using a loader. This type is a common subset of both types.
-export type StarlightDocsCollectionEntry = Omit<
- CollectionEntry<'docs'>,
- 'id' | 'filePath' | 'render' | 'slug'
-> & {
- // Update the `id` property to be a string like in the loader type.
- id: string;
- // Add the `filePath` property which is only present in the loader type.
- filePath?: string;
- // Add the `slug` property which is only present in the legacy type.
- slug?: string;
-};
-
-export type StarlightDocsEntry = StarlightDocsCollectionEntry & {
- filePath: string;
- 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;
- /** @deprecated Migrate to the new Content Layer API and use `id` instead. */
- slug: string;
- /** The slug or unique ID if using the `legacy.collections` flag. */
- 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;
-}
-
interface Path extends GetStaticPathsItem {
params: { slug: string | undefined };
props: Route;
diff --git a/packages/starlight/utils/routing/middleware.ts b/packages/starlight/utils/routing/middleware.ts
new file mode 100644
index 00000000..9aa09a55
--- /dev/null
+++ b/packages/starlight/utils/routing/middleware.ts
@@ -0,0 +1,81 @@
+import type { APIContext } from 'astro';
+import { klona } from 'klona/lite';
+import { routeMiddleware } from 'virtual:starlight/route-middleware';
+import type { StarlightRouteData } from './types';
+
+/**
+ * Adds a deep clone of the passed `routeData` object to locals and then runs middleware.
+ * @param context Astro context object
+ * @param routeData Initial route data object to attach.
+ */
+export async function attachRouteDataAndRunMiddleware(
+ context: APIContext,
+ routeData: StarlightRouteData
+) {
+ context.locals.starlightRoute = klona(routeData);
+ const runner = new MiddlewareRunner(context, routeMiddleware);
+ await runner.run();
+}
+
+type MiddlewareHandler<T> = (context: T, next: () => Promise<void>) => void | Promise<void>;
+
+/**
+ * A middleware function wrapper that only allows a single execution of the wrapped function.
+ * Subsequent calls to `run()` are no-ops.
+ */
+class MiddlewareRunnerStep<T> {
+ #callback: MiddlewareHandler<T> | null;
+ constructor(callback: MiddlewareHandler<T>) {
+ this.#callback = callback;
+ }
+ async run(context: T, next: () => Promise<void>): Promise<void> {
+ if (this.#callback) {
+ await this.#callback(context, next);
+ this.#callback = null;
+ }
+ }
+}
+
+/**
+ * Class that runs a stack of middleware handlers with an initial context object.
+ * Middleware functions can mutate properties of the `context` object, but cannot replace it.
+ *
+ * @example
+ * const context = { value: 10 };
+ * const timesTwo = async (ctx, next) => {
+ * await next();
+ * ctx.value *= 2;
+ * };
+ * const addFive = async (ctx) => {
+ * ctx.value += 5;
+ * }
+ * const runner = new MiddlewareRunner(context, [timesTwo, addFive]);
+ * runner.run();
+ * console.log(context); // { value: 30 }
+ */
+class MiddlewareRunner<T> {
+ #context: T;
+ #steps: Array<MiddlewareRunnerStep<T>>;
+
+ constructor(
+ /** Context object passed as the first argument to each middleware function. */
+ context: T,
+ /** Array of middleware functions to run in sequence. */
+ stack: Array<MiddlewareHandler<T>> = []
+ ) {
+ this.#context = context;
+ this.#steps = stack.map((callback) => new MiddlewareRunnerStep(callback));
+ }
+
+ async #stepThrough(steps: Array<MiddlewareRunnerStep<T>>) {
+ let currentStep: MiddlewareRunnerStep<T>;
+ while (steps.length > 0) {
+ [currentStep, ...steps] = steps as [MiddlewareRunnerStep<T>, ...MiddlewareRunnerStep<T>[]];
+ await currentStep.run(this.#context, async () => this.#stepThrough(steps));
+ }
+ }
+
+ async run() {
+ await this.#stepThrough(this.#steps);
+ }
+}
diff --git a/packages/starlight/utils/routing/types.ts b/packages/starlight/utils/routing/types.ts
new file mode 100644
index 00000000..1f352dd3
--- /dev/null
+++ b/packages/starlight/utils/routing/types.ts
@@ -0,0 +1,96 @@
+import type { MarkdownHeading } from 'astro';
+import type { CollectionEntry, RenderResult } from 'astro:content';
+import type { TocItem } from '../generateToC';
+import type { LinkHTMLAttributes } from '../../schemas/sidebar';
+import type { Badge } from '../../schemas/badge';
+
+export interface LocaleData {
+ /** Writing direction. */
+ dir: 'ltr' | 'rtl';
+ /** BCP-47 language tag. */
+ lang: string;
+ /** The base path at which a language is served. `undefined` for root locale slugs. */
+ locale: string | undefined;
+}
+
+export interface SidebarLink {
+ type: 'link';
+ label: string;
+ href: string;
+ isCurrent: boolean;
+ badge: Badge | undefined;
+ attrs: LinkHTMLAttributes;
+}
+
+export interface SidebarGroup {
+ type: 'group';
+ label: string;
+ entries: (SidebarLink | SidebarGroup)[];
+ collapsed: boolean;
+ badge: Badge | undefined;
+}
+
+export type SidebarEntry = SidebarLink | SidebarGroup;
+
+export interface PaginationLinks {
+ /** Link to previous page in the sidebar. */
+ prev: SidebarLink | undefined;
+ /** Link to next page in the sidebar. */
+ next: SidebarLink | undefined;
+}
+
+// The type returned from `CollectionEntry` is different for legacy collections and collections
+// using a loader. This type is a common subset of both types.
+export type StarlightDocsCollectionEntry = Omit<
+ CollectionEntry<'docs'>,
+ 'id' | 'filePath' | 'render' | 'slug'
+> & {
+ // Update the `id` property to be a string like in the loader type.
+ id: string;
+ // Add the `filePath` property which is only present in the loader type.
+ filePath?: string;
+ // Add the `slug` property which is only present in the legacy type.
+ slug?: string;
+};
+
+export type StarlightDocsEntry = StarlightDocsCollectionEntry & {
+ filePath: string;
+ 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;
+ /** @deprecated Migrate to the new Content Layer API and use `id` instead. */
+ slug: string;
+ /** The slug or unique ID if using the `legacy.collections` flag. */
+ 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;
+}
+
+export interface StarlightRouteData extends Route {
+ /** Title of the site. */
+ siteTitle: string;
+ /** URL or path used as the link when clicking on the site title. */
+ siteTitleHref: string;
+ /** 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: PaginationLinks;
+ /** 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;
+ /** An Astro component to render the current page’s content if this route is a Markdown page. */
+ Content?: RenderResult['Content'];
+}
diff --git a/packages/starlight/utils/slugs.ts b/packages/starlight/utils/slugs.ts
index c6b0b739..c54e095d 100644
--- a/packages/starlight/utils/slugs.ts
+++ b/packages/starlight/utils/slugs.ts
@@ -1,16 +1,8 @@
import config from 'virtual:starlight/user-config';
+import { slugToLocale as getLocaleFromSlug } from '../integrations/shared/slugToLocale';
import { BuiltInDefaultLocale } from './i18n';
import { stripTrailingSlash } from './path';
-import { slugToLocale as getLocaleFromSlug } from '../integrations/shared/slugToLocale';
-
-export interface LocaleData {
- /** Writing direction. */
- dir: 'ltr' | 'rtl';
- /** BCP-47 language tag. */
- lang: string;
- /** The base path at which a language is served. `undefined` for root locale slugs. */
- locale: string | undefined;
-}
+import type { LocaleData } from './routing/types';
/**
* Get the “locale” of a slug. This is the base path at which a language is served.
diff --git a/packages/starlight/utils/starlight-page.ts b/packages/starlight/utils/starlight-page.ts
index fb4e0c11..97a3d7d9 100644
--- a/packages/starlight/utils/starlight-page.ts
+++ b/packages/starlight/utils/starlight-page.ts
@@ -5,19 +5,12 @@ import config from 'virtual:starlight/user-config';
import { getCollectionPathFromRoot } from './collection';
import { parseWithFriendlyErrors, parseAsyncWithFriendlyErrors } from './error-map';
import { stripLeadingAndTrailingSlashes } from './path';
-import {
- getSiteTitle,
- getSiteTitleHref,
- getToC,
- type PageProps,
- type StarlightRouteData,
-} from './route-data';
-import type { StarlightDocsEntry } from './routing';
+import { getSiteTitle, getSiteTitleHref, getToC, type PageProps } from './routing/data';
+import type { StarlightDocsEntry, StarlightRouteData } from './routing/types';
import { slugToLocaleData, urlToSlug } from './slugs';
import { getPrevNextLinks, getSidebar, getSidebarFromConfig } from './navigation';
import { docsSchema } from '../schema';
import type { Prettify, RemoveIndexSignature } from './types';
-import { DeprecatedLabelsPropProxy } from './i18n';
import { SidebarItemSchema } from '../schemas/sidebar';
import type { StarlightConfig, StarlightUserConfig } from './user-config';
@@ -153,7 +146,6 @@ export async function generateStarlightPageRouteData({
entryMeta,
hasSidebar: props.hasSidebar ?? entry.data.template !== 'splash',
headings,
- labels: DeprecatedLabelsPropProxy,
lastUpdated,
pagination: getPrevNextLinks(sidebar, config.pagination, entry.data),
sidebar,
diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts
index 280f881e..ad6612db 100644
--- a/packages/starlight/utils/user-config.ts
+++ b/packages/starlight/utils/user-config.ts
@@ -226,6 +226,14 @@ const UserConfigSchema = z.object({
.boolean()
.default(false)
.describe('Enable displaying a “Built with Starlight” link in your site’s footer.'),
+
+ /** Add middleware to process Starlight’s route data for each page. */
+ routeMiddleware: z
+ .string()
+ .transform((string) => [string])
+ .or(z.string().array())
+ .default([])
+ .describe('Add middleware to process Starlight’s route data for each page.'),
});
export const StarlightConfigSchema = UserConfigSchema.strict()
diff --git a/packages/starlight/virtual-internal.d.ts b/packages/starlight/virtual-internal.d.ts
index ebf7c8ed..111760e9 100644
--- a/packages/starlight/virtual-internal.d.ts
+++ b/packages/starlight/virtual-internal.d.ts
@@ -16,6 +16,10 @@ declare module 'virtual:starlight/collection-config' {
export const collections: import('astro:content').ContentConfig['collections'] | undefined;
}
+declare module 'virtual:starlight/route-middleware' {
+ export const routeMiddleware: Array<import('./route-data').RouteMiddlewareHandler>;
+}
+
declare module 'virtual:starlight/pagefind-config' {
export const pagefindUserConfig: Partial<
Extract<import('./types').StarlightConfig['pagefind'], object>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fbae1638..7319ec13 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -206,6 +206,9 @@ importers:
js-yaml:
specifier: ^4.1.0
version: 4.1.0
+ klona:
+ specifier: ^2.0.6
+ version: 2.0.6
mdast-util-directive:
specifier: ^3.0.0
version: 3.0.0
@@ -4073,6 +4076,11 @@ packages:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
+ /klona@2.0.6:
+ resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
+ engines: {node: '>= 8'}
+ dev: false
+
/lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}