diff --git a/.changeset/slow-dragons-notice.md b/.changeset/slow-dragons-notice.md new file mode 100644 index 00000000000..a5750b3d560 --- /dev/null +++ b/.changeset/slow-dragons-notice.md @@ -0,0 +1,28 @@ +--- +'@astrojs/starlight': minor +--- + +**⚠️ BREAKING CHANGE:** This release changes how autogenerated links work in Starlight’s sidebar configuration. + +If you have sidebar groups using the `autogenerate` key, you must now wrap that configuration in an `items` array: + +```diff +{ + label: 'My group', +- autogenerate: { directory: 'some-dir' }, ++ items: [{ autogenerate: { directory: 'some-dir' } }], +} +``` + +This change unlocks the possibility to mix autogenerated links and other links in a single group, for example: + +```js +{ + label: 'Mixed group', + items: [ + 'example-page', + { autogenerate: { directory: 'examples' } }, + { label: 'More examples', link: 'https://example.com' }, + ], +} +``` \ No newline at end of file diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index a059280810c..e7748fd5b88 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -131,7 +131,7 @@ export default defineConfig({ hi: 'गाइड', uk: 'Ґайди', }, - autogenerate: { directory: 'guides' }, + items: [{ autogenerate: { directory: 'guides' } }], }, { label: 'Components', @@ -144,7 +144,7 @@ export default defineConfig({ 'zh-CN': '组件', uk: 'Компоненти', }, - autogenerate: { directory: 'components' }, + items: [{ autogenerate: { directory: 'components' } }], }, { label: 'Reference', @@ -163,7 +163,7 @@ export default defineConfig({ hi: 'संदर्भ', uk: 'Довідник', }, - autogenerate: { directory: 'reference' }, + items: [{ autogenerate: { directory: 'reference' } }], }, { label: 'Resources', @@ -178,7 +178,7 @@ export default defineConfig({ ko: '리소스', uk: 'Ресурси', }, - autogenerate: { directory: 'resources' }, + items: [{ autogenerate: { directory: 'resources' } }], }, ], expressiveCode: { shiki: { langs: [markdocGrammar] } }, diff --git a/docs/src/components/sidebar-preview.astro b/docs/src/components/sidebar-preview.astro index 2ecdd9d32d5..aa438014dea 100644 --- a/docs/src/components/sidebar-preview.astro +++ b/docs/src/components/sidebar-preview.astro @@ -1,6 +1,6 @@ --- import type { - AutoSidebarGroup, + AutoSidebarEntries, SidebarItem, InternalSidebarLinkItem, } from '../../../packages/starlight/schemas/sidebar'; @@ -12,7 +12,7 @@ interface Props { config: SidebarConfig; } -type SidebarConfig = (Exclude & { +type SidebarConfig = (Exclude & { badge?: Badge; })[]; diff --git a/docs/src/content/docs/guides/sidebar.mdx b/docs/src/content/docs/guides/sidebar.mdx index 411a1881a5e..38d1f6c3652 100644 --- a/docs/src/content/docs/guides/sidebar.mdx +++ b/docs/src/content/docs/guides/sidebar.mdx @@ -45,7 +45,7 @@ The following sidebar will be automatically generated: ]} /> -Learn more about autogenerated sidebars in the [autogenerated groups](#autogenerated-groups) section. +Learn more about autogenerated sidebars in the [autogenerated links](#autogenerated-links) section. ## Add links and link groups @@ -186,22 +186,24 @@ The configuration above generates the following sidebar: ]} /> -### Autogenerated groups +### Autogenerated links -Starlight can automatically generate a group in your sidebar based on a directory of your docs. +Starlight can automatically generate links 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 [`id`](/reference/route-data/#id). -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: +Add autogenerated links using an object with the `autogenerate` property. +Your `autogenerate` configuration must specify the `directory` to use for sidebar entries. +For example, with the following configuration: ```js "label:" "autogenerate:" starlight({ sidebar: [ { label: 'Constellations', - // Autogenerate a group of links for the 'constellations' directory. - autogenerate: { directory: 'constellations' }, + // Autogenerate links for the 'constellations' directory. + items: [{ autogenerate: { directory: 'constellations' } }], }, ], }); @@ -262,7 +264,7 @@ sidebar: --- ``` -An autogenerated group including a page with the frontmatter above will generate the following sidebar: +A group with autogenerated links including a page with the frontmatter above will generate the following sidebar: :::note -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. +The `sidebar` frontmatter configuration is only used for autogenerated links 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. +Links and groups can also include a `badge` property to display a badge next to their label. ```js {9,16} starlight({ @@ -302,11 +304,11 @@ starlight({ }, ], }, - // An autogenerated group with an "Outdated" badge. + // A group with an "Outdated" badge. { label: 'Moons', badge: 'Outdated', - autogenerate: { directory: 'moons' }, + items: [{ autogenerate: { directory: 'moons' } }], }, ], }); @@ -438,20 +440,19 @@ The configuration above generates the following sidebar: ### Custom HTML attributes for autogenerated links -Customize HTML attributes of all links in [autogenerated groups](#autogenerated-groups) by defining the `attrs` property in the `autogenerate` configuration. +Customize HTML attributes of all [autogenerated links](#autogenerated-links) by defining the `attrs` property in the `autogenerate` configuration. Individual pages can specify custom attributes using the [`sidebar.attrs` frontmatter field](/reference/frontmatter/#attrs) which will be merged with the `autogenerate.attrs` configuration. For example, with the following configuration: -```js {9} +```js {8} starlight({ sidebar: [ { - label: 'Constellations', autogenerate: { - // Autogenerate a group of links for the 'constellations' directory. + // Autogenerate links for the 'constellations' directory. directory: 'constellations', - // Italicize all link labels in this group. + // Italicize all autogenerated link labels. attrs: { style: 'font-style: italic' }, }, }, @@ -478,25 +479,16 @@ The following sidebar will be generated with all autogenerated links italicized: -[Autogenerated groups](#autogenerated-groups) respect the `collapsed` value of their parent group: +[Autogenerated links](#autogenerated-links) respect the `collapsed` value of their parent group: ```js {5-6} starlight({ @@ -675,7 +667,7 @@ starlight({ label: 'Constellations', // Collapse the group and its autogenerated subgroups by default. collapsed: true, - autogenerate: { directory: 'constellations' }, + items: [{ autogenerate: { directory: 'constellations' } }], }, ], }); @@ -711,7 +703,9 @@ starlight({ // Do not collapse the "Constellations" group but collapse its // autogenerated subgroups. collapsed: false, - autogenerate: { directory: 'constellations', collapsed: true }, + items: [ + { autogenerate: { directory: 'constellations', collapsed: true } }, + ], }, ], }); diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index 31e361093cf..a85c4f8471d 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -284,7 +284,7 @@ draft: true ``` Because draft pages are not included in build output, you cannot add draft pages directly to your site sidebar config using [slugs](/guides/sidebar/#internal-links). -Draft pages in directories used for [autogenerated sidebar groups](/guides/sidebar/#autogenerated-groups) are automatically excluded in production builds. +Draft pages in directories used for [autogenerated sidebar links](/guides/sidebar/#autogenerated-links) are automatically excluded in production builds. ### `sidebar` diff --git a/examples/basics/astro.config.mjs b/examples/basics/astro.config.mjs index 9a25601baa4..69b83b38960 100644 --- a/examples/basics/astro.config.mjs +++ b/examples/basics/astro.config.mjs @@ -18,7 +18,7 @@ export default defineConfig({ }, { label: 'Reference', - autogenerate: { directory: 'reference' }, + items: [{ autogenerate: { directory: 'reference' } }], }, ], }), diff --git a/examples/markdoc/astro.config.mjs b/examples/markdoc/astro.config.mjs index 949f3fcc1bc..33f02c27617 100644 --- a/examples/markdoc/astro.config.mjs +++ b/examples/markdoc/astro.config.mjs @@ -20,7 +20,7 @@ export default defineConfig({ }, { label: 'Reference', - autogenerate: { directory: 'reference' }, + items: [{ autogenerate: { directory: 'reference' } }], }, ], }), diff --git a/examples/tailwind/astro.config.mjs b/examples/tailwind/astro.config.mjs index c9fd5e4ccc6..adade8af4f6 100644 --- a/examples/tailwind/astro.config.mjs +++ b/examples/tailwind/astro.config.mjs @@ -19,7 +19,7 @@ export default defineConfig({ }, { label: 'Reference', - autogenerate: { directory: 'reference' }, + items: [{ autogenerate: { directory: 'reference' } }], }, ], customCss: ['./src/styles/global.css'], diff --git a/packages/starlight/__tests__/basics/config-errors.test.ts b/packages/starlight/__tests__/basics/config-errors.test.ts index c5833ed0141..6e5a5b5d1c5 100644 --- a/packages/starlight/__tests__/basics/config-errors.test.ts +++ b/packages/starlight/__tests__/basics/config-errors.test.ts @@ -196,11 +196,11 @@ test('errors with bad nested sidebar config', () => { parseStarlightConfigWithFriendlyErrors({ title: 'Test', sidebar: [ + // @ts-expect-error - Testing invalid config { label: 'Example', items: [ { label: 'Nested Example 1', link: '/' }, - // @ts-expect-error - Testing invalid config { label: 'Nested Example 2', link: true }, ], }, @@ -238,7 +238,14 @@ test('errors with sidebar entry that includes `link` and `autogenerate`', () => expect(() => parseStarlightConfigWithFriendlyErrors({ title: 'Test', - sidebar: [{ label: 'Parent', link: '/parent', autogenerate: { directory: 'test' } }], + sidebar: [ + { + label: 'Parent', + link: '/parent', + // @ts-expect-error - Testing invalid config + autogenerate: { directory: 'test' }, + }, + ], }) ).toThrowErrorMatchingInlineSnapshot(` "[AstroUserError]: @@ -258,6 +265,7 @@ test('errors with sidebar entry that includes `items` and `autogenerate`', () => { label: 'Parent', items: [{ label: 'Child', link: '/parent/child' }], + // @ts-expect-error - Testing invalid config autogenerate: { directory: 'test' }, }, ], @@ -318,3 +326,55 @@ test('errors if an invalid customCss file path is provided', () => { You should move these CSS files into the \`src/\` directory and update the path in \`customCss\` to match." `); }); + +test('errors on removed autogenerated sidebar groups with no attributes', () => { + expect(() => + parseStarlightConfigWithFriendlyErrors({ + title: 'Test', + sidebar: [ + { + label: 'Example', + // @ts-expect-error - Testing invalid config + autogenerate: { directory: 'test', collapsed: true }, + }, + ], + }) + ).toThrowErrorMatchingInlineSnapshot(` + "[AstroUserError]: + Invalid config passed to starlight integration + Hint: + Found an \`autogenerate\` object with a \`label\`. Support for autogenerated sidebar groups was removed in Starlight v0.38.0. + You should instead create a group with the desired \`label\` and an \`items\` array containing the autogenerate config: + + { + label: 'Example', + items: [{ autogenerate: { "directory": "test", "collapsed": true } }] + }" + `); +}); + +test('errors on removed autogenerated sidebar groups with attributes', () => { + expect(() => + parseStarlightConfigWithFriendlyErrors({ + title: 'Test', + sidebar: [ + { + label: 'Example', + // @ts-expect-error - Testing invalid config + autogenerate: { directory: 'test', attrs: { 'data-test': 'test' } }, + }, + ], + }) + ).toThrowErrorMatchingInlineSnapshot(` + "[AstroUserError]: + Invalid config passed to starlight integration + Hint: + Found an \`autogenerate\` object with a \`label\`. Support for autogenerated sidebar groups was removed in Starlight v0.38.0. + You should instead create a group with the desired \`label\` and an \`items\` array containing the autogenerate config: + + { + label: 'Example', + items: [{ autogenerate: { "directory": "test", "attrs": { "data-test": "test" } } }] + }" + `); +}); diff --git a/packages/starlight/__tests__/basics/config.test-d.ts b/packages/starlight/__tests__/basics/config.test-d.ts index 8e1e4ebf1d9..17a07873d91 100644 --- a/packages/starlight/__tests__/basics/config.test-d.ts +++ b/packages/starlight/__tests__/basics/config.test-d.ts @@ -2,9 +2,9 @@ import { describe, expectTypeOf, test } from 'vitest'; import type { StarlightUserConfig } from '../../utils/user-config'; describe('sidebar', () => { - test('emits a type error for custom attributes on groups', () => { - type SidebarUserItem = NonNullable[number]; + type SidebarUserItem = NonNullable[number]; + test('emits a type error for custom attributes on groups', () => { // Links expectTypeOf('getting-started').toExtend(); expectTypeOf({ slug: 'getting-started' }).toExtend(); @@ -18,10 +18,6 @@ describe('sidebar', () => { label: 'References', items: [], }).toExtend(); - expectTypeOf({ - label: 'References', - autogenerate: { directory: 'references' }, - }).toExtend(); // Links with attributes expectTypeOf({ @@ -41,11 +37,20 @@ describe('sidebar', () => { attrs: { class: 'test' }, // @ts-expect-error - Attributes are not supported on groups }).toExtend(); + }); + + test('emits a type error for autogenerated groups', () => { + // Groups with autogenerated items + expectTypeOf({ + label: 'References', + items: [{ autogenerate: { directory: 'references' } }], + }).toExtend(); + + // Autogenerated groups are no longer supported expectTypeOf({ label: 'References', autogenerate: { directory: 'references' }, - attrs: { class: 'test' }, - // @ts-expect-error - Attributes are not supported on groups + // @ts-expect-error - Autogenerated groups are no longer supported }).toExtend(); }); }); diff --git a/packages/starlight/__tests__/basics/navigation-labels.test.ts b/packages/starlight/__tests__/basics/navigation-labels.test.ts index 7784c04b00f..aa04b55952d 100644 --- a/packages/starlight/__tests__/basics/navigation-labels.test.ts +++ b/packages/starlight/__tests__/basics/navigation-labels.test.ts @@ -24,6 +24,7 @@ describe('getSidebar', () => { [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/", "isCurrent": true, @@ -32,6 +33,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/environmental-impact/", "isCurrent": false, @@ -39,11 +41,13 @@ describe('getSidebar', () => { "type": "link", }, { + "autogenerated": true, "badge": undefined, "collapsed": false, "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, @@ -52,6 +56,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/guides/project-structure/", "isCurrent": false, diff --git a/packages/starlight/__tests__/basics/navigation-order.test.ts b/packages/starlight/__tests__/basics/navigation-order.test.ts index f1935e7c208..ed4afc75532 100644 --- a/packages/starlight/__tests__/basics/navigation-order.test.ts +++ b/packages/starlight/__tests__/basics/navigation-order.test.ts @@ -17,11 +17,13 @@ describe('getSidebar', () => { expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` [ { + "autogenerated": true, "badge": undefined, "collapsed": false, "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/guides/project-structure/", "isCurrent": false, @@ -30,6 +32,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, @@ -42,6 +45,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/environmental-impact/", "isCurrent": false, @@ -50,6 +54,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/", "isCurrent": true, diff --git a/packages/starlight/__tests__/basics/navigation.test.ts b/packages/starlight/__tests__/basics/navigation.test.ts index 0b4e5bd7d4e..ad56c812312 100644 --- a/packages/starlight/__tests__/basics/navigation.test.ts +++ b/packages/starlight/__tests__/basics/navigation.test.ts @@ -20,6 +20,7 @@ describe('getSidebar', () => { [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/", "isCurrent": true, @@ -28,6 +29,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/environmental-impact/", "isCurrent": false, @@ -35,11 +37,13 @@ describe('getSidebar', () => { "type": "link", }, { + "autogenerated": true, "badge": undefined, "collapsed": false, "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/getting-started/intro/", "isCurrent": false, @@ -51,11 +55,13 @@ describe('getSidebar', () => { "type": "group", }, { + "autogenerated": true, "badge": undefined, "collapsed": false, "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, @@ -64,6 +70,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/guides/project-structure/", "isCurrent": false, @@ -98,6 +105,7 @@ describe('getSidebar', () => { [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/environmental-impact/", "isCurrent": true, @@ -155,6 +163,7 @@ describe('flattenSidebar', () => { [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/", "isCurrent": true, @@ -163,6 +172,7 @@ describe('flattenSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/environmental-impact/", "isCurrent": false, @@ -171,6 +181,7 @@ describe('flattenSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/getting-started/intro/", "isCurrent": false, @@ -179,6 +190,7 @@ describe('flattenSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, @@ -187,6 +199,7 @@ describe('flattenSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/guides/project-structure/", "isCurrent": false, @@ -206,6 +219,7 @@ describe('getPrevNextLinks', () => { { "next": { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/getting-started/intro/", "isCurrent": false, @@ -214,6 +228,7 @@ describe('getPrevNextLinks', () => { }, "prev": { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/", "isCurrent": false, diff --git a/packages/starlight/__tests__/basics/route-data.test.ts b/packages/starlight/__tests__/basics/route-data.test.ts index feb5377f2bd..0b9284b494a 100644 --- a/packages/starlight/__tests__/basics/route-data.test.ts +++ b/packages/starlight/__tests__/basics/route-data.test.ts @@ -40,6 +40,7 @@ test('adds data to route shape', () => { { "next": { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/environmental-impact/", "isCurrent": false, 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 78af73041f0..062b14cbd49 100644 --- a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts +++ b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts @@ -116,6 +116,7 @@ test('uses generated sidebar when no sidebar is provided', async () => { [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/", "isCurrent": false, @@ -124,6 +125,7 @@ test('uses generated sidebar when no sidebar is provided', async () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/getting-started/", "isCurrent": true, @@ -131,11 +133,13 @@ test('uses generated sidebar when no sidebar is provided', async () => { "type": "link", }, { + "autogenerated": true, "badge": undefined, "collapsed": false, "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, @@ -144,6 +148,7 @@ test('uses generated sidebar when no sidebar is provided', async () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/guides/project-structure/", "isCurrent": false, @@ -155,11 +160,13 @@ test('uses generated sidebar when no sidebar is provided', async () => { "type": "group", }, { + "autogenerated": true, "badge": undefined, "collapsed": false, "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/reference/frontmatter/", "isCurrent": false, @@ -190,7 +197,7 @@ test('uses provided sidebar if any', async () => { }, { label: 'Guides', - autogenerate: { directory: 'guides' }, + items: [{ autogenerate: { directory: 'guides' } }], }, 'reference/frontmatter', ], @@ -224,6 +231,7 @@ test('uses provided sidebar if any', async () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, @@ -232,6 +240,7 @@ test('uses provided sidebar if any', async () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/guides/project-structure/", "isCurrent": false, diff --git a/packages/starlight/__tests__/build-format-file/navigation.test.ts b/packages/starlight/__tests__/build-format-file/navigation.test.ts index 16445cb64c2..a3fcb97027a 100644 --- a/packages/starlight/__tests__/build-format-file/navigation.test.ts +++ b/packages/starlight/__tests__/build-format-file/navigation.test.ts @@ -84,6 +84,7 @@ describe('getSidebar with build.format = "file"', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/reference/configuration.html", "isCurrent": false, @@ -92,6 +93,7 @@ describe('getSidebar with build.format = "file"', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/reference/frontmatter.html", "isCurrent": false, @@ -108,6 +110,7 @@ describe('getSidebar with build.format = "file"', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/api/v1/users.html", "isCurrent": false, diff --git a/packages/starlight/__tests__/build-format-file/vitest.config.ts b/packages/starlight/__tests__/build-format-file/vitest.config.ts index 6d91108183a..ada063ba1f8 100644 --- a/packages/starlight/__tests__/build-format-file/vitest.config.ts +++ b/packages/starlight/__tests__/build-format-file/vitest.config.ts @@ -34,12 +34,12 @@ export default defineVitestConfig( { label: 'Reference', badge: 'Experimental', - autogenerate: { directory: 'reference' }, + items: [{ autogenerate: { directory: 'reference' } }], }, // A group linking to all pages in the api/v1 directory. { label: 'API v1', - autogenerate: { directory: '/api/v1/' }, + items: [{ autogenerate: { directory: '/api/v1/' } }], }, ], }, diff --git a/packages/starlight/__tests__/i18n/navigation-order.test.ts b/packages/starlight/__tests__/i18n/navigation-order.test.ts index 96e60f9ca73..0d02de25b11 100644 --- a/packages/starlight/__tests__/i18n/navigation-order.test.ts +++ b/packages/starlight/__tests__/i18n/navigation-order.test.ts @@ -23,6 +23,7 @@ describe('getSidebar', () => { [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/en/", "isCurrent": true, @@ -31,6 +32,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/en/404/", "isCurrent": false, @@ -38,11 +40,13 @@ describe('getSidebar', () => { "type": "link", }, { + "autogenerated": true, "badge": undefined, "collapsed": false, "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/en/guides/authoring-content/", "isCurrent": false, @@ -62,6 +66,7 @@ describe('getSidebar', () => { [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/fr/", "isCurrent": true, @@ -70,6 +75,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/fr/404/", "isCurrent": false, @@ -77,11 +83,13 @@ describe('getSidebar', () => { "type": "link", }, { + "autogenerated": true, "badge": undefined, "collapsed": false, "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/fr/guides/authoring-content/", "isCurrent": false, @@ -93,11 +101,13 @@ describe('getSidebar', () => { "type": "group", }, { + "autogenerated": true, "badge": undefined, "collapsed": false, "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/fr/référence/bénéfice/", "isCurrent": false, @@ -106,6 +116,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/fr/référence/bricolage/", "isCurrent": false, @@ -117,11 +128,13 @@ describe('getSidebar', () => { "type": "group", }, { + "autogenerated": true, "badge": undefined, "collapsed": false, "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/fr/route/décoder/", "isCurrent": false, @@ -130,6 +143,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/fr/route/distribuer/", "isCurrent": false, diff --git a/packages/starlight/__tests__/sidebar-autogenerated-collapsed/navigation.test.ts b/packages/starlight/__tests__/sidebar-autogenerated-collapsed/navigation.test.ts new file mode 100644 index 00000000000..7db0abde4d9 --- /dev/null +++ b/packages/starlight/__tests__/sidebar-autogenerated-collapsed/navigation.test.ts @@ -0,0 +1,230 @@ +import { describe, expect, test, vi } from 'vitest'; +import { getSidebar } from '../../utils/navigation'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['reference/configuration.mdx', { title: 'Config Reference' }], + ['reference/frontmatter/foo.mdx', { title: 'Foo' }], + ], + }) +); + +describe('getSidebar', () => { + test('returns an array of sidebar entries', () => { + expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` + [ + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "autogenerated": true, + "badge": undefined, + "href": "/reference/configuration/", + "isCurrent": false, + "label": "Config Reference", + "type": "link", + }, + { + "autogenerated": true, + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "autogenerated": true, + "badge": undefined, + "href": "/reference/frontmatter/foo/", + "isCurrent": false, + "label": "Foo", + "type": "link", + }, + ], + "label": "frontmatter", + "type": "group", + }, + ], + "label": "Reference (group: default - autogenerated: default)", + "type": "group", + }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "autogenerated": true, + "badge": undefined, + "href": "/reference/configuration/", + "isCurrent": false, + "label": "Config Reference", + "type": "link", + }, + { + "autogenerated": true, + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "autogenerated": true, + "badge": undefined, + "href": "/reference/frontmatter/foo/", + "isCurrent": false, + "label": "Foo", + "type": "link", + }, + ], + "label": "frontmatter", + "type": "group", + }, + ], + "label": "Reference (group: not collapsed - autogenerated: default)", + "type": "group", + }, + { + "badge": undefined, + "collapsed": true, + "entries": [ + { + "attrs": {}, + "autogenerated": true, + "badge": undefined, + "href": "/reference/configuration/", + "isCurrent": false, + "label": "Config Reference", + "type": "link", + }, + { + "autogenerated": true, + "badge": undefined, + "collapsed": true, + "entries": [ + { + "attrs": {}, + "autogenerated": true, + "badge": undefined, + "href": "/reference/frontmatter/foo/", + "isCurrent": false, + "label": "Foo", + "type": "link", + }, + ], + "label": "frontmatter", + "type": "group", + }, + ], + "label": "Reference (group: collapsed - autogenerated: default)", + "type": "group", + }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "autogenerated": true, + "badge": undefined, + "href": "/reference/configuration/", + "isCurrent": false, + "label": "Config Reference", + "type": "link", + }, + { + "autogenerated": true, + "badge": undefined, + "collapsed": true, + "entries": [ + { + "attrs": {}, + "autogenerated": true, + "badge": undefined, + "href": "/reference/frontmatter/foo/", + "isCurrent": false, + "label": "Foo", + "type": "link", + }, + ], + "label": "frontmatter", + "type": "group", + }, + ], + "label": "Reference (group: default - autogenerated: collapsed)", + "type": "group", + }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "autogenerated": true, + "badge": undefined, + "href": "/reference/configuration/", + "isCurrent": false, + "label": "Config Reference", + "type": "link", + }, + { + "autogenerated": true, + "badge": undefined, + "collapsed": true, + "entries": [ + { + "attrs": {}, + "autogenerated": true, + "badge": undefined, + "href": "/reference/frontmatter/foo/", + "isCurrent": false, + "label": "Foo", + "type": "link", + }, + ], + "label": "frontmatter", + "type": "group", + }, + ], + "label": "Reference (group: not collapsed - autogenerated: collapsed)", + "type": "group", + }, + { + "badge": undefined, + "collapsed": true, + "entries": [ + { + "attrs": {}, + "autogenerated": true, + "badge": undefined, + "href": "/reference/configuration/", + "isCurrent": false, + "label": "Config Reference", + "type": "link", + }, + { + "autogenerated": true, + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "autogenerated": true, + "badge": undefined, + "href": "/reference/frontmatter/foo/", + "isCurrent": false, + "label": "Foo", + "type": "link", + }, + ], + "label": "frontmatter", + "type": "group", + }, + ], + "label": "Reference (group: collapsed - autogenerated: not collapsed)", + "type": "group", + }, + ] + `); + }); +}); diff --git a/packages/starlight/__tests__/sidebar-autogenerated-collapsed/vitest.config.ts b/packages/starlight/__tests__/sidebar-autogenerated-collapsed/vitest.config.ts new file mode 100644 index 00000000000..f5dd0748149 --- /dev/null +++ b/packages/starlight/__tests__/sidebar-autogenerated-collapsed/vitest.config.ts @@ -0,0 +1,35 @@ +import { defineVitestConfig } from '../test-config'; + +export default defineVitestConfig({ + title: 'Sidebar Autogenerated Collapsed Test', + sidebar: [ + { + label: 'Reference (group: default - autogenerated: default)', + items: [{ autogenerate: { directory: 'reference' } }], + }, + { + label: 'Reference (group: not collapsed - autogenerated: default)', + collapsed: false, + items: [{ autogenerate: { directory: 'reference' } }], + }, + { + label: 'Reference (group: collapsed - autogenerated: default)', + collapsed: true, + items: [{ autogenerate: { directory: 'reference' } }], + }, + { + label: 'Reference (group: default - autogenerated: collapsed)', + items: [{ autogenerate: { directory: 'reference', collapsed: true } }], + }, + { + label: 'Reference (group: not collapsed - autogenerated: collapsed)', + collapsed: false, + items: [{ autogenerate: { directory: 'reference', collapsed: true } }], + }, + { + label: 'Reference (group: collapsed - autogenerated: not collapsed)', + collapsed: true, + items: [{ autogenerate: { directory: 'reference', collapsed: false } }], + }, + ], +}); diff --git a/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts b/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts index 7424e9eb307..4bb836289a4 100644 --- a/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts @@ -121,6 +121,7 @@ describe('getSidebar', () => { "class": "advanced", "ping": "https://example.com", }, + "autogenerated": true, "badge": undefined, "href": "/reference/frontmatter/", "isCurrent": false, @@ -136,6 +137,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "autogenerated": true, "badge": undefined, "collapsed": false, "entries": [ @@ -144,6 +146,7 @@ describe('getSidebar', () => { "class": "current", "data-version": "1", }, + "autogenerated": true, "badge": undefined, "href": "/api/v1/products/add/", "isCurrent": false, @@ -156,6 +159,7 @@ describe('getSidebar', () => { "data-experimental": true, "data-version": "1", }, + "autogenerated": true, "badge": undefined, "href": "/api/v1/products/remove/", "isCurrent": false, @@ -171,6 +175,7 @@ describe('getSidebar', () => { "class": "current", "data-version": "1", }, + "autogenerated": true, "badge": undefined, "href": "/api/v1/users/", "isCurrent": false, @@ -187,6 +192,7 @@ describe('getSidebar', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/deprecated-api/users/", "isCurrent": false, diff --git a/packages/starlight/__tests__/sidebar/navigation-badges.test.ts b/packages/starlight/__tests__/sidebar/navigation-badges.test.ts index 5cc37fd67fe..9c848cd19d8 100644 --- a/packages/starlight/__tests__/sidebar/navigation-badges.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-badges.test.ts @@ -114,6 +114,7 @@ describe('getSidebar', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": { "text": "Experimental", "variant": "tip", @@ -125,6 +126,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": { "text": "New", "variant": "default", @@ -147,6 +149,7 @@ describe('getSidebar', () => { "class": "current", "data-version": "1", }, + "autogenerated": true, "badge": undefined, "href": "/api/v1/users/", "isCurrent": false, @@ -163,6 +166,7 @@ describe('getSidebar', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/deprecated-api/users/", "isCurrent": false, diff --git a/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts b/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts index 567bccdd58e..d66136f43f8 100644 --- a/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts @@ -103,6 +103,7 @@ describe('getSidebar', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/reference/configuration/", "isCurrent": false, @@ -122,6 +123,7 @@ describe('getSidebar', () => { "class": "current", "data-version": "1", }, + "autogenerated": true, "badge": undefined, "href": "/api/v1/users/", "isCurrent": false, @@ -138,6 +140,7 @@ describe('getSidebar', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/deprecated-api/users/", "isCurrent": false, diff --git a/packages/starlight/__tests__/sidebar/navigation-order.test.ts b/packages/starlight/__tests__/sidebar/navigation-order.test.ts index 87b25a569bc..1bfec8047af 100644 --- a/packages/starlight/__tests__/sidebar/navigation-order.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-order.test.ts @@ -103,6 +103,7 @@ describe('getSidebar', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/reference/frontmatter/", "isCurrent": false, @@ -111,6 +112,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/reference/configuration/", "isCurrent": false, @@ -130,6 +132,7 @@ describe('getSidebar', () => { "class": "current", "data-version": "1", }, + "autogenerated": true, "badge": undefined, "href": "/api/v1/users/", "isCurrent": false, @@ -146,6 +149,7 @@ describe('getSidebar', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/deprecated-api/users/", "isCurrent": false, diff --git a/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts b/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts index 0f59f83fa42..1c671aa6ce2 100644 --- a/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts @@ -103,6 +103,7 @@ describe('getSidebar', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/reference/configuration/", "isCurrent": false, @@ -111,6 +112,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/reference/frontmatter/", "isCurrent": false, @@ -130,6 +132,7 @@ describe('getSidebar', () => { "class": "current", "data-version": "1", }, + "autogenerated": true, "badge": undefined, "href": "/api/v1/用户/", "isCurrent": true, @@ -146,6 +149,7 @@ describe('getSidebar', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/deprecated-api/用户/", "isCurrent": false, diff --git a/packages/starlight/__tests__/sidebar/navigation.test.ts b/packages/starlight/__tests__/sidebar/navigation.test.ts index faa8855e662..a7e27c5d4f8 100644 --- a/packages/starlight/__tests__/sidebar/navigation.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation.test.ts @@ -105,6 +105,7 @@ describe('getSidebar', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/reference/configuration/", "isCurrent": false, @@ -112,11 +113,13 @@ describe('getSidebar', () => { "type": "link", }, { + "autogenerated": true, "badge": undefined, "collapsed": false, "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/reference/frontmatter/", "isCurrent": false, @@ -125,6 +128,7 @@ describe('getSidebar', () => { }, { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/reference/frontmatter/foo/", "isCurrent": false, @@ -148,6 +152,7 @@ describe('getSidebar', () => { "class": "current", "data-version": "1", }, + "autogenerated": true, "badge": undefined, "href": "/api/v1/users/", "isCurrent": false, @@ -164,6 +169,7 @@ describe('getSidebar', () => { "entries": [ { "attrs": {}, + "autogenerated": true, "badge": undefined, "href": "/deprecated-api/users/", "isCurrent": false, diff --git a/packages/starlight/__tests__/sidebar/vitest.config.ts b/packages/starlight/__tests__/sidebar/vitest.config.ts index 0528a3b14a7..ce7a738a2e4 100644 --- a/packages/starlight/__tests__/sidebar/vitest.config.ts +++ b/packages/starlight/__tests__/sidebar/vitest.config.ts @@ -34,17 +34,21 @@ export default defineVitestConfig({ { label: 'Reference', badge: 'Experimental', - autogenerate: { directory: 'reference' }, + items: [{ autogenerate: { directory: 'reference' } }], }, // A group linking to all pages in the `api/v1` directory. { label: 'API v1', - autogenerate: { directory: '/api/v1/', attrs: { class: 'current', 'data-version': '1' } }, + items: [ + { + autogenerate: { directory: '/api/v1/', attrs: { class: 'current', 'data-version': '1' } }, + }, + ], }, // A group linking to all pages in the `Deprecated API/` directory. { label: 'API (deprecated)', - autogenerate: { directory: '/Deprecated API/' }, + items: [{ autogenerate: { directory: '/Deprecated API/' } }], }, ], }); diff --git a/packages/starlight/schemas/sidebar.ts b/packages/starlight/schemas/sidebar.ts index 54dae52245b..ae6f8baf4a5 100644 --- a/packages/starlight/schemas/sidebar.ts +++ b/packages/starlight/schemas/sidebar.ts @@ -50,31 +50,65 @@ const SidebarLinkItemSchema = z.strictObject({ }); export type SidebarLinkItem = z.infer; -const AutoSidebarGroupSchema = z.strictObject({ - ...SidebarGroupSchema.shape, - /** Enable autogenerating a sidebar category from a specific docs directory. */ - autogenerate: z.object({ - /** The directory to generate sidebar items for. */ - directory: z.string().transform(stripLeadingAndTrailingSlashes), +const AutoSidebarEntriesSchema = z + .object({ /** - * Whether the autogenerated subgroups should be collapsed by default. - * Defaults to the `AutoSidebarGroup` `collapsed` value. + * Explicitly prevent autogenerated groups which are no longer supported as the final type for + * supported sidebar item is a non-discriminated union where TypeScript will not perform excess + * property checks. This means that a user could define a sidebar group with an autogenerated + * property, not getting a TypeScript error, and only have it fail at runtime. + * @see https://github.com/microsoft/TypeScript/issues/20863 */ - collapsed: z.boolean().optional(), - /** HTML attributes to add to the autogenerated link items. */ - attrs: SidebarLinkItemHTMLAttributesSchema(), - // TODO: not supported by Docusaurus but would be good to have - /** How many directories deep to include from this directory in the sidebar. Default: `Infinity`. */ - // depth: z.number().optional(), - }), -}); -export type AutoSidebarGroup = z.infer; + label: z.custom().optional(), + /** Enable autogenerating entries from a specific docs directory. */ + autogenerate: z.object({ + /** The directory to generate sidebar items for. */ + directory: z.string().transform(stripLeadingAndTrailingSlashes), + /** + * Whether the autogenerated subgroups should be collapsed by default. + * Defaults to the parent group `collapsed` value. + */ + collapsed: z.boolean().optional(), + /** HTML attributes to add to the autogenerated link items. */ + attrs: SidebarLinkItemHTMLAttributesSchema(), + // TODO: not supported by Docusaurus but would be good to have + /** How many directories deep to include from this directory in the sidebar. Default: `Infinity`. */ + // depth: z.number().optional(), + }), + }) + .strict() + .superRefine((config, ctx) => { + if (!('label' in config)) return; + + ctx.addIssue({ + code: 'custom', + message: + `Found an \`autogenerate\` object with a \`label\`. Support for autogenerated sidebar groups was removed in Starlight v0.38.0.\n` + + `You should instead create a group with the desired \`label\` and an \`items\` array containing the autogenerate config:\n\n` + + `{\n` + + ` label: '${config.label}',\n` + + ` items: [{ autogenerate: ${JSON.stringify( + config.autogenerate, + // Hide empty attrs object that is automatically added by the schema default value. + (key, value: unknown) => + key === 'attrs' && + typeof value === 'object' && + value !== null && + Object.keys(value).length === 0 + ? undefined + : value, + ' ' + ).replace(/\n\s*/g, ' ')} }]\n` + + `}`, + }); + }); +export type AutoSidebarEntries = z.infer; type ManualSidebarGroupInput = z.input & { /** Array of links and subcategories to display in this category. */ items: Array< | z.input - | z.input + | z.input | z.input | z.input | ManualSidebarGroupInput @@ -85,7 +119,7 @@ type ManualSidebarGroupOutput = z.output & { /** Array of links and subcategories to display in this category. */ items: Array< | z.output - | z.output + | z.output | z.output | z.output | ManualSidebarGroupOutput @@ -101,7 +135,7 @@ const ManualSidebarGroupSchema: z.ZodType { for (const unionError of issue.errors) { const expectedShape: string[] = []; for (const issue of unionError) { + // We sometimes use `z.NEVER` to explicitly error on certain property combinations in + // non-discriminated unions to make it easier for users to identify invalid config, e.g. + // autogenerated groups in the sidebar no longer being supported. Having these properties + // show up in the error message with an expected type of `never` is noisy and not helpful + // so we skip them here. + if (issue.code === 'invalid_type' && issue.expected === 'never') continue; // If the issue is a nested union error, show the associated error message instead of the // base error message. if (issue.code === 'invalid_union') { diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts index e089ff79dc5..77d902ed7e2 100644 --- a/packages/starlight/utils/navigation.ts +++ b/packages/starlight/utils/navigation.ts @@ -4,7 +4,7 @@ import config from 'virtual:starlight/user-config'; import type { Badge, I18nBadge, I18nBadgeConfig } from '../schemas/badge'; import type { PrevNextLinkConfig } from '../schemas/prevNextLink'; import type { - AutoSidebarGroup, + AutoSidebarEntries, InternalSidebarLinkItem, LinkHTMLAttributes, SidebarItem, @@ -24,9 +24,12 @@ import { getLocaleRoutes, routes } from './routing'; import type { SidebarGroup, SidebarLink, + SidebarManualLink, PaginationLinks, Route, SidebarEntry, + SidebarAutoLink, + SidebarAutoGroup, } from './routing/types'; import { localeToLang, localizedFilePath, slugToPathname } from './slugs'; import { isAbsoluteUrl } from './url'; @@ -70,12 +73,13 @@ function configItemToEntry( item: SidebarItem, currentPathname: string, locale: string | undefined, - routes: Route[] -): SidebarEntry { + routes: Route[], + isParentCollapsed = false +): SidebarEntry | SidebarEntry[] { if ('link' in item) { return linkFromSidebarLinkItem(item, locale); } else if ('autogenerate' in item) { - return groupFromAutogenerateConfig(item, locale, routes, currentPathname); + return entriesFromAutogenerateConfig(item, locale, routes, currentPathname, isParentCollapsed); } else if ('slug' in item) { return linkFromInternalSidebarLinkItem(item, locale); } else { @@ -83,21 +87,24 @@ function configItemToEntry( return { type: 'group', label, - entries: item.items.map((i) => configItemToEntry(i, currentPathname, locale, routes)), + entries: item.items.flatMap((i) => + configItemToEntry(i, currentPathname, locale, routes, item.collapsed) + ), collapsed: item.collapsed, badge: getSidebarBadge(item.badge, locale, label), }; } } -/** Autogenerate a group of links from a user’s sidebar config. */ -function groupFromAutogenerateConfig( - item: AutoSidebarGroup, +/** Autogenerate links and groups from a user’s sidebar config. */ +function entriesFromAutogenerateConfig( + item: AutoSidebarEntries, locale: string | undefined, routes: Route[], - currentPathname: string -): SidebarGroup { - const { attrs, collapsed: subgroupCollapsed, directory } = item.autogenerate; + currentPathname: string, + isParentCollapsed: boolean +): (SidebarAutoLink | SidebarGroup)[] { + const { attrs, collapsed, directory } = item.autogenerate; const localeDir = locale ? locale + '/' + directory : directory; const dirDocs = routes.filter((doc) => { const filePathFromContentDir = getRoutePathRelativeToCollectionRoot(doc, locale); @@ -109,20 +116,7 @@ function groupFromAutogenerateConfig( ); }); const tree = treeify(dirDocs, locale, localeDir); - const label = pickLang(item.translations, localeToLang(locale)) || item.label; - return { - type: 'group', - label, - entries: sidebarFromDir( - tree, - currentPathname, - locale, - subgroupCollapsed ?? item.collapsed, - attrs - ), - collapsed: item.collapsed, - badge: getSidebarBadge(item.badge, locale, label), - }; + return sidebarFromDir(tree, currentPathname, locale, collapsed ?? isParentCollapsed, attrs); } /** Create a link entry from a manual link item in user config. */ @@ -134,7 +128,12 @@ function linkFromSidebarLinkItem(item: SidebarLinkItem, locale: string | undefin if (locale) href = '/' + locale + href; } const label = pickLang(item.translations, localeToLang(locale)) || item.label; - return makeSidebarLink(href, label, getSidebarBadge(item.badge, locale, label), item.attrs); + return makeSidebarLink({ + href, + label, + badge: getSidebarBadge(item.badge, locale, label), + attrs: item.attrs, + }); } /** Create a link entry from an automatic internal link item in user config. */ @@ -169,39 +168,47 @@ function linkFromInternalSidebarLinkItem( frontmatter.title; const badge = item.badge ?? frontmatter.sidebar?.badge; const attrs = { ...frontmatter.sidebar?.attrs, ...item.attrs }; - return makeSidebarLink( - slugToPathname(route.id), + return makeSidebarLink({ + href: slugToPathname(route.id), label, - getSidebarBadge(badge, locale, label), - attrs - ); + badge: getSidebarBadge(badge, locale, label), + attrs, + }); +} + +interface MakeLinkOptions { + autogenerated?: boolean | undefined; + href: string; + label: string; + badge?: Badge | undefined; + attrs?: LinkHTMLAttributes | undefined; } /** Process sidebar link options to create a link entry. */ -function makeSidebarLink( - href: string, - label: string, - badge?: Badge, - attrs?: LinkHTMLAttributes -): SidebarLink { +function makeSidebarLink(opts: MakeLinkOptions & { autogenerated?: false }): SidebarManualLink; +function makeSidebarLink(opts: MakeLinkOptions & { autogenerated: true }): SidebarAutoLink; +function makeSidebarLink({ attrs, badge, href, label, autogenerated }: MakeLinkOptions) { if (!isAbsoluteUrl(href)) { href = formatPath(href); } - return makeLink({ label, href, badge, attrs }); + return makeLink({ label, href, badge, attrs, autogenerated }); } /** Create a link entry */ function makeLink({ attrs = {}, - badge = undefined, + badge, + autogenerated = false, ...opts -}: { - label: string; - href: string; - badge?: Badge | undefined; - attrs?: LinkHTMLAttributes | undefined; -}): SidebarLink { - return { type: 'link', ...opts, badge, isCurrent: false, attrs }; +}: MakeLinkOptions): SidebarLink { + return { + type: 'link', + ...opts, + badge, + isCurrent: false, + attrs, + ...(autogenerated ? { autogenerated: true } : {}), + }; } /** Test if two paths are equivalent even if formatted differently. */ @@ -273,13 +280,14 @@ function treeify(routes: Route[], locale: string | undefined, baseDir: string): } /** Create a link entry for a given content collection entry. */ -function linkFromRoute(route: Route, attrs?: LinkHTMLAttributes): SidebarLink { - return makeSidebarLink( - slugToPathname(route.id), - route.entry.data.sidebar.label || route.entry.data.title, - route.entry.data.sidebar.badge, - { ...attrs, ...route.entry.data.sidebar.attrs } - ); +function linkFromRoute(route: Route, attrs?: LinkHTMLAttributes): SidebarAutoLink { + return makeSidebarLink({ + href: slugToPathname(route.id), + label: route.entry.data.sidebar.label || route.entry.data.title, + badge: route.entry.data.sidebar.badge, + attrs: { ...attrs, ...route.entry.data.sidebar.attrs }, + autogenerated: true, + }); } /** @@ -314,7 +322,7 @@ function groupFromDir( locale: string | undefined, collapsed: boolean, attrs?: LinkHTMLAttributes -): SidebarGroup { +): SidebarAutoGroup { const entries = sortDirEntries(Object.entries(dir)).map(([key, dirOrRoute]) => dirToItem(dirOrRoute, `${fullPath}/${key}`, key, currentPathname, locale, collapsed, attrs) ); @@ -324,6 +332,7 @@ function groupFromDir( entries, collapsed, badge: undefined, + autogenerated: true, }; } @@ -336,7 +345,7 @@ function dirToItem( locale: string | undefined, collapsed: boolean, attrs?: LinkHTMLAttributes -): SidebarEntry { +): SidebarAutoGroup | SidebarAutoLink { return isDir(dirOrRoute) ? groupFromDir(dirOrRoute, fullPath, dirName, currentPathname, locale, collapsed, attrs) : linkFromRoute(dirOrRoute, attrs); @@ -401,7 +410,7 @@ function getIntermediateSidebarFromConfig( ): SidebarEntry[] { const routes = getLocaleRoutes(locale); if (sidebarConfig) { - return sidebarConfig.map((group) => configItemToEntry(group, pathname, locale, routes)); + return sidebarConfig.flatMap((group) => configItemToEntry(group, pathname, locale, routes)); } else { const tree = treeify(routes, locale, locale || ''); return sidebarFromDir(tree, pathname, locale, false); diff --git a/packages/starlight/utils/routing/types.ts b/packages/starlight/utils/routing/types.ts index a0dbe675b86..d7bde475e73 100644 --- a/packages/starlight/utils/routing/types.ts +++ b/packages/starlight/utils/routing/types.ts @@ -14,7 +14,7 @@ export interface LocaleData { locale: string | undefined; } -export interface SidebarLink { +export interface SidebarManualLink { type: 'link'; label: string; href: string; @@ -23,7 +23,11 @@ export interface SidebarLink { attrs: LinkHTMLAttributes; } -export interface SidebarGroup { +export interface SidebarAutoLink extends SidebarManualLink { + autogenerated: true; +} + +export interface SidebarManualGroup { type: 'group'; label: string; entries: (SidebarLink | SidebarGroup)[]; @@ -31,6 +35,12 @@ export interface SidebarGroup { badge: Badge | undefined; } +export interface SidebarAutoGroup extends SidebarManualGroup { + autogenerated: true; +} + +export type SidebarLink = SidebarManualLink | SidebarAutoLink; +export type SidebarGroup = SidebarManualGroup | SidebarAutoGroup; export type SidebarEntry = SidebarLink | SidebarGroup; export interface PaginationLinks {