diff options
author | HiDeoo | 2023-10-06 18:28:35 +0200 |
---|---|---|
committer | GitHub | 2023-10-06 18:28:35 +0200 |
commit | 903a57942ceb99b68672c3fa54622b39cc5d76f8 (patch) | |
tree | 84923d39e7b0cd523a9edd7f1440f36ba135b7c7 | |
parent | b7b23a2c90a25fe8ea08338379b83d19c74d9037 (diff) | |
download | IT.starlight-903a57942ceb99b68672c3fa54622b39cc5d76f8.tar.gz IT.starlight-903a57942ceb99b68672c3fa54622b39cc5d76f8.tar.bz2 IT.starlight-903a57942ceb99b68672c3fa54622b39cc5d76f8.zip |
Add support for custom HTML attributes to sidebar links (#774)
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
21 files changed, 390 insertions, 88 deletions
diff --git a/.changeset/fuzzy-foxes-confess.md b/.changeset/fuzzy-foxes-confess.md new file mode 100644 index 00000000..6dbec2dc --- /dev/null +++ b/.changeset/fuzzy-foxes-confess.md @@ -0,0 +1,5 @@ +--- +'@astrojs/starlight': minor +--- + +Support adding HTML attributes to sidebar links from config and frontmatter diff --git a/docs/src/components/sidebar-preview.astro b/docs/src/components/sidebar-preview.astro index 0865d2f6..732e766e 100644 --- a/docs/src/components/sidebar-preview.astro +++ b/docs/src/components/sidebar-preview.astro @@ -1,5 +1,5 @@ --- -import type { AutoSidebarGroup, SidebarItem } from '../../../packages/starlight/utils/user-config'; +import type { AutoSidebarGroup, SidebarItem } from '../../../packages/starlight/schemas/sidebar'; import SidebarSublist from '../../../packages/starlight/components/SidebarSublist.astro'; import type { SidebarEntry } from '../../../packages/starlight/utils/navigation'; @@ -20,11 +20,10 @@ function makeEntries(items: SidebarConfig): SidebarEntry[] { href: item.link, isCurrent: false, badge: item.badge, + attrs: item.attrs ?? {}, }; } - item; - return { type: 'group', label: item.label, diff --git a/docs/src/content/docs/guides/sidebar.mdx b/docs/src/content/docs/guides/sidebar.mdx index 5f775c7a..6a732e6d 100644 --- a/docs/src/content/docs/guides/sidebar.mdx +++ b/docs/src/content/docs/guides/sidebar.mdx @@ -316,6 +316,50 @@ The configuration above generates the following sidebar: ]} /> +## Custom HTML attributes + +Links can also include an `attrs` property to add custom HTML attributes to the link element. + +In the following example, `attrs` is used to add a `target="_blank"` attribute, so that the link opens in a new tab, and to apply a custom `style` attribute to italicize the link label: + +```js +starlight({ + sidebar: [ + { + label: 'Guides', + items: [ + // An external link to the Astro docs opening in a new tab. + { + label: 'Astro Docs', + link: 'https://docs.astro.build/', + attrs: { target: '_blank', style: 'font-style: italic' }, + }, + ], + }, + ], +}); +``` + +The configuration above generates the following sidebar: + +<SidebarPreview + config={[ + { + label: 'Guides', + items: [ + { + label: 'Astro Docs', + link: 'https://docs.astro.build/', + attrs: { + target: '_blank', + style: 'font-style: italic', + }, + }, + ], + }, + ]} +/> + ## Internationalization Use the `translations` property on link and group entries to translate the link or group label for each supported language. diff --git a/docs/src/content/docs/reference/configuration.md b/docs/src/content/docs/reference/configuration.md index 9d742e46..ad2d8591 100644 --- a/docs/src/content/docs/reference/configuration.md +++ b/docs/src/content/docs/reference/configuration.md @@ -187,6 +187,7 @@ type SidebarItem = { | { link: string; badge?: string | BadgeConfig; + attrs?: Record<string, string | number | boolean | undefined>; } | { items: SidebarItem[]; collapsed?: boolean } | { diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index 7b305411..9b401383 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -224,10 +224,22 @@ pagefind: false ### `sidebar` -**type:** `{ label?: string; order?: number; hidden?: boolean; badge?: string | BadgeConfig }` +**type:** [`SidebarConfig`](#sidebarconfig) Control how this page is displayed in the [sidebar](/reference/configuration/#sidebar), when using an autogenerated link group. +#### `SidebarConfig` + +```ts +interface SidebarConfig { + label?: string; + order?: number; + hidden?: boolean; + badge?: string | BadgeConfig; + attrs?: Record<string, string | number | boolean | undefined>; +} +``` + #### `label` **type:** `string` @@ -299,3 +311,19 @@ sidebar: variant: caution --- ``` + +#### `attrs` + +**type:** `Record<string, string | number | boolean | undefined>` + +HTML attributes to add to the page link in the sidebar when displayed in an autogenerated group of links. + +```md +--- +title: Page opening in a new tab +sidebar: + # Opens the page in a new tab + attrs: + target: _blank +--- +``` diff --git a/packages/starlight/__tests__/basics/navigation-labels.test.ts b/packages/starlight/__tests__/basics/navigation-labels.test.ts index f51cac35..64625350 100644 --- a/packages/starlight/__tests__/basics/navigation-labels.test.ts +++ b/packages/starlight/__tests__/basics/navigation-labels.test.ts @@ -20,6 +20,7 @@ describe('getSidebar', () => { expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` [ { + "attrs": {}, "badge": undefined, "href": "/", "isCurrent": true, @@ -27,6 +28,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/environmental-impact/", "isCurrent": false, @@ -37,6 +39,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, @@ -44,6 +47,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/guides/components/", "isCurrent": false, diff --git a/packages/starlight/__tests__/basics/navigation-order.test.ts b/packages/starlight/__tests__/basics/navigation-order.test.ts index 2eb2c88d..089ad8db 100644 --- a/packages/starlight/__tests__/basics/navigation-order.test.ts +++ b/packages/starlight/__tests__/basics/navigation-order.test.ts @@ -20,6 +20,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": undefined, "href": "/guides/components/", "isCurrent": false, @@ -27,6 +28,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, @@ -38,6 +40,7 @@ describe('getSidebar', () => { "type": "group", }, { + "attrs": {}, "badge": undefined, "href": "/environmental-impact/", "isCurrent": false, @@ -45,6 +48,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/", "isCurrent": true, diff --git a/packages/starlight/__tests__/basics/navigation.test.ts b/packages/starlight/__tests__/basics/navigation.test.ts index 023edb76..76611322 100644 --- a/packages/starlight/__tests__/basics/navigation.test.ts +++ b/packages/starlight/__tests__/basics/navigation.test.ts @@ -18,6 +18,7 @@ describe('getSidebar', () => { expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` [ { + "attrs": {}, "badge": undefined, "href": "/", "isCurrent": true, @@ -25,6 +26,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/environmental-impact/", "isCurrent": false, @@ -35,6 +37,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, @@ -42,6 +45,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/guides/components/", "isCurrent": false, @@ -103,6 +107,7 @@ describe('flattenSidebar', () => { expect(flattened).toMatchInlineSnapshot(` [ { + "attrs": {}, "badge": undefined, "href": "/", "isCurrent": true, @@ -110,6 +115,7 @@ describe('flattenSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/environmental-impact/", "isCurrent": false, @@ -117,6 +123,7 @@ describe('flattenSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, @@ -124,6 +131,7 @@ describe('flattenSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/guides/components/", "isCurrent": false, @@ -142,6 +150,7 @@ describe('getPrevNextLinks', () => { expect(links).toMatchInlineSnapshot(` { "next": { + "attrs": {}, "badge": undefined, "href": "/guides/authoring-content/", "isCurrent": false, @@ -149,6 +158,7 @@ describe('getPrevNextLinks', () => { "type": "link", }, "prev": { + "attrs": {}, "badge": undefined, "href": "/", "isCurrent": false, @@ -235,6 +245,7 @@ describe('getPrevNextLinks', () => { href: '/x/', label: 'X', isCurrent: false, + attrs: {}, }); }); diff --git a/packages/starlight/__tests__/i18n/navigation-order.test.ts b/packages/starlight/__tests__/i18n/navigation-order.test.ts index 989f8a99..95ecf27c 100644 --- a/packages/starlight/__tests__/i18n/navigation-order.test.ts +++ b/packages/starlight/__tests__/i18n/navigation-order.test.ts @@ -30,6 +30,7 @@ describe('getSidebar', () => { expect(getSidebar('/en/', 'en')).toMatchInlineSnapshot(` [ { + "attrs": {}, "badge": undefined, "href": "/en/", "isCurrent": true, @@ -37,6 +38,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/en/404/", "isCurrent": false, @@ -47,6 +49,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": undefined, "href": "/en/guides/authoring-content/", "isCurrent": false, @@ -65,6 +68,7 @@ describe('getSidebar', () => { expect(getSidebar('/fr/', 'fr')).toMatchInlineSnapshot(` [ { + "attrs": {}, "badge": undefined, "href": "/fr/", "isCurrent": true, @@ -72,6 +76,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/fr/404/", "isCurrent": false, @@ -82,6 +87,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": undefined, "href": "/fr/guides/authoring-content/", "isCurrent": false, @@ -96,6 +102,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": undefined, "href": "/fr/référence/bénéfice/", "isCurrent": false, @@ -103,6 +110,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/fr/référence/bricolage/", "isCurrent": false, @@ -117,6 +125,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": undefined, "href": "/fr/route/décoder/", "isCurrent": false, @@ -124,6 +133,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/fr/route/distribuer/", "isCurrent": false, diff --git a/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts b/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts new file mode 100644 index 00000000..a6c64961 --- /dev/null +++ b/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts @@ -0,0 +1,93 @@ +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: 'Home Page' }], + ['environmental-impact.md', { title: 'Eco-friendly docs' }], + [ + 'reference/frontmatter.md', + { + title: 'Frontmatter Reference', + sidebar: { attrs: { class: 'advanced', ping: 'https://example.com' } }, + }, + ], + ], + }) +); + +describe('getSidebar', () => { + test('passes down custom HTML link attributes', () => { + expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` + [ + { + "attrs": {}, + "badge": undefined, + "href": "/", + "isCurrent": true, + "label": "Home", + "type": "link", + }, + { + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": { + "text": "New", + "variant": "success", + }, + "href": "/intro/", + "isCurrent": false, + "label": "Introduction", + "type": "link", + }, + { + "attrs": {}, + "badge": { + "text": "Deprecated", + "variant": "default", + }, + "href": "/next-steps/", + "isCurrent": false, + "label": "Next Steps", + "type": "link", + }, + { + "attrs": { + "class": "showcase-link", + "target": "_blank", + }, + "badge": undefined, + "href": "/showcase/", + "isCurrent": false, + "label": "Showcase", + "type": "link", + }, + ], + "label": "Start Here", + "type": "group", + }, + { + "collapsed": false, + "entries": [ + { + "attrs": { + "class": "advanced", + "ping": "https://example.com", + }, + "badge": undefined, + "href": "/reference/frontmatter/", + "isCurrent": false, + "label": "Frontmatter Reference", + "type": "link", + }, + ], + "label": "Reference", + "type": "group", + }, + ] + `); + }); +}); diff --git a/packages/starlight/__tests__/sidebar/navigation-badges.test.ts b/packages/starlight/__tests__/sidebar/navigation-badges.test.ts index 6c4b1863..cfde6293 100644 --- a/packages/starlight/__tests__/sidebar/navigation-badges.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-badges.test.ts @@ -29,6 +29,7 @@ describe('getSidebar', () => { expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` [ { + "attrs": {}, "badge": undefined, "href": "/", "isCurrent": true, @@ -39,6 +40,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": { "text": "New", "variant": "success", @@ -49,6 +51,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": { "text": "Deprecated", "variant": "default", @@ -58,6 +61,17 @@ describe('getSidebar', () => { "label": "Next Steps", "type": "link", }, + { + "attrs": { + "class": "showcase-link", + "target": "_blank", + }, + "badge": undefined, + "href": "/showcase/", + "isCurrent": false, + "label": "Showcase", + "type": "link", + }, ], "label": "Start Here", "type": "group", @@ -66,6 +80,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": { "text": "Experimental", "variant": "tip", @@ -76,6 +91,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": { "text": "New", "variant": "default", diff --git a/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts b/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts index df539ef7..db45111d 100644 --- a/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts @@ -18,6 +18,7 @@ describe('getSidebar', () => { expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` [ { + "attrs": {}, "badge": undefined, "href": "/", "isCurrent": true, @@ -28,6 +29,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": { "text": "New", "variant": "success", @@ -38,6 +40,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": { "text": "Deprecated", "variant": "default", @@ -47,6 +50,17 @@ describe('getSidebar', () => { "label": "Next Steps", "type": "link", }, + { + "attrs": { + "class": "showcase-link", + "target": "_blank", + }, + "badge": undefined, + "href": "/showcase/", + "isCurrent": false, + "label": "Showcase", + "type": "link", + }, ], "label": "Start Here", "type": "group", @@ -55,6 +69,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": undefined, "href": "/reference/configuration/", "isCurrent": false, diff --git a/packages/starlight/__tests__/sidebar/navigation-order.test.ts b/packages/starlight/__tests__/sidebar/navigation-order.test.ts index f0257bc5..e5b5d367 100644 --- a/packages/starlight/__tests__/sidebar/navigation-order.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation-order.test.ts @@ -18,6 +18,7 @@ describe('getSidebar', () => { expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` [ { + "attrs": {}, "badge": undefined, "href": "/", "isCurrent": true, @@ -28,6 +29,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": { "text": "New", "variant": "success", @@ -38,6 +40,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": { "text": "Deprecated", "variant": "default", @@ -47,6 +50,17 @@ describe('getSidebar', () => { "label": "Next Steps", "type": "link", }, + { + "attrs": { + "class": "showcase-link", + "target": "_blank", + }, + "badge": undefined, + "href": "/showcase/", + "isCurrent": false, + "label": "Showcase", + "type": "link", + }, ], "label": "Start Here", "type": "group", @@ -55,6 +69,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": undefined, "href": "/reference/frontmatter/", "isCurrent": false, @@ -62,6 +77,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/reference/configuration/", "isCurrent": false, diff --git a/packages/starlight/__tests__/sidebar/navigation.test.ts b/packages/starlight/__tests__/sidebar/navigation.test.ts index dd580740..9ea3cdb4 100644 --- a/packages/starlight/__tests__/sidebar/navigation.test.ts +++ b/packages/starlight/__tests__/sidebar/navigation.test.ts @@ -18,6 +18,7 @@ describe('getSidebar', () => { expect(getSidebar('/', undefined)).toMatchInlineSnapshot(` [ { + "attrs": {}, "badge": undefined, "href": "/", "isCurrent": true, @@ -28,6 +29,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": { "text": "New", "variant": "success", @@ -38,6 +40,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": { "text": "Deprecated", "variant": "default", @@ -47,6 +50,17 @@ describe('getSidebar', () => { "label": "Next Steps", "type": "link", }, + { + "attrs": { + "class": "showcase-link", + "target": "_blank", + }, + "badge": undefined, + "href": "/showcase/", + "isCurrent": false, + "label": "Showcase", + "type": "link", + }, ], "label": "Start Here", "type": "group", @@ -55,6 +69,7 @@ describe('getSidebar', () => { "collapsed": false, "entries": [ { + "attrs": {}, "badge": undefined, "href": "/reference/configuration/", "isCurrent": false, @@ -62,6 +77,7 @@ describe('getSidebar', () => { "type": "link", }, { + "attrs": {}, "badge": undefined, "href": "/reference/frontmatter/", "isCurrent": false, diff --git a/packages/starlight/__tests__/sidebar/vitest.config.ts b/packages/starlight/__tests__/sidebar/vitest.config.ts index 31860cae..31457b7a 100644 --- a/packages/starlight/__tests__/sidebar/vitest.config.ts +++ b/packages/starlight/__tests__/sidebar/vitest.config.ts @@ -18,6 +18,11 @@ export default defineVitestConfig({ }, }, { label: 'Next Steps', link: '/next-steps', badge: 'Deprecated' }, + { + label: 'Showcase', + link: '/showcase', + attrs: { class: 'showcase-link', target: '_blank' }, + }, ], }, // A group linking to all pages in the reference directory. diff --git a/packages/starlight/components/SidebarSublist.astro b/packages/starlight/components/SidebarSublist.astro index 08c3f3b8..393edf58 100644 --- a/packages/starlight/components/SidebarSublist.astro +++ b/packages/starlight/components/SidebarSublist.astro @@ -17,7 +17,8 @@ interface Props { <a href={entry.href} aria-current={entry.isCurrent && 'page'} - class:list={{ large: !Astro.props.nested }} + class:list={[{ large: !Astro.props.nested }, entry.attrs.class]} + {...entry.attrs} > <span>{entry.label}</span> {entry.badge && ( diff --git a/packages/starlight/schema.ts b/packages/starlight/schema.ts index 89fc47e2..5f73dfdc 100644 --- a/packages/starlight/schema.ts +++ b/packages/starlight/schema.ts @@ -5,6 +5,7 @@ import { PrevNextLinkConfigSchema } from './schemas/prevNextLink'; import { TableOfContentsSchema } from './schemas/tableOfContents'; import { Icons } from './components/Icons'; import { BadgeConfigSchema } from './schemas/badge'; +import { SidebarLinkItemHTMLAttributesSchema } from './schemas/sidebar'; export { i18nSchema } from './schemas/i18n'; type IconName = keyof typeof Icons; @@ -139,6 +140,8 @@ export function docsSchema() { * Passing only a string defaults to the 'default' variant which uses the site accent color. */ badge: BadgeConfigSchema(), + /** HTML attributes to add to the sidebar link. */ + attrs: SidebarLinkItemHTMLAttributesSchema(), }) .default({}), diff --git a/packages/starlight/schemas/sidebar.ts b/packages/starlight/schemas/sidebar.ts new file mode 100644 index 00000000..176a77cd --- /dev/null +++ b/packages/starlight/schemas/sidebar.ts @@ -0,0 +1,89 @@ +import type { AstroBuiltinAttributes } from 'astro'; +import type { HTMLAttributes } from 'astro/types'; +import { z } from 'astro/zod'; +import { BadgeConfigSchema } from './badge'; + +const SidebarBaseSchema = z.object({ + /** The visible label for this item in the sidebar. */ + label: z.string(), + /** Translations of the `label` for each supported language. */ + translations: z.record(z.string()).default({}), +}); + +const SidebarGroupSchema = SidebarBaseSchema.extend({ + /** Whether this item should be collapsed by default. */ + collapsed: z.boolean().default(false), +}); + +// HTML attributes that can be added to an anchor element, validated as +// `Record<string, string | number | boolean | undefined>` but typed as `HTMLAttributes<'a'>` +// for user convenience. +const linkHTMLAttributesSchema = z.record( + z.union([z.string(), z.number(), z.boolean(), z.undefined()]) +) as z.Schema<Omit<HTMLAttributes<'a'>, keyof AstroBuiltinAttributes | 'children'>>; +export type LinkHTMLAttributes = z.infer<typeof linkHTMLAttributesSchema>; + +export const SidebarLinkItemHTMLAttributesSchema = () => linkHTMLAttributesSchema.default({}); + +const SidebarLinkItemSchema = SidebarBaseSchema.extend({ + /** The link to this item’s content. Can be a relative link to local files or the full URL of an external page. */ + link: z.string(), + /** Adds a badge to the link item */ + badge: BadgeConfigSchema(), + /** HTML attributes to add to the link item. */ + attrs: SidebarLinkItemHTMLAttributesSchema(), +}); +export type SidebarLinkItem = z.infer<typeof SidebarLinkItemSchema>; + +const AutoSidebarGroupSchema = SidebarGroupSchema.extend({ + /** Enable autogenerating a sidebar category from a specific docs directory. */ + autogenerate: z.object({ + /** The directory to generate sidebar items for. */ + directory: z.string(), + /** + * Whether the autogenerated subgroups should be collapsed by default. + * Defaults to the `AutoSidebarGroup` `collapsed` value. + */ + collapsed: z.boolean().optional(), + // 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<typeof AutoSidebarGroupSchema>; + +type ManualSidebarGroupInput = z.input<typeof SidebarGroupSchema> & { + /** Array of links and subcategories to display in this category. */ + items: Array< + | z.input<typeof SidebarLinkItemSchema> + | z.input<typeof AutoSidebarGroupSchema> + | ManualSidebarGroupInput + >; +}; + +type ManualSidebarGroupOutput = z.output<typeof SidebarGroupSchema> & { + /** Array of links and subcategories to display in this category. */ + items: Array< + | z.output<typeof SidebarLinkItemSchema> + | z.output<typeof AutoSidebarGroupSchema> + | ManualSidebarGroupOutput + >; +}; + +const ManualSidebarGroupSchema: z.ZodType< + ManualSidebarGroupOutput, + z.ZodTypeDef, + ManualSidebarGroupInput +> = SidebarGroupSchema.extend({ + /** Array of links and subcategories to display in this category. */ + items: z.lazy(() => + z.union([SidebarLinkItemSchema, ManualSidebarGroupSchema, AutoSidebarGroupSchema]).array() + ), +}); + +export const SidebarItemSchema = z.union([ + SidebarLinkItemSchema, + ManualSidebarGroupSchema, + AutoSidebarGroupSchema, +]); +export type SidebarItem = z.infer<typeof SidebarItemSchema>; diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts index 88d9e3e1..a8af46b5 100644 --- a/packages/starlight/utils/navigation.ts +++ b/packages/starlight/utils/navigation.ts @@ -5,9 +5,14 @@ import { pathWithBase } from './base'; import { pickLang } from './i18n'; import { getLocaleRoutes, type Route } from './routing'; import { localeToLang, slugToPathname } from './slugs'; -import type { AutoSidebarGroup, SidebarItem, SidebarLinkItem } from './user-config'; import { ensureLeadingAndTrailingSlashes, ensureTrailingSlash } from './path'; import type { Badge } from '../schemas/badge'; +import type { + AutoSidebarGroup, + LinkHTMLAttributes, + SidebarItem, + SidebarLinkItem, +} from '../schemas/sidebar'; const DirKey = Symbol('DirKey'); @@ -17,6 +22,7 @@ export interface Link { href: string; isCurrent: boolean; badge: Badge | undefined; + attrs: LinkHTMLAttributes; } interface Group { @@ -114,14 +120,20 @@ function linkFromConfig( if (locale) href = '/' + locale + href; } const label = pickLang(item.translations, localeToLang(locale)) || item.label; - return makeLink(href, label, currentPathname, item.badge); + return makeLink(href, label, currentPathname, item.badge, item.attrs); } /** Create a link entry. */ -function makeLink(href: string, label: string, currentPathname: string, badge?: Badge): Link { +function makeLink( + href: string, + label: string, + currentPathname: string, + badge?: Badge, + attrs?: LinkHTMLAttributes +): Link { if (!isAbsolute(href)) href = pathWithBase(href); const isCurrent = href === ensureTrailingSlash(currentPathname); - return { type: 'link', label, href, isCurrent, badge }; + return { type: 'link', label, href, isCurrent, badge, attrs: attrs ?? {} }; } /** Get the segments leading to a page. */ @@ -171,7 +183,8 @@ function linkFromRoute(route: Route, currentPathname: string): Link { slugToPathname(route.slug), route.entry.data.sidebar.label || route.entry.data.title, currentPathname, - route.entry.data.sidebar.badge + route.entry.data.sidebar.badge, + route.entry.data.sidebar.attrs ); } @@ -308,6 +321,8 @@ function applyPrevNextLinkConfig( ...link, label: config.label ?? link.label, href: config.link ?? link.href, + // Explicitly remove sidebar link attributes for prev/next links. + attrs: {}, }; } else if (config.link && config.label) { // If there is no link and the frontmatter contains both a URL and a label, diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts index ccaf0745..4cb01293 100644 --- a/packages/starlight/utils/user-config.ts +++ b/packages/starlight/utils/user-config.ts @@ -4,7 +4,7 @@ import { HeadConfigSchema } from '../schemas/head'; import { LogoConfigSchema } from '../schemas/logo'; import { TableOfContentsSchema } from '../schemas/tableOfContents'; import { FaviconSchema } from '../schemas/favicon'; -import { BadgeConfigSchema } from '../schemas/badge'; +import { SidebarItemSchema } from '../schemas/sidebar'; const LocaleSchema = z.object({ /** The label for this language to show in UI, e.g. `"English"`, `"العربية"`, or `"ç®€ä½“ä¸æ–‡"`. */ @@ -28,79 +28,6 @@ const LocaleSchema = z.object({ ), }); -const SidebarBaseSchema = z.object({ - /** The visible label for this item in the sidebar. */ - label: z.string(), - /** Translations of the `label` for each supported language. */ - translations: z.record(z.string()).default({}), -}); - -const SidebarGroupSchema = SidebarBaseSchema.extend({ - /** Whether this item should be collapsed by default. */ - collapsed: z.boolean().default(false), -}); - -const SidebarLinkItemSchema = SidebarBaseSchema.extend({ - /** The link to this item’s content. Can be a relative link to local files or the full URL of an external page. */ - link: z.string(), - /** Adds a badge to the link item */ - badge: BadgeConfigSchema(), -}); -export type SidebarLinkItem = z.infer<typeof SidebarLinkItemSchema>; - -const AutoSidebarGroupSchema = SidebarGroupSchema.extend({ - /** Enable autogenerating a sidebar category from a specific docs directory. */ - autogenerate: z.object({ - /** The directory to generate sidebar items for. */ - directory: z.string(), - /** - * Whether the autogenerated subgroups should be collapsed by default. - * Defaults to the `AutoSidebarGroup` `collapsed` value. - */ - collapsed: z.boolean().optional(), - // 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<typeof AutoSidebarGroupSchema>; - -type ManualSidebarGroupInput = z.input<typeof SidebarGroupSchema> & { - /** Array of links and subcategories to display in this category. */ - items: Array< - | z.input<typeof SidebarLinkItemSchema> - | z.input<typeof AutoSidebarGroupSchema> - | ManualSidebarGroupInput - >; -}; - -type ManualSidebarGroupOutput = z.output<typeof SidebarGroupSchema> & { - /** Array of links and subcategories to display in this category. */ - items: Array< - | z.output<typeof SidebarLinkItemSchema> - | z.output<typeof AutoSidebarGroupSchema> - | ManualSidebarGroupOutput - >; -}; - -const ManualSidebarGroupSchema: z.ZodType< - ManualSidebarGroupOutput, - z.ZodTypeDef, - ManualSidebarGroupInput -> = SidebarGroupSchema.extend({ - /** Array of links and subcategories to display in this category. */ - items: z.lazy(() => - z.union([SidebarLinkItemSchema, ManualSidebarGroupSchema, AutoSidebarGroupSchema]).array() - ), -}); - -const SidebarItemSchema = z.union([ - SidebarLinkItemSchema, - ManualSidebarGroupSchema, - AutoSidebarGroupSchema, -]); -export type SidebarItem = z.infer<typeof SidebarItemSchema>; - const UserConfigSchema = z.object({ /** Title for your website. Will be used in metadata and as browser tab title. */ title: z diff --git a/packages/starlight/vitest.config.ts b/packages/starlight/vitest.config.ts index b80690dd..ab9b2f4f 100644 --- a/packages/starlight/vitest.config.ts +++ b/packages/starlight/vitest.config.ts @@ -22,10 +22,10 @@ export default defineConfig({ reportsDirectory: './__coverage__', exclude: [...defaultCoverageExcludes, '**/vitest.*', 'components.ts', 'types.ts'], thresholdAutoUpdate: true, - lines: 66.57, - functions: 88.46, - branches: 90.14, - statements: 66.57, + lines: 69.21, + functions: 90.24, + branches: 90.62, + statements: 69.21, }, }, }); |