@astrojs/starlight 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/404.astro CHANGED
@@ -26,7 +26,7 @@ const fallbackEntry: StarlightDocsEntry = {
26
26
  head: [],
27
27
  hero: { tagline: t('404.text'), actions: [] },
28
28
  pagefind: false,
29
- sidebar: { hidden: false },
29
+ sidebar: { hidden: false, attrs: {} },
30
30
  },
31
31
  render: async () => ({
32
32
  Content: EmptyContent,
package/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1238](https://github.com/withastro/starlight/pull/1238) [`02a808e`](https://github.com/withastro/starlight/commit/02a808e4a0b9ac2383576e3495f6a766b663d773) Thanks [@delucis](https://github.com/delucis)! - Add support for Astro v4, drop support for Astro v3
8
+
9
+ ⚠️ **BREAKING CHANGE** Astro v3 is no longer supported. Make sure you [update Astro](https://docs.astro.build/en/guides/upgrade-to/v4/) and any other integrations at the same time as updating Starlight.
10
+
11
+ Use the new `@astrojs/upgrade` command to upgrade Astro and Starlight together:
12
+
13
+ ```sh
14
+ npx @astrojs/upgrade
15
+ ```
16
+
17
+ - [#1242](https://github.com/withastro/starlight/pull/1242) [`d8fc9e1`](https://github.com/withastro/starlight/commit/d8fc9e15bd2ae4c945b5a3856a6ce3b5629e8b29) Thanks [@delucis](https://github.com/delucis)! - Enables link prefetching on hover by default
18
+
19
+ Astro v4’s [prefetch](https://docs.astro.build/en/guides/prefetch) support is now enabled by default. If `prefetch` is not set in `astro.config.mjs`, Starlight will use `prefetch: { prefetchAll: true, defaultStrategy: 'hover' }` by default.
20
+
21
+ If you want to preserve previous behaviour, disable link prefetching in `astro.config.mjs`:
22
+
23
+ ```js
24
+ import { defineConfig } from 'astro/config';
25
+ import starlight from '@astrojs/starlight';
26
+
27
+ export default defineConfig({
28
+ // Disable link prefetching:
29
+ prefetch: false,
30
+
31
+ integrations: [
32
+ starlight({
33
+ // ...
34
+ }),
35
+ ],
36
+ });
37
+ ```
38
+
39
+ ### Patch Changes
40
+
41
+ - [#1226](https://github.com/withastro/starlight/pull/1226) [`909afa2`](https://github.com/withastro/starlight/commit/909afa2d468099e237bfbd25eda56270b7b00082) Thanks [@tlandmangh](https://github.com/tlandmangh)! - Add Dutch translations of default aside labels
42
+
43
+ - [#1243](https://github.com/withastro/starlight/pull/1243) [`ee234eb`](https://github.com/withastro/starlight/commit/ee234ebddcba8d07e2c879f33e38631c8955ffcf) Thanks [@khajimatov](https://github.com/khajimatov)! - Fix typo in Russian untranslated content notice
44
+
45
+ - [#1170](https://github.com/withastro/starlight/pull/1170) [`bcc2301`](https://github.com/withastro/starlight/commit/bcc2301c06796edec3923c666078e82eaf5a1990) Thanks [@tmcw](https://github.com/tmcw)! - Fix timezone-reliance in LastUpdated
46
+
47
+ - [#1203](https://github.com/withastro/starlight/pull/1203) [`4601449`](https://github.com/withastro/starlight/commit/4601449894bbbd619e4149788113090b67697fe1) Thanks [@orhun](https://github.com/orhun)! - Adds Matrix social link icon
48
+
49
+ ## 0.14.0
50
+
51
+ ### Minor Changes
52
+
53
+ - [#1144](https://github.com/withastro/starlight/pull/1144) [`7c0b8cb`](https://github.com/withastro/starlight/commit/7c0b8cb334c501678f7ab87cce372cddfdde34ed) Thanks [@delucis](https://github.com/delucis)! - Adds a configuration option to disable site indexing with Pagefind and the default search UI
54
+
55
+ - [#942](https://github.com/withastro/starlight/pull/942) [`efd7fdc`](https://github.com/withastro/starlight/commit/efd7fdcb55b39988f157c1a4b2c368c86a39520f) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds plugin API
56
+
57
+ See the [plugins reference](https://starlight.astro.build/reference/plugins/) to learn more about creating plugins for Starlight using this new API.
58
+
59
+ - [#1135](https://github.com/withastro/starlight/pull/1135) [`e5a863a`](https://github.com/withastro/starlight/commit/e5a863a98b2e5335e122ca440dcb84e9426939b4) Thanks [@delucis](https://github.com/delucis)! - Exposes localized UI strings in route data
60
+
61
+ Component overrides can now access a `labels` object in their props which includes all the localized UI strings for the current page.
62
+
63
+ - [#1162](https://github.com/withastro/starlight/pull/1162) [`00d101b`](https://github.com/withastro/starlight/commit/00d101b159bfa4bb307a66ccae53dd417d9564e0) Thanks [@delucis](https://github.com/delucis)! - Adds support for extending Starlight’s content collection schemas
64
+
3
65
  ## 0.13.1
4
66
 
5
67
  ### Patch Changes
@@ -1,17 +1,15 @@
1
1
  ---
2
2
  import Icon from '../user-components/Icon.astro';
3
3
  import type { Props } from '../props';
4
- import { useTranslations } from '../utils/translations';
5
4
 
6
- const t = useTranslations(Astro.props.locale);
7
- const { editUrl } = Astro.props;
5
+ const { editUrl, labels } = Astro.props;
8
6
  ---
9
7
 
10
8
  {
11
9
  editUrl && (
12
10
  <a href={editUrl} class="sl-flex">
13
11
  <Icon name="pencil" size="1.2em" />
14
- {t('page.editLink')}
12
+ {labels['page.editLink']}
15
13
  </a>
16
14
  )
17
15
  }
@@ -1,14 +1,13 @@
1
1
  ---
2
2
  import Icon from '../user-components/Icon.astro';
3
3
  import type { Props } from '../props';
4
- import { useTranslations } from '../utils/translations';
5
4
 
6
- const t = useTranslations(Astro.props.locale);
5
+ const { labels } = Astro.props;
7
6
  ---
8
7
 
9
8
  <p class="sl-flex">
10
9
  <Icon name={'warning'} size="1.5em" color="var(--sl-color-orange-high)" /><span
11
- >{t('i18n.untranslatedContent')}</span
10
+ >{labels['i18n.untranslatedContent']}</span
12
11
  >
13
12
  </p>
14
13
 
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import config from 'virtual:starlight/user-config';
2
3
  import type { Props } from '../props';
3
4
 
4
5
  import {
@@ -8,6 +9,12 @@ import {
8
9
  SocialIcons,
9
10
  ThemeSelect,
10
11
  } from 'virtual:starlight/components';
12
+
13
+ /**
14
+ * Render the `Search` component if Pagefind is enabled or the default search component has been overridden.
15
+ */
16
+ const shouldRenderSearch =
17
+ config.pagefind || config.components.Search !== '@astrojs/starlight/components/Search.astro';
11
18
  ---
12
19
 
13
20
  <div class="header sl-flex">
@@ -15,7 +22,7 @@ import {
15
22
  <SiteTitle {...Astro.props} />
16
23
  </div>
17
24
  <div class="sl-flex">
18
- <Search {...Astro.props} />
25
+ {shouldRenderSearch && <Search {...Astro.props} />}
19
26
  </div>
20
27
  <div class="sl-hidden md:sl-flex right-group">
21
28
  <div class="sl-flex social-icons">
@@ -101,4 +101,6 @@ export const Icons = {
101
101
  '<path d="M22.04 7.6c0-2.8-2.19-5.1-4.75-5.93a15.19 15.19 0 0 0-10.44.55C3.16 3.96 2 7.78 1.95 11.58c-.02 3.12.3 11.36 4.94 11.42 3.45.04 3.97-4.4 5.56-6.55 1.14-1.52 2.6-1.95 4.4-2.4 3.1-.76 5.2-3.2 5.2-6.44Z"/>',
102
102
  slack:
103
103
  '<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52Zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313ZM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834Zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312Zm10.122 2.521a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834Zm-1.268 0a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312Zm-2.523 10.122a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52Zm0-1.268a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313Z"/>',
104
+ matrix:
105
+ '<path d="M22.5 1.5v21h-2.25V24H24V0h-3.75v1.5h2.25ZM7.46 7.95V9.1h.04a3.02 3.02 0 0 1 2.61-1.39c.54 0 1.03.1 1.48.32.44.2.78.58 1.01 1.1.26-.37.6-.7 1.03-.99.44-.28.95-.43 1.55-.43.45 0 .87.06 1.26.17.38.11.71.29.99.53.27.24.49.56.64.95.15.4.23.86.23 1.42v5.72h-2.34v-4.85c0-.29-.01-.56-.04-.8a1.73 1.73 0 0 0-.18-.67 1.1 1.1 0 0 0-.44-.45 1.6 1.6 0 0 0-.78-.16c-.33 0-.6.06-.8.19-.2.12-.37.29-.48.5a2 2 0 0 0-.23.69c-.04.26-.06.52-.06.78v4.77H10.6v-4.8l-.01-.75a2.29 2.29 0 0 0-.14-.69c-.08-.2-.23-.38-.42-.5a1.5 1.5 0 0 0-.85-.2c-.15.01-.3.04-.44.08-.19.06-.37.15-.52.28-.18.14-.32.34-.44.6-.12.26-.18.6-.18 1.02v4.96H5.25V7.94h2.21ZM1.5 1.5v21h2.25V24H0V0h3.75v1.5H1.5Z"/>',
104
106
  };
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  import config from 'virtual:starlight/user-config';
3
3
  import { localizedUrl } from '../utils/localizedUrl';
4
- import { useTranslations } from '../utils/translations';
5
4
  import Select from './Select.astro';
6
5
  import type { Props } from '../props';
7
6
 
@@ -12,7 +11,7 @@ function localizedPathname(locale: string | undefined): string {
12
11
  return localizedUrl(Astro.url, locale).pathname;
13
12
  }
14
13
 
15
- const t = useTranslations(Astro.props.locale);
14
+ const { labels } = Astro.props;
16
15
  ---
17
16
 
18
17
  {
@@ -20,7 +19,7 @@ const t = useTranslations(Astro.props.locale);
20
19
  <starlight-lang-select>
21
20
  <Select
22
21
  icon="translate"
23
- label={t('languageSelect.accessibleLabel')}
22
+ label={labels['languageSelect.accessibleLabel']}
24
23
  value={localizedPathname(Astro.props.locale)}
25
24
  options={Object.entries(config.locales).map(([code, locale]) => ({
26
25
  value: localizedPathname(code),
@@ -1,17 +1,15 @@
1
1
  ---
2
2
  import type { Props } from '../props';
3
- import { useTranslations } from '../utils/translations';
4
3
 
5
- const { lang, lastUpdated, locale } = Astro.props;
6
- const t = useTranslations(locale);
4
+ const { labels, lang, lastUpdated } = Astro.props;
7
5
  ---
8
6
 
9
7
  {
10
8
  lastUpdated && (
11
9
  <p>
12
- {t('page.lastUpdated')}{' '}
10
+ {labels['page.lastUpdated']}{' '}
13
11
  <time datetime={lastUpdated.toISOString()}>
14
- {lastUpdated.toLocaleDateString(lang, { dateStyle: 'medium' })}
12
+ {lastUpdated.toLocaleDateString(lang, { dateStyle: 'medium', timeZone: 'UTC' })}
15
13
  </time>
16
14
  </p>
17
15
  )
@@ -1,16 +1,14 @@
1
1
  ---
2
2
  import type { Props } from '../props';
3
- import { useTranslations } from '../utils/translations';
4
-
5
3
  import Icon from '../user-components/Icon.astro';
6
4
 
7
- const t = useTranslations(Astro.props.locale);
5
+ const { labels } = Astro.props;
8
6
  ---
9
7
 
10
8
  <starlight-menu-button>
11
9
  <button
12
10
  aria-expanded="false"
13
- aria-label={t('menuButton.accessibleLabel')}
11
+ aria-label={labels['menuButton.accessibleLabel']}
14
12
  aria-controls="starlight__sidebar"
15
13
  class="sl-flex md:sl-hidden"
16
14
  >
@@ -1,11 +1,9 @@
1
1
  ---
2
- import { useTranslations } from '../utils/translations';
3
2
  import Icon from '../user-components/Icon.astro';
4
3
  import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
5
4
  import type { Props } from '../props';
6
5
 
7
- const { locale, toc } = Astro.props;
8
- const t = useTranslations(locale);
6
+ const { labels, toc } = Astro.props;
9
7
  ---
10
8
 
11
9
  {
@@ -15,7 +13,7 @@ const t = useTranslations(locale);
15
13
  <details id="starlight__mobile-toc">
16
14
  <summary id="starlight__on-this-page--mobile" class="sl-flex">
17
15
  <div class="toggle sl-flex">
18
- {t('tableOfContents.onThisPage')}
16
+ {labels['tableOfContents.onThisPage']}
19
17
  <Icon name={'right-caret'} class="caret" size="1rem" />
20
18
  </div>
21
19
  <span class="display-current" />
@@ -1,18 +1,15 @@
1
1
  ---
2
- import type { Props } from '../props';
3
- import { useTranslations } from '../utils/translations';
4
-
5
2
  import { MobileMenuToggle } from 'virtual:starlight/components';
3
+ import type { Props } from '../props';
6
4
 
7
- const { hasSidebar, locale } = Astro.props;
8
- const t = useTranslations(locale);
5
+ const { hasSidebar, labels } = Astro.props;
9
6
  ---
10
7
 
11
8
  <div class="page sl-flex">
12
9
  <header class="header"><slot name="header" /></header>
13
10
  {
14
11
  hasSidebar && (
15
- <nav class="sidebar" aria-label={t('sidebarNav.accessibleLabel')}>
12
+ <nav class="sidebar" aria-label={labels['sidebarNav.accessibleLabel']}>
16
13
  <MobileMenuToggle {...Astro.props} />
17
14
  <div id="starlight__sidebar" class="sidebar-pane">
18
15
  <div class="sidebar-content sl-flex">
@@ -1,12 +1,10 @@
1
1
  ---
2
- import { useTranslations } from '../utils/translations';
3
2
  import Icon from '../user-components/Icon.astro';
4
3
  import type { Props } from '../props';
5
4
 
6
- const { dir, locale, pagination } = Astro.props;
5
+ const { dir, labels, pagination } = Astro.props;
7
6
  const { prev, next } = pagination;
8
7
  const isRtl = dir === 'rtl';
9
- const t = useTranslations(locale);
10
8
  ---
11
9
 
12
10
  <div class="pagination-links" dir={dir}>
@@ -15,7 +13,7 @@ const t = useTranslations(locale);
15
13
  <a href={prev.href} rel="prev">
16
14
  <Icon name={isRtl ? 'right-arrow' : 'left-arrow'} size="1.5rem" />
17
15
  <span>
18
- {t('page.previousLink')}
16
+ {labels['page.previousLink']}
19
17
  <br />
20
18
  <span class="link-title">{prev.label}</span>
21
19
  </span>
@@ -27,7 +25,7 @@ const t = useTranslations(locale);
27
25
  <a href={next.href} rel="next">
28
26
  <Icon name={isRtl ? 'left-arrow' : 'right-arrow'} size="1.5rem" />
29
27
  <span>
30
- {t('page.nextLink')}
28
+ {labels['page.nextLink']}
31
29
  <br />
32
30
  <span class="link-title">{next.label}</span>
33
31
  </span>
@@ -1,14 +1,16 @@
1
1
  ---
2
2
  import '@pagefind/default-ui/css/ui.css';
3
- import { useTranslations } from '../utils/translations';
4
3
  import Icon from '../user-components/Icon.astro';
5
4
  import type { Props } from '../props';
6
5
 
7
- const t = useTranslations(Astro.props.locale);
6
+ const { labels } = Astro.props;
7
+
8
8
  const pagefindTranslations = {
9
- placeholder: t('search.label'),
9
+ placeholder: labels['search.label'],
10
10
  ...Object.fromEntries(
11
- Object.entries(t.pick('pagefind.')).map(([key, value]) => [key.replace('pagefind.', ''), value])
11
+ Object.entries(labels)
12
+ .filter(([key]) => key.startsWith('pagefind.'))
13
+ .map(([key, value]) => [key.replace('pagefind.', ''), value])
12
14
  ),
13
15
  };
14
16
  ---
@@ -18,23 +20,27 @@ const pagefindTranslations = {
18
20
  {
19
21
  /* The span is `aria-hidden` because it is not shown on small screens. Instead, the icon label is used for accessibility purposes. */
20
22
  }
21
- <Icon name="magnifier" label={t('search.label')} />
22
- <span class="sl-hidden md:sl-block" aria-hidden="true">{t('search.label')}</span>
23
- <Icon name="forward-slash" class="sl-hidden md:sl-block" label={t('search.shortcutLabel')} />
23
+ <Icon name="magnifier" label={labels['search.label']} />
24
+ <span class="sl-hidden md:sl-block" aria-hidden="true">{labels['search.label']}</span>
25
+ <Icon
26
+ name="forward-slash"
27
+ class="sl-hidden md:sl-block"
28
+ label={labels['search.shortcutLabel']}
29
+ />
24
30
  </button>
25
31
 
26
- <dialog style="padding:0" aria-label={t('search.label')}>
32
+ <dialog style="padding:0" aria-label={labels['search.label']}>
27
33
  <div class="dialog-frame sl-flex">
28
34
  {
29
35
  /* TODO: Make the layout of this button flexible to accommodate different word lengths. Currently hard-coded for English: “Cancel” */
30
36
  }
31
37
  <button data-close-modal class="sl-flex md:sl-hidden">
32
- {t('search.cancelLabel')}
38
+ {labels['search.cancelLabel']}
33
39
  </button>
34
40
  {
35
41
  import.meta.env.DEV ? (
36
42
  <div style="margin: auto; text-align: center; white-space: pre-line;" dir="ltr">
37
- <p>{t('search.devWarning')}</p>
43
+ <p>{labels['search.devWarning']}</p>
38
44
  </div>
39
45
  ) : (
40
46
  <div class="search-container">
@@ -1,12 +1,11 @@
1
1
  ---
2
2
  import { PAGE_TITLE_ID } from '../constants';
3
- import { useTranslations } from '../utils/translations';
4
3
  import type { Props } from '../props';
5
4
 
6
- const t = useTranslations(Astro.props.locale);
5
+ const { labels } = Astro.props;
7
6
  ---
8
7
 
9
- <a href={`#${PAGE_TITLE_ID}`}>{t('skipLink.label')}</a>
8
+ <a href={`#${PAGE_TITLE_ID}`}>{labels['skipLink.label']}</a>
10
9
 
11
10
  <style>
12
11
  a {
@@ -1,17 +1,15 @@
1
1
  ---
2
- import { useTranslations } from '../utils/translations';
3
2
  import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
4
3
  import type { Props } from '../props';
5
4
 
6
- const { locale, toc } = Astro.props;
7
- const t = useTranslations(locale);
5
+ const { labels, toc } = Astro.props;
8
6
  ---
9
7
 
10
8
  {
11
9
  toc && (
12
10
  <starlight-toc data-min-h={toc.minHeadingLevel} data-max-h={toc.maxHeadingLevel}>
13
11
  <nav aria-labelledby="starlight__on-this-page">
14
- <h2 id="starlight__on-this-page">{t('tableOfContents.onThisPage')}</h2>
12
+ <h2 id="starlight__on-this-page">{labels['tableOfContents.onThisPage']}</h2>
15
13
  <TableOfContentsList toc={toc.items} />
16
14
  </nav>
17
15
  </starlight-toc>
@@ -1,21 +1,20 @@
1
1
  ---
2
- import { useTranslations } from '../utils/translations';
3
2
  import Select from './Select.astro';
4
3
  import type { Props } from '../props';
5
4
 
6
- const t = useTranslations(Astro.props.locale);
5
+ const { labels } = Astro.props;
7
6
  ---
8
7
 
9
8
  <starlight-theme-select>
10
9
  {/* TODO: Can we give this select a width that works well for each language’s strings? */}
11
10
  <Select
12
11
  icon="laptop"
13
- label={t('themeSelect.accessibleLabel')}
12
+ label={labels['themeSelect.accessibleLabel']}
14
13
  value="auto"
15
14
  options={[
16
- { label: t('themeSelect.dark'), selected: false, value: 'dark' },
17
- { label: t('themeSelect.light'), selected: false, value: 'light' },
18
- { label: t('themeSelect.auto'), selected: true, value: 'auto' },
15
+ { label: labels['themeSelect.dark'], selected: false, value: 'dark' },
16
+ { label: labels['themeSelect.light'], selected: false, value: 'light' },
17
+ { label: labels['themeSelect.auto'], selected: true, value: 'auto' },
19
18
  ]}
20
19
  width="6.25em"
21
20
  />
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import mdx from '@astrojs/mdx';
2
- import type { AstroIntegration, AstroUserConfig } from 'astro';
2
+ import type { AstroIntegration } from 'astro';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { dirname, relative } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
@@ -7,64 +7,68 @@ import { starlightAsides } from './integrations/asides';
7
7
  import { starlightExpressiveCode } from './integrations/expressive-code/index';
8
8
  import { starlightSitemap } from './integrations/sitemap';
9
9
  import { vitePluginStarlightUserConfig } from './integrations/virtual-user-config';
10
- import { errorMap } from './utils/error-map';
11
- import { StarlightConfigSchema, type StarlightUserConfig } from './utils/user-config';
12
10
  import { rehypeRtlCodeSupport } from './integrations/code-rtl-support';
13
11
  import { createTranslationSystemFromFs } from './utils/translations-fs';
12
+ import { runPlugins, type StarlightUserConfigWithPlugins } from './utils/plugins';
13
+ import type { StarlightConfig } from './types';
14
14
 
15
- export default function StarlightIntegration(opts: StarlightUserConfig): AstroIntegration {
16
- const parsedConfig = StarlightConfigSchema.safeParse(opts, { errorMap });
17
-
18
- if (!parsedConfig.success) {
19
- throw new Error(
20
- 'Invalid config passed to starlight integration\n' +
21
- parsedConfig.error.issues.map((i) => i.message).join('\n')
22
- );
23
- }
24
-
25
- const userConfig = parsedConfig.data;
26
-
27
- const Starlight: AstroIntegration = {
15
+ export default function StarlightIntegration({
16
+ plugins,
17
+ ...opts
18
+ }: StarlightUserConfigWithPlugins): AstroIntegration {
19
+ let userConfig: StarlightConfig;
20
+ return {
28
21
  name: '@astrojs/starlight',
29
22
  hooks: {
30
- 'astro:config:setup': ({ config, injectRoute, updateConfig }) => {
31
- const useTranslations = createTranslationSystemFromFs(userConfig, config);
23
+ 'astro:config:setup': async ({
24
+ command,
25
+ config,
26
+ injectRoute,
27
+ isRestart,
28
+ logger,
29
+ updateConfig,
30
+ }) => {
31
+ // Run plugins to get the final configuration and any extra Astro integrations to load.
32
+ const { integrations, starlightConfig } = await runPlugins(opts, plugins, {
33
+ command,
34
+ config,
35
+ isRestart,
36
+ logger,
37
+ });
38
+ userConfig = starlightConfig;
39
+
40
+ const useTranslations = createTranslationSystemFromFs(starlightConfig, config);
41
+
32
42
  injectRoute({
33
43
  pattern: '404',
34
- entryPoint: '@astrojs/starlight/404.astro',
44
+ entrypoint: '@astrojs/starlight/404.astro',
35
45
  });
36
46
  injectRoute({
37
47
  pattern: '[...slug]',
38
- entryPoint: '@astrojs/starlight/index.astro',
48
+ entrypoint: '@astrojs/starlight/index.astro',
39
49
  });
40
- const integrations: AstroIntegration[] = [];
41
- if (!config.integrations.find(({ name }) => name === 'astro-expressive-code')) {
50
+ // Add built-in integrations only if they are not already added by the user through the
51
+ // config or by a plugin.
52
+ const allIntegrations = [...config.integrations, ...integrations];
53
+ if (!allIntegrations.find(({ name }) => name === 'astro-expressive-code')) {
42
54
  integrations.push(
43
- ...starlightExpressiveCode({
44
- starlightConfig: userConfig,
45
- astroConfig: config,
46
- useTranslations,
47
- })
55
+ ...starlightExpressiveCode({ starlightConfig, astroConfig: config, useTranslations })
48
56
  );
49
57
  }
50
- if (!config.integrations.find(({ name }) => name === '@astrojs/sitemap')) {
51
- integrations.push(starlightSitemap(userConfig));
58
+ if (!allIntegrations.find(({ name }) => name === '@astrojs/sitemap')) {
59
+ integrations.push(starlightSitemap(starlightConfig));
52
60
  }
53
- if (!config.integrations.find(({ name }) => name === '@astrojs/mdx')) {
61
+ if (!allIntegrations.find(({ name }) => name === '@astrojs/mdx')) {
54
62
  integrations.push(mdx());
55
63
  }
56
- const newConfig: AstroUserConfig = {
64
+ updateConfig({
57
65
  integrations,
58
66
  vite: {
59
- plugins: [vitePluginStarlightUserConfig(userConfig, config)],
67
+ plugins: [vitePluginStarlightUserConfig(starlightConfig, config)],
60
68
  },
61
69
  markdown: {
62
70
  remarkPlugins: [
63
- ...starlightAsides({
64
- starlightConfig: userConfig,
65
- astroConfig: config,
66
- useTranslations,
67
- }),
71
+ ...starlightAsides({ starlightConfig, astroConfig: config, useTranslations }),
68
72
  ],
69
73
  rehypePlugins: [rehypeRtlCodeSupport()],
70
74
  shikiConfig:
@@ -72,11 +76,13 @@ export default function StarlightIntegration(opts: StarlightUserConfig): AstroIn
72
76
  config.markdown.shikiConfig.theme !== 'github-dark' ? {} : { theme: 'css-variables' },
73
77
  },
74
78
  scopedStyleStrategy: 'where',
75
- };
76
- updateConfig(newConfig);
79
+ // If not already configured, default to prefetching all links on hover.
80
+ prefetch: config.prefetch ?? { prefetchAll: true },
81
+ });
77
82
  },
78
83
 
79
84
  'astro:build:done': ({ dir }) => {
85
+ if (!userConfig.pagefind) return;
80
86
  const targetDir = fileURLToPath(dir);
81
87
  const cwd = dirname(fileURLToPath(import.meta.url));
82
88
  const relativeDir = relative(cwd, targetDir);
@@ -90,6 +96,4 @@ export default function StarlightIntegration(opts: StarlightUserConfig): AstroIn
90
96
  },
91
97
  },
92
98
  };
93
-
94
- return Starlight;
95
99
  }
@@ -1,3 +1,5 @@
1
+ /// <reference types="mdast-util-directive" />
2
+
1
3
  import type { AstroConfig, AstroUserConfig } from 'astro';
2
4
  import { h as _h, s as _s, type Properties } from 'hastscript';
3
5
  import type { Paragraph as P, Root } from 'mdast';
@@ -100,7 +102,7 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
100
102
  const locale = pathToLocale(file.history[0], options);
101
103
  const t = options.useTranslations(locale);
102
104
  visit(tree, (node, index, parent) => {
103
- if (!parent || index === null || node.type !== 'containerDirective') {
105
+ if (!parent || index === undefined || node.type !== 'containerDirective') {
104
106
  return;
105
107
  }
106
108
  const variant = node.name;
@@ -112,7 +114,7 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
112
114
  // title prop, and remove the paragraph from children.
113
115
  let title = t(`aside.${variant}`);
114
116
  remove(node, (child): boolean | void => {
115
- if (child.data?.directiveLabel) {
117
+ if (child.data && 'directiveLabel' in child.data && child.data.directiveLabel) {
116
118
  if (
117
119
  'children' in child &&
118
120
  Array.isArray(child.children) &&
@@ -1,4 +1,4 @@
1
- import type { Root } from 'hastscript/lib/core';
1
+ import type { Root } from 'hast';
2
2
  import { CONTINUE, SKIP, visit } from 'unist-util-visit';
3
3
 
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
@@ -159,32 +159,34 @@
159
159
  "./style/markdown.css": "./style/markdown.css"
160
160
  },
161
161
  "peerDependencies": {
162
- "astro": "^3.2.0"
162
+ "astro": "^4.0.0"
163
163
  },
164
164
  "devDependencies": {
165
- "@astrojs/markdown-remark": "^3.2.1",
165
+ "@astrojs/markdown-remark": "^4.0.0",
166
166
  "@types/node": "^18.16.19",
167
167
  "@vitest/coverage-v8": "^0.33.0",
168
- "astro": "^3.2.3",
168
+ "astro": "^4.0.1",
169
169
  "vitest": "^0.33.0"
170
170
  },
171
171
  "dependencies": {
172
- "@astrojs/mdx": "^1.1.0",
172
+ "@astrojs/mdx": "^2.0.0",
173
173
  "@astrojs/sitemap": "^3.0.3",
174
174
  "@pagefind/default-ui": "^1.0.3",
175
- "@types/mdast": "^3.0.11",
176
- "astro-expressive-code": "^0.29.0",
175
+ "@types/hast": "^3.0.3",
176
+ "@types/mdast": "^4.0.3",
177
+ "astro-expressive-code": "^0.29.3",
177
178
  "bcp-47": "^2.1.0",
178
179
  "execa": "^8.0.1",
179
- "hast-util-select": "^5.0.5",
180
- "hastscript": "^7.2.0",
180
+ "hast-util-select": "^6.0.2",
181
+ "hastscript": "^8.0.0",
182
+ "mdast-util-directive": "^3.0.0",
181
183
  "pagefind": "^1.0.3",
182
- "rehype": "^12.0.1",
183
- "remark-directive": "^2.0.1",
184
- "unified": "^10.1.2",
185
- "unist-util-remove": "^3.1.1",
186
- "unist-util-visit": "^4.1.2",
187
- "vfile": "^5.3.7"
184
+ "rehype": "^13.0.1",
185
+ "remark-directive": "^3.0.0",
186
+ "unified": "^11.0.4",
187
+ "unist-util-remove": "^4.0.0",
188
+ "unist-util-visit": "^5.0.0",
189
+ "vfile": "^6.0.1"
188
190
  },
189
191
  "scripts": {
190
192
  "test": "vitest",
package/schema.ts CHANGED
@@ -8,100 +8,153 @@ import { HeroSchema } from './schemas/hero';
8
8
  import { SidebarLinkItemHTMLAttributesSchema } from './schemas/sidebar';
9
9
  export { i18nSchema } from './schemas/i18n';
10
10
 
11
- export function docsSchema() {
12
- return (context: SchemaContext) =>
13
- z.object({
14
- /** The title of the current page. Required. */
15
- title: z.string(),
16
-
17
- /**
18
- * A short description of the current page’s content. Optional, but recommended.
19
- * A good description is 150–160 characters long and outlines the key content
20
- * of the page in a clear and engaging way.
21
- */
22
- description: z.string().optional(),
23
-
24
- /**
25
- * Custom URL where a reader can edit this page.
26
- * Overrides the `editLink.baseUrl` global config if set.
27
- *
28
- * Can also be set to `false` to disable showing an edit link on this page.
29
- */
30
- editUrl: z.union([z.string().url(), z.boolean()]).optional().default(true),
31
-
32
- /** Set custom `<head>` tags just for this page. */
33
- head: HeadConfigSchema(),
34
-
35
- /** Override global table of contents configuration for this page. */
36
- tableOfContents: TableOfContentsSchema().optional(),
37
-
38
- /**
39
- * Set the layout style for this page.
40
- * Can be `'doc'` (the default) or `'splash'` for a wider layout without any sidebars.
41
- */
42
- template: z.enum(['doc', 'splash']).default('doc'),
43
-
44
- /** Display a hero section on this page. */
45
- hero: HeroSchema(context).optional(),
46
-
47
- /**
48
- * The last update date of the current page.
49
- * Overrides the `lastUpdated` global config or the date generated from the Git history.
50
- */
51
- lastUpdated: z.union([z.date(), z.boolean()]).optional(),
52
-
53
- /**
54
- * The previous navigation link configuration.
55
- * Overrides the `pagination` global config or the link text and/or URL.
56
- */
57
- prev: PrevNextLinkConfigSchema(),
58
- /**
59
- * The next navigation link configuration.
60
- * Overrides the `pagination` global config or the link text and/or URL.
61
- */
62
- next: PrevNextLinkConfigSchema(),
63
-
64
- sidebar: z
65
- .object({
66
- /**
67
- * The order of this page in the navigation.
68
- * Pages are sorted by this value in ascending order. Then by slug.
69
- * If not provided, pages will be sorted alphabetically by slug.
70
- * If two pages have the same order value, they will be sorted alphabetically by slug.
71
- */
72
- order: z.number().optional(),
73
-
74
- /**
75
- * The label for this page in the navigation.
76
- * Defaults to the page `title` if not set.
77
- */
78
- label: z.string().optional(),
79
-
80
- /**
81
- * Prevents this page from being included in autogenerated sidebar groups.
82
- */
83
- hidden: z.boolean().default(false),
84
- /**
85
- * Adds a badge to the sidebar link.
86
- * Can be a string or an object with a variant and text.
87
- * Variants include 'note', 'tip', 'caution', 'danger', 'success', and 'default'.
88
- * Passing only a string defaults to the 'default' variant which uses the site accent color.
89
- */
90
- badge: BadgeConfigSchema(),
91
- /** HTML attributes to add to the sidebar link. */
92
- attrs: SidebarLinkItemHTMLAttributesSchema(),
93
- })
94
- .default({}),
95
-
96
- /** Display an announcement banner at the top of this page. */
97
- banner: z
98
- .object({
99
- /** The content of the banner. Supports HTML syntax. */
100
- content: z.string(),
101
- })
102
- .optional(),
103
-
104
- /** Pagefind indexing for this page - set to false to disable. */
105
- pagefind: z.boolean().default(true),
106
- });
11
+ /** Default content collection schema for Starlight’s `docs` collection. */
12
+ const StarlightFrontmatterSchema = (context: SchemaContext) =>
13
+ z.object({
14
+ /** The title of the current page. Required. */
15
+ title: z.string(),
16
+
17
+ /**
18
+ * A short description of the current page’s content. Optional, but recommended.
19
+ * A good description is 150–160 characters long and outlines the key content
20
+ * of the page in a clear and engaging way.
21
+ */
22
+ description: z.string().optional(),
23
+
24
+ /**
25
+ * Custom URL where a reader can edit this page.
26
+ * Overrides the `editLink.baseUrl` global config if set.
27
+ *
28
+ * Can also be set to `false` to disable showing an edit link on this page.
29
+ */
30
+ editUrl: z.union([z.string().url(), z.boolean()]).optional().default(true),
31
+
32
+ /** Set custom `<head>` tags just for this page. */
33
+ head: HeadConfigSchema(),
34
+
35
+ /** Override global table of contents configuration for this page. */
36
+ tableOfContents: TableOfContentsSchema().optional(),
37
+
38
+ /**
39
+ * Set the layout style for this page.
40
+ * Can be `'doc'` (the default) or `'splash'` for a wider layout without any sidebars.
41
+ */
42
+ template: z.enum(['doc', 'splash']).default('doc'),
43
+
44
+ /** Display a hero section on this page. */
45
+ hero: HeroSchema(context).optional(),
46
+
47
+ /**
48
+ * The last update date of the current page.
49
+ * Overrides the `lastUpdated` global config or the date generated from the Git history.
50
+ */
51
+ lastUpdated: z.union([z.date(), z.boolean()]).optional(),
52
+
53
+ /**
54
+ * The previous navigation link configuration.
55
+ * Overrides the `pagination` global config or the link text and/or URL.
56
+ */
57
+ prev: PrevNextLinkConfigSchema(),
58
+ /**
59
+ * The next navigation link configuration.
60
+ * Overrides the `pagination` global config or the link text and/or URL.
61
+ */
62
+ next: PrevNextLinkConfigSchema(),
63
+
64
+ sidebar: z
65
+ .object({
66
+ /**
67
+ * The order of this page in the navigation.
68
+ * Pages are sorted by this value in ascending order. Then by slug.
69
+ * If not provided, pages will be sorted alphabetically by slug.
70
+ * If two pages have the same order value, they will be sorted alphabetically by slug.
71
+ */
72
+ order: z.number().optional(),
73
+
74
+ /**
75
+ * The label for this page in the navigation.
76
+ * Defaults to the page `title` if not set.
77
+ */
78
+ label: z.string().optional(),
79
+
80
+ /**
81
+ * Prevents this page from being included in autogenerated sidebar groups.
82
+ */
83
+ hidden: z.boolean().default(false),
84
+ /**
85
+ * Adds a badge to the sidebar link.
86
+ * Can be a string or an object with a variant and text.
87
+ * Variants include 'note', 'tip', 'caution', 'danger', 'success', and 'default'.
88
+ * Passing only a string defaults to the 'default' variant which uses the site accent color.
89
+ */
90
+ badge: BadgeConfigSchema(),
91
+ /** HTML attributes to add to the sidebar link. */
92
+ attrs: SidebarLinkItemHTMLAttributesSchema(),
93
+ })
94
+ .default({}),
95
+
96
+ /** Display an announcement banner at the top of this page. */
97
+ banner: z
98
+ .object({
99
+ /** The content of the banner. Supports HTML syntax. */
100
+ content: z.string(),
101
+ })
102
+ .optional(),
103
+
104
+ /** Pagefind indexing for this page - set to false to disable. */
105
+ pagefind: z.boolean().default(true),
106
+ });
107
+ /** Type of Starlight’s default frontmatter schema. */
108
+ type DefaultSchema = ReturnType<typeof StarlightFrontmatterSchema>;
109
+
110
+ /** Plain object, union, and intersection Zod types. */
111
+ type BaseSchemaWithoutEffects =
112
+ | z.AnyZodObject
113
+ | z.ZodUnion<[BaseSchemaWithoutEffects, ...BaseSchemaWithoutEffects[]]>
114
+ | z.ZodDiscriminatedUnion<string, z.AnyZodObject[]>
115
+ | z.ZodIntersection<BaseSchemaWithoutEffects, BaseSchemaWithoutEffects>;
116
+ /** Base subset of Zod types that we support passing to the `extend` option. */
117
+ type BaseSchema = BaseSchemaWithoutEffects | z.ZodEffects<BaseSchemaWithoutEffects>;
118
+
119
+ /** Type that extends Starlight’s default schema with an optional, user-defined schema. */
120
+ type ExtendedSchema<T extends BaseSchema> = T extends BaseSchema
121
+ ? z.ZodIntersection<DefaultSchema, T>
122
+ : DefaultSchema;
123
+
124
+ interface DocsSchemaOpts<T extends BaseSchema> {
125
+ /**
126
+ * Extend Starlight’s schema with additional fields.
127
+ *
128
+ * @example
129
+ * // Extend the built-in schema with a Zod schema.
130
+ * docsSchema({
131
+ * extend: z.object({
132
+ * // Add a new field to the schema.
133
+ * category: z.enum(['tutorial', 'guide', 'reference']).optional(),
134
+ * }),
135
+ * })
136
+ *
137
+ * // Use the Astro image helper.
138
+ * docsSchema({
139
+ * extend: ({ image }) => {
140
+ * return z.object({
141
+ * cover: image(),
142
+ * });
143
+ * },
144
+ * })
145
+ */
146
+ extend?: T | ((context: SchemaContext) => T);
147
+ }
148
+
149
+ /** Content collection schema for Starlight’s `docs` collection. */
150
+ export function docsSchema<T extends BaseSchema>({ extend }: DocsSchemaOpts<T> = {}) {
151
+ return (context: SchemaContext): ExtendedSchema<T> => {
152
+ const UserSchema = typeof extend === 'function' ? extend(context) : extend;
153
+
154
+ return (
155
+ UserSchema
156
+ ? StarlightFrontmatterSchema(context).and(UserSchema)
157
+ : StarlightFrontmatterSchema(context)
158
+ ) as ExtendedSchema<T>;
159
+ };
107
160
  }
package/schemas/i18n.ts CHANGED
@@ -1,7 +1,31 @@
1
1
  import { z } from 'astro/zod';
2
2
 
3
- export function i18nSchema() {
4
- return starlightI18nSchema().merge(pagefindI18nSchema()).merge(expressiveCodeI18nSchema());
3
+ interface i18nSchemaOpts<T extends z.AnyZodObject = z.ZodObject<{}>> {
4
+ /**
5
+ * Extend Starlight’s i18n schema with additional fields.
6
+ *
7
+ * @example
8
+ * // Add two optional fields to the default schema.
9
+ * i18nSchema({
10
+ * extend: z
11
+ * .object({
12
+ * 'customUi.heading': z.string(),
13
+ * 'customUi.text': z.string(),
14
+ * })
15
+ * .partial(),
16
+ * })
17
+ */
18
+ extend?: T;
19
+ }
20
+
21
+ /** Content collection schema for Starlight’s optional `i18n` collection. */
22
+ export function i18nSchema<T extends z.AnyZodObject = z.ZodObject<{}>>({
23
+ extend = z.object({}) as T,
24
+ }: i18nSchemaOpts<T> = {}) {
25
+ return starlightI18nSchema()
26
+ .merge(pagefindI18nSchema())
27
+ .merge(expressiveCodeI18nSchema())
28
+ .merge(extend);
5
29
  }
6
30
  export type i18nSchemaOutput = z.output<ReturnType<typeof i18nSchema>>;
7
31
 
package/schemas/social.ts CHANGED
@@ -25,6 +25,7 @@ export const socialLinks = [
25
25
  'reddit',
26
26
  'patreon',
27
27
  'slack',
28
+ 'matrix',
28
29
  ] as const;
29
30
 
30
31
  export const SocialLinksSchema = () =>
@@ -65,6 +66,7 @@ export const SocialLinksSchema = () =>
65
66
  reddit: 'Reddit',
66
67
  patreon: 'Patreon',
67
68
  slack: 'Slack',
69
+ matrix: 'Matrix',
68
70
  }[key];
69
71
  labelledLinks[key] = { label, url };
70
72
  }
@@ -19,8 +19,8 @@
19
19
  "page.previousLink": "Vorige",
20
20
  "page.nextLink": "Volgende",
21
21
  "404.text": "Pagina niet gevonden. Controleer de URL of probeer de zoekbalk.",
22
- "aside.note": "Note",
22
+ "aside.note": "Opmerking",
23
23
  "aside.tip": "Tip",
24
- "aside.caution": "Caution",
25
- "aside.danger": "Danger"
24
+ "aside.caution": "Opgepast",
25
+ "aside.danger": "Gevaar"
26
26
  }
@@ -13,7 +13,7 @@
13
13
  "sidebarNav.accessibleLabel": "Основное",
14
14
  "tableOfContents.onThisPage": "На этой странице",
15
15
  "tableOfContents.overview": "Обзор",
16
- "i18n.untranslatedContent": "Этот содержимое пока не доступно на вашем языке.",
16
+ "i18n.untranslatedContent": "Это содержимое пока не доступно на вашем языке.",
17
17
  "page.editLink": "Редактировать страницу",
18
18
  "page.lastUpdated": "Последнее обновление:",
19
19
  "page.previousLink": "Предыдущая",
package/types.ts CHANGED
@@ -1 +1,2 @@
1
1
  export type { StarlightConfig } from './utils/user-config';
2
+ export type { StarlightPlugin } from './utils/plugins';
@@ -1,3 +1,4 @@
1
+ import type { Element } from 'hast';
1
2
  import { select } from 'hast-util-select';
2
3
  import { rehype } from 'rehype';
3
4
  import { CONTINUE, SKIP, visit } from 'unist-util-visit';
@@ -50,7 +51,7 @@ const getIDs = () => {
50
51
  const tabsProcessor = rehype()
51
52
  .data('settings', { fragment: true })
52
53
  .use(function tabs() {
53
- return (tree, file) => {
54
+ return (tree: Element, file) => {
54
55
  file.data.panels = [];
55
56
  let isFirst = true;
56
57
  visit(tree, 'element', (node) => {
@@ -19,10 +19,16 @@ export function createTranslationSystem(
19
19
 
20
20
  /**
21
21
  * Generate a utility function that returns UI strings for the given `locale`.
22
+ *
23
+ * Also includes an `all()` method for getting the entire dictionary.
24
+ *
22
25
  * @param {string | undefined} [locale]
23
26
  * @example
24
27
  * const t = useTranslations('en');
25
- * const label = t('search.label'); // => 'Search'
28
+ * const label = t('search.label');
29
+ * // => 'Search'
30
+ * const dictionary = t.all();
31
+ * // => { 'skipLink.label': 'Skip to content', 'search.label': 'Search', ... }
26
32
  */
27
33
  return function useTranslations(locale: string | undefined) {
28
34
  const lang = localeToLang(locale, config.locales, config.defaultLocale);
@@ -32,8 +38,7 @@ export function createTranslationSystem(
32
38
  userTranslations[lang]
33
39
  );
34
40
  const t = <K extends keyof typeof dictionary>(key: K) => dictionary[key];
35
- t.pick = (startOfKey: string) =>
36
- Object.fromEntries(Object.entries(dictionary).filter(([k]) => k.startsWith(startOfKey)));
41
+ t.all = () => dictionary;
37
42
  return t;
38
43
  };
39
44
  }
@@ -0,0 +1,226 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ import { z } from 'astro/zod';
3
+ import { StarlightConfigSchema, type StarlightUserConfig } from '../utils/user-config';
4
+ import { errorMap } from '../utils/error-map';
5
+
6
+ /**
7
+ * Runs Starlight plugins in the order that they are configured after validating the user-provided
8
+ * configuration and returns the final validated user config that may have been updated by the
9
+ * plugins and a list of any integrations added by the plugins.
10
+ */
11
+ export async function runPlugins(
12
+ starlightUserConfig: StarlightUserConfig,
13
+ pluginsUserConfig: StarlightPluginsUserConfig,
14
+ context: StarlightPluginContext
15
+ ) {
16
+ // Validate the user-provided configuration.
17
+ let userConfig = starlightUserConfig;
18
+ let starlightConfig = StarlightConfigSchema.safeParse(userConfig, { errorMap });
19
+
20
+ if (!starlightConfig.success) {
21
+ throwValidationError(starlightConfig.error, 'Invalid config passed to starlight integration');
22
+ }
23
+
24
+ // Validate the user-provided plugins configuration.
25
+ const pluginsConfig = starlightPluginsConfigSchema.safeParse(pluginsUserConfig, {
26
+ errorMap,
27
+ });
28
+
29
+ if (!pluginsConfig.success) {
30
+ throwValidationError(
31
+ pluginsConfig.error,
32
+ 'Invalid plugins config passed to starlight integration'
33
+ );
34
+ }
35
+
36
+ // A list of Astro integrations added by the various plugins.
37
+ const integrations: AstroIntegration[] = [];
38
+
39
+ for (const {
40
+ name,
41
+ hooks: { setup },
42
+ } of pluginsConfig.data) {
43
+ await setup({
44
+ config: pluginsUserConfig ? { ...userConfig, plugins: pluginsUserConfig } : userConfig,
45
+ updateConfig(newConfig) {
46
+ // Ensure that plugins do not update the `plugins` config key.
47
+ if ('plugins' in newConfig) {
48
+ throw new Error(
49
+ `The '${name}' plugin tried to update the 'plugins' config key which is not supported.`
50
+ );
51
+ }
52
+
53
+ // If the plugin is updating the user config, re-validate it.
54
+ const mergedUserConfig = { ...userConfig, ...newConfig };
55
+ const mergedConfig = StarlightConfigSchema.safeParse(mergedUserConfig, { errorMap });
56
+
57
+ if (!mergedConfig.success) {
58
+ throwValidationError(
59
+ mergedConfig.error,
60
+ `Invalid config update provided by the '${name}' plugin`
61
+ );
62
+ }
63
+
64
+ // If the updated config is valid, keep track of both the user config and parsed config.
65
+ userConfig = mergedUserConfig;
66
+ starlightConfig = mergedConfig;
67
+ },
68
+ addIntegration(integration) {
69
+ // Collect any Astro integrations added by the plugin.
70
+ integrations.push(integration);
71
+ },
72
+ astroConfig: {
73
+ ...context.config,
74
+ integrations: [...context.config.integrations, ...integrations],
75
+ },
76
+ command: context.command,
77
+ isRestart: context.isRestart,
78
+ logger: context.logger.fork(name),
79
+ });
80
+ }
81
+
82
+ return { integrations, starlightConfig: starlightConfig.data };
83
+ }
84
+
85
+ function throwValidationError(error: z.ZodError, message: string): never {
86
+ throw new Error(`${message}\n${error.issues.map((i) => i.message).join('\n')}`);
87
+ }
88
+
89
+ // https://github.com/withastro/astro/blob/910eb00fe0b70ca80bd09520ae100e8c78b675b5/packages/astro/src/core/config/schema.ts#L113
90
+ const astroIntegrationSchema = z.object({
91
+ name: z.string(),
92
+ hooks: z.object({}).passthrough().default({}),
93
+ }) as z.Schema<AstroIntegration>;
94
+
95
+ const baseStarlightPluginSchema = z.object({
96
+ /** Name of the Starlight plugin. */
97
+ name: z.string(),
98
+ });
99
+
100
+ /**
101
+ * A plugin `config` and `updateConfig` argument are purposely not validated using the Starlight
102
+ * user config schema but properly typed for user convenience because we do not want to run any of
103
+ * the Zod `transform`s used in the user config schema when running plugins.
104
+ */
105
+ const starlightPluginSchema = baseStarlightPluginSchema.extend({
106
+ /** The different hooks available to the plugin. */
107
+ hooks: z.object({
108
+ /**
109
+ * Plugin setup function called with an object containing various values that can be used by
110
+ * the plugin to interact with Starlight.
111
+ */
112
+ setup: z.function(
113
+ z.tuple([
114
+ z.object({
115
+ /**
116
+ * A read-only copy of the user-supplied Starlight configuration.
117
+ *
118
+ * Note that this configuration may have been updated by other plugins configured
119
+ * before this one.
120
+ */
121
+ config: z.any() as z.Schema<
122
+ // The configuration passed to plugins should contains the list of plugins.
123
+ StarlightUserConfig & { plugins?: z.input<typeof baseStarlightPluginSchema>[] }
124
+ >,
125
+ /**
126
+ * A callback function to update the user-supplied Starlight configuration.
127
+ *
128
+ * You only need to provide the configuration values that you want to update but no deep
129
+ * merge is performed.
130
+ *
131
+ * @example
132
+ * {
133
+ * name: 'My Starlight Plugin',
134
+ * hooks: {
135
+ * setup({ updateConfig }) {
136
+ * updateConfig({
137
+ * description: 'Custom description',
138
+ * });
139
+ * }
140
+ * }
141
+ * }
142
+ */
143
+ updateConfig: z.function(
144
+ z.tuple([z.record(z.any()) as z.Schema<Partial<StarlightUserConfig>>]),
145
+ z.void()
146
+ ),
147
+ /**
148
+ * A callback function to add an Astro integration required by this plugin.
149
+ *
150
+ * @see https://docs.astro.build/en/reference/integrations-reference/
151
+ *
152
+ * @example
153
+ * {
154
+ * name: 'My Starlight Plugin',
155
+ * hooks: {
156
+ * setup({ addIntegration }) {
157
+ * addIntegration({
158
+ * name: 'My Plugin Astro Integration',
159
+ * hooks: {
160
+ * 'astro:config:setup': () => {
161
+ * // …
162
+ * },
163
+ * },
164
+ * });
165
+ * }
166
+ * }
167
+ * }
168
+ */
169
+ addIntegration: z.function(z.tuple([astroIntegrationSchema]), z.void()),
170
+ /**
171
+ * A read-only copy of the user-supplied Astro configuration.
172
+ *
173
+ * Note that this configuration is resolved before any other integrations have run.
174
+ *
175
+ * @see https://docs.astro.build/en/reference/integrations-reference/#config-option
176
+ */
177
+ astroConfig: z.any() as z.Schema<StarlightPluginContext['config']>,
178
+ /**
179
+ * The command used to run Starlight.
180
+ *
181
+ * @see https://docs.astro.build/en/reference/integrations-reference/#command-option
182
+ */
183
+ command: z.any() as z.Schema<StarlightPluginContext['command']>,
184
+ /**
185
+ * `false` when the dev server starts, `true` when a reload is triggered.
186
+ *
187
+ * @see https://docs.astro.build/en/reference/integrations-reference/#isrestart-option
188
+ */
189
+ isRestart: z.any() as z.Schema<StarlightPluginContext['isRestart']>,
190
+ /**
191
+ * An instance of the Astro integration logger with all logged messages prefixed with the
192
+ * plugin name.
193
+ *
194
+ * @see https://docs.astro.build/en/reference/integrations-reference/#astrointegrationlogger
195
+ */
196
+ logger: z.any() as z.Schema<StarlightPluginContext['logger']>,
197
+ }),
198
+ ]),
199
+ z.union([z.void(), z.promise(z.void())])
200
+ ),
201
+ }),
202
+ });
203
+
204
+ const starlightPluginsConfigSchema = z.array(starlightPluginSchema).default([]);
205
+
206
+ type StarlightPluginsUserConfig = z.input<typeof starlightPluginsConfigSchema>;
207
+
208
+ export type StarlightPlugin = z.input<typeof starlightPluginSchema>;
209
+
210
+ export type StarlightUserConfigWithPlugins = StarlightUserConfig & {
211
+ /**
212
+ * A list of plugins to extend Starlight with.
213
+ *
214
+ * @example
215
+ * // Add Starlight Algolia plugin.
216
+ * starlight({
217
+ * plugins: [starlightAlgolia({ … })],
218
+ * })
219
+ */
220
+ plugins?: StarlightPluginsUserConfig;
221
+ };
222
+
223
+ export type StarlightPluginContext = Pick<
224
+ Parameters<NonNullable<AstroIntegration['hooks']['astro:config:setup']>>[0],
225
+ 'command' | 'config' | 'isRestart' | 'logger'
226
+ >;
@@ -29,6 +29,8 @@ export interface StarlightRouteData extends Route {
29
29
  lastUpdated: Date | undefined;
30
30
  /** URL object for the address where this page can be edited if enabled. */
31
31
  editUrl: URL | undefined;
32
+ /** Record of UI strings localized for the current page. */
33
+ labels: ReturnType<ReturnType<typeof useTranslations>['all']>;
32
34
  }
33
35
 
34
36
  export function generateRouteData({
@@ -48,6 +50,7 @@ export function generateRouteData({
48
50
  toc: getToC(props),
49
51
  lastUpdated: getLastUpdated(props),
50
52
  editUrl: getEditUrl(props),
53
+ labels: useTranslations(locale).all(),
51
54
  };
52
55
  }
53
56
 
@@ -190,6 +190,13 @@ const UserConfigSchema = z.object({
190
190
  */
191
191
  expressiveCode: ExpressiveCodeSchema(),
192
192
 
193
+ /**
194
+ * Define whether Starlight’s default site search provider Pagefind is enabled.
195
+ * Set to `false` to disable indexing your site with Pagefind.
196
+ * This will also hide the default search UI if in use.
197
+ */
198
+ pagefind: z.boolean().default(true),
199
+
193
200
  /** Specify paths to components that should override Starlight’s default components */
194
201
  components: ComponentConfigSchema(),
195
202