summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLorenzo Lewis2024-07-05 12:57:23 +0200
committerGitHub2024-07-05 12:57:23 +0200
commiteeba06ea7df962e8f0520e145d28b8c17cd32c18 (patch)
tree54c377789392cb068d4b1bd1f9c343620a7c5582
parent48ce1257988ad239cf7a5aeed4a57f1e820b4a7f (diff)
downloadIT.starlight-eeba06ea7df962e8f0520e145d28b8c17cd32c18.tar.gz
IT.starlight-eeba06ea7df962e8f0520e145d28b8c17cd32c18.tar.bz2
IT.starlight-eeba06ea7df962e8f0520e145d28b8c17cd32c18.zip
Feat: Autogenerate sidebar labels (#1874)
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
-rw-r--r--.changeset/seven-owls-taste.md19
-rw-r--r--docs/astro.config.mjs44
-rw-r--r--docs/src/components/sidebar-preview.astro8
-rw-r--r--docs/src/content/docs/guides/sidebar.mdx137
-rw-r--r--docs/src/content/docs/reference/configuration.mdx69
-rw-r--r--examples/basics/astro.config.mjs2
-rw-r--r--examples/tailwind/astro.config.mjs2
-rw-r--r--packages/starlight/__tests__/basics/config-errors.test.ts4
-rw-r--r--packages/starlight/__tests__/i18n-sidebar/i18n-sidebar-fallback-slug.test.ts159
-rw-r--r--packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts246
-rw-r--r--packages/starlight/__tests__/i18n-sidebar/sidebar-internal-link-error.test.ts33
-rw-r--r--packages/starlight/__tests__/i18n-sidebar/vitest.config.ts20
-rw-r--r--packages/starlight/__tests__/sidebar-slug-error/sidebar-slug-error.test.ts19
-rw-r--r--packages/starlight/__tests__/sidebar-slug-error/vitest.config.ts6
-rw-r--r--packages/starlight/schemas/sidebar.ts27
-rw-r--r--packages/starlight/utils/error-map.ts7
-rw-r--r--packages/starlight/utils/navigation.ts42
17 files changed, 742 insertions, 102 deletions
diff --git a/.changeset/seven-owls-taste.md b/.changeset/seven-owls-taste.md
new file mode 100644
index 00000000..ce108075
--- /dev/null
+++ b/.changeset/seven-owls-taste.md
@@ -0,0 +1,19 @@
+---
+'@astrojs/starlight': patch
+---
+
+Adds a new syntax for specifying sidebar link items for internal links
+
+You can now specify an internal page using only its slug, either as a string, or as an object with a `slug` property:
+
+```js
+starlight({
+ title: 'Docs with easier sidebars',
+ sidebar: [
+ 'getting-started',
+ { slug: 'guides/installation' },
+ ],
+})
+```
+
+Starlight will use the linked page’s frontmatter to configure the sidebar link.
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index a94376ee..5e8f0ba0 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -87,49 +87,11 @@ export default defineConfig({
uk: 'Почніть звідси',
},
items: [
- {
- label: 'Getting Started',
- link: 'getting-started',
- translations: {
- de: 'Erste Schritte',
- es: 'Empezando',
- ja: '入門',
- fr: 'Mise en route',
- it: 'Iniziamo',
- id: 'Memulai',
- 'zh-CN': '开始使用',
- 'pt-BR': 'Introdução',
- 'pt-PT': 'Introdução',
- ko: '시작하기',
- tr: 'Başlarken',
- ru: 'Введение',
- hi: 'पहले कदम',
- uk: 'Вступ',
- },
- },
- {
- label: 'Manual Setup',
- link: 'manual-setup',
- translations: {
- de: 'Manuelle Einrichtung',
- es: 'Configuración Manual',
- ja: '手動セットアップ',
- fr: 'Installation manuelle',
- // it: 'Manual Setup',
- id: 'Instalasi Manual',
- 'zh-CN': '手动配置',
- 'pt-BR': 'Instalação Manual',
- 'pt-PT': 'Instalação Manual',
- ko: '수동으로 설정하기',
- tr: 'Elle Kurulum',
- ru: 'Установка вручную',
- hi: 'मैनुअल सेटअप',
- uk: 'Ручне встановлення',
- },
- },
+ 'getting-started',
+ 'manual-setup',
{
label: 'Environmental Impact',
- link: 'environmental-impact',
+ slug: 'environmental-impact',
translations: {
de: 'Umweltbelastung',
es: 'Documentación ecológica',
diff --git a/docs/src/components/sidebar-preview.astro b/docs/src/components/sidebar-preview.astro
index f392972a..810ac1cc 100644
--- a/docs/src/components/sidebar-preview.astro
+++ b/docs/src/components/sidebar-preview.astro
@@ -1,5 +1,9 @@
---
-import type { AutoSidebarGroup, SidebarItem } from '../../../packages/starlight/schemas/sidebar';
+import type {
+ AutoSidebarGroup,
+ SidebarItem,
+ InternalSidebarLinkItem,
+} from '../../../packages/starlight/schemas/sidebar';
import SidebarSublist from '../../../packages/starlight/components/SidebarSublist.astro';
import type { SidebarEntry } from '../../../packages/starlight/utils/navigation';
@@ -7,7 +11,7 @@ interface Props {
config: SidebarConfig;
}
-type SidebarConfig = Exclude<SidebarItem, AutoSidebarGroup>[];
+type SidebarConfig = Exclude<SidebarItem, AutoSidebarGroup | InternalSidebarLinkItem>[];
const { config } = Astro.props;
diff --git a/docs/src/content/docs/guides/sidebar.mdx b/docs/src/content/docs/guides/sidebar.mdx
index 0632b537..8e368b15 100644
--- a/docs/src/content/docs/guides/sidebar.mdx
+++ b/docs/src/content/docs/guides/sidebar.mdx
@@ -49,19 +49,73 @@ Learn more about autogenerated sidebars in the [autogenerated groups](#autogener
## Add links and link groups
-To configure your sidebar [links](#links) and [groups of links](#groups) (within a collapsible header), use the [`starlight.sidebar`](/reference/configuration/#sidebar) property in `astro.config.mjs`.
+To configure your sidebar links and groups of links (within a collapsible header), use the [`starlight.sidebar`](/reference/configuration/#sidebar) property in `astro.config.mjs`.
By combining links and groups, you can create a wide variety of sidebar layouts.
-### Links
+### Internal links
-Add a link to an internal or external page using an object with `label` and `link` properties.
+Add a link to a page in `src/content/docs/` using an object with the `slug` property.
+The linked page’s title will be used as the label by default.
+
+For example, with the following configuration:
+
+```js "slug:"
+starlight({
+ sidebar: [
+ { slug: 'constellations/andromeda' },
+ { slug: 'constellations/orion' },
+ ],
+});
+```
+
+And the following file structure:
+
+<FileTree>
+
+- src/
+ - content/
+ - docs/
+ - constellations/
+ - andromeda.md
+ - orion.md
+
+</FileTree>
+
+The following sidebar will be generated:
+
+<SidebarPreview
+ config={[
+ { label: 'Andromeda', link: '' },
+ { label: 'Orion', link: '' },
+ ]}
+/>
+
+To override the values inferred from a linked page’s frontmatter, you can add `label`, [`translations`](#internationalization), and [`attrs`](#custom-html-attributes) properties.
+
+See [“Customizing autogenerated links”](#customizing-autogenerated-links-in-frontmatter) for more details about controlling the sidebar appearance from page frontmatter.
+
+#### Shorthand for internal links
+
+Internal links can also be specified by providing only a string for the page slug as a shorthand.
+
+For example, the following configuration is equivalent to the configuration above, which used `slug`:
+
+```js "slug:"
+starlight({
+ sidebar: ['constellations/andromeda', 'constellations/orion'],
+});
+```
+
+### Other links
+
+Add a link to an external or non-docs page using an object with `label` and `link` properties.
```js "label:" "link:"
starlight({
sidebar: [
- // A link to the Ganymede moon page.
- { label: 'Ganymede', link: '/moons/ganymede/' },
+ // A link to a non-docs page on this site.
+ { label: 'Meteor Store', link: '/shop/' },
// An external link to the NASA website.
{ label: 'NASA', link: 'https://www.nasa.gov/' },
],
@@ -72,7 +126,7 @@ The configuration above generates the following sidebar:
<SidebarPreview
config={[
- { label: 'Ganymede', link: '' },
+ { label: 'Meteor Store', link: '' },
{ label: 'NASA', link: 'https://www.nasa.gov/' },
]}
/>
@@ -93,15 +147,15 @@ starlight({
{
label: 'Constellations',
items: [
- { label: 'Carina', link: '/constellations/carina/' },
- { label: 'Centaurus', link: '/constellations/centaurus/' },
+ 'constellations/carina',
+ 'constellations/centaurus',
// A nested group of links for seasonal constellations.
{
label: 'Seasonal',
items: [
- { label: 'Andromeda', link: '/constellations/andromeda/' },
- { label: 'Orion', link: '/constellations/orion/' },
- { label: 'Ursa Minor', link: '/constellations/ursa-minor/' },
+ 'constellations/andromeda',
+ 'constellations/orion',
+ 'constellations/ursa-minor',
],
},
],
@@ -186,7 +240,7 @@ The following sidebar will be generated:
]}
/>
-#### Customizing autogenerated links in frontmatter
+## Customizing autogenerated links in frontmatter
Use the [`sidebar` frontmatter field](/reference/frontmatter/#sidebar) in individual pages to customize autogenerated links.
@@ -228,14 +282,14 @@ An autogenerated group including a page with the frontmatter above will generate
/>
:::note
-The `sidebar` frontmatter configuration is only used for autogenerated links and will be ignored for manually defined links.
+The `sidebar` frontmatter configuration is only used for links in autogenerated groups and docs links defined with the `slug` property. It does not apply to links defined with the `link` property.
:::
## Badges
Links, groups, and autogenerated groups can also include a `badge` property to display a badge next to their label.
-```js {10,17}
+```js {9,16}
starlight({
sidebar: [
{
@@ -243,8 +297,7 @@ starlight({
items: [
// A link with a "Supergiant" badge.
{
- label: 'Persei',
- link: '/stars/persei/',
+ slug: 'stars/persei',
badge: 'Supergiant',
},
],
@@ -303,7 +356,7 @@ By default, the badge will use the accent color of your site. To use a built-in
Optionally, you can create a custom badge style by setting the `class` property to a CSS class name.
-```js {10}
+```js {9}
starlight({
sidebar: [
{
@@ -311,8 +364,7 @@ starlight({
items: [
// A link with a yellow "Stub" badge.
{
- label: 'Sirius',
- link: '/stars/sirius/',
+ slug: 'stars/sirius',
badge: { text: 'Stub', variant: 'caution' },
},
],
@@ -401,14 +453,14 @@ starlight({
translations: {
'pt-BR': 'Andrômeda',
},
- link: '/constellations/andromeda/',
+ slug: 'constellations/andromeda',
},
{
label: 'Scorpius',
translations: {
'pt-BR': 'Escorpião',
},
- link: '/constellations/scorpius/',
+ slug: 'constellations/scorpius',
},
],
},
@@ -430,6 +482,44 @@ Browsing the documentation in Brazilian Portuguese will generate the following s
]}
/>
+### Internationalization with internal links
+
+[Internal links](#internal-links) will automatically use translated page titles from content frontmatter by default:
+
+```js {9-10}
+starlight({
+ sidebar: [
+ {
+ label: 'Constellations',
+ translations: {
+ 'pt-BR': 'Constelações',
+ },
+ items: [
+ { slug: 'constellations/andromeda' },
+ { slug: 'constellations/scorpius' },
+ ],
+ },
+ ],
+});
+```
+
+Browsing the documentation in Brazilian Portuguese will generate the following sidebar:
+
+<SidebarPreview
+ config={[
+ {
+ label: 'Constelações',
+ items: [
+ { label: 'Andrômeda', link: '' },
+ { label: 'Escorpião', link: '' },
+ ],
+ },
+ ]}
+/>
+
+In multilingual sites, the value of `slug` does not include the language portion of the URL.
+For example, if you have pages at `en/intro` and `pt-br/intro`, the slug is `intro` when configuring the sidebar.
+
## Collapsing groups
Groups of links can be collapsed by default by setting the `collapsed` property to `true`.
@@ -441,10 +531,7 @@ starlight({
label: 'Constellations',
// Collapse the group by default.
collapsed: true,
- items: [
- { label: 'Andromeda', link: '/constellations/andromeda/' },
- { label: 'Orion', link: '/constellations/orion/' },
- ],
+ items: ['constellations/andromeda', 'constellations/orion'],
},
],
});
diff --git a/docs/src/content/docs/reference/configuration.mdx b/docs/src/content/docs/reference/configuration.mdx
index 40841629..8b97b0d3 100644
--- a/docs/src/content/docs/reference/configuration.mdx
+++ b/docs/src/content/docs/reference/configuration.mdx
@@ -100,25 +100,33 @@ With this config, a `/introduction` page would have an edit link pointing to `ht
Configure your site’s sidebar navigation items.
A sidebar is an array of links and groups of links.
-Each item must have a `label` and one of the following properties:
+With the exception of items using `slug`, each item must have a `label` and one of the following properties:
- `link` — a single link to a specific URL, e.g. `'/home'` or `'https://example.com'`.
+- `slug` — a reference to an internal page, e.g. `'guides/getting-started'`.
+
- `items` — an array containing more sidebar links and subgroups.
- `autogenerate` — an object specifying a directory of your docs to automatically generate a group of links from.
+Internal links can also be specified as a string instead of an object with a `slug` property.
+
```js
starlight({
sidebar: [
// A single link item labelled “Home”.
{ label: 'Home', link: '/' },
- // A group labelled “Start Here” containing two links.
+ // A group labelled “Start Here” containing four links.
{
label: 'Start Here',
items: [
- { label: 'Introduction', link: '/intro' },
- { label: 'Next Steps', link: '/next-steps' },
+ // Using `slug` for internal links.
+ { slug: 'intro' },
+ { slug: 'installation' },
+ // Or using the shorthand for internal links.
+ 'tutorial',
+ 'next-steps',
],
},
// A group linking to all pages in the reference directory.
@@ -141,16 +149,13 @@ Groups of links are expanded by default. You can change this behavior by setting
Autogenerated subgroups respect the `collapsed` property of their parent group by default. Set the `autogenerate.collapsed` property to override this.
-```js {5,16}
+```js {5,13}
sidebar: [
// A collapsed group of links.
{
label: 'Collapsed Links',
collapsed: true,
- items: [
- { label: 'Introduction', link: '/intro' },
- { label: 'Next Steps', link: '/next-steps' },
- ],
+ items: ['intro', 'next-steps'],
},
// An expanded group containing collapsed autogenerated subgroups.
{
@@ -192,21 +197,37 @@ sidebar: [
#### `SidebarItem`
```ts
-type SidebarItem = {
- label: string;
- translations?: Record<string, string>;
- badge?: string | BadgeConfig;
-} & (
- | {
- link: string;
- attrs?: Record<string, string | number | boolean | undefined>;
- }
- | { items: SidebarItem[]; collapsed?: boolean }
- | {
- autogenerate: { directory: string; collapsed?: boolean };
- collapsed?: boolean;
- }
-);
+type SidebarItem =
+ | string
+ | ({
+ translations?: Record<string, string>;
+ badge?: string | BadgeConfig;
+ } & (
+ | {
+ // Link
+ link: string;
+ label: string;
+ attrs?: Record<string, string | number | boolean | undefined>;
+ }
+ | {
+ // Internal link
+ slug: string;
+ label?: string;
+ attrs?: Record<string, string | number | boolean | undefined>;
+ }
+ | {
+ // Group of links
+ label: string;
+ items: SidebarItem[];
+ collapsed?: boolean;
+ }
+ | {
+ // Autogenerated link group
+ label: string;
+ autogenerate: { directory: string; collapsed?: boolean };
+ collapsed?: boolean;
+ }
+ ));
```
#### `BadgeConfig`
diff --git a/examples/basics/astro.config.mjs b/examples/basics/astro.config.mjs
index 9eacfd17..78078c32 100644
--- a/examples/basics/astro.config.mjs
+++ b/examples/basics/astro.config.mjs
@@ -14,7 +14,7 @@ export default defineConfig({
label: 'Guides',
items: [
// Each item here is one entry in the navigation menu.
- { label: 'Example Guide', link: '/guides/example/' },
+ { label: 'Example Guide', slug: 'guides/example' },
],
},
{
diff --git a/examples/tailwind/astro.config.mjs b/examples/tailwind/astro.config.mjs
index 99448763..2a81ef78 100644
--- a/examples/tailwind/astro.config.mjs
+++ b/examples/tailwind/astro.config.mjs
@@ -15,7 +15,7 @@ export default defineConfig({
label: 'Guides',
items: [
// Each item here is one entry in the navigation menu.
- { label: 'Example Guide', link: '/guides/example/' },
+ { label: 'Example Guide', slug: 'guides/example' },
],
},
{
diff --git a/packages/starlight/__tests__/basics/config-errors.test.ts b/packages/starlight/__tests__/basics/config-errors.test.ts
index 1639bd72..d4eeebc8 100644
--- a/packages/starlight/__tests__/basics/config-errors.test.ts
+++ b/packages/starlight/__tests__/basics/config-errors.test.ts
@@ -165,7 +165,7 @@ test('errors with bad sidebar config', () => {
Invalid config passed to starlight integration
Hint:
**sidebar.0**: Did not match union.
- > Expected type \`{ link: string; } | { items: array; } | { autogenerate: object; }\`
+ > Expected type \`{ link: string; } | { items: array; } | { autogenerate: object; } | { slug: string } | string\`
> Received \`{ "label": "Example", "href": "/" }\`"
`
);
@@ -190,7 +190,7 @@ test('errors with bad nested sidebar config', () => {
Invalid config passed to starlight integration
Hint:
**sidebar.0.items.1**: Did not match union.
- > Expected type \`{ link: string } | { items: array; } | { autogenerate: object; }\`
+ > Expected type \`{ link: string } | { items: array; } | { autogenerate: object; } | { slug: string } | string\`
> Received \`{ "label": "Example", "items": [ { "label": "Nested Example 1", "link": "/" }, { "label": "Nested Example 2", "link": true } ] }\`"
`);
});
diff --git a/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar-fallback-slug.test.ts b/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar-fallback-slug.test.ts
new file mode 100644
index 00000000..e67e558e
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar-fallback-slug.test.ts
@@ -0,0 +1,159 @@
+import { describe, expect, test, vi } from 'vitest';
+import { getSidebar } from '../../utils/navigation';
+
+vi.mock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ docs: [
+ ['index.mdx', { title: 'Starlight 🌟 Build documentation sites with Astro' }],
+ ['getting-started.mdx', { title: 'Getting Started' }],
+ ['manual-setup.mdx', { title: 'Manual Setup' }],
+ ['environmental-impact.md', { title: 'Eco-friendly docs' }],
+ ['guides/pages.mdx', { title: 'Pages' }],
+ ['guides/authoring-content.md', { title: 'Authoring Content in Markdown' }],
+ ['resources/plugins.mdx', { title: 'Plugins and Integrations' }],
+ ],
+ })
+);
+
+describe('getSidebar', () => {
+ test('returns an array of sidebar entries', () => {
+ expect(getSidebar('/', undefined)).toMatchInlineSnapshot(`
+ [
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/",
+ "isCurrent": true,
+ "label": "Starlight 🌟 Build documentation sites with Astro",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/getting-started",
+ "isCurrent": false,
+ "label": "Getting Started",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/manual-setup",
+ "isCurrent": false,
+ "label": "Do it yourself",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/environmental-impact",
+ "isCurrent": false,
+ "label": "Eco-friendly docs",
+ "type": "link",
+ },
+ {
+ "badge": undefined,
+ "collapsed": false,
+ "entries": [
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/guides/pages",
+ "isCurrent": false,
+ "label": "Pages",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/guides/authoring-content",
+ "isCurrent": false,
+ "label": "Authoring Content in Markdown",
+ "type": "link",
+ },
+ ],
+ "label": "Guides",
+ "type": "group",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/resources/plugins",
+ "isCurrent": false,
+ "label": "Plugins and Integrations",
+ "type": "link",
+ },
+ ]
+ `);
+ });
+ test('uses fallback labels from the default locale', () => {
+ expect(getSidebar('/fr', 'fr')).toMatchInlineSnapshot(`
+ [
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr",
+ "isCurrent": true,
+ "label": "Starlight 🌟 Build documentation sites with Astro",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/getting-started",
+ "isCurrent": false,
+ "label": "Getting Started",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/manual-setup",
+ "isCurrent": false,
+ "label": "Fait maison",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/environmental-impact",
+ "isCurrent": false,
+ "label": "Eco-friendly docs",
+ "type": "link",
+ },
+ {
+ "badge": undefined,
+ "collapsed": false,
+ "entries": [
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/guides/pages",
+ "isCurrent": false,
+ "label": "Pages",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/guides/authoring-content",
+ "isCurrent": false,
+ "label": "Authoring Content in Markdown",
+ "type": "link",
+ },
+ ],
+ "label": "Guides",
+ "type": "group",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/resources/plugins",
+ "isCurrent": false,
+ "label": "Plugins and Integrations",
+ "type": "link",
+ },
+ ]
+ `);
+ });
+});
diff --git a/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts b/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts
new file mode 100644
index 00000000..dad2b413
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts
@@ -0,0 +1,246 @@
+import { describe, expect, test, vi } from 'vitest';
+import { getSidebar } from '../../utils/navigation';
+
+vi.mock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ docs: [
+ ['index.mdx', { title: 'Starlight 🌟 Build documentation sites with Astro' }],
+ ['fr/index.mdx', { title: 'Starlight 🌟 Construire des sites de documentation avec Astro' }],
+ ['getting-started.mdx', { title: 'Getting Started' }],
+ ['fr/getting-started.mdx', { title: 'Mise en route' }],
+ ['manual-setup.mdx', { title: 'Manual Setup' }],
+ ['fr/manual-setup.mdx', { title: 'Installation manuelle' }],
+ ['environmental-impact.md', { title: 'Eco-friendly docs' }],
+ ['fr/environmental-impact.md', { title: 'Documents écologiques' }],
+ ['guides/pages.mdx', { title: 'Pages' }],
+ ['fr/guides/pages.mdx', { title: 'Pages' }],
+ ['guides/authoring-content.md', { title: 'Authoring Content in Markdown' }],
+ ['fr/guides/authoring-content.md', { title: 'Création de contenu en Markdown' }],
+ ['resources/plugins.mdx', { title: 'Plugins and Integrations' }],
+ ['fr/resources/plugins.mdx', { title: "Modules d'extension et outils" }],
+ ],
+ })
+);
+
+describe('getSidebar', () => {
+ test('returns an array of sidebar entries', () => {
+ expect(getSidebar('/', undefined)).toMatchInlineSnapshot(`
+ [
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/",
+ "isCurrent": true,
+ "label": "Starlight 🌟 Build documentation sites with Astro",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/getting-started",
+ "isCurrent": false,
+ "label": "Getting Started",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/manual-setup",
+ "isCurrent": false,
+ "label": "Do it yourself",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/environmental-impact",
+ "isCurrent": false,
+ "label": "Eco-friendly docs",
+ "type": "link",
+ },
+ {
+ "badge": undefined,
+ "collapsed": false,
+ "entries": [
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/guides/pages",
+ "isCurrent": false,
+ "label": "Pages",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/guides/authoring-content",
+ "isCurrent": false,
+ "label": "Authoring Content in Markdown",
+ "type": "link",
+ },
+ ],
+ "label": "Guides",
+ "type": "group",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/resources/plugins",
+ "isCurrent": false,
+ "label": "Plugins and Integrations",
+ "type": "link",
+ },
+ ]
+ `);
+ });
+ test('returns an array of sidebar entries for a locale', () => {
+ expect(getSidebar('/fr', 'fr')).toMatchInlineSnapshot(`
+ [
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr",
+ "isCurrent": true,
+ "label": "Starlight 🌟 Construire des sites de documentation avec Astro",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/getting-started",
+ "isCurrent": false,
+ "label": "Mise en route",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/manual-setup",
+ "isCurrent": false,
+ "label": "Fait maison",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/environmental-impact",
+ "isCurrent": false,
+ "label": "Documents écologiques",
+ "type": "link",
+ },
+ {
+ "badge": undefined,
+ "collapsed": false,
+ "entries": [
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/guides/pages",
+ "isCurrent": false,
+ "label": "Pages",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/guides/authoring-content",
+ "isCurrent": false,
+ "label": "Création de contenu en Markdown",
+ "type": "link",
+ },
+ ],
+ "label": "Guides",
+ "type": "group",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/resources/plugins",
+ "isCurrent": false,
+ "label": "Modules d'extension et outils",
+ "type": "link",
+ },
+ ]
+ `);
+ });
+ test('returns an array of sidebar entries for a locale on current page', () => {
+ expect(getSidebar('/fr/getting-started', 'fr')).toMatchInlineSnapshot(`
+ [
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr",
+ "isCurrent": false,
+ "label": "Starlight 🌟 Construire des sites de documentation avec Astro",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/getting-started",
+ "isCurrent": true,
+ "label": "Mise en route",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/manual-setup",
+ "isCurrent": false,
+ "label": "Fait maison",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/environmental-impact",
+ "isCurrent": false,
+ "label": "Documents écologiques",
+ "type": "link",
+ },
+ {
+ "badge": undefined,
+ "collapsed": false,
+ "entries": [
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/guides/pages",
+ "isCurrent": false,
+ "label": "Pages",
+ "type": "link",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/guides/authoring-content",
+ "isCurrent": false,
+ "label": "Création de contenu en Markdown",
+ "type": "link",
+ },
+ ],
+ "label": "Guides",
+ "type": "group",
+ },
+ {
+ "attrs": {},
+ "badge": undefined,
+ "href": "/fr/resources/plugins",
+ "isCurrent": false,
+ "label": "Modules d'extension et outils",
+ "type": "link",
+ },
+ ]
+ `);
+ });
+ test('uses label from config for internal links', () => {
+ const sidebar = getSidebar('/', undefined);
+ const entry = sidebar.find((item) => item.type === 'link' && item.href === '/manual-setup');
+ expect(entry?.label).toBe('Do it yourself');
+ });
+ test('uses translation from config for internal links', () => {
+ const sidebar = getSidebar('/fr', 'fr');
+ const entry = sidebar.find((item) => item.type === 'link' && item.href === '/fr/manual-setup');
+ expect(entry?.label).toBe('Fait maison');
+ });
+});
diff --git a/packages/starlight/__tests__/i18n-sidebar/sidebar-internal-link-error.test.ts b/packages/starlight/__tests__/i18n-sidebar/sidebar-internal-link-error.test.ts
new file mode 100644
index 00000000..9489830b
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-sidebar/sidebar-internal-link-error.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, test, vi } from 'vitest';
+import { getSidebar } from '../../utils/navigation';
+
+vi.mock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ docs: [
+ ['index.mdx', { title: 'Starlight 🌟 Build documentation sites with Astro' }],
+ ['fr/index.mdx', { title: 'Starlight 🌟 Construire des sites de documentation avec Astro' }],
+ ['manual-setup.mdx', { title: 'Manual Setup' }],
+ ['fr/manual-setup.mdx', { title: 'Installation manuelle' }],
+ ['environmental-impact.md', { title: 'Eco-friendly docs' }],
+ ['fr/environmental-impact.md', { title: 'Documents écologiques' }],
+ ['guides/pages.mdx', { title: 'Pages' }],
+ ['fr/guides/pages.mdx', { title: 'Pages' }],
+ ['guides/authoring-content.md', { title: 'Authoring Content in Markdown' }],
+ ['fr/guides/authoring-content.md', { title: 'Création de contenu en Markdown' }],
+ ['resources/plugins.mdx', { title: 'Plugins and Integrations' }],
+ ['fr/resources/plugins.mdx', { title: "Modules d'extension et outils" }],
+ ],
+ })
+);
+
+describe('getSidebar', () => {
+ test('throws an error if slug doesn’t match a content collection entry', () => {
+ expect(() => getSidebar('/', undefined)).toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ The slug \`"getting-started"\` specified in the Starlight sidebar config does not exist.
+ Hint:
+ Update the Starlight config to reference a valid entry slug in the docs content collection.
+ Learn more about Astro content collection slugs at https://docs.astro.build/en/reference/api-reference/#getentry"
+ `);
+ });
+});
diff --git a/packages/starlight/__tests__/i18n-sidebar/vitest.config.ts b/packages/starlight/__tests__/i18n-sidebar/vitest.config.ts
new file mode 100644
index 00000000..59f3bdf1
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-sidebar/vitest.config.ts
@@ -0,0 +1,20 @@
+import { defineVitestConfig } from '../test-config';
+
+export default defineVitestConfig({
+ title: 'i18n sidebar',
+ locales: {
+ fr: { label: 'French' },
+ root: { label: 'English', lang: 'en-US' },
+ },
+ sidebar: [
+ { slug: 'index' },
+ 'getting-started',
+ { slug: 'manual-setup', label: 'Do it yourself', translations: { fr: 'Fait maison' } },
+ { slug: 'environmental-impact' },
+ {
+ label: 'Guides',
+ items: [{ slug: 'guides/pages' }, { slug: 'guides/authoring-content' }],
+ },
+ 'resources/plugins',
+ ],
+});
diff --git a/packages/starlight/__tests__/sidebar-slug-error/sidebar-slug-error.test.ts b/packages/starlight/__tests__/sidebar-slug-error/sidebar-slug-error.test.ts
new file mode 100644
index 00000000..72d79291
--- /dev/null
+++ b/packages/starlight/__tests__/sidebar-slug-error/sidebar-slug-error.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, test, vi } from 'vitest';
+import { getSidebar } from '../../utils/navigation';
+
+vi.mock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ docs: [['getting-started.mdx', { title: 'Getting Started' }]],
+ })
+);
+
+describe('getSidebar', () => {
+ test('throws an error if slug doesn’t match a content collection entry', () => {
+ expect(() => getSidebar('/', undefined)).toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ The slug \`"/getting-started/"\` specified in the Starlight sidebar config must not start or end with a slash.
+ Hint:
+ Please try updating \`"/getting-started/"\` to \`"getting-started"\`."
+ `);
+ });
+});
diff --git a/packages/starlight/__tests__/sidebar-slug-error/vitest.config.ts b/packages/starlight/__tests__/sidebar-slug-error/vitest.config.ts
new file mode 100644
index 00000000..96fc6c74
--- /dev/null
+++ b/packages/starlight/__tests__/sidebar-slug-error/vitest.config.ts
@@ -0,0 +1,6 @@
+import { defineVitestConfig } from '../test-config';
+
+export default defineVitestConfig({
+ title: 'sidebar slug error',
+ sidebar: ['/getting-started/'],
+});
diff --git a/packages/starlight/schemas/sidebar.ts b/packages/starlight/schemas/sidebar.ts
index 41b1c3ae..b71d159e 100644
--- a/packages/starlight/schemas/sidebar.ts
+++ b/packages/starlight/schemas/sidebar.ts
@@ -58,6 +58,8 @@ type ManualSidebarGroupInput = z.input<typeof SidebarGroupSchema> & {
items: Array<
| z.input<typeof SidebarLinkItemSchema>
| z.input<typeof AutoSidebarGroupSchema>
+ | z.input<typeof InternalSidebarLinkItemSchema>
+ | z.input<typeof InternalSidebarLinkItemShorthandSchema>
| ManualSidebarGroupInput
>;
};
@@ -67,6 +69,8 @@ type ManualSidebarGroupOutput = z.output<typeof SidebarGroupSchema> & {
items: Array<
| z.output<typeof SidebarLinkItemSchema>
| z.output<typeof AutoSidebarGroupSchema>
+ | z.output<typeof InternalSidebarLinkItemSchema>
+ | z.output<typeof InternalSidebarLinkItemShorthandSchema>
| ManualSidebarGroupOutput
>;
};
@@ -78,13 +82,34 @@ const ManualSidebarGroupSchema: z.ZodType<
> = SidebarGroupSchema.extend({
/** Array of links and subcategories to display in this category. */
items: z.lazy(() =>
- z.union([SidebarLinkItemSchema, ManualSidebarGroupSchema, AutoSidebarGroupSchema]).array()
+ z
+ .union([
+ SidebarLinkItemSchema,
+ ManualSidebarGroupSchema,
+ AutoSidebarGroupSchema,
+ InternalSidebarLinkItemSchema,
+ InternalSidebarLinkItemShorthandSchema,
+ ])
+ .array()
),
}).strict();
+const InternalSidebarLinkItemSchema = SidebarBaseSchema.partial({ label: true }).extend({
+ /** The link to this item’s content. Must be a slug of a Content Collection entry. */
+ slug: z.string(),
+ /** HTML attributes to add to the link item. */
+ attrs: SidebarLinkItemHTMLAttributesSchema(),
+});
+const InternalSidebarLinkItemShorthandSchema = z
+ .string()
+ .transform((slug) => InternalSidebarLinkItemSchema.parse({ slug }));
+export type InternalSidebarLinkItem = z.output<typeof InternalSidebarLinkItemSchema>;
+
export const SidebarItemSchema = z.union([
SidebarLinkItemSchema,
ManualSidebarGroupSchema,
AutoSidebarGroupSchema,
+ InternalSidebarLinkItemSchema,
+ InternalSidebarLinkItemShorthandSchema,
]);
export type SidebarItem = z.infer<typeof SidebarItemSchema>;
diff --git a/packages/starlight/utils/error-map.ts b/packages/starlight/utils/error-map.ts
index c153e296..0b2d2c5c 100644
--- a/packages/starlight/utils/error-map.ts
+++ b/packages/starlight/utils/error-map.ts
@@ -88,7 +88,12 @@ const errorMap: z.ZodErrorMap = (baseError, ctx) => {
expectedShape.push(relativePath);
}
}
- expectedShapes.push(`{ ${expectedShape.join('; ')} }`);
+ if (expectedShape.length === 1 && !expectedShape[0]?.includes(':')) {
+ // In this case the expected shape is not an object, but probably a literal type, e.g. `['string']`.
+ expectedShapes.push(expectedShape.join(''));
+ } else {
+ expectedShapes.push(`{ ${expectedShape.join('; ')} }`);
+ }
}
if (expectedShapes.length) {
details.push('> Expected type `' + expectedShapes.join(' | ') + '`');
diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts
index aa4ab1f4..85829f53 100644
--- a/packages/starlight/utils/navigation.ts
+++ b/packages/starlight/utils/navigation.ts
@@ -1,8 +1,10 @@
+import { AstroError } from 'astro/errors';
import config from 'virtual:starlight/user-config';
import type { Badge } from '../schemas/badge';
import type { PrevNextLinkConfig } from '../schemas/prevNextLink';
import type {
AutoSidebarGroup,
+ InternalSidebarLinkItem,
LinkHTMLAttributes,
SidebarItem,
SidebarLinkItem,
@@ -11,7 +13,7 @@ import { createPathFormatter } from './createPathFormatter';
import { formatPath } from './format-path';
import { pickLang } from './i18n';
import { ensureLeadingSlash, ensureTrailingSlash, stripLeadingAndTrailingSlashes } from './path';
-import { getLocaleRoutes, type Route } from './routing';
+import { getLocaleRoutes, routes, type Route } from './routing';
import { localeToLang, slugToPathname } from './slugs';
const DirKey = Symbol('DirKey');
@@ -70,9 +72,11 @@ function configItemToEntry(
routes: Route[]
): SidebarEntry {
if ('link' in item) {
- return linkFromConfig(item, locale, currentPathname);
+ return linkFromSidebarLinkItem(item, locale, currentPathname);
} else if ('autogenerate' in item) {
return groupFromAutogenerateConfig(item, locale, routes, currentPathname);
+ } else if ('slug' in item) {
+ return linkFromInternalSidebarLinkItem(item, locale, currentPathname);
} else {
return {
type: 'group',
@@ -113,8 +117,8 @@ function groupFromAutogenerateConfig(
/** Check if a string starts with one of `http://` or `https://`. */
const isAbsolute = (link: string) => /^https?:\/\//.test(link);
-/** Create a link entry from a user config object. */
-function linkFromConfig(
+/** Create a link entry from a manual link item in user config. */
+function linkFromSidebarLinkItem(
item: SidebarLinkItem,
locale: string | undefined,
currentPathname: string
@@ -129,6 +133,36 @@ function linkFromConfig(
return makeLink(href, label, currentPathname, item.badge, item.attrs);
}
+/** Create a link entry from an automatic internal link item in user config. */
+function linkFromInternalSidebarLinkItem(
+ item: InternalSidebarLinkItem,
+ locale: string | undefined,
+ currentPathname: string
+) {
+ let slugWithLocale = locale ? locale + '/' + item.slug : item.slug;
+ // Astro passes root `index.[md|mdx]` entries with a slug of `index`
+ slugWithLocale = slugWithLocale.replace(/\/?index$/, '');
+ const entry = routes.find((entry) => slugWithLocale === entry.slug);
+ if (!entry) {
+ const hasExternalSlashes = item.slug.at(0) === '/' || item.slug.at(-1) === '/';
+ if (hasExternalSlashes) {
+ throw new AstroError(
+ `The slug \`"${item.slug}"\` specified in the Starlight sidebar config must not start or end with a slash.`,
+ `Please try updating \`"${item.slug}"\` to \`"${stripLeadingAndTrailingSlashes(item.slug)}"\`.`
+ );
+ } else {
+ throw new AstroError(
+ `The slug \`"${item.slug}"\` specified in the Starlight sidebar config does not exist.`,
+ 'Update the Starlight config to reference a valid entry slug in the docs content collection.\n' +
+ 'Learn more about Astro content collection slugs at https://docs.astro.build/en/reference/api-reference/#getentry'
+ );
+ }
+ }
+ const label =
+ pickLang(item.translations, localeToLang(locale)) || item.label || entry.entry.data.title;
+ return makeLink(entry.slug, label, currentPathname, item.badge, item.attrs);
+}
+
/** Create a link entry. */
function makeLink(
href: string,