diff --git a/.changeset/shaggy-toes-smile.md b/.changeset/shaggy-toes-smile.md new file mode 100644 index 00000000000..9ffdd68d04d --- /dev/null +++ b/.changeset/shaggy-toes-smile.md @@ -0,0 +1,5 @@ +--- +'@astrojs/starlight': patch +--- + +Fixes an issue with autogenerated sidebars when using Starlight with Astro's new Content Layer API with directories containing spaces or special characters. diff --git a/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts b/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts index 3ce50a768e6..2dda3324893 100644 --- a/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts @@ -14,6 +14,7 @@ vi.mock('astro:content', async () => }, ], ['api/v1/users.md', { title: 'Users API' }], + ['Deprecated API/users.md', { title: 'Deprecated Users API' }], ], }) ); @@ -109,6 +110,22 @@ describe('getSidebar', () => { "label": "API v1", "type": "group", }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": undefined, + "href": "/deprecated-api/users/", + "isCurrent": false, + "label": "Deprecated Users API", + "type": "link", + }, + ], + "label": "API (deprecated)", + "type": "group", + }, ] `); }); diff --git a/packages/starlight/__tests__/sidebar/navigation-badges.test.ts b/packages/starlight/__tests__/sidebar/navigation-badges.test.ts index 2f48330da5c..d0963c2cc1f 100644 --- a/packages/starlight/__tests__/sidebar/navigation-badges.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-badges.test.ts @@ -21,6 +21,7 @@ vi.mock('astro:content', async () => ['reference/frontmatter.md', { title: 'Frontmatter Reference', sidebar: { badge: 'New' } }], ['api/v1/users.md', { title: 'Users API' }], ['guides/project-structure.mdx', { title: 'Project Structure' }], + ['Deprecated API/users.md', { title: 'Deprecated Users API' }], ], }) ); @@ -127,6 +128,22 @@ describe('getSidebar', () => { "label": "API v1", "type": "group", }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": undefined, + "href": "/deprecated-api/users/", + "isCurrent": false, + "label": "Deprecated Users API", + "type": "link", + }, + ], + "label": "API (deprecated)", + "type": "group", + }, ] `); }); diff --git a/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts b/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts index dc46686cedc..ee5cc0c5027 100644 --- a/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts @@ -10,6 +10,7 @@ vi.mock('astro:content', async () => ['reference/frontmatter.md', { title: 'Frontmatter Reference', sidebar: { hidden: true } }], ['api/v1/users.md', { title: 'Users API' }], ['guides/project-structure.mdx', { title: 'Project Structure' }], + ['Deprecated API/users.md', { title: 'Deprecated Users API' }], ], }) ); @@ -102,6 +103,22 @@ describe('getSidebar', () => { "label": "API v1", "type": "group", }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": undefined, + "href": "/deprecated-api/users/", + "isCurrent": false, + "label": "Deprecated Users API", + "type": "link", + }, + ], + "label": "API (deprecated)", + "type": "group", + }, ] `); }); diff --git a/packages/starlight/__tests__/sidebar/navigation-order.test.ts b/packages/starlight/__tests__/sidebar/navigation-order.test.ts index aa1396c5714..cce9a694dd3 100644 --- a/packages/starlight/__tests__/sidebar/navigation-order.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-order.test.ts @@ -10,6 +10,7 @@ vi.mock('astro:content', async () => ['reference/frontmatter.md', { title: 'Frontmatter Reference', sidebar: { order: 1 } }], ['api/v1/users.md', { title: 'Users API' }], ['guides/project-structure.mdx', { title: 'Project Structure' }], + ['Deprecated API/users.md', { title: 'Deprecated Users API' }], ], }) ); @@ -110,6 +111,22 @@ describe('getSidebar', () => { "label": "API v1", "type": "group", }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": undefined, + "href": "/deprecated-api/users/", + "isCurrent": false, + "label": "Deprecated Users API", + "type": "link", + }, + ], + "label": "API (deprecated)", + "type": "group", + }, ] `); }); diff --git a/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts b/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts index e74f7f6c18f..02c16bb4d08 100644 --- a/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts @@ -10,6 +10,7 @@ vi.mock('astro:content', async () => ['reference/frontmatter.md', { title: 'Frontmatter Reference' }], ['api/v1/用户.md', { title: 'Path with non-ASCII characters' }], ['guides/project-structure.mdx', { title: 'Project Structure' }], + ['Deprecated API/用户.md', { title: 'Another path with non-ASCII characters' }], ], }) ); @@ -110,6 +111,22 @@ describe('getSidebar', () => { "label": "API v1", "type": "group", }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": undefined, + "href": "/deprecated-api/用户/", + "isCurrent": false, + "label": "Another path with non-ASCII characters", + "type": "link", + }, + ], + "label": "API (deprecated)", + "type": "group", + }, ] `); }); diff --git a/packages/starlight/__tests__/sidebar/navigation.test.ts b/packages/starlight/__tests__/sidebar/navigation.test.ts index bda1c68075a..f113d4313ee 100644 --- a/packages/starlight/__tests__/sidebar/navigation.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation.test.ts @@ -11,6 +11,7 @@ vi.mock('astro:content', async () => ['reference/frontmatter/foo.mdx', { title: 'Foo' }], ['api/v1/users.md', { title: 'Users API' }], ['guides/project-structure.mdx', { title: 'Project Structure' }], + ['Deprecated API/users.md', { title: 'Deprecated Users API' }], ], }) ); @@ -127,6 +128,22 @@ describe('getSidebar', () => { "label": "API v1", "type": "group", }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": undefined, + "href": "/deprecated-api/users/", + "isCurrent": false, + "label": "Deprecated Users API", + "type": "link", + }, + ], + "label": "API (deprecated)", + "type": "group", + }, ] `); }); diff --git a/packages/starlight/__tests__/sidebar/vitest.config.ts b/packages/starlight/__tests__/sidebar/vitest.config.ts index a33f9541b7a..8eceebbc892 100644 --- a/packages/starlight/__tests__/sidebar/vitest.config.ts +++ b/packages/starlight/__tests__/sidebar/vitest.config.ts @@ -25,16 +25,21 @@ export default defineVitestConfig({ }, ], }, - // A group linking to all pages in the reference directory. + // A group linking to all pages in the `reference` directory. { label: 'Reference', badge: 'Experimental', autogenerate: { directory: 'reference' }, }, - // A group linking to all pages in the api/v1 directory. + // A group linking to all pages in the `api/v1` directory. { label: 'API v1', autogenerate: { directory: '/api/v1/' }, }, + // A group linking to all pages in the `Deprecated API/` directory. + { + label: 'API (deprecated)', + autogenerate: { directory: '/Deprecated API/' }, + }, ], }); diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts index 921dc2fe0a7..0944eb5dacc 100644 --- a/packages/starlight/utils/navigation.ts +++ b/packages/starlight/utils/navigation.ts @@ -24,6 +24,8 @@ const SlugKey = Symbol('SlugKey'); const neverPathFormatter = createPathFormatter({ trailingSlash: 'never' }); +const docsCollectionPathFromRoot = getCollectionPathFromRoot('docs', project); + export interface Link { type: 'link'; label: string; @@ -103,13 +105,15 @@ function groupFromAutogenerateConfig( ): Group { const { collapsed: subgroupCollapsed, directory } = item.autogenerate; const localeDir = locale ? locale + '/' + directory : directory; - const dirDocs = routes.filter( - (doc) => + const dirDocs = routes.filter((doc) => { + const filePathFromContentDir = getRoutePathRelativeToCollectionRoot(doc, locale); + return ( // Match against `foo.md` or `foo/index.md`. - stripExtension(doc.id) === localeDir || + stripExtension(filePathFromContentDir) === localeDir || // Match against `foo/anything/else.md`. - doc.id.startsWith(localeDir + '/') - ); + filePathFromContentDir.startsWith(localeDir + '/') + ); + }); const tree = treeify(dirDocs, locale, localeDir); const label = pickLang(item.translations, localeToLang(locale)) || item.label; return { @@ -219,24 +223,22 @@ function getBreadcrumbs(path: string, baseDir: string): string[] { return relativePath.split('/'); } +/** Return the path of a route relative to the root of the collection, which is equivalent to legacy IDs. */ +function getRoutePathRelativeToCollectionRoot(route: Route, locale: string | undefined) { + return project.legacyCollections + ? route.id + : // For collections with a loader, use a localized filePath relative to the collection + localizedId(route.entry.filePath.replace(`${docsCollectionPathFromRoot}/`, ''), locale); +} + /** Turn a flat array of routes into a tree structure. */ function treeify(routes: Route[], locale: string | undefined, baseDir: string): Dir { const treeRoot: Dir = makeDir(baseDir); - const collectionPathFromRoot = getCollectionPathFromRoot('docs', project); routes // Remove any entries that should be hidden .filter((doc) => !doc.entry.data.sidebar.hidden) // Compute the path of each entry from the root of the collection ahead of time. - .map( - (doc) => - [ - project.legacyCollections - ? doc.id - : // For collections with a loader, use a localized filePath relative to the collection - localizedId(doc.entry.filePath.replace(`${collectionPathFromRoot}/`, ''), locale), - doc, - ] as const - ) + .map((doc) => [getRoutePathRelativeToCollectionRoot(doc, locale), doc] as const) // Sort by depth, to build the tree depth first. .sort(([a], [b]) => b.split('/').length - a.split('/').length) // Build the tree