From f493361d7b64a3279980e0f046c3a52196ab94e0 Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Sat, 15 Feb 2025 11:32:41 +0100 Subject: Move route data to `Astro.locals` (#2390) Co-authored-by: Chris Swithinbank Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com> Co-authored-by: trueberryless <99918022+trueberryless@users.noreply.github.com> --- .changeset/chilled-bees-pump.md | 36 ++++ docs/src/components/sidebar-preview.astro | 2 +- .../content/docs/guides/overriding-components.mdx | 38 ++-- docs/src/content/docs/guides/route-data.mdx | 139 +++++++++++++++ docs/src/content/docs/guides/sidebar.mdx | 2 +- .../docs/hi/guides/overriding-components.md | 2 +- docs/src/content/docs/hi/guides/sidebar.mdx | 2 +- .../docs/id/guides/overriding-components.md | 2 +- docs/src/content/docs/id/guides/sidebar.mdx | 2 +- docs/src/content/docs/reference/overrides.md | 145 +-------------- docs/src/content/docs/reference/plugins.md | 37 ++++ docs/src/content/docs/reference/route-data.mdx | 197 +++++++++++++++++++++ packages/docsearch/DocSearch.astro | 1 - .../__tests__/basics/config-errors.test.ts | 1 + .../starlight/__tests__/basics/route-data.test.ts | 25 +-- .../starlight/__tests__/basics/routing.test.ts | 3 +- .../basics/starlight-page-route-data.test.ts | 16 +- .../starlight/__tests__/edit-url/edit-url.test.ts | 2 +- .../__tests__/i18n-root-locale/routing.test.ts | 2 +- .../__tests__/middleware/middleware.test.ts | 30 ++++ .../__tests__/middleware/vitest.config.ts | 3 + .../starlight/__tests__/plugins/config.test.ts | 25 ++- .../__tests__/plugins/route-middleware.test.ts | 46 +++++ packages/starlight/__tests__/test-utils.ts | 2 +- packages/starlight/components/Banner.astro | 4 +- packages/starlight/components/ContentPanel.astro | 4 - .../starlight/components/DraftContentNotice.astro | 1 - packages/starlight/components/EditLink.astro | 3 +- .../components/FallbackContentNotice.astro | 1 - packages/starlight/components/Footer.astro | 8 +- packages/starlight/components/Head.astro | 3 +- packages/starlight/components/Header.astro | 11 +- packages/starlight/components/Hero.astro | 3 +- packages/starlight/components/LanguageSelect.astro | 5 +- packages/starlight/components/LastUpdated.astro | 4 +- .../starlight/components/MarkdownContent.astro | 1 - .../starlight/components/MobileMenuFooter.astro | 7 +- .../starlight/components/MobileMenuToggle.astro | 1 - .../components/MobileTableOfContents.astro | 3 +- packages/starlight/components/Page.astro | 64 +++---- packages/starlight/components/PageFrame.astro | 5 +- packages/starlight/components/PageSidebar.astro | 8 +- packages/starlight/components/PageTitle.astro | 3 +- packages/starlight/components/Pagination.astro | 3 +- packages/starlight/components/Search.astro | 1 - packages/starlight/components/Sidebar.astro | 8 +- .../starlight/components/SidebarPersister.astro | 3 +- packages/starlight/components/SidebarSublist.astro | 3 +- packages/starlight/components/SiteTitle.astro | 3 +- packages/starlight/components/SkipLink.astro | 1 - packages/starlight/components/SocialIcons.astro | 1 - packages/starlight/components/StarlightPage.astro | 10 +- .../starlight/components/TableOfContents.astro | 3 +- packages/starlight/components/ThemeProvider.astro | 1 - packages/starlight/components/ThemeSelect.astro | 1 - .../starlight/components/TwoColumnContent.astro | 6 +- .../starlight/integrations/virtual-user-config.ts | 27 +++ packages/starlight/locals.d.ts | 26 +++ packages/starlight/locals.ts | 38 +++- packages/starlight/package.json | 2 + packages/starlight/props.ts | 14 +- packages/starlight/route-data.ts | 11 ++ packages/starlight/routes/common.astro | 16 +- packages/starlight/routes/ssr/index.astro | 2 +- packages/starlight/routes/static/404.astro | 42 +---- packages/starlight/routes/static/index.astro | 5 +- packages/starlight/utils/i18n.ts | 20 --- packages/starlight/utils/navigation.ts | 55 ++---- packages/starlight/utils/plugins.ts | 69 +++++++- packages/starlight/utils/route-data.ts | 123 ------------- packages/starlight/utils/routing.ts | 181 ------------------- packages/starlight/utils/routing/data.ts | 149 ++++++++++++++++ packages/starlight/utils/routing/index.ts | 143 +++++++++++++++ packages/starlight/utils/routing/middleware.ts | 81 +++++++++ packages/starlight/utils/routing/types.ts | 96 ++++++++++ packages/starlight/utils/slugs.ts | 12 +- packages/starlight/utils/starlight-page.ts | 12 +- packages/starlight/utils/user-config.ts | 8 + packages/starlight/virtual-internal.d.ts | 4 + pnpm-lock.yaml | 8 + 80 files changed, 1317 insertions(+), 770 deletions(-) create mode 100644 .changeset/chilled-bees-pump.md create mode 100644 docs/src/content/docs/guides/route-data.mdx create mode 100644 docs/src/content/docs/reference/route-data.mdx create mode 100644 packages/starlight/__tests__/middleware/middleware.test.ts create mode 100644 packages/starlight/__tests__/middleware/vitest.config.ts create mode 100644 packages/starlight/__tests__/plugins/route-middleware.test.ts create mode 100644 packages/starlight/route-data.ts delete mode 100644 packages/starlight/utils/route-data.ts delete mode 100644 packages/starlight/utils/routing.ts create mode 100644 packages/starlight/utils/routing/data.ts create mode 100644 packages/starlight/utils/routing/index.ts create mode 100644 packages/starlight/utils/routing/middleware.ts create mode 100644 packages/starlight/utils/routing/types.ts 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 && ( +- ++ +)} +``` + +_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'; --- - E-mail Me + E-mail Me ``` 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'; --- E-mail Me - + ``` -When rendering a built-in component inside a custom component: - -- Spread `Astro.props` into it. This makes sure that it receives all the data it needs to render. -- Add a [``](https://docs.astro.build/en/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 [``](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'; --- - + @@ -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; ---

{title}

@@ -127,28 +122,27 @@ const { title } = Astro.props.entry.data; ``` -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 ? (
Built with Starlight 🌟
) : ( - + ) 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; +--- + +

The title of this site is “{siteTitle}”

+``` + +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. + + + +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 + '!'; + }); + ``` + + + +#### 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; ``` -[ओवरराइड्स संदर्भ](/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; ``` -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-1), [``](#search), [``](#socialicons), [``](#themeselect), and [``](#languageselect). +The default implementation displays [``](#sitetitle), [``](#search), [``](#socialicons), [``](#themeselect), and [``](#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. + ```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 { }; } ``` + ## `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 &&
} 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'; ---- -
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'; --- 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'; --- 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';