@astrojs/starlight 0.34.8 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.35.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#2261](https://github.com/withastro/starlight/pull/2261) [`778b743`](https://github.com/withastro/starlight/commit/778b743cdb832551ed576c745728358d8bbf9d7a) Thanks [@shubham-padia](https://github.com/shubham-padia)! - Adds support for using any of Starlight’s built-in icons in asides.
8
+
9
+ - [#3272](https://github.com/withastro/starlight/pull/3272) [`e7fe267`](https://github.com/withastro/starlight/commit/e7fe26720b7527c39e3aab42ea9fd0468f92167c) Thanks [@delucis](https://github.com/delucis)! - Adds a new `generateId` option to Starlight’s `docsLoader()`
10
+
11
+ This enables overriding the default sluggifier used to convert content filenames to URLs.
12
+
13
+ - [#3276](https://github.com/withastro/starlight/pull/3276) [`3917b20`](https://github.com/withastro/starlight/commit/3917b206da26522f73bbe0c1120de9acae5972c5) Thanks [@delucis](https://github.com/delucis)! - Excludes banner content from search results
14
+
15
+ Previously, content set in [`banner`](https://starlight.astro.build/reference/frontmatter/#banner) in page frontmatter was indexed by Starlight’s default search provider Pagefind. This could cause unexpected search results, especially for sites setting a common banner content on multiple pages. Starlight’s default `Banner` component is now excluded from search indexing.
16
+
17
+ This change does not impact `Banner` overrides using custom components.
18
+
19
+ - [#3266](https://github.com/withastro/starlight/pull/3266) [`1161af0`](https://github.com/withastro/starlight/commit/1161af0c2fe26485da6123f8fd7205c53b0e45e5) Thanks [@HiDeoo](https://github.com/HiDeoo)! - 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.
20
+
21
+ - [#3274](https://github.com/withastro/starlight/pull/3274) [`80ccff7`](https://github.com/withastro/starlight/commit/80ccff7c542794e04a4d2abb17227a076fa57c5d) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes an issue where some Starlight remark and rehype plugins were transforming Markdown and MDX content in non-Starlight pages.
22
+
23
+ ⚠️ **BREAKING CHANGE:**
24
+
25
+ Previously, some of Starlight’s remark and rehype plugins, most notably the plugin transforming Starlight's custom Markdown syntax for [rendering asides](https://starlight.astro.build/guides/authoring-content/#asides), were applied to all Markdown and MDX content. This included content from [individual Markdown pages](https://docs.astro.build/en/guides/markdown-content/#individual-markdown-pages) and content from [content collections](https://docs.astro.build/en/guides/content-collections/) other than the `docs` collection used by Starlight.
26
+
27
+ This change restricts the application of Starlight’s remark and rehype plugins to only Markdown and MDX content loaded using Starlight's [`docsLoader()`](https://starlight.astro.build/reference/configuration/#docsloader). If you were relying on this behavior, please let us know about your use case in the dedicated `#starlight` channel in the [Astro Discord](https://astro.build/chat/) or by [opening an issue](https://github.com/withastro/starlight/issues/new?template=---01-bug-report.yml).
28
+
29
+ ### Patch Changes
30
+
31
+ - [#3266](https://github.com/withastro/starlight/pull/3266) [`1161af0`](https://github.com/withastro/starlight/commit/1161af0c2fe26485da6123f8fd7205c53b0e45e5) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Ensures invalid sidebar group configurations using the `attrs` option are properly reported as a type error.
32
+
33
+ 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.
34
+
35
+ - [#3274](https://github.com/withastro/starlight/pull/3274) [`80ccff7`](https://github.com/withastro/starlight/commit/80ccff7c542794e04a4d2abb17227a076fa57c5d) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Prevents Starlight remark and rehype plugins from transforming Markdown and MDX content when using the Astro [`renderMarkdown()`](https://docs.astro.build/en/reference/content-loader-reference/#rendermarkdown) content loader API.
36
+
3
37
  ## 0.34.8
4
38
 
5
39
  ### Patch Changes
@@ -2,7 +2,7 @@
2
2
  const { banner } = Astro.locals.starlightRoute.entry.data;
3
3
  ---
4
4
 
5
- {banner && <div class="sl-banner" set:html={banner.content} />}
5
+ {banner && <div class="sl-banner" data-pagefind-ignore set:html={banner.content} />}
6
6
 
7
7
  <style>
8
8
  @layer starlight.core {
package/index.ts CHANGED
@@ -129,7 +129,7 @@ export default function StarlightIntegration(
129
129
  }),
130
130
  ],
131
131
  rehypePlugins: [
132
- rehypeRtlCodeSupport(),
132
+ rehypeRtlCodeSupport({ astroConfig: config }),
133
133
  // Process headings and add anchor links.
134
134
  ...starlightAutolinkHeadings({
135
135
  starlightConfig,
@@ -1,7 +1,7 @@
1
1
  /// <reference types="mdast-util-directive" />
2
2
 
3
3
  import type { AstroConfig, AstroIntegration, AstroUserConfig } from 'astro';
4
- import { h as _h, s as _s, type Properties } from 'hastscript';
4
+ import { h as _h, s as _s, type Properties, type Result } from 'hastscript';
5
5
  import type { Node, Paragraph as P, Parent, PhrasingContent, Root } from 'mdast';
6
6
  import {
7
7
  type Directives,
@@ -14,7 +14,12 @@ import { toString } from 'mdast-util-to-string';
14
14
  import remarkDirective from 'remark-directive';
15
15
  import type { Plugin, Transformer } from 'unified';
16
16
  import { visit } from 'unist-util-visit';
17
- import type { HookParameters, StarlightConfig } from '../types';
17
+ import type { HookParameters, StarlightConfig, StarlightIcon } from '../types';
18
+ import { getRemarkRehypeDocsCollectionPath, shouldTransformFile } from './remark-rehype-utils';
19
+ import { Icons } from '../components/Icons';
20
+ import { fromHtml } from 'hast-util-from-html';
21
+ import type { Element } from 'hast';
22
+ import { AstroError } from 'astro/errors';
18
23
 
19
24
  interface AsidesOptions {
20
25
  starlightConfig: Pick<StarlightConfig, 'defaultLocale' | 'locales'>;
@@ -87,6 +92,20 @@ function transformUnhandledDirective(
87
92
  }
88
93
  }
89
94
 
95
+ /** Hacky function that generates the children of an mdast SVG tree. */
96
+ function makeSvgChildNodes(children: Result['children']): any[] {
97
+ const nodes: P[] = [];
98
+ for (const child of children) {
99
+ if (child.type !== 'element') continue;
100
+ nodes.push({
101
+ type: 'paragraph',
102
+ data: { hName: child.tagName, hProperties: child.properties },
103
+ children: makeSvgChildNodes(child.children),
104
+ });
105
+ }
106
+ return nodes;
107
+ }
108
+
90
109
  /**
91
110
  * remark plugin that converts blocks delimited with `:::` into styled
92
111
  * asides (a.k.a. “callouts”, “admonitions”, etc.). Depends on the
@@ -148,7 +167,11 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
148
167
  ],
149
168
  };
150
169
 
170
+ const docsCollectionPath = getRemarkRehypeDocsCollectionPath(options.astroConfig.srcDir);
171
+
151
172
  const transformer: Transformer<Root> = (tree, file) => {
173
+ if (!shouldTransformFile(file, docsCollectionPath)) return;
174
+
152
175
  const lang = options.absolutePathToLang(file.path);
153
176
  const t = options.useTranslations(lang);
154
177
  visit(tree, (node, index, parent) => {
@@ -159,6 +182,7 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
159
182
  return;
160
183
  }
161
184
  const variant = node.name;
185
+ const attributes = node.attributes;
162
186
  if (!isAsideVariant(variant)) return;
163
187
 
164
188
  // remark-directive converts a container’s “label” to a paragraph added as the head of its
@@ -180,6 +204,19 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
180
204
  node.children.splice(0, 1);
181
205
  }
182
206
 
207
+ let iconPath = iconPaths[variant];
208
+
209
+ if (attributes?.['icon']) {
210
+ const iconName = attributes['icon'] as StarlightIcon;
211
+ const icon = Icons[iconName];
212
+ if (!icon) throwInvalidAsideIconError(iconName);
213
+ // Omit the root node and return only the first child which is the SVG element.
214
+ const iconHastTree = fromHtml(`<svg>${icon}</svg>`, { fragment: true, space: 'svg' })
215
+ .children[0] as Element;
216
+ // Render all SVG child nodes.
217
+ iconPath = makeSvgChildNodes(iconHastTree.children);
218
+ }
219
+
183
220
  const aside = h(
184
221
  'aside',
185
222
  {
@@ -197,7 +234,7 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
197
234
  fill: 'currentColor',
198
235
  class: 'starlight-aside__icon',
199
236
  },
200
- iconPaths[variant]
237
+ iconPath
201
238
  ),
202
239
  ...titleNode,
203
240
  ]),
@@ -216,6 +253,14 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
216
253
 
217
254
  type RemarkPlugins = NonNullable<NonNullable<AstroUserConfig['markdown']>['remarkPlugins']>;
218
255
 
256
+ export function throwInvalidAsideIconError(icon: string) {
257
+ throw new AstroError(
258
+ 'Invalid aside icon',
259
+ `An aside custom icon must be set to the name of one of Starlight\’s built-in icons, but received \`${icon}\`.\n\n` +
260
+ 'See https://starlight.astro.build/reference/icons/#all-icons for a list of available icons.'
261
+ );
262
+ }
263
+
219
264
  export function starlightAsides(options: AsidesOptions): RemarkPlugins {
220
265
  return [remarkDirective, remarkAsides(options)];
221
266
  }
@@ -1,5 +1,12 @@
1
+ import type { AstroConfig } from 'astro';
1
2
  import type { Root } from 'hast';
2
3
  import { CONTINUE, SKIP, visit } from 'unist-util-visit';
4
+ import type { VFile } from 'vfile';
5
+ import { getRemarkRehypeDocsCollectionPath, shouldTransformFile } from './remark-rehype-utils';
6
+
7
+ interface RtlCodeSupportOptions {
8
+ astroConfig: Pick<AstroConfig, 'srcDir'>;
9
+ }
3
10
 
4
11
  /**
5
12
  * rehype plugin that adds `dir` attributes to `<code>` and `<pre>`
@@ -15,8 +22,12 @@ import { CONTINUE, SKIP, visit } from 'unist-util-visit';
15
22
  * - `<code>` is often LTR, but could also be RTL. `dir="auto"` ensures the bidirectional
16
23
  * algorithm treats the contents of `<code>` in isolation and gives its best guess.
17
24
  */
18
- export function rehypeRtlCodeSupport() {
19
- return () => (root: Root) => {
25
+ export function rehypeRtlCodeSupport({ astroConfig }: RtlCodeSupportOptions) {
26
+ const docsCollectionPath = getRemarkRehypeDocsCollectionPath(astroConfig.srcDir);
27
+
28
+ return () => (root: Root, file: VFile) => {
29
+ if (!shouldTransformFile(file, docsCollectionPath)) return;
30
+
20
31
  visit(root, 'element', (el) => {
21
32
  if (el.tagName === 'pre' || el.tagName === 'code') {
22
33
  el.properties ||= {};
@@ -6,7 +6,7 @@ import { h } from 'hastscript';
6
6
  import type { Transformer } from 'unified';
7
7
  import { SKIP, visit } from 'unist-util-visit';
8
8
  import type { HookParameters, StarlightConfig } from '../types';
9
- import { resolveCollectionPath } from '../utils/collection';
9
+ import { getRemarkRehypeDocsCollectionPath, shouldTransformFile } from './remark-rehype-utils';
10
10
 
11
11
  const AnchorLinkIcon = h(
12
12
  'span',
@@ -30,8 +30,7 @@ export default function rehypeAutolinkHeadings(
30
30
  absolutePathToLang: AutolinkHeadingsOptions['absolutePathToLang']
31
31
  ) {
32
32
  const transformer: Transformer<Root> = (tree, file) => {
33
- // If the document is not part of the Starlight docs collection, skip it.
34
- if (!normalizePath(file.path).startsWith(docsCollectionPath)) return;
33
+ if (!shouldTransformFile(file, docsCollectionPath)) return;
35
34
 
36
35
  const pageLang = absolutePathToLang(file.path);
37
36
  const t = useTranslationsForLang(pageLang);
@@ -93,23 +92,13 @@ export const starlightAutolinkHeadings = ({
93
92
  { experimentalHeadingIdCompat: astroConfig.experimental?.headingIdCompat },
94
93
  ],
95
94
  rehypeAutolinkHeadings(
96
- normalizePath(resolveCollectionPath('docs', astroConfig.srcDir)),
95
+ getRemarkRehypeDocsCollectionPath(astroConfig.srcDir),
97
96
  useTranslations,
98
97
  absolutePathToLang
99
98
  ),
100
99
  ]
101
100
  : [];
102
101
 
103
- /**
104
- * File path separators seems to be inconsistent on Windows when the rehype plugin is used on
105
- * Markdown vs MDX files.
106
- * For the time being, we normalize the path to unix style path.
107
- */
108
- const backSlashRegex = /\\/g;
109
- function normalizePath(path: string) {
110
- return path.replace(backSlashRegex, '/');
111
- }
112
-
113
102
  // This utility is inlined from https://github.com/syntax-tree/hast-util-heading-rank
114
103
  // Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
115
104
  // MIT License: https://github.com/syntax-tree/hast-util-heading-rank/blob/main/license
@@ -0,0 +1,38 @@
1
+ import type { AstroConfig } from 'astro';
2
+ import type { VFile } from 'vfile';
3
+ import { resolveCollectionPath } from '../utils/collection';
4
+
5
+ /**
6
+ * Returns the path to the Starlight docs collection ready to be used in remark/rehype plugins,
7
+ * e.g. with the `shouldTransformFile()` utility to determine if a file should be transformed
8
+ * by a plugin or not.
9
+ */
10
+ export function getRemarkRehypeDocsCollectionPath(srcDir: AstroConfig['srcDir']) {
11
+ return normalizePath(resolveCollectionPath('docs', srcDir));
12
+ }
13
+
14
+ /**
15
+ * Determines if a file should be transformed by a remark/rehype plugin, e.g. files without a known
16
+ * path or files that are not part of the Starlight docs collection should be skipped.
17
+ */
18
+ export function shouldTransformFile(file: VFile, docsCollectionPath: string) {
19
+ // If the content is rendered using the content loader `renderMarkdown()` API, a file path
20
+ // is not provided.
21
+ // In that case, we skip the file.
22
+ if (!file?.path) return false;
23
+
24
+ // If the document is not part of the Starlight docs collection, skip it.
25
+ if (!normalizePath(file.path).startsWith(docsCollectionPath)) return false;
26
+
27
+ return true;
28
+ }
29
+
30
+ /**
31
+ * File path separators seems to be inconsistent on Windows between remark/rehype plugins used on
32
+ * Markdown vs MDX files.
33
+ * For the time being, we normalize all paths to unix style paths.
34
+ */
35
+ const backSlashRegex = /\\/g;
36
+ function normalizePath(path: string) {
37
+ return path.replace(backSlashRegex, '/');
38
+ }
package/loaders.ts CHANGED
@@ -6,13 +6,30 @@ import { getCollectionPathFromRoot, type StarlightCollection } from './utils/col
6
6
  const docsExtensions = ['markdown', 'mdown', 'mkdn', 'mkd', 'mdwn', 'md', 'mdx'];
7
7
  const i18nExtensions = ['json', 'yml', 'yaml'];
8
8
 
9
- export function docsLoader(): Loader {
9
+ type GlobOptions = Parameters<typeof glob>[0];
10
+ type GenerateIdFunction = NonNullable<GlobOptions['generateId']>;
11
+
12
+ /**
13
+ * Loads content files from the `src/content/docs/` directory, ignoring filenames starting with `_`.
14
+ */
15
+ export function docsLoader({
16
+ generateId,
17
+ }: {
18
+ /**
19
+ * Function that generates an ID for an entry. Default implementation generates a slug from the entry path.
20
+ * @returns The ID of the entry. Must be unique per collection.
21
+ **/
22
+ generateId?: GenerateIdFunction;
23
+ } = {}): Loader {
10
24
  return {
11
25
  name: 'starlight-docs-loader',
12
- load: createGlobLoadFn('docs'),
26
+ load: createGlobLoadFn('docs', generateId),
13
27
  };
14
28
  }
15
29
 
30
+ /**
31
+ * Loads data files from the `src/content/i18n/` directory, ignoring filenames starting with `_`.
32
+ */
16
33
  export function i18nLoader(): Loader {
17
34
  return {
18
35
  name: 'starlight-i18n-loader',
@@ -20,7 +37,10 @@ export function i18nLoader(): Loader {
20
37
  };
21
38
  }
22
39
 
23
- function createGlobLoadFn(collection: StarlightCollection): Loader['load'] {
40
+ function createGlobLoadFn(
41
+ collection: StarlightCollection,
42
+ generateId?: GenerateIdFunction
43
+ ): Loader['load'] {
24
44
  return (context: LoaderContext) => {
25
45
  const extensions = collection === 'docs' ? docsExtensions : i18nExtensions;
26
46
 
@@ -32,9 +52,12 @@ function createGlobLoadFn(collection: StarlightCollection): Loader['load'] {
32
52
  extensions.push('mdoc');
33
53
  }
34
54
 
35
- return glob({
55
+ const options: GlobOptions = {
36
56
  base: getCollectionPathFromRoot(collection, context.config),
37
57
  pattern: `**/[^_]*.{${extensions.join(',')}}`,
38
- }).load(context);
58
+ };
59
+ if (generateId) options.generateId = generateId;
60
+
61
+ return glob(options).load(context);
39
62
  };
40
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.34.8",
3
+ "version": "0.35.0",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
@@ -14,6 +14,14 @@ const SidebarBaseSchema = z.object({
14
14
  });
15
15
 
16
16
  const SidebarGroupSchema = SidebarBaseSchema.extend({
17
+ /**
18
+ * Explicitly prevent custom attributes on groups as the final type for supported sidebar item
19
+ * is a non-discriminated union where TypeScript will not perform excess property checks.
20
+ * This means that a user could define a sidebar group with custom attributes, not getting a
21
+ * TypeScript error, and only have it fail at runtime.
22
+ * @see https://github.com/microsoft/TypeScript/issues/20863
23
+ */
24
+ attrs: z.never().optional(),
17
25
  /** Whether this item should be collapsed by default. */
18
26
  collapsed: z.boolean().default(false),
19
27
  });
@@ -22,7 +30,7 @@ const SidebarGroupSchema = SidebarBaseSchema.extend({
22
30
  // `Record<string, string | number | boolean | undefined>` but typed as `HTMLAttributes<'a'>`
23
31
  // for user convenience.
24
32
  const linkHTMLAttributesSchema = z.record(
25
- z.union([z.string(), z.number(), z.boolean(), z.undefined()])
33
+ z.union([z.string(), z.number(), z.boolean(), z.undefined(), z.null()])
26
34
  ) as z.Schema<Omit<HTMLAttributes<'a'>, keyof AstroBuiltinAttributes | 'children'>>;
27
35
  export type LinkHTMLAttributes = z.infer<typeof linkHTMLAttributesSchema>;
28
36
 
@@ -46,6 +54,8 @@ const AutoSidebarGroupSchema = SidebarGroupSchema.extend({
46
54
  * Defaults to the `AutoSidebarGroup` `collapsed` value.
47
55
  */
48
56
  collapsed: z.boolean().optional(),
57
+ /** HTML attributes to add to the autogenerated link items. */
58
+ attrs: SidebarLinkItemHTMLAttributesSchema(),
49
59
  // TODO: not supported by Docusaurus but would be good to have
50
60
  /** How many directories deep to include from this directory in the sidebar. Default: `Infinity`. */
51
61
  // depth: z.number().optional(),
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  import { AstroError } from 'astro/errors';
3
3
  import Icon from './Icon.astro';
4
+ import { Icons, type StarlightIcon } from '../components/Icons';
5
+ import { throwInvalidAsideIconError } from '../integrations/asides';
4
6
 
5
7
  const asideVariants = ['note', 'tip', 'caution', 'danger'] as const;
6
8
  const icons = { note: 'information', tip: 'rocket', caution: 'warning', danger: 'error' } as const;
@@ -8,9 +10,10 @@ const icons = { note: 'information', tip: 'rocket', caution: 'warning', danger:
8
10
  interface Props {
9
11
  type?: (typeof asideVariants)[number];
10
12
  title?: string;
13
+ icon?: StarlightIcon;
11
14
  }
12
15
 
13
- let { type = 'note', title } = Astro.props;
16
+ let { type = 'note', title, icon } = Astro.props;
14
17
 
15
18
  if (!asideVariants.includes(type)) {
16
19
  throw new AstroError(
@@ -20,6 +23,8 @@ if (!asideVariants.includes(type)) {
20
23
  );
21
24
  }
22
25
 
26
+ if (icon && !Icons[icon]) throwInvalidAsideIconError(icon);
27
+
23
28
  if (!title) {
24
29
  title = Astro.locals.t(`aside.${type}`);
25
30
  }
@@ -27,7 +32,7 @@ if (!title) {
27
32
 
28
33
  <aside aria-label={title} class={`starlight-aside starlight-aside--${type}`}>
29
34
  <p class="starlight-aside__title" aria-hidden="true">
30
- <Icon name={icons[type]} class="starlight-aside__icon" />{title}
35
+ <Icon name={icon || icons[type]} class="starlight-aside__icon" />{title}
31
36
  </p>
32
37
  <div class="starlight-aside__content">
33
38
  <slot />
@@ -96,7 +96,7 @@ function groupFromAutogenerateConfig(
96
96
  routes: Route[],
97
97
  currentPathname: string
98
98
  ): SidebarGroup {
99
- const { collapsed: subgroupCollapsed, directory } = item.autogenerate;
99
+ const { attrs, collapsed: subgroupCollapsed, directory } = item.autogenerate;
100
100
  const localeDir = locale ? locale + '/' + directory : directory;
101
101
  const dirDocs = routes.filter((doc) => {
102
102
  const filePathFromContentDir = getRoutePathRelativeToCollectionRoot(doc, locale);
@@ -112,7 +112,13 @@ function groupFromAutogenerateConfig(
112
112
  return {
113
113
  type: 'group',
114
114
  label,
115
- entries: sidebarFromDir(tree, currentPathname, locale, subgroupCollapsed ?? item.collapsed),
115
+ entries: sidebarFromDir(
116
+ tree,
117
+ currentPathname,
118
+ locale,
119
+ subgroupCollapsed ?? item.collapsed,
120
+ attrs
121
+ ),
116
122
  collapsed: item.collapsed,
117
123
  badge: getSidebarBadge(item.badge, locale, label),
118
124
  };
@@ -268,12 +274,12 @@ function treeify(routes: Route[], locale: string | undefined, baseDir: string):
268
274
  }
269
275
 
270
276
  /** Create a link entry for a given content collection entry. */
271
- function linkFromRoute(route: Route): SidebarLink {
277
+ function linkFromRoute(route: Route, attrs?: LinkHTMLAttributes): SidebarLink {
272
278
  return makeSidebarLink(
273
279
  slugToPathname(route.slug),
274
280
  route.entry.data.sidebar.label || route.entry.data.title,
275
281
  route.entry.data.sidebar.badge,
276
- route.entry.data.sidebar.attrs
282
+ { ...attrs, ...route.entry.data.sidebar.attrs }
277
283
  );
278
284
  }
279
285
 
@@ -307,10 +313,11 @@ function groupFromDir(
307
313
  dirName: string,
308
314
  currentPathname: string,
309
315
  locale: string | undefined,
310
- collapsed: boolean
316
+ collapsed: boolean,
317
+ attrs?: LinkHTMLAttributes
311
318
  ): SidebarGroup {
312
319
  const entries = sortDirEntries(Object.entries(dir)).map(([key, dirOrRoute]) =>
313
- dirToItem(dirOrRoute, `${fullPath}/${key}`, key, currentPathname, locale, collapsed)
320
+ dirToItem(dirOrRoute, `${fullPath}/${key}`, key, currentPathname, locale, collapsed, attrs)
314
321
  );
315
322
  return {
316
323
  type: 'group',
@@ -328,11 +335,12 @@ function dirToItem(
328
335
  dirName: string,
329
336
  currentPathname: string,
330
337
  locale: string | undefined,
331
- collapsed: boolean
338
+ collapsed: boolean,
339
+ attrs?: LinkHTMLAttributes
332
340
  ): SidebarEntry {
333
341
  return isDir(dirOrRoute)
334
- ? groupFromDir(dirOrRoute, fullPath, dirName, currentPathname, locale, collapsed)
335
- : linkFromRoute(dirOrRoute);
342
+ ? groupFromDir(dirOrRoute, fullPath, dirName, currentPathname, locale, collapsed, attrs)
343
+ : linkFromRoute(dirOrRoute, attrs);
336
344
  }
337
345
 
338
346
  /** Create a sidebar entry for a given content directory. */
@@ -340,10 +348,11 @@ function sidebarFromDir(
340
348
  tree: Dir,
341
349
  currentPathname: string,
342
350
  locale: string | undefined,
343
- collapsed: boolean
351
+ collapsed: boolean,
352
+ attrs?: LinkHTMLAttributes
344
353
  ) {
345
354
  return sortDirEntries(Object.entries(tree)).map(([key, dirOrRoute]) =>
346
- dirToItem(dirOrRoute, key, key, currentPathname, locale, collapsed)
355
+ dirToItem(dirOrRoute, key, key, currentPathname, locale, collapsed, attrs)
347
356
  );
348
357
  }
349
358