diff options
author | HiDeoo | 2024-08-16 19:03:48 +0200 |
---|---|---|
committer | GitHub | 2024-08-16 19:03:48 +0200 |
commit | e044feeae9a336a87db526107e5772b54ddc567f (patch) | |
tree | 262bd7f73ea5e96838e4159595a61333cfe415f6 | |
parent | 68f56a7ffd314b760443a057db9ed1a982dbe191 (diff) | |
download | IT.starlight-e044feeae9a336a87db526107e5772b54ddc567f.tar.gz IT.starlight-e044feeae9a336a87db526107e5772b54ddc567f.tar.bz2 IT.starlight-e044feeae9a336a87db526107e5772b54ddc567f.zip |
Use Starlight `sidebar` user-config format for `<StarlightPage />` `sidebar` prop (#2168)
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
-rw-r--r-- | .changeset/chilled-kiwis-count.md | 56 | ||||
-rw-r--r-- | docs/src/content/docs/guides/pages.mdx | 15 | ||||
-rw-r--r-- | packages/starlight/__tests__/basics/starlight-page-route-data.test.ts | 235 | ||||
-rw-r--r-- | packages/starlight/utils/navigation.ts | 16 | ||||
-rw-r--r-- | packages/starlight/utils/starlight-page.ts | 102 |
5 files changed, 213 insertions, 211 deletions
diff --git a/.changeset/chilled-kiwis-count.md b/.changeset/chilled-kiwis-count.md new file mode 100644 index 00000000..dac9116a --- /dev/null +++ b/.changeset/chilled-kiwis-count.md @@ -0,0 +1,56 @@ +--- +'@astrojs/starlight': minor +--- + +⚠️ **BREAKING CHANGE:** Updates the `<StarlightPage />` component `sidebar` prop to accept an array of [`SidebarItem`](https://starlight.astro.build/reference/configuration/#sidebaritem)s like the main Starlight `sidebar` configuration in `astro.config.mjs`. + +This change simplifies the definition of sidebar items in the `<StarlightPage />` component, allows for shared sidebar configuration between the global `sidebar` option and `<StarlightPage />` component, and also enables the usage of autogenerated sidebar groups with the `<StarlightPage />` component. +If you are using the `<StarlightPage />` component with a custom `sidebar` configuration, you will need to update the `sidebar` prop to an array of [`SidebarItem`](https://starlight.astro.build/reference/configuration/#sidebaritem) objects. + +For example, the following custom page with a custom `sidebar` configuration defines a “Resources” group with a “New” badge, a link to the “Showcase” page which is part of the `docs` content collection, and a link to the Starlight website: + +```astro +--- +// src/pages/custom-page/example.astro +--- + +<StarlightPage + frontmatter={{ title: 'My custom page' }} + sidebar={[ + { + type: 'group', + label: 'Resources', + badge: { text: 'New' }, + items: [ + { type: 'link', label: 'Showcase', href: '/showcase/' }, + { type: 'link', label: 'Starlight', href: 'https://starlight.astro.build/' }, + ], + }, + ]} +> + <p>This is a custom page with a custom component.</p> +</StarlightPage> +``` + +This configuration will now need to be updated to the following: + +```astro +--- +// src/pages/custom-page/example.astro +--- + +<StarlightPage + frontmatter={{ title: 'My custom page' }} + sidebar={[ + { + label: 'Resources', + badge: { text: 'New' }, + items: ['showcase', { label: 'Starlight', link: 'https://starlight.astro.build/' }], + }, + ]} +> + <p>This is a custom page with a custom component.</p> +</StarlightPage> +``` + +See the [“Sidebar Navigation”](https://starlight.astro.build/guides/sidebar/) guide to learn more about the available options for customizing the sidebar. diff --git a/docs/src/content/docs/guides/pages.mdx b/docs/src/content/docs/guides/pages.mdx index bb382590..f112a572 100644 --- a/docs/src/content/docs/guides/pages.mdx +++ b/docs/src/content/docs/guides/pages.mdx @@ -109,26 +109,25 @@ The following properties differ from Markdown frontmatter: ##### `sidebar` -**type:** `SidebarEntry[]` +**type:** [`SidebarItem[]`](/reference/configuration/#sidebaritem) **default:** the sidebar generated based on the [global `sidebar` config](/reference/configuration/#sidebar) Provide a custom site navigation sidebar for this page. If not set, the page will use the default global sidebar. -For example, the following page overrides the default sidebar with a link to the homepage and a group of links to different constellations. -The current page in the sidebar is set using the `isCurrent` property and an optional `badge` has been added to a link item. +For example, the following page overrides the default sidebar with a link to the homepage and a group of links to various other custom pages. ```astro {3-13} <StarlightPage frontmatter={{ title: 'Orion' }} sidebar={[ - { label: 'Home', href: '/' }, + { label: 'Home', link: '/' }, { label: 'Constellations', items: [ - { label: 'Andromeda', href: '/andromeda/' }, - { label: 'Orion', href: '/orion/', isCurrent: true }, - { label: 'Ursa Minor', href: '/ursa-minor/', badge: 'Stub' }, + { label: 'Andromeda', link: '/andromeda/' }, + { label: 'Orion', link: '/orion/' }, + { label: 'Ursa Minor', link: '/ursa-minor/', badge: 'Stub' }, ], }, ]} @@ -137,6 +136,8 @@ The current page in the sidebar is set using the `isCurrent` property and an opt </StarlightPage> ``` +See the [“Sidebar Navigation”](/guides/sidebar/) guide to learn more about the available options for customizing the sidebar. + ##### `hasSidebar` **type:** `boolean` 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 e5164e04..3592f09b 100644 --- a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts +++ b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts @@ -1,4 +1,4 @@ -import { assert, expect, test, vi } from 'vitest'; +import { expect, test, vi } from 'vitest'; import { generateRouteData } from '../../utils/route-data'; import { routes } from '../../utils/routing'; import { @@ -15,6 +15,9 @@ vi.mock('astro:content', async () => docs: [ ['index.mdx', { title: 'Home Page' }], ['getting-started.mdx', { title: 'Getting Started' }], + ['guides/authoring-content.md', { title: 'Authoring Markdown' }], + ['guides/components.mdx', { title: 'Components' }], + ['reference/frontmatter.md', { title: 'Frontmatter Reference' }], ], }) ); @@ -102,10 +105,64 @@ test('uses generated sidebar when no sidebar is provided', async () => { props: starlightPageProps, url: starlightPageUrl, }); - expect(data.sidebar.map((entry) => entry.label)).toMatchInlineSnapshot(` + expect(data.sidebar).toMatchInlineSnapshot(` [ - "Home Page", - "Getting Started", + { + "attrs": {}, + "badge": undefined, + "href": "/", + "isCurrent": false, + "label": "Home Page", + "type": "link", + }, + { + "attrs": {}, + "badge": undefined, + "href": "/getting-started/", + "isCurrent": false, + "label": "Getting Started", + "type": "link", + }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": undefined, + "href": "/guides/authoring-content/", + "isCurrent": false, + "label": "Authoring Markdown", + "type": "link", + }, + { + "attrs": {}, + "badge": undefined, + "href": "/guides/components/", + "isCurrent": false, + "label": "Components", + "type": "link", + }, + ], + "label": "guides", + "type": "group", + }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": undefined, + "href": "/reference/frontmatter/", + "isCurrent": false, + "label": "Frontmatter Reference", + "type": "link", + }, + ], + "label": "reference", + "type": "group", + }, ] `); }); @@ -116,98 +173,76 @@ test('uses provided sidebar if any', async () => { ...starlightPageProps, sidebar: [ { - type: 'link', label: 'Custom link 1', - href: '/test/1', - isCurrent: false, - badge: undefined, - attrs: {}, + link: '/test/1', + badge: 'New', }, { - type: 'link', label: 'Custom link 2', - href: '/test/2', - isCurrent: false, - badge: undefined, - attrs: {}, + link: '/test/2', }, - ], - }, - url: starlightPageUrl, - }); - expect(data.sidebar.map((entry) => entry.label)).toMatchInlineSnapshot(` - [ - "Custom link 1", - "Custom link 2", - ] - `); -}); - -test('uses provided sidebar with minimal config', async () => { - const data = await generateStarlightPageRouteData({ - props: { - ...starlightPageProps, - sidebar: [ - { label: 'Custom link 1', href: '/test/1' }, - { label: 'Custom link 2', href: '/test/2' }, - ], - }, - url: starlightPageUrl, - }); - expect(data.sidebar.map((entry) => entry.label)).toMatchInlineSnapshot(` - [ - "Custom link 1", - "Custom link 2", - ] - `); -}); - -test('supports deprecated `entries` field for sidebar groups', async () => { - const data = await generateStarlightPageRouteData({ - props: { - ...starlightPageProps, - sidebar: [ { - label: 'Group', - entries: [ - { label: 'Custom link 1', href: '/test/1' }, - { label: 'Custom link 2', href: '/test/2' }, - ], + label: 'Guides', + autogenerate: { directory: 'guides' }, }, + 'reference/frontmatter', ], }, url: starlightPageUrl, }); - assert(data.sidebar[0]!.type === 'group'); - expect(data.sidebar[0]!.entries.map((entry) => entry.label)).toMatchInlineSnapshot(` + expect(data.sidebar).toMatchInlineSnapshot(` [ - "Custom link 1", - "Custom link 2", - ] - `); -}); - -test('supports `items` field for sidebar groups', async () => { - const data = await generateStarlightPageRouteData({ - props: { - ...starlightPageProps, - sidebar: [ - { - label: 'Group', - items: [ - { label: 'Custom link 1', href: '/test/1' }, - { label: 'Custom link 2', href: '/test/2' }, - ], - }, - ], - }, - url: starlightPageUrl, - }); - assert(data.sidebar[0]!.type === 'group'); - expect(data.sidebar[0]!.entries.map((entry) => entry.label)).toMatchInlineSnapshot(` - [ - "Custom link 1", - "Custom link 2", + { + "attrs": {}, + "badge": { + "text": "New", + "variant": "default", + }, + "href": "/test/1", + "isCurrent": false, + "label": "Custom link 1", + "type": "link", + }, + { + "attrs": {}, + "badge": undefined, + "href": "/test/2", + "isCurrent": false, + "label": "Custom link 2", + "type": "link", + }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": undefined, + "href": "/guides/authoring-content/", + "isCurrent": false, + "label": "Authoring Markdown", + "type": "link", + }, + { + "attrs": {}, + "badge": undefined, + "href": "/guides/components/", + "isCurrent": false, + "label": "Components", + "type": "link", + }, + ], + "label": "Guides", + "type": "group", + }, + { + "attrs": {}, + "badge": undefined, + "href": "/reference/frontmatter", + "isCurrent": false, + "label": "Frontmatter Reference", + "type": "link", + }, ] `); }); @@ -221,34 +256,7 @@ test('throws error if sidebar is malformated', async () => { { label: 'Custom link 1', //@ts-expect-error Intentionally bad type to cause error. - href: 5, - }, - ], - }, - url: starlightPageUrl, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(` - "[AstroUserError]: - Invalid sidebar prop passed to the \`<StarlightPage/>\` component. - Hint: - **0**: Did not match union. - > Expected type \`{ href: string } | { entries: array }\` - > Received \`{ "label": "Custom link 1", "href": 5 }\`" - `); -}); - -test('throws error if sidebar uses wrong literal for entry type', async () => { - // This test also makes sure we show a helpful error for incorrect literals. - expect(() => - generateStarlightPageRouteData({ - props: { - ...starlightPageProps, - sidebar: [ - { - //@ts-expect-error Intentionally bad type to cause error. - type: 'typo', - label: 'Custom link 1', - href: '/', + href: '/test/1', }, ], }, @@ -259,7 +267,8 @@ test('throws error if sidebar uses wrong literal for entry type', async () => { Invalid sidebar prop passed to the \`<StarlightPage/>\` component. Hint: **0**: Did not match union. - > **0.type**: Expected \`"link" | "group"\`, received \`"typo"\`" + > Expected type \`{ link: string; } | { items: array; } | { autogenerate: object; } | { slug: string } | string\` + > Received \`{ "label": "Custom link 1", "href": "/test/1" }\`" `); }); diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts index d6a67a65..cd1475d1 100644 --- a/packages/starlight/utils/navigation.ts +++ b/packages/starlight/utils/navigation.ts @@ -15,6 +15,7 @@ import { pickLang } from './i18n'; import { ensureLeadingSlash, ensureTrailingSlash, stripLeadingAndTrailingSlashes } from './path'; import { getLocaleRoutes, routes, type Route } from './routing'; import { localeToLang, slugToPathname } from './slugs'; +import type { StarlightConfig } from './user-config'; const DirKey = Symbol('DirKey'); const SlugKey = Symbol('SlugKey'); @@ -333,11 +334,20 @@ function sidebarFromDir( ); } -/** Get the sidebar for the current page. */ +/** Get the sidebar for the current page using the global config. */ export function getSidebar(pathname: string, locale: string | undefined): SidebarEntry[] { + return getSidebarFromConfig(config.sidebar, pathname, locale); +} + +/** Get the sidebar for the current page using the specified sidebar config. */ +export function getSidebarFromConfig( + sidebarConfig: StarlightConfig['sidebar'], + pathname: string, + locale: string | undefined +): SidebarEntry[] { const routes = getLocaleRoutes(locale); - if (config.sidebar) { - return config.sidebar.map((group) => configItemToEntry(group, pathname, locale, routes)); + if (sidebarConfig) { + return sidebarConfig.map((group) => configItemToEntry(group, pathname, locale, routes)); } else { const tree = treeify(routes, locale || ''); return sidebarFromDir(tree, pathname, locale, false); diff --git a/packages/starlight/utils/starlight-page.ts b/packages/starlight/utils/starlight-page.ts index d4310800..2777431c 100644 --- a/packages/starlight/utils/starlight-page.ts +++ b/packages/starlight/utils/starlight-page.ts @@ -12,11 +12,11 @@ import { } from './route-data'; import type { StarlightDocsEntry } from './routing'; import { slugToLocaleData, urlToSlug } from './slugs'; -import { getPrevNextLinks, getSidebar } from './navigation'; +import { getPrevNextLinks, getSidebarFromConfig } from './navigation'; import { useTranslations } from './translations'; import { docsSchema } from '../schema'; -import { BadgeConfigSchema } from '../schemas/badge'; -import { SidebarLinkItemHTMLAttributesSchema } from '../schemas/sidebar'; +import { SidebarItemSchema } from '../schemas/sidebar'; +import type { StarlightConfig, StarlightUserConfig } from './user-config'; /** * The frontmatter schema for Starlight pages derived from the default schema for Starlight’s @@ -64,88 +64,12 @@ type StarlightPageFrontmatter = Omit< 'editUrl' | 'sidebar' > & { editUrl?: string | false }; -/** - * Link configuration schema for `<StarlightPage>`. - * Sets default values where possible to be more user friendly than raw `SidebarEntry` type. - */ -const LinkSchema = z - .object({ - /** @deprecated Specifying `type` is no longer required. */ - type: z.literal('link').default('link'), - label: z.string(), - href: z.string(), - isCurrent: z.boolean().default(false), - badge: BadgeConfigSchema(), - attrs: SidebarLinkItemHTMLAttributesSchema(), - }) - // Make sure badge is in the object even if undefined — Zod doesn’t seem to have a way to set `undefined` as a default. - .transform((item) => ({ badge: undefined, ...item })); - -/** Base schema for link groups without the recursive `items` array. */ -const LinkGroupBase = z.object({ - /** @deprecated Specifying `type` is no longer required. */ - type: z.literal('group').default('group'), - label: z.string(), - collapsed: z.boolean().default(false), - badge: BadgeConfigSchema(), -}); - -// These manual types are needed to correctly type the recursive link group type. -type ManualLinkGroupInput = Prettify< - z.input<typeof LinkGroupBase> & - // The original implementation of `<StarlightPage>` in v0.19.0 used `entries`. - // We want to use `items` so it matches the sidebar config in `astro.config.mjs`. - // Keeping `entries` support for now to not break anyone. - // TODO: warn about `entries` usage in a future version - // TODO: remove support for `entries` in a future version - (| { - /** Array of links and subcategories to display in this category. */ - items: Array<z.input<typeof LinkSchema> | ManualLinkGroupInput>; - } - | { - /** - * @deprecated Use `items` instead of `entries`. - * Support for `entries` will be removed in a future version of Starlight. - */ - entries: Array<z.input<typeof LinkSchema> | ManualLinkGroupInput>; - } - ) ->; -type ManualLinkGroupOutput = z.output<typeof LinkGroupBase> & { - entries: Array<z.output<typeof LinkSchema> | ManualLinkGroupOutput>; - badge: z.output<typeof LinkGroupBase>['badge']; -}; -type LinkGroupSchemaType = z.ZodType<ManualLinkGroupOutput, z.ZodTypeDef, ManualLinkGroupInput>; -/** - * Link group configuration schema for `<StarlightPage>`. - * Sets default values where possible to be more user friendly than raw `SidebarEntry` type. - */ -const LinkGroupSchema: LinkGroupSchemaType = z.preprocess( - // Map `items` to `entries` as expected by the `SidebarEntry` type. - (arg) => { - if (arg && typeof arg === 'object' && 'items' in arg) { - const { items, ...rest } = arg; - return { ...rest, entries: items }; - } - return arg; - }, - LinkGroupBase.extend({ - entries: z.lazy(() => z.union([LinkSchema, LinkGroupSchema]).array()), - }) - // Make sure badge is in the object even if undefined. - .transform((item) => ({ badge: undefined, ...item })) -) as LinkGroupSchemaType; - -/** Sidebar configuration schema for `<StarlightPage>` */ -const StarlightPageSidebarSchema = z.union([LinkSchema, LinkGroupSchema]).array(); -type StarlightPageSidebarUserConfig = z.input<typeof StarlightPageSidebarSchema>; - -/** Parse sidebar prop to ensure all required defaults are in place. */ -const normalizeSidebarProp = ( - sidebarProp: StarlightPageSidebarUserConfig -): StarlightRouteData['sidebar'] => { +/** Parse sidebar prop to ensure it's valid. */ +const validateSidebarProp = ( + sidebarProp: StarlightUserConfig['sidebar'] +): StarlightConfig['sidebar'] => { return parseWithFriendlyErrors( - StarlightPageSidebarSchema, + SidebarItemSchema.array().optional(), sidebarProp, 'Invalid sidebar prop passed to the `<StarlightPage/>` component.' ); @@ -159,7 +83,7 @@ export type StarlightPageProps = Prettify< Partial<Omit<RemoveIndexSignature<PageProps>, 'entry' | 'entryMeta' | 'id' | 'locale' | 'slug'>> & // Add the sidebar definitions for a Starlight page. Partial<Pick<StarlightRouteData, 'hasSidebar'>> & { - sidebar?: StarlightPageSidebarUserConfig; + sidebar?: StarlightUserConfig['sidebar']; // And finally add the Starlight page frontmatter properties in a `frontmatter` property. frontmatter: StarlightPageFrontmatter; } @@ -190,9 +114,11 @@ export async function generateStarlightPageRouteData({ const pageFrontmatter = await getStarlightPageFrontmatter(frontmatter); const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; const localeData = slugToLocaleData(slug); - const sidebar = props.sidebar - ? normalizeSidebarProp(props.sidebar) - : getSidebar(url.pathname, localeData.locale); + const sidebar = getSidebarFromConfig( + props.sidebar ? validateSidebarProp(props.sidebar) : config.sidebar, + url.pathname, + localeData.locale + ); const headings = props.headings ?? []; const pageDocsEntry: StarlightPageDocsEntry = { id, |