@astrojs/starlight 0.38.4 → 0.39.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,99 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.39.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#3618](https://github.com/withastro/starlight/pull/3618) [`dcf6d09`](https://github.com/withastro/starlight/commit/dcf6d094bbcfa1f83e45742901f4178df07c2156) Thanks [@HiDeoo](https://github.com/HiDeoo)! - **⚠️ BREAKING CHANGE:** This release changes how autogenerated links work in Starlight’s sidebar configuration.
8
+
9
+ If you have sidebar groups using the `autogenerate` key, you must now wrap that configuration in an `items` array:
10
+
11
+ ```diff
12
+ {
13
+ label: 'My group',
14
+ - autogenerate: { directory: 'some-dir' },
15
+ + items: [{ autogenerate: { directory: 'some-dir' } }],
16
+ }
17
+ ```
18
+
19
+ This change unlocks the possibility to mix autogenerated links and other links in a single group, for example:
20
+
21
+ ```js
22
+ {
23
+ label: 'Mixed group',
24
+ items: [
25
+ 'example-page',
26
+ { autogenerate: { directory: 'examples' } },
27
+ { label: 'More examples', link: 'https://example.com' },
28
+ ],
29
+ }
30
+ ```
31
+
32
+ This release also updates the shape of autogenerated sidebar entries in route data. Autogenerated links and groups in `Astro.locals.starlightRoute.sidebar` now include an `autogenerate` object with the [configured `directory` value](https://starlight.astro.build/guides/sidebar/#autogenerated-groups):
33
+
34
+ ```js
35
+ {
36
+ type: 'link',
37
+ label: 'Example',
38
+ href: '/examples/example/',
39
+ isCurrent: false,
40
+ autogenerate: { directory: 'examples' }
41
+ }
42
+ ```
43
+
44
+ - [#3618](https://github.com/withastro/starlight/pull/3618) [`dcf6d09`](https://github.com/withastro/starlight/commit/dcf6d094bbcfa1f83e45742901f4178df07c2156) Thanks [@HiDeoo](https://github.com/HiDeoo)! - **⚠️ BREAKING CHANGE:** This release changes the default collapsed state of autogenerated sidebar subgroups.
45
+
46
+ Autogenerated subgroups no longer inherit the `collapsed` value from their parent group. They are now expanded by default unless explicitly configured with [`autogenerate.collapsed`](https://starlight.astro.build/reference/configuration/#collapsing-groups).
47
+
48
+ If your sidebar configuration relies on a collapsed parent group to also collapse its autogenerated subgroups, update your configuration to set `autogenerate.collapsed` to `true`:
49
+
50
+ ```diff
51
+ {
52
+ label: 'Reference',
53
+ collapsed: true,
54
+ items: [
55
+ - { autogenerate: { directory: 'reference' } },
56
+ + { autogenerate: { directory: 'reference', collapsed: true } },
57
+ ],
58
+ }
59
+ ```
60
+
61
+ - [#3845](https://github.com/withastro/starlight/pull/3845) [`4d755f5`](https://github.com/withastro/starlight/commit/4d755f591bbca682d01a70162a10c12ab5187c61) Thanks [@delucis](https://github.com/delucis)! - Adds a `<link rel="alternate" hreflang="x-default" href="...">` tag pointing to the default locale in multilingual sites. The `x-default` alternate is used as a signal of which language to fall back to if no other is available. Learn more in Google’s [SEO localization docs](https://developers.google.com/search/docs/specialty/international/localized-versions#xdefault).
62
+
63
+ - [#3862](https://github.com/withastro/starlight/pull/3862) [`ec70630`](https://github.com/withastro/starlight/commit/ec70630cbbe43bb5b026ddabfe7224759c798a4b) Thanks [@itrew](https://github.com/itrew)! - Makes spacing of items in nested lists more consistent
64
+
65
+ - [#3872](https://github.com/withastro/starlight/pull/3872) [`417a66c`](https://github.com/withastro/starlight/commit/417a66c4acf3a7408fc0cea701aa5f143c540e5c) Thanks [@tats-u](https://github.com/tats-u)! - Enables [the CSS property `text-autospace`](https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/text-autospace) in Chinese and Japanese documents.
66
+
67
+ If you would prefer to disable autospacing in Chinese and Japanese pages, you can add the following custom CSS to your site:
68
+
69
+ ```css
70
+ [lang]:where(:lang(zh, ja)) {
71
+ text-autospace: initial;
72
+ }
73
+ ```
74
+
75
+ - [#3797](https://github.com/withastro/starlight/pull/3797) [`9764ebd`](https://github.com/withastro/starlight/commit/9764ebdab8d12983064d11515b6d92355a4d95eb) Thanks [@delucis](https://github.com/delucis)! - Avoids the risk of layout shift when users expand and collapse sidebar groups
76
+
77
+ This release can introduce additional padding to the site sidebar on certain devices to reserve space for scrollbars. You may wish to inspect your site sidebar visually when upgrading.
78
+
79
+ If you would prefer to keep the previous styling, you can add the following custom CSS to your site:
80
+
81
+ ```css
82
+ .sidebar-pane {
83
+ scrollbar-gutter: auto;
84
+ }
85
+ ```
86
+
87
+ - [#3858](https://github.com/withastro/starlight/pull/3858) [`6672c35`](https://github.com/withastro/starlight/commit/6672c35b914cd53f1641a43a7ea8e7ef08d9cc8f) Thanks [@delucis](https://github.com/delucis)! - Updates `i18next`, used for Starlight’s localization APIs, from v23 to v26
88
+
89
+ There should not be any user-facing changes from this update
90
+
91
+ ## 0.38.5
92
+
93
+ ### Patch Changes
94
+
95
+ - [#3854](https://github.com/withastro/starlight/pull/3854) [`ccf6000`](https://github.com/withastro/starlight/commit/ccf6000549c580796a8a8730637074c25e3ccc44), [#3877](https://github.com/withastro/starlight/pull/3877) [`47451bc`](https://github.com/withastro/starlight/commit/47451bc56d84ed19bee73e3d4dbb58d28951ef38) Thanks [@delucis](https://github.com/delucis)! - Updates internal dependencies
96
+
3
97
  ## 0.38.4
4
98
 
5
99
  ### Patch Changes
@@ -56,6 +56,7 @@ const { hasSidebar } = Astro.locals.starlightRoute;
56
56
  width: 100%;
57
57
  background-color: var(--sl-color-black);
58
58
  overflow-y: auto;
59
+ scrollbar-gutter: stable;
59
60
  }
60
61
 
61
62
  :global([aria-expanded='true']) ~ .sidebar-pane {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.38.4",
3
+ "version": "0.39.0",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
@@ -45,42 +45,42 @@
45
45
  "astro": "^6.0.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@playwright/test": "^1.57.0",
49
- "@types/node": "^22.19.3",
50
- "@vitest/coverage-v8": "^4.1.0",
51
- "astro": "^6.0.1",
52
- "linkedom": "^0.18.4",
53
- "vitest": "^4.1.0"
48
+ "@playwright/test": "^1.59.1",
49
+ "@types/node": "^22.19.17",
50
+ "@vitest/coverage-v8": "^4.1.5",
51
+ "astro": "^6.2.2",
52
+ "linkedom": "^0.18.12",
53
+ "vitest": "^4.1.5"
54
54
  },
55
55
  "dependencies": {
56
- "@astrojs/markdown-remark": "^7.0.0",
57
- "@astrojs/mdx": "^5.0.0",
58
- "@astrojs/sitemap": "^3.7.1",
56
+ "@astrojs/markdown-remark": "^7.1.1",
57
+ "@astrojs/mdx": "^5.0.4",
58
+ "@astrojs/sitemap": "^3.7.2",
59
59
  "@pagefind/default-ui": "^1.3.0",
60
60
  "@types/hast": "^3.0.4",
61
61
  "@types/js-yaml": "^4.0.9",
62
62
  "@types/mdast": "^4.0.4",
63
- "astro-expressive-code": "^0.41.6",
63
+ "astro-expressive-code": "^0.42.0",
64
64
  "bcp-47": "^2.1.0",
65
- "hast-util-from-html": "^2.0.1",
66
- "hast-util-select": "^6.0.2",
67
- "hast-util-to-string": "^3.0.0",
68
- "hastscript": "^9.0.0",
69
- "i18next": "^23.11.5",
70
- "js-yaml": "^4.1.0",
65
+ "hast-util-from-html": "^2.0.3",
66
+ "hast-util-select": "^6.0.4",
67
+ "hast-util-to-string": "^3.0.1",
68
+ "hastscript": "^9.0.1",
69
+ "i18next": "^26.0.7",
70
+ "js-yaml": "^4.1.1",
71
71
  "klona": "^2.0.6",
72
- "magic-string": "^0.30.17",
73
- "mdast-util-directive": "^3.0.0",
74
- "mdast-util-to-markdown": "^2.1.0",
72
+ "magic-string": "^0.30.21",
73
+ "mdast-util-directive": "^3.1.0",
74
+ "mdast-util-to-markdown": "^2.1.2",
75
75
  "mdast-util-to-string": "^4.0.0",
76
76
  "pagefind": "^1.3.0",
77
- "rehype": "^13.0.1",
78
- "rehype-format": "^5.0.0",
79
- "remark-directive": "^3.0.0",
77
+ "rehype": "^13.0.2",
78
+ "rehype-format": "^5.0.1",
79
+ "remark-directive": "^4.0.0",
80
80
  "ultrahtml": "^1.6.0",
81
81
  "unified": "^11.0.5",
82
- "unist-util-visit": "^5.0.0",
83
- "vfile": "^6.0.2"
82
+ "unist-util-visit": "^5.1.0",
83
+ "vfile": "^6.0.3"
84
84
  },
85
85
  "publishConfig": {
86
86
  "provenance": true
@@ -50,31 +50,63 @@ const SidebarLinkItemSchema = z.strictObject({
50
50
  });
51
51
  export type SidebarLinkItem = z.infer<typeof SidebarLinkItemSchema>;
52
52
 
53
- const AutoSidebarGroupSchema = z.strictObject({
54
- ...SidebarGroupSchema.shape,
55
- /** Enable autogenerating a sidebar category from a specific docs directory. */
56
- autogenerate: z.object({
57
- /** The directory to generate sidebar items for. */
58
- directory: z.string().transform(stripLeadingAndTrailingSlashes),
53
+ const AutoSidebarEntriesSchema = z
54
+ .object({
59
55
  /**
60
- * Whether the autogenerated subgroups should be collapsed by default.
61
- * Defaults to the `AutoSidebarGroup` `collapsed` value.
56
+ * Explicitly prevent autogenerated groups which are no longer supported as the final type for
57
+ * supported sidebar item is a non-discriminated union where TypeScript will not perform excess
58
+ * property checks. This means that a user could define a sidebar group with an autogenerated
59
+ * property, not getting a TypeScript error, and only have it fail at runtime.
60
+ * @see https://github.com/microsoft/TypeScript/issues/20863
62
61
  */
63
- collapsed: z.boolean().optional(),
64
- /** HTML attributes to add to the autogenerated link items. */
65
- attrs: SidebarLinkItemHTMLAttributesSchema(),
66
- // TODO: not supported by Docusaurus but would be good to have
67
- /** How many directories deep to include from this directory in the sidebar. Default: `Infinity`. */
68
- // depth: z.number().optional(),
69
- }),
70
- });
71
- export type AutoSidebarGroup = z.infer<typeof AutoSidebarGroupSchema>;
62
+ label: z.custom<never>().optional(),
63
+ /** Enable autogenerating entries from a specific docs directory. */
64
+ autogenerate: z.object({
65
+ /** The directory to generate sidebar items for. */
66
+ directory: z.string().transform(stripLeadingAndTrailingSlashes),
67
+ /** Whether the autogenerated subgroups should be collapsed by default. Default: `false`. */
68
+ collapsed: z.boolean().optional(),
69
+ /** HTML attributes to add to the autogenerated link items. */
70
+ attrs: SidebarLinkItemHTMLAttributesSchema(),
71
+ // TODO: not supported by Docusaurus but would be good to have
72
+ /** How many directories deep to include from this directory in the sidebar. Default: `Infinity`. */
73
+ // depth: z.number().optional(),
74
+ }),
75
+ })
76
+ .strict()
77
+ .superRefine((config, ctx) => {
78
+ if (!('label' in config)) return;
79
+
80
+ // TODO: Remove this error message in a future release once most users have migrated
81
+ ctx.addIssue({
82
+ code: 'custom',
83
+ message:
84
+ `Found an \`autogenerate\` object with a \`label\`. Support for autogenerated sidebar groups was removed in Starlight v0.38.0.\n` +
85
+ `You should instead create a group with the desired \`label\` and an \`items\` array containing the autogenerate config:\n\n` +
86
+ `{\n` +
87
+ ` label: '${config.label}',\n` +
88
+ ` items: [{ autogenerate: ${JSON.stringify(
89
+ config.autogenerate,
90
+ // Hide empty attrs object that is automatically added by the schema default value.
91
+ (key, value: unknown) =>
92
+ key === 'attrs' &&
93
+ typeof value === 'object' &&
94
+ value !== null &&
95
+ Object.keys(value).length === 0
96
+ ? undefined
97
+ : value,
98
+ ' '
99
+ ).replace(/\n\s*/g, ' ')} }]\n` +
100
+ `}`,
101
+ });
102
+ });
103
+ export type AutoSidebarEntries = z.infer<typeof AutoSidebarEntriesSchema>;
72
104
 
73
105
  type ManualSidebarGroupInput = z.input<typeof SidebarGroupSchema> & {
74
106
  /** Array of links and subcategories to display in this category. */
75
107
  items: Array<
76
108
  | z.input<typeof SidebarLinkItemSchema>
77
- | z.input<typeof AutoSidebarGroupSchema>
109
+ | z.input<typeof AutoSidebarEntriesSchema>
78
110
  | z.input<typeof InternalSidebarLinkItemSchema>
79
111
  | z.input<typeof InternalSidebarLinkItemShorthandSchema>
80
112
  | ManualSidebarGroupInput
@@ -85,7 +117,7 @@ type ManualSidebarGroupOutput = z.output<typeof SidebarGroupSchema> & {
85
117
  /** Array of links and subcategories to display in this category. */
86
118
  items: Array<
87
119
  | z.output<typeof SidebarLinkItemSchema>
88
- | z.output<typeof AutoSidebarGroupSchema>
120
+ | z.output<typeof AutoSidebarEntriesSchema>
89
121
  | z.output<typeof InternalSidebarLinkItemSchema>
90
122
  | z.output<typeof InternalSidebarLinkItemShorthandSchema>
91
123
  | ManualSidebarGroupOutput
@@ -101,7 +133,7 @@ const ManualSidebarGroupSchema: z.ZodType<ManualSidebarGroupOutput, ManualSideba
101
133
  .union([
102
134
  SidebarLinkItemSchema,
103
135
  ManualSidebarGroupSchema,
104
- AutoSidebarGroupSchema,
136
+ AutoSidebarEntriesSchema,
105
137
  InternalSidebarLinkItemSchema,
106
138
  InternalSidebarLinkItemShorthandSchema,
107
139
  ])
@@ -124,7 +156,7 @@ export type InternalSidebarLinkItem = z.output<typeof InternalSidebarLinkItemSch
124
156
  export const SidebarItemSchema = z.union([
125
157
  SidebarLinkItemSchema,
126
158
  ManualSidebarGroupSchema,
127
- AutoSidebarGroupSchema,
159
+ AutoSidebarEntriesSchema,
128
160
  InternalSidebarLinkItemSchema,
129
161
  InternalSidebarLinkItemShorthandSchema,
130
162
  ]);
@@ -31,31 +31,31 @@
31
31
  overflow-wrap: anywhere;
32
32
  }
33
33
 
34
+ /*
35
+ * This complex selector manages spacing inside lists.
36
+ *
37
+ * - Finds all lists containing items with non-inline direct children (e.g.
38
+ * ul > li > p)
39
+ * - Applies a bottom margin to the last non-inline child of each list item
40
+ * in this list
41
+ *
42
+ * This ensures that even if only one list item contains a block element
43
+ * (most commonly a paragraph), siblings of that list item are spaced
44
+ * consistently.
45
+ */
34
46
  .sl-markdown-content
35
- li
36
- > :is(
37
- :last-child:not(
38
- li,
39
- ul,
40
- ol,
41
- a,
42
- strong,
43
- em,
44
- del,
45
- span,
46
- input,
47
- code,
48
- br,
49
- script,
50
- :where(.not-content *)
51
- ),
52
- /**
53
- * For list items ending with 1 or multiple script elements (`:has(~ script:last-child)`), we
54
- * need to style the last non-script element (`:not(script)`) that doesn't have a subsequent
55
- * sibling that is not a script (`:not(:has(~ :not(script)))`).
47
+ :is(ol, ul):has(> li > :not(a, strong, em, del, span, input, code, br, script, ol, ul))
48
+ > li
49
+ > :is(
50
+ :last-child:not(a, strong, em, del, span, input, code, br, script, :where(.not-content *)),
51
+ /*
52
+ * For list items ending with 1 or multiple script elements (`:has(~
53
+ * script:last-child)`), we need to style the last non-script
54
+ * element (`:not(script)`) that doesn't have a subsequent sibling
55
+ * that is not a script (`:not(:has(~ :not(script)))`).
56
56
  */
57
- :not(script):has(~ script:last-child):not(:has(~ :not(script)))
58
- ) {
57
+ :not(script):has(~ script:last-child):not(:has(~ :not(script)))
58
+ ) {
59
59
  margin-bottom: 1.25rem;
60
60
  }
61
61
 
package/style/reset.css CHANGED
@@ -28,6 +28,21 @@
28
28
  background-color: var(--sl-color-bg);
29
29
  }
30
30
 
31
+ [lang]:where(:lang(zh, ja)) {
32
+ text-autospace: normal;
33
+ }
34
+
35
+ [lang]:where(:not(:lang(zh, ja))) {
36
+ text-autospace: initial;
37
+ }
38
+
39
+ pre,
40
+ code,
41
+ samp,
42
+ kbd {
43
+ text-autospace: no-autospace;
44
+ }
45
+
31
46
  input,
32
47
  button,
33
48
  textarea,
@@ -72,7 +72,11 @@ export async function createTranslationSystem<T extends i18nSchemaOutput>(
72
72
 
73
73
  const t = i18n.getFixedT(lang, I18nextNamespace) as I18nT;
74
74
  t.all = () => i18n.getResourceBundle(lang, I18nextNamespace) as ReturnType<I18nT['all']>;
75
- t.exists = (key, options) => i18n.exists(key, { lng: lang, ns: I18nextNamespace, ...options });
75
+ // Since i18next 25.10.4 the `ExistsFunction` type has a signature including a predicate that
76
+ // TS cannot preserve when composing a new function to add default options.
77
+ // See: https://github.com/i18next/i18next/issues/2425
78
+ t.exists = ((key, options) =>
79
+ i18n.exists(key, { lng: lang, ns: I18nextNamespace, ...options })) as ExistsFunction;
76
80
  t.dir = (dirLang = lang) => i18n.dir(dirLang);
77
81
 
78
82
  return t;
@@ -107,6 +107,12 @@ const errorMap: z.core.$ZodErrorMap = (issue) => {
107
107
  for (const unionError of issue.errors) {
108
108
  const expectedShape: string[] = [];
109
109
  for (const issue of unionError) {
110
+ // We sometimes use `z.NEVER` to explicitly error on certain property combinations in
111
+ // non-discriminated unions to make it easier for users to identify invalid config, e.g.
112
+ // autogenerated groups in the sidebar no longer being supported. Having these properties
113
+ // show up in the error message with an expected type of `never` is noisy and not helpful
114
+ // so we skip them here.
115
+ if (issue.code === 'invalid_type' && issue.expected === 'never') continue;
110
116
  // If the issue is a nested union error, show the associated error message instead of the
111
117
  // base error message.
112
118
  if (issue.code === 'invalid_union') {
package/utils/head.ts CHANGED
@@ -91,6 +91,14 @@ export function getHead(
91
91
  },
92
92
  });
93
93
  }
94
+ headDefaults.push({
95
+ tag: 'link',
96
+ attrs: {
97
+ rel: 'alternate',
98
+ hreflang: 'x-default',
99
+ href: localizedUrl(canonical, config.defaultLocale.locale, project.trailingSlash).href,
100
+ },
101
+ });
94
102
  }
95
103
 
96
104
  // Link to sitemap, but only when `site` is set.
package/utils/i18n.ts CHANGED
@@ -187,8 +187,9 @@ function getLocaleDir(locale: Intl.Locale): 'ltr' | 'rtl' {
187
187
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
188
188
  return locale.textInfo.direction;
189
189
  } else if ('getTextInfo' in locale) {
190
- // @ts-expect-error - `getTextInfo` is not typed but is available in some non-v8 based environments.
191
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
190
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
191
+ // @ts-ignore - `getTextInfo` is not typed in older versions of TypeScript but is available in some browsers and newer versions of Node.
192
+ // TODO: remove or switch to @ts-expect-error in #3572
192
193
  return locale.getTextInfo().direction;
193
194
  }
194
195
  // Firefox does not support `textInfo` or `getTextInfo` yet so we fallback to a well-known list
@@ -196,7 +197,7 @@ function getLocaleDir(locale: Intl.Locale): 'ltr' | 'rtl' {
196
197
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
197
198
  // @ts-ignore — This is a type error with newer versions of TypeScript’s DOM types.
198
199
  // TODO: remove or switch to @ts-expect-error in #3572
199
- return wellKnownRTL.includes(locale.language) ? 'rtl' : 'ltr';
200
+ return wellKnownRTL.includes((locale as Intl.Locale).language) ? 'rtl' : 'ltr';
200
201
  }
201
202
 
202
203
  /**
@@ -4,7 +4,7 @@ import config from 'virtual:starlight/user-config';
4
4
  import type { Badge, I18nBadge, I18nBadgeConfig } from '../schemas/badge';
5
5
  import type { PrevNextLinkConfig } from '../schemas/prevNextLink';
6
6
  import type {
7
- AutoSidebarGroup,
7
+ AutoSidebarEntries,
8
8
  InternalSidebarLinkItem,
9
9
  LinkHTMLAttributes,
10
10
  SidebarItem,
@@ -24,9 +24,13 @@ import { getLocaleRoutes, routes } from './routing';
24
24
  import type {
25
25
  SidebarGroup,
26
26
  SidebarLink,
27
+ SidebarManualLink,
27
28
  PaginationLinks,
28
29
  Route,
29
30
  SidebarEntry,
31
+ SidebarAutoLink,
32
+ SidebarAutoGroup,
33
+ SidebarAutogenerateRouteData,
30
34
  } from './routing/types';
31
35
  import { localeToLang, localizedFilePath, slugToPathname } from './slugs';
32
36
  import { isAbsoluteUrl } from './url';
@@ -35,6 +39,8 @@ import type { StarlightConfig } from './user-config';
35
39
  const DirKey = Symbol('DirKey');
36
40
  const SlugKey = Symbol('SlugKey');
37
41
 
42
+ const rootAutogenerate: SidebarAutogenerateRouteData = { directory: '' };
43
+
38
44
  const neverPathFormatter = createPathFormatter({ trailingSlash: 'never' });
39
45
 
40
46
  const docsCollectionPathFromRoot = getCollectionPathFromRoot('docs', project);
@@ -68,14 +74,13 @@ function isDir(data: Record<string, unknown>): data is Dir {
68
74
  /** Convert an item in a user’s sidebar config to a sidebar entry. */
69
75
  function configItemToEntry(
70
76
  item: SidebarItem,
71
- currentPathname: string,
72
77
  locale: string | undefined,
73
78
  routes: Route[]
74
- ): SidebarEntry {
79
+ ): SidebarEntry | SidebarEntry[] {
75
80
  if ('link' in item) {
76
81
  return linkFromSidebarLinkItem(item, locale);
77
82
  } else if ('autogenerate' in item) {
78
- return groupFromAutogenerateConfig(item, locale, routes, currentPathname);
83
+ return entriesFromAutogenerateConfig(item, locale, routes);
79
84
  } else if ('slug' in item) {
80
85
  return linkFromInternalSidebarLinkItem(item, locale);
81
86
  } else {
@@ -83,22 +88,22 @@ function configItemToEntry(
83
88
  return {
84
89
  type: 'group',
85
90
  label,
86
- entries: item.items.map((i) => configItemToEntry(i, currentPathname, locale, routes)),
91
+ entries: item.items.flatMap((i) => configItemToEntry(i, locale, routes)),
87
92
  collapsed: item.collapsed,
88
93
  badge: getSidebarBadge(item.badge, locale, label),
89
94
  };
90
95
  }
91
96
  }
92
97
 
93
- /** Autogenerate a group of links from a user’s sidebar config. */
94
- function groupFromAutogenerateConfig(
95
- item: AutoSidebarGroup,
98
+ /** Autogenerate links and groups from a user’s sidebar config. */
99
+ function entriesFromAutogenerateConfig(
100
+ item: AutoSidebarEntries,
96
101
  locale: string | undefined,
97
- routes: Route[],
98
- currentPathname: string
99
- ): SidebarGroup {
100
- const { attrs, collapsed: subgroupCollapsed, directory } = item.autogenerate;
102
+ routes: Route[]
103
+ ): (SidebarAutoLink | SidebarGroup)[] {
104
+ const { attrs, collapsed, directory } = item.autogenerate;
101
105
  const localeDir = locale ? locale + '/' + directory : directory;
106
+ const autogenerate = { directory };
102
107
  const dirDocs = routes.filter((doc) => {
103
108
  const filePathFromContentDir = getRoutePathRelativeToCollectionRoot(doc, locale);
104
109
  return (
@@ -109,20 +114,7 @@ function groupFromAutogenerateConfig(
109
114
  );
110
115
  });
111
116
  const tree = treeify(dirDocs, locale, localeDir);
112
- const label = pickLang(item.translations, localeToLang(locale)) || item.label;
113
- return {
114
- type: 'group',
115
- label,
116
- entries: sidebarFromDir(
117
- tree,
118
- currentPathname,
119
- locale,
120
- subgroupCollapsed ?? item.collapsed,
121
- attrs
122
- ),
123
- collapsed: item.collapsed,
124
- badge: getSidebarBadge(item.badge, locale, label),
125
- };
117
+ return sidebarFromDir(tree, { collapsed: collapsed ?? false, attrs }, autogenerate);
126
118
  }
127
119
 
128
120
  /** Create a link entry from a manual link item in user config. */
@@ -134,7 +126,12 @@ function linkFromSidebarLinkItem(item: SidebarLinkItem, locale: string | undefin
134
126
  if (locale) href = '/' + locale + href;
135
127
  }
136
128
  const label = pickLang(item.translations, localeToLang(locale)) || item.label;
137
- return makeSidebarLink(href, label, getSidebarBadge(item.badge, locale, label), item.attrs);
129
+ return makeSidebarLink({
130
+ href,
131
+ label,
132
+ badge: getSidebarBadge(item.badge, locale, label),
133
+ attrs: item.attrs,
134
+ });
138
135
  }
139
136
 
140
137
  /** Create a link entry from an automatic internal link item in user config. */
@@ -169,39 +166,44 @@ function linkFromInternalSidebarLinkItem(
169
166
  frontmatter.title;
170
167
  const badge = item.badge ?? frontmatter.sidebar?.badge;
171
168
  const attrs = { ...frontmatter.sidebar?.attrs, ...item.attrs };
172
- return makeSidebarLink(
173
- slugToPathname(route.id),
169
+ return makeSidebarLink({
170
+ href: slugToPathname(route.id),
174
171
  label,
175
- getSidebarBadge(badge, locale, label),
176
- attrs
177
- );
172
+ badge: getSidebarBadge(badge, locale, label),
173
+ attrs,
174
+ });
175
+ }
176
+
177
+ interface MakeLinkOptions {
178
+ autogenerate?: SidebarAutogenerateRouteData | undefined;
179
+ href: string;
180
+ label: string;
181
+ badge?: Badge | undefined;
182
+ attrs?: LinkHTMLAttributes | undefined;
178
183
  }
179
184
 
180
185
  /** Process sidebar link options to create a link entry. */
186
+ function makeSidebarLink(opts: MakeLinkOptions & { autogenerate?: undefined }): SidebarManualLink;
181
187
  function makeSidebarLink(
182
- href: string,
183
- label: string,
184
- badge?: Badge,
185
- attrs?: LinkHTMLAttributes
186
- ): SidebarLink {
188
+ opts: MakeLinkOptions & { autogenerate: SidebarAutogenerateRouteData }
189
+ ): SidebarAutoLink;
190
+ function makeSidebarLink({ attrs, badge, href, label, autogenerate }: MakeLinkOptions) {
187
191
  if (!isAbsoluteUrl(href)) {
188
192
  href = formatPath(href);
189
193
  }
190
- return makeLink({ label, href, badge, attrs });
194
+ return makeLink({ label, href, badge, attrs, autogenerate });
191
195
  }
192
196
 
193
197
  /** Create a link entry */
194
- function makeLink({
195
- attrs = {},
196
- badge = undefined,
197
- ...opts
198
- }: {
199
- label: string;
200
- href: string;
201
- badge?: Badge | undefined;
202
- attrs?: LinkHTMLAttributes | undefined;
203
- }): SidebarLink {
204
- return { type: 'link', ...opts, badge, isCurrent: false, attrs };
198
+ function makeLink({ attrs = {}, badge, autogenerate, ...opts }: MakeLinkOptions): SidebarLink {
199
+ return {
200
+ type: 'link',
201
+ ...opts,
202
+ badge,
203
+ isCurrent: false,
204
+ attrs,
205
+ ...(autogenerate ? { autogenerate } : {}),
206
+ };
205
207
  }
206
208
 
207
209
  /** Test if two paths are equivalent even if formatted differently. */
@@ -273,13 +275,18 @@ function treeify(routes: Route[], locale: string | undefined, baseDir: string):
273
275
  }
274
276
 
275
277
  /** Create a link entry for a given content collection entry. */
276
- function linkFromRoute(route: Route, attrs?: LinkHTMLAttributes): SidebarLink {
277
- return makeSidebarLink(
278
- slugToPathname(route.id),
279
- route.entry.data.sidebar.label || route.entry.data.title,
280
- route.entry.data.sidebar.badge,
281
- { ...attrs, ...route.entry.data.sidebar.attrs }
282
- );
278
+ function linkFromRoute(
279
+ route: Route,
280
+ attrs: LinkHTMLAttributes | undefined,
281
+ autogenerate: SidebarAutogenerateRouteData
282
+ ): SidebarAutoLink {
283
+ return makeSidebarLink({
284
+ href: slugToPathname(route.id),
285
+ label: route.entry.data.sidebar.label || route.entry.data.title,
286
+ badge: route.entry.data.sidebar.badge,
287
+ attrs: { ...attrs, ...route.entry.data.sidebar.attrs },
288
+ autogenerate,
289
+ });
283
290
  }
284
291
 
285
292
  /**
@@ -305,18 +312,22 @@ function sortDirEntries(dir: [string, Dir | Route][]): [string, Dir | Route][] {
305
312
  });
306
313
  }
307
314
 
315
+ interface SidebarDirOptions {
316
+ collapsed: boolean;
317
+ attrs: LinkHTMLAttributes | undefined;
318
+ }
319
+
320
+ interface SidebarDirContext extends SidebarDirOptions {
321
+ fullPath: string;
322
+ dirName: string;
323
+ autogenerate: SidebarAutogenerateRouteData;
324
+ }
325
+
308
326
  /** Create a group entry for a given content collection directory. */
309
- function groupFromDir(
310
- dir: Dir,
311
- fullPath: string,
312
- dirName: string,
313
- currentPathname: string,
314
- locale: string | undefined,
315
- collapsed: boolean,
316
- attrs?: LinkHTMLAttributes
317
- ): SidebarGroup {
327
+ function groupFromDir(dir: Dir, context: SidebarDirContext): SidebarAutoGroup {
328
+ const { fullPath, dirName, collapsed, autogenerate } = context;
318
329
  const entries = sortDirEntries(Object.entries(dir)).map(([key, dirOrRoute]) =>
319
- dirToItem(dirOrRoute, `${fullPath}/${key}`, key, currentPathname, locale, collapsed, attrs)
330
+ dirToItem(dirOrRoute, { ...context, fullPath: `${fullPath}/${key}`, dirName: key })
320
331
  );
321
332
  return {
322
333
  type: 'group',
@@ -324,34 +335,29 @@ function groupFromDir(
324
335
  entries,
325
336
  collapsed,
326
337
  badge: undefined,
338
+ autogenerate,
327
339
  };
328
340
  }
329
341
 
330
342
  /** Create a sidebar entry for a directory or content entry. */
331
343
  function dirToItem(
332
344
  dirOrRoute: Dir[string],
333
- fullPath: string,
334
- dirName: string,
335
- currentPathname: string,
336
- locale: string | undefined,
337
- collapsed: boolean,
338
- attrs?: LinkHTMLAttributes
339
- ): SidebarEntry {
345
+ context: SidebarDirContext
346
+ ): SidebarAutoGroup | SidebarAutoLink {
347
+ const { attrs, autogenerate } = context;
340
348
  return isDir(dirOrRoute)
341
- ? groupFromDir(dirOrRoute, fullPath, dirName, currentPathname, locale, collapsed, attrs)
342
- : linkFromRoute(dirOrRoute, attrs);
349
+ ? groupFromDir(dirOrRoute, context)
350
+ : linkFromRoute(dirOrRoute, attrs, autogenerate);
343
351
  }
344
352
 
345
353
  /** Create a sidebar entry for a given content directory. */
346
354
  function sidebarFromDir(
347
355
  tree: Dir,
348
- currentPathname: string,
349
- locale: string | undefined,
350
- collapsed: boolean,
351
- attrs?: LinkHTMLAttributes
356
+ options: SidebarDirOptions,
357
+ autogenerate: SidebarAutogenerateRouteData = rootAutogenerate
352
358
  ) {
353
359
  return sortDirEntries(Object.entries(tree)).map(([key, dirOrRoute]) =>
354
- dirToItem(dirOrRoute, key, key, currentPathname, locale, collapsed, attrs)
360
+ dirToItem(dirOrRoute, { ...options, fullPath: key, dirName: key, autogenerate })
355
361
  );
356
362
  }
357
363
 
@@ -374,7 +380,7 @@ const lastCurrentEntryByLocale = new Map<string | undefined, SidebarLink>();
374
380
  export function getSidebar(pathname: string, locale: string | undefined): SidebarEntry[] {
375
381
  let intermediateSidebar = intermediateSidebars.get(locale);
376
382
  if (!intermediateSidebar) {
377
- intermediateSidebar = getIntermediateSidebarFromConfig(config.sidebar, pathname, locale);
383
+ intermediateSidebar = getIntermediateSidebarFromConfig(config.sidebar, locale);
378
384
  intermediateSidebars.set(locale, intermediateSidebar);
379
385
  }
380
386
  setIntermediateSidebarCurrentEntry(intermediateSidebar, pathname, locale);
@@ -387,24 +393,23 @@ export function getSidebarFromConfig(
387
393
  pathname: string,
388
394
  locale: string | undefined
389
395
  ): SidebarEntry[] {
390
- const sidebar = getIntermediateSidebarFromConfig(sidebarConfig, pathname, locale);
396
+ const sidebar = getIntermediateSidebarFromConfig(sidebarConfig, locale);
391
397
  const currentEntry = getSidebarCurrentEntry(sidebar, pathname);
392
398
  if (currentEntry) currentEntry.isCurrent = true;
393
399
  return sidebar;
394
400
  }
395
401
 
396
- /** Get the intermediate sidebar for the current page using the specified sidebar config. */
402
+ /** Get the intermediate sidebar for a locale using the specified sidebar config. */
397
403
  function getIntermediateSidebarFromConfig(
398
404
  sidebarConfig: StarlightConfig['sidebar'],
399
- pathname: string,
400
405
  locale: string | undefined
401
406
  ): SidebarEntry[] {
402
407
  const routes = getLocaleRoutes(locale);
403
408
  if (sidebarConfig) {
404
- return sidebarConfig.map((group) => configItemToEntry(group, pathname, locale, routes));
409
+ return sidebarConfig.flatMap((group) => configItemToEntry(group, locale, routes));
405
410
  } else {
406
411
  const tree = treeify(routes, locale, locale || '');
407
- return sidebarFromDir(tree, pathname, locale, false);
412
+ return sidebarFromDir(tree, { collapsed: false, attrs: undefined });
408
413
  }
409
414
  }
410
415
 
package/utils/plugins.ts CHANGED
@@ -199,6 +199,8 @@ const baseStarlightPluginSchema = z.object({
199
199
  type StarlightConfigUpdate = Partial<Omit<StarlightUserConfig, 'routeMiddleware'>>;
200
200
  type StarlightI18nTFactory = Awaited<ReturnType<typeof createTranslationSystemFromFs>>;
201
201
 
202
+ /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion --
203
+ The configSetupHookSchema casts `z.any()` to provide types but this rule can’t understand that. */
202
204
  const configSetupHookSchema = z
203
205
  .function({
204
206
  input: [
@@ -14,7 +14,11 @@ export interface LocaleData {
14
14
  locale: string | undefined;
15
15
  }
16
16
 
17
- export interface SidebarLink {
17
+ export interface SidebarAutogenerateRouteData {
18
+ directory: string;
19
+ }
20
+
21
+ export interface SidebarManualLink {
18
22
  type: 'link';
19
23
  label: string;
20
24
  href: string;
@@ -23,7 +27,11 @@ export interface SidebarLink {
23
27
  attrs: LinkHTMLAttributes;
24
28
  }
25
29
 
26
- export interface SidebarGroup {
30
+ export interface SidebarAutoLink extends SidebarManualLink {
31
+ autogenerate: SidebarAutogenerateRouteData;
32
+ }
33
+
34
+ export interface SidebarManualGroup {
27
35
  type: 'group';
28
36
  label: string;
29
37
  entries: (SidebarLink | SidebarGroup)[];
@@ -31,6 +39,12 @@ export interface SidebarGroup {
31
39
  badge: Badge | undefined;
32
40
  }
33
41
 
42
+ export interface SidebarAutoGroup extends SidebarManualGroup {
43
+ autogenerate: SidebarAutogenerateRouteData;
44
+ }
45
+
46
+ export type SidebarLink = SidebarManualLink | SidebarAutoLink;
47
+ export type SidebarGroup = SidebarManualGroup | SidebarAutoGroup;
34
48
  export type SidebarEntry = SidebarLink | SidebarGroup;
35
49
 
36
50
  export interface PaginationLinks {