summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiDeoo2025-07-16 15:52:11 +0200
committerGitHub2025-07-16 15:52:11 +0200
commit1161af0c2fe26485da6123f8fd7205c53b0e45e5 (patch)
tree9be931235e28027fb9b369cc7542f5aabe782c3a
parent778b743cdb832551ed576c745728358d8bbf9d7a (diff)
downloadIT.starlight-1161af0c2fe26485da6123f8fd7205c53b0e45e5.tar.gz
IT.starlight-1161af0c2fe26485da6123f8fd7205c53b0e45e5.tar.bz2
IT.starlight-1161af0c2fe26485da6123f8fd7205c53b0e45e5.zip
Autogenerated link custom attributes (#3266)
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> Co-authored-by: Kevin Zuniga Cuellar <kzunigac@uvm.edu> Co-authored-by: Felix Schneider <99918022+trueberryless@users.noreply.github.com>
-rw-r--r--.changeset/loud-guests-pay.md7
-rw-r--r--.changeset/twenty-donuts-greet.md5
-rw-r--r--docs/src/content/docs/guides/sidebar.mdx68
-rw-r--r--docs/src/content/docs/reference/configuration.mdx6
-rw-r--r--docs/src/content/docs/reference/frontmatter.md1
-rw-r--r--packages/starlight/__tests__/basics/config.test-d.ts51
-rw-r--r--packages/starlight/__tests__/sidebar/navigation-attributes.test.ts47
-rw-r--r--packages/starlight/__tests__/sidebar/navigation-badges.test.ts5
-rw-r--r--packages/starlight/__tests__/sidebar/navigation-hidden.test.ts5
-rw-r--r--packages/starlight/__tests__/sidebar/navigation-order.test.ts5
-rw-r--r--packages/starlight/__tests__/sidebar/navigation-unicode.test.ts5
-rw-r--r--packages/starlight/__tests__/sidebar/navigation.test.ts5
-rw-r--r--packages/starlight/__tests__/sidebar/vitest.config.ts2
-rw-r--r--packages/starlight/schemas/sidebar.ts12
-rw-r--r--packages/starlight/utils/navigation.ts31
15 files changed, 234 insertions, 21 deletions
diff --git a/.changeset/loud-guests-pay.md b/.changeset/loud-guests-pay.md
new file mode 100644
index 00000000..7f802016
--- /dev/null
+++ b/.changeset/loud-guests-pay.md
@@ -0,0 +1,7 @@
+---
+'@astrojs/starlight': patch
+---
+
+Ensures invalid sidebar group configurations using the `attrs` option are properly reported as a type error.
+
+Previously, invalid sidebar group configurations using the `attrs` option were not reported as a type error but only surfaced at runtime. This change is only a type-level change and does not affect the runtime behavior of Starlight which does not support the `attrs` option for sidebar groups.
diff --git a/.changeset/twenty-donuts-greet.md b/.changeset/twenty-donuts-greet.md
new file mode 100644
index 00000000..1a816d82
--- /dev/null
+++ b/.changeset/twenty-donuts-greet.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/starlight': minor
+---
+
+Adds support for custom HTML attributes on autogenerated sidebar links using the [`autogenerate.attrs`](https://starlight.astro.build/guides/sidebar/#custom-html-attributes-for-autogenerated-links) option.
diff --git a/docs/src/content/docs/guides/sidebar.mdx b/docs/src/content/docs/guides/sidebar.mdx
index 1e0f55f5..270eabaf 100644
--- a/docs/src/content/docs/guides/sidebar.mdx
+++ b/docs/src/content/docs/guides/sidebar.mdx
@@ -244,7 +244,7 @@ The following sidebar will be generated:
Use the [`sidebar` frontmatter field](/reference/frontmatter/#sidebar) in individual pages to customize autogenerated links.
-Sidebar frontmatter options allow you to set a [custom label](/reference/frontmatter/#label) or add a [badge](/reference/frontmatter/#badge) to a link, [hide](/reference/frontmatter/#hidden) a link from the sidebar, or define a [custom sort weighting](/reference/frontmatter/#order).
+Sidebar frontmatter options allow you to set a [custom label](/reference/frontmatter/#label), use [custom attributes](/reference/frontmatter/#attrs), add a [badge](/reference/frontmatter/#badge) to a link, [hide](/reference/frontmatter/#hidden) a link from the sidebar, or define a [custom sort weighting](/reference/frontmatter/#order).
```md "sidebar:"
---
@@ -436,6 +436,72 @@ 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.
+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}
+starlight({
+ sidebar: [
+ {
+ label: 'Constellations',
+ autogenerate: {
+ // Autogenerate a group of links for the 'constellations' directory.
+ directory: 'constellations',
+ // Italicize all link labels in this group.
+ attrs: { style: 'font-style: italic' },
+ },
+ },
+ ],
+});
+```
+
+And the following file structure:
+
+<FileTree>
+
+- src/
+ - content/
+ - docs/
+ - constellations/
+ - carina.md
+ - centaurus.md
+ - seasonal/
+ - andromeda.md
+
+</FileTree>
+
+The following sidebar will be generated with all autogenerated links italicized:
+
+<SidebarPreview
+ config={[
+ {
+ label: 'Constellations',
+ items: [
+ { label: 'Carina', link: '', attrs: { style: 'font-style: italic' } },
+ {
+ label: 'Centaurus',
+ link: '',
+ attrs: { style: 'font-style: italic' },
+ },
+ {
+ label: 'seasonal',
+ items: [
+ {
+ label: 'Andromeda',
+ link: '',
+ attrs: { 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 by specifying a [BCP-47](https://www.w3.org/International/questions/qa-choosing-language-tags) language tag, e.g. `"en"`, `"ar"`, or `"zh-CN"`, as the key and the translated label as the value.
diff --git a/docs/src/content/docs/reference/configuration.mdx b/docs/src/content/docs/reference/configuration.mdx
index 7166da59..9d245531 100644
--- a/docs/src/content/docs/reference/configuration.mdx
+++ b/docs/src/content/docs/reference/configuration.mdx
@@ -224,7 +224,11 @@ type SidebarItem =
| {
// Autogenerated link group
label: string;
- autogenerate: { directory: string; collapsed?: boolean };
+ autogenerate: {
+ directory: string;
+ collapsed?: boolean;
+ attrs?: Record<string, string | number | boolean | undefined>;
+ };
collapsed?: boolean;
}
));
diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md
index 37c4513e..b8028a7d 100644
--- a/docs/src/content/docs/reference/frontmatter.md
+++ b/docs/src/content/docs/reference/frontmatter.md
@@ -386,6 +386,7 @@ sidebar:
**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.
+If [`autogenerate.attrs`](/guides/sidebar/#custom-html-attributes-for-autogenerated-links) is set on the autogenerated group this page belongs to, frontmatter attributes will be merged with the group attributes.
```md
---
diff --git a/packages/starlight/__tests__/basics/config.test-d.ts b/packages/starlight/__tests__/basics/config.test-d.ts
new file mode 100644
index 00000000..8e1e4ebf
--- /dev/null
+++ b/packages/starlight/__tests__/basics/config.test-d.ts
@@ -0,0 +1,51 @@
+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<StarlightUserConfig['sidebar']>[number];
+
+ // Links
+ expectTypeOf('getting-started').toExtend<SidebarUserItem>();
+ expectTypeOf({ slug: 'getting-started' }).toExtend<SidebarUserItem>();
+ expectTypeOf({
+ label: 'Getting Started',
+ link: '/getting-started/',
+ }).toExtend<SidebarUserItem>();
+
+ // Groups
+ expectTypeOf({
+ label: 'References',
+ items: [],
+ }).toExtend<SidebarUserItem>();
+ expectTypeOf({
+ label: 'References',
+ autogenerate: { directory: 'references' },
+ }).toExtend<SidebarUserItem>();
+
+ // Links with attributes
+ expectTypeOf({
+ slug: 'getting-started',
+ attrs: { class: 'test' },
+ }).toExtend<SidebarUserItem>();
+ expectTypeOf({
+ label: 'Getting Started',
+ link: '/getting-started/',
+ attrs: { class: 'test' },
+ }).toExtend<SidebarUserItem>();
+
+ // Groups with attributes which are not supported
+ expectTypeOf({
+ label: 'References',
+ items: [],
+ attrs: { class: 'test' },
+ // @ts-expect-error - Attributes are not supported on groups
+ }).toExtend<SidebarUserItem>();
+ expectTypeOf({
+ label: 'References',
+ autogenerate: { directory: 'references' },
+ attrs: { class: 'test' },
+ // @ts-expect-error - Attributes are not supported on groups
+ }).toExtend<SidebarUserItem>();
+ });
+});
diff --git a/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts b/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts
index 60d24a04..7424e9eb 100644
--- a/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts
+++ b/packages/starlight/__tests__/sidebar/navigation-attributes.test.ts
@@ -15,7 +15,18 @@ vi.mock('astro:content', async () =>
sidebar: { attrs: { class: 'advanced', ping: 'https://example.com' } },
},
],
+ // Links to pages in the `api/v1/` directory have custom attributes, even nested ones.
['api/v1/users.md', { title: 'Users API' }],
+ ['api/v1/products/add.md', { title: 'Add Product' }],
+ [
+ 'api/v1/products/remove.md',
+ // A page in the `api/v1/` directory can specify custom attributes to be merged with the
+ // default ones.
+ {
+ title: 'Remove Product',
+ sidebar: { attrs: { 'data-experimental': true } },
+ },
+ ],
['Deprecated API/users.md', { title: 'Deprecated Users API' }],
],
})
@@ -125,7 +136,41 @@ describe('getSidebar', () => {
"collapsed": false,
"entries": [
{
- "attrs": {},
+ "badge": undefined,
+ "collapsed": false,
+ "entries": [
+ {
+ "attrs": {
+ "class": "current",
+ "data-version": "1",
+ },
+ "badge": undefined,
+ "href": "/api/v1/products/add/",
+ "isCurrent": false,
+ "label": "Add Product",
+ "type": "link",
+ },
+ {
+ "attrs": {
+ "class": "current",
+ "data-experimental": true,
+ "data-version": "1",
+ },
+ "badge": undefined,
+ "href": "/api/v1/products/remove/",
+ "isCurrent": false,
+ "label": "Remove Product",
+ "type": "link",
+ },
+ ],
+ "label": "products",
+ "type": "group",
+ },
+ {
+ "attrs": {
+ "class": "current",
+ "data-version": "1",
+ },
"badge": undefined,
"href": "/api/v1/users/",
"isCurrent": false,
diff --git a/packages/starlight/__tests__/sidebar/navigation-badges.test.ts b/packages/starlight/__tests__/sidebar/navigation-badges.test.ts
index 1a4e9e25..5cc37fd6 100644
--- a/packages/starlight/__tests__/sidebar/navigation-badges.test.ts
+++ b/packages/starlight/__tests__/sidebar/navigation-badges.test.ts
@@ -143,7 +143,10 @@ describe('getSidebar', () => {
"collapsed": false,
"entries": [
{
- "attrs": {},
+ "attrs": {
+ "class": "current",
+ "data-version": "1",
+ },
"badge": undefined,
"href": "/api/v1/users/",
"isCurrent": false,
diff --git a/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts b/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts
index d2d92024..567bccdd 100644
--- a/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts
+++ b/packages/starlight/__tests__/sidebar/navigation-hidden.test.ts
@@ -118,7 +118,10 @@ describe('getSidebar', () => {
"collapsed": false,
"entries": [
{
- "attrs": {},
+ "attrs": {
+ "class": "current",
+ "data-version": "1",
+ },
"badge": undefined,
"href": "/api/v1/users/",
"isCurrent": false,
diff --git a/packages/starlight/__tests__/sidebar/navigation-order.test.ts b/packages/starlight/__tests__/sidebar/navigation-order.test.ts
index 31e64413..87b25a56 100644
--- a/packages/starlight/__tests__/sidebar/navigation-order.test.ts
+++ b/packages/starlight/__tests__/sidebar/navigation-order.test.ts
@@ -126,7 +126,10 @@ describe('getSidebar', () => {
"collapsed": false,
"entries": [
{
- "attrs": {},
+ "attrs": {
+ "class": "current",
+ "data-version": "1",
+ },
"badge": undefined,
"href": "/api/v1/users/",
"isCurrent": false,
diff --git a/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts b/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts
index c5bb2643..0f59f83f 100644
--- a/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts
+++ b/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts
@@ -126,7 +126,10 @@ describe('getSidebar', () => {
"collapsed": false,
"entries": [
{
- "attrs": {},
+ "attrs": {
+ "class": "current",
+ "data-version": "1",
+ },
"badge": undefined,
"href": "/api/v1/用户/",
"isCurrent": true,
diff --git a/packages/starlight/__tests__/sidebar/navigation.test.ts b/packages/starlight/__tests__/sidebar/navigation.test.ts
index a2291819..faa8855e 100644
--- a/packages/starlight/__tests__/sidebar/navigation.test.ts
+++ b/packages/starlight/__tests__/sidebar/navigation.test.ts
@@ -144,7 +144,10 @@ describe('getSidebar', () => {
"collapsed": false,
"entries": [
{
- "attrs": {},
+ "attrs": {
+ "class": "current",
+ "data-version": "1",
+ },
"badge": undefined,
"href": "/api/v1/users/",
"isCurrent": false,
diff --git a/packages/starlight/__tests__/sidebar/vitest.config.ts b/packages/starlight/__tests__/sidebar/vitest.config.ts
index 205238c9..0528a3b1 100644
--- a/packages/starlight/__tests__/sidebar/vitest.config.ts
+++ b/packages/starlight/__tests__/sidebar/vitest.config.ts
@@ -39,7 +39,7 @@ export default defineVitestConfig({
// A group linking to all pages in the `api/v1` directory.
{
label: 'API v1',
- autogenerate: { directory: '/api/v1/' },
+ autogenerate: { directory: '/api/v1/', attrs: { class: 'current', 'data-version': '1' } },
},
// A group linking to all pages in the `Deprecated API/` directory.
{
diff --git a/packages/starlight/schemas/sidebar.ts b/packages/starlight/schemas/sidebar.ts
index 2c4cc1d5..b76bc80e 100644
--- a/packages/starlight/schemas/sidebar.ts
+++ b/packages/starlight/schemas/sidebar.ts
@@ -14,6 +14,14 @@ const SidebarBaseSchema = z.object({
});
const SidebarGroupSchema = SidebarBaseSchema.extend({
+ /**
+ * Explicitly prevent custom attributes on groups 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 custom attributes, not getting a
+ * TypeScript error, and only have it fail at runtime.
+ * @see https://github.com/microsoft/TypeScript/issues/20863
+ */
+ attrs: z.never().optional(),
/** Whether this item should be collapsed by default. */
collapsed: z.boolean().default(false),
});
@@ -22,7 +30,7 @@ const SidebarGroupSchema = SidebarBaseSchema.extend({
// `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()])
+ z.union([z.string(), z.number(), z.boolean(), z.undefined(), z.null()])
) as z.Schema<Omit<HTMLAttributes<'a'>, keyof AstroBuiltinAttributes | 'children'>>;
export type LinkHTMLAttributes = z.infer<typeof linkHTMLAttributesSchema>;
@@ -46,6 +54,8 @@ const AutoSidebarGroupSchema = SidebarGroupSchema.extend({
* Defaults to the `AutoSidebarGroup` `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(),
diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts
index 2870d619..9e0e96dc 100644
--- a/packages/starlight/utils/navigation.ts
+++ b/packages/starlight/utils/navigation.ts
@@ -96,7 +96,7 @@ function groupFromAutogenerateConfig(
routes: Route[],
currentPathname: string
): SidebarGroup {
- const { collapsed: subgroupCollapsed, directory } = item.autogenerate;
+ const { attrs, collapsed: subgroupCollapsed, directory } = item.autogenerate;
const localeDir = locale ? locale + '/' + directory : directory;
const dirDocs = routes.filter((doc) => {
const filePathFromContentDir = getRoutePathRelativeToCollectionRoot(doc, locale);
@@ -112,7 +112,13 @@ function groupFromAutogenerateConfig(
return {
type: 'group',
label,
- entries: sidebarFromDir(tree, currentPathname, locale, subgroupCollapsed ?? item.collapsed),
+ entries: sidebarFromDir(
+ tree,
+ currentPathname,
+ locale,
+ subgroupCollapsed ?? item.collapsed,
+ attrs
+ ),
collapsed: item.collapsed,
badge: getSidebarBadge(item.badge, locale, label),
};
@@ -268,12 +274,12 @@ function treeify(routes: Route[], locale: string | undefined, baseDir: string):
}
/** Create a link entry for a given content collection entry. */
-function linkFromRoute(route: Route): SidebarLink {
+function linkFromRoute(route: Route, attrs?: LinkHTMLAttributes): SidebarLink {
return makeSidebarLink(
slugToPathname(route.slug),
route.entry.data.sidebar.label || route.entry.data.title,
route.entry.data.sidebar.badge,
- route.entry.data.sidebar.attrs
+ { ...attrs, ...route.entry.data.sidebar.attrs }
);
}
@@ -307,10 +313,11 @@ function groupFromDir(
dirName: string,
currentPathname: string,
locale: string | undefined,
- collapsed: boolean
+ collapsed: boolean,
+ attrs?: LinkHTMLAttributes
): SidebarGroup {
const entries = sortDirEntries(Object.entries(dir)).map(([key, dirOrRoute]) =>
- dirToItem(dirOrRoute, `${fullPath}/${key}`, key, currentPathname, locale, collapsed)
+ dirToItem(dirOrRoute, `${fullPath}/${key}`, key, currentPathname, locale, collapsed, attrs)
);
return {
type: 'group',
@@ -328,11 +335,12 @@ function dirToItem(
dirName: string,
currentPathname: string,
locale: string | undefined,
- collapsed: boolean
+ collapsed: boolean,
+ attrs?: LinkHTMLAttributes
): SidebarEntry {
return isDir(dirOrRoute)
- ? groupFromDir(dirOrRoute, fullPath, dirName, currentPathname, locale, collapsed)
- : linkFromRoute(dirOrRoute);
+ ? groupFromDir(dirOrRoute, fullPath, dirName, currentPathname, locale, collapsed, attrs)
+ : linkFromRoute(dirOrRoute, attrs);
}
/** Create a sidebar entry for a given content directory. */
@@ -340,10 +348,11 @@ function sidebarFromDir(
tree: Dir,
currentPathname: string,
locale: string | undefined,
- collapsed: boolean
+ collapsed: boolean,
+ attrs?: LinkHTMLAttributes
) {
return sortDirEntries(Object.entries(tree)).map(([key, dirOrRoute]) =>
- dirToItem(dirOrRoute, key, key, currentPathname, locale, collapsed)
+ dirToItem(dirOrRoute, key, key, currentPathname, locale, collapsed, attrs)
);
}