@astrojs/starlight 0.37.7 → 0.38.1

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/components/ContentNotice.astro +1 -1
  3. package/components/MobileTableOfContents.astro +2 -2
  4. package/components/Page.astro +3 -3
  5. package/components/Select.astro +2 -2
  6. package/components/SocialIcons.astro +1 -1
  7. package/components/TableOfContents.astro +2 -2
  8. package/global.d.ts +9 -3
  9. package/index.ts +39 -1
  10. package/integrations/asides.ts +1 -1
  11. package/integrations/remark-rehype.ts +2 -11
  12. package/integrations/virtual-user-config.ts +31 -17
  13. package/integrations/vite-layer-order.ts +38 -35
  14. package/package.json +12 -148
  15. package/schema.ts +5 -11
  16. package/schemas/badge.ts +9 -8
  17. package/schemas/components.ts +1 -1
  18. package/schemas/expressiveCode.ts +0 -3
  19. package/schemas/favicon.ts +4 -6
  20. package/schemas/head.ts +3 -2
  21. package/schemas/hero.ts +1 -1
  22. package/schemas/i18n.ts +148 -153
  23. package/schemas/icon.ts +1 -1
  24. package/schemas/pagefind.ts +5 -4
  25. package/schemas/prevNextLink.ts +6 -8
  26. package/schemas/sidebar.ts +33 -27
  27. package/schemas/site-title.ts +4 -6
  28. package/schemas/social.ts +3 -2
  29. package/schemas/tableOfContents.ts +17 -15
  30. package/style/props.css +7 -2
  31. package/types.ts +1 -1
  32. package/user-components/Aside.astro +1 -1
  33. package/user-components/Card.astro +1 -1
  34. package/user-components/Icon.astro +1 -1
  35. package/user-components/LinkButton.astro +1 -1
  36. package/user-components/TabItem.astro +1 -1
  37. package/user-components/rehype-file-tree.ts +1 -1
  38. package/user-components/rehype-tabs.ts +1 -1
  39. package/utils/createTranslationSystem.ts +1 -1
  40. package/utils/error-map.ts +98 -57
  41. package/utils/navigation.ts +11 -10
  42. package/utils/plugins.ts +75 -40
  43. package/utils/routing/data.ts +3 -9
  44. package/utils/routing/index.ts +16 -24
  45. package/utils/routing/types.ts +5 -17
  46. package/utils/slugs.ts +11 -12
  47. package/utils/starlight-page.ts +13 -23
  48. package/utils/translations.ts +3 -4
  49. package/utils/user-config.ts +30 -67
  50. package/virtual.d.ts +6 -3
  51. /package/{components → components-internals}/Icons.ts +0 -0
  52. /package/{components → components-internals}/SidebarPersistState.ts +0 -0
  53. /package/{components → components-internals}/TableOfContents/TableOfContentsList.astro +0 -0
  54. /package/{components → components-internals}/TableOfContents/starlight-toc.ts +0 -0
@@ -8,12 +8,13 @@ const SidebarBaseSchema = z.object({
8
8
  /** The visible label for this item in the sidebar. */
9
9
  label: z.string(),
10
10
  /** Translations of the `label` for each supported language. */
11
- translations: z.record(z.string()).default({}),
11
+ translations: z.record(z.string(), z.string()).default({}),
12
12
  /** Adds a badge to the item */
13
13
  badge: I18nBadgeConfigSchema(),
14
14
  });
15
15
 
16
- const SidebarGroupSchema = SidebarBaseSchema.extend({
16
+ const SidebarGroupSchema = z.object({
17
+ ...SidebarBaseSchema.shape,
17
18
  /**
18
19
  * Explicitly prevent custom attributes on groups as the final type for supported sidebar item
19
20
  * is a non-discriminated union where TypeScript will not perform excess property checks.
@@ -30,21 +31,27 @@ const SidebarGroupSchema = SidebarBaseSchema.extend({
30
31
  // `Record<string, string | number | boolean | undefined>` but typed as `HTMLAttributes<'a'>`
31
32
  // for user convenience.
32
33
  const linkHTMLAttributesSchema = z.record(
34
+ z.string(),
33
35
  z.union([z.string(), z.number(), z.boolean(), z.undefined(), z.null()])
34
- ) as z.Schema<Omit<HTMLAttributes<'a'>, keyof AstroBuiltinAttributes | 'children'>>;
35
- export type LinkHTMLAttributes = z.infer<typeof linkHTMLAttributesSchema>;
36
+ ) as z.ZodType<LinkHTMLAttributes, LinkHTMLAttributes>;
37
+ export type LinkHTMLAttributes = Omit<
38
+ HTMLAttributes<'a'>,
39
+ keyof AstroBuiltinAttributes | 'children'
40
+ >;
36
41
 
37
42
  export const SidebarLinkItemHTMLAttributesSchema = () => linkHTMLAttributesSchema.default({});
38
43
 
39
- const SidebarLinkItemSchema = SidebarBaseSchema.extend({
44
+ const SidebarLinkItemSchema = z.strictObject({
45
+ ...SidebarBaseSchema.shape,
40
46
  /** The link to this item’s content. Can be a relative link to local files or the full URL of an external page. */
41
47
  link: z.string(),
42
48
  /** HTML attributes to add to the link item. */
43
49
  attrs: SidebarLinkItemHTMLAttributesSchema(),
44
- }).strict();
50
+ });
45
51
  export type SidebarLinkItem = z.infer<typeof SidebarLinkItemSchema>;
46
52
 
47
- const AutoSidebarGroupSchema = SidebarGroupSchema.extend({
53
+ const AutoSidebarGroupSchema = z.strictObject({
54
+ ...SidebarGroupSchema.shape,
48
55
  /** Enable autogenerating a sidebar category from a specific docs directory. */
49
56
  autogenerate: z.object({
50
57
  /** The directory to generate sidebar items for. */
@@ -60,7 +67,7 @@ const AutoSidebarGroupSchema = SidebarGroupSchema.extend({
60
67
  /** How many directories deep to include from this directory in the sidebar. Default: `Infinity`. */
61
68
  // depth: z.number().optional(),
62
69
  }),
63
- }).strict();
70
+ });
64
71
  export type AutoSidebarGroup = z.infer<typeof AutoSidebarGroupSchema>;
65
72
 
66
73
  type ManualSidebarGroupInput = z.input<typeof SidebarGroupSchema> & {
@@ -85,26 +92,25 @@ type ManualSidebarGroupOutput = z.output<typeof SidebarGroupSchema> & {
85
92
  >;
86
93
  };
87
94
 
88
- const ManualSidebarGroupSchema: z.ZodType<
89
- ManualSidebarGroupOutput,
90
- z.ZodTypeDef,
91
- ManualSidebarGroupInput
92
- > = SidebarGroupSchema.extend({
93
- /** Array of links and subcategories to display in this category. */
94
- items: z.lazy(() =>
95
- z
96
- .union([
97
- SidebarLinkItemSchema,
98
- ManualSidebarGroupSchema,
99
- AutoSidebarGroupSchema,
100
- InternalSidebarLinkItemSchema,
101
- InternalSidebarLinkItemShorthandSchema,
102
- ])
103
- .array()
104
- ),
105
- }).strict();
95
+ const ManualSidebarGroupSchema: z.ZodType<ManualSidebarGroupOutput, ManualSidebarGroupInput> =
96
+ z.strictObject({
97
+ ...SidebarGroupSchema.shape,
98
+ /** Array of links and subcategories to display in this category. */
99
+ items: z.lazy(() =>
100
+ z
101
+ .union([
102
+ SidebarLinkItemSchema,
103
+ ManualSidebarGroupSchema,
104
+ AutoSidebarGroupSchema,
105
+ InternalSidebarLinkItemSchema,
106
+ InternalSidebarLinkItemShorthandSchema,
107
+ ])
108
+ .array()
109
+ ),
110
+ });
106
111
 
107
- const InternalSidebarLinkItemSchema = SidebarBaseSchema.partial({ label: true }).extend({
112
+ const InternalSidebarLinkItemSchema = z.object({
113
+ ...SidebarBaseSchema.partial({ label: true }).shape,
108
114
  /** The link to this item’s content. Must be a slug of a Content Collection entry. */
109
115
  slug: z.string(),
110
116
  /** HTML attributes to add to the link item. */
@@ -1,9 +1,6 @@
1
1
  import { z } from 'astro/zod';
2
2
 
3
- export const TitleConfigSchema = () =>
4
- z
5
- .union([z.string(), z.record(z.string())])
6
- .describe('Title for your website. Will be used in metadata and as browser tab title.');
3
+ export const TitleConfigSchema = () => z.union([z.string(), z.record(z.string(), z.string())]);
7
4
 
8
5
  // transform the title for runtime use
9
6
  export const TitleTransformConfigSchema = (defaultLang: string) =>
@@ -12,9 +9,10 @@ export const TitleTransformConfigSchema = (defaultLang: string) =>
12
9
  return { [defaultLang]: title };
13
10
  }
14
11
  if (!title[defaultLang] && title[defaultLang] !== '') {
15
- ctx.addIssue({
16
- code: z.ZodIssueCode.custom,
12
+ ctx.issues.push({
13
+ code: 'custom',
17
14
  message: `Title must have a key for the default language "${defaultLang}"`,
15
+ input: title,
18
16
  });
19
17
  return z.NEVER;
20
18
  }
package/schemas/social.ts CHANGED
@@ -11,11 +11,12 @@ export const SocialLinksSchema = () =>
11
11
  // TODO: remove once most people have updated to v0.33 or higher (e.g. when releasing Starlight v1)
12
12
  z.preprocess((value, ctx) => {
13
13
  if (value && typeof value === 'object' && !Array.isArray(value)) {
14
- ctx.addIssue({
15
- code: z.ZodIssueCode.custom,
14
+ ctx.issues.push({
15
+ code: 'custom',
16
16
  message:
17
17
  'Starlight v0.33.0 changed the `social` configuration syntax. Please specify an array of link items instead of an object.\n' +
18
18
  'See the Starlight changelog for details: https://github.com/withastro/starlight/blob/main/packages/starlight/CHANGELOG.md#0330\n',
19
+ input: value,
19
20
  });
20
21
  }
21
22
  return value;
@@ -2,18 +2,20 @@ import { z } from 'astro/zod';
2
2
 
3
3
  const defaults = { minHeadingLevel: 2, maxHeadingLevel: 3 };
4
4
 
5
- export const TableOfContentsSchema = () =>
6
- z
7
- .union([
8
- z.object({
9
- /** The level to start including headings at in the table of contents. Default: 2. */
10
- minHeadingLevel: z.number().int().min(1).max(6).optional().default(2),
11
- /** The level to stop including headings at in the table of contents. Default: 3. */
12
- maxHeadingLevel: z.number().int().min(1).max(6).optional().default(3),
13
- }),
14
- z.boolean().transform((enabled) => (enabled ? defaults : false)),
15
- ])
16
- .default(defaults)
17
- .refine((toc) => (toc ? toc.minHeadingLevel <= toc.maxHeadingLevel : true), {
18
- message: 'minHeadingLevel must be less than or equal to maxHeadingLevel',
19
- });
5
+ const TableOfContentsBaseSchema = z
6
+ .union([
7
+ z.object({
8
+ /** The level to start including headings at in the table of contents. Default: 2. */
9
+ minHeadingLevel: z.int().min(1).max(6).optional().default(2),
10
+ /** The level to stop including headings at in the table of contents. Default: 3. */
11
+ maxHeadingLevel: z.int().min(1).max(6).optional().default(3),
12
+ }),
13
+ z.boolean().transform((enabled) => (enabled ? defaults : false)),
14
+ ])
15
+ .refine((toc) => (toc ? toc.minHeadingLevel <= toc.maxHeadingLevel : true), {
16
+ error: 'minHeadingLevel must be less than or equal to maxHeadingLevel',
17
+ });
18
+
19
+ export const UserConfigTableOfContentsSchema = () => TableOfContentsBaseSchema.default(defaults);
20
+
21
+ export const FrontmatterTableOfContentsSchema = () => TableOfContentsBaseSchema.optional();
package/style/props.css CHANGED
@@ -84,8 +84,13 @@
84
84
  --sl-line-height: 1.75;
85
85
  --sl-line-height-headings: 1.2;
86
86
 
87
- --sl-font-system: ui-sans-serif, system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
88
- 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
87
+ /*
88
+ Although technically superseded by the `system-ui` font-family, we use `-apple-system` and
89
+ `BlinkMacSystemFont` because `system-ui` causes issues on Windows computers in some languages.
90
+ See: https://github.com/withastro/starlight/issues/3721
91
+ */
92
+ --sl-font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
93
+ 'Noto Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
89
94
  'Noto Color Emoji';
90
95
  --sl-font-system-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
91
96
  'Courier New', monospace;
package/types.ts CHANGED
@@ -4,4 +4,4 @@ export type {
4
4
  StarlightUserConfigWithPlugins as StarlightUserConfig,
5
5
  HookParameters,
6
6
  } from './utils/plugins';
7
- export type { StarlightIcon } from './components/Icons';
7
+ export type { StarlightIcon } from './components-internals/Icons';
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import { AstroError } from 'astro/errors';
3
3
  import Icon from './Icon.astro';
4
- import { Icons, type StarlightIcon } from '../components/Icons';
4
+ import { Icons, type StarlightIcon } from '../components-internals/Icons';
5
5
  import { throwInvalidAsideIconError } from '../integrations/asides-error';
6
6
 
7
7
  const asideVariants = ['note', 'tip', 'caution', 'danger'] as const;
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import Icon from './Icon.astro';
3
- import type { StarlightIcon } from '../components/Icons';
3
+ import type { StarlightIcon } from '../components-internals/Icons';
4
4
 
5
5
  interface Props {
6
6
  icon?: StarlightIcon;
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { Icons, type StarlightIcon } from '../components/Icons';
2
+ import { Icons, type StarlightIcon } from '../components-internals/Icons';
3
3
 
4
4
  interface Props {
5
5
  name: StarlightIcon;
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types';
3
- import type { StarlightIcon } from '../components/Icons';
3
+ import type { StarlightIcon } from '../components-internals/Icons';
4
4
  import Icon from './Icon.astro';
5
5
 
6
6
  interface Props extends Omit<HTMLAttributes<'a'>, 'href'> {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import { TabItemTagname } from './rehype-tabs';
3
- import type { StarlightIcon } from '../components/Icons';
3
+ import type { StarlightIcon } from '../components-internals/Icons';
4
4
 
5
5
  interface Props {
6
6
  icon?: StarlightIcon;
@@ -6,7 +6,7 @@ import { fromHtml } from 'hast-util-from-html';
6
6
  import { toString } from 'hast-util-to-string';
7
7
  import { rehype } from 'rehype';
8
8
  import { CONTINUE, SKIP, visit } from 'unist-util-visit';
9
- import { Icons, type StarlightIcon } from '../components/Icons';
9
+ import { Icons, type StarlightIcon } from '../components-internals/Icons';
10
10
  import { definitions } from './file-tree-icons';
11
11
 
12
12
  declare module 'vfile' {
@@ -2,7 +2,7 @@ import type { Element } from 'hast';
2
2
  import { select } from 'hast-util-select';
3
3
  import { rehype } from 'rehype';
4
4
  import { CONTINUE, SKIP, visit } from 'unist-util-visit';
5
- import type { StarlightIcon } from '../components/Icons';
5
+ import type { StarlightIcon } from '../components-internals/Icons';
6
6
 
7
7
  interface Panel {
8
8
  panelId: string;
@@ -107,7 +107,7 @@ function localeToLang(
107
107
  type BuiltInStrings = (typeof builtinTranslations)['en'];
108
108
 
109
109
  /** Build an i18next resources dictionary by layering preferred translation sources. */
110
- function buildResources<T extends Record<string, string | undefined>>(
110
+ function buildResources<T extends i18nSchemaOutput>(
111
111
  ...dictionaries: (T | BuiltInStrings | undefined)[]
112
112
  ): { [I18nextNamespace]: BuiltInStrings & T } {
113
113
  const dictionary: Partial<BuiltInStrings> = {};
@@ -4,12 +4,16 @@
4
4
  */
5
5
 
6
6
  import { AstroError } from 'astro/errors';
7
- import type { z } from 'astro:content';
7
+ import { z, locales } from 'astro/zod';
8
8
 
9
- type TypeOrLiteralErrByPathEntry = {
10
- code: 'invalid_type' | 'invalid_literal';
9
+ // The default Zod error map that we use to retrieve default error messages.
10
+ const zodErrorMap = locales.en().localeError;
11
+
12
+ type TypeErrByPathEntry = {
13
+ code: 'invalid_type';
11
14
  received: unknown;
12
15
  expected: unknown[];
16
+ message: string | undefined;
13
17
  };
14
18
 
15
19
  /**
@@ -20,12 +24,15 @@ type TypeOrLiteralErrByPathEntry = {
20
24
  * @param message Error message preamble to use if the input fails to parse.
21
25
  * @returns Validated data parsed by Zod.
22
26
  */
23
- export function parseWithFriendlyErrors<T extends z.Schema>(
27
+ export function parseWithFriendlyErrors<T extends z.ZodType>(
24
28
  schema: T,
25
29
  input: z.input<T>,
26
30
  message: string
27
31
  ): z.output<T> {
28
- return processParsedData<T>(schema.safeParse(input, { errorMap }), message);
32
+ return processParsedData<T>(
33
+ schema.safeParse(input, { error: errorMap, reportInput: true }),
34
+ message
35
+ );
29
36
  }
30
37
 
31
38
  /**
@@ -37,16 +44,19 @@ export function parseWithFriendlyErrors<T extends z.Schema>(
37
44
  * @param message Error message preamble to use if the input fails to parse.
38
45
  * @returns Validated data parsed by Zod.
39
46
  */
40
- export async function parseAsyncWithFriendlyErrors<T extends z.Schema>(
47
+ export async function parseAsyncWithFriendlyErrors<T extends z.ZodType>(
41
48
  schema: T,
42
49
  input: z.input<T>,
43
50
  message: string
44
51
  ): Promise<z.output<T>> {
45
- return processParsedData<T>(await schema.safeParseAsync(input, { errorMap }), message);
52
+ return processParsedData<T>(
53
+ await schema.safeParseAsync(input, { error: errorMap, reportInput: true }),
54
+ message
55
+ );
46
56
  }
47
57
 
48
- function processParsedData<T extends z.Schema>(
49
- parsedData: z.SafeParseReturnType<T, T>,
58
+ function processParsedData<T extends z.ZodType>(
59
+ parsedData: z.ZodSafeParseResult<z.output<T>>,
50
60
  message: string
51
61
  ) {
52
62
  if (!parsedData.success) {
@@ -55,25 +65,27 @@ function processParsedData<T extends z.Schema>(
55
65
  return parsedData.data;
56
66
  }
57
67
 
58
- const errorMap: z.ZodErrorMap = (baseError, ctx) => {
59
- const baseErrorPath = flattenErrorPath(baseError.path);
60
- if (baseError.code === 'invalid_union') {
68
+ const errorMap: z.core.$ZodErrorMap = (issue) => {
69
+ const baseErrorPath = flattenErrorPath(issue.path ?? []);
70
+ if (issue.code === 'invalid_union') {
61
71
  // Optimization: Combine type and literal errors for keys that are common across ALL union types
62
72
  // Ex. a union between `{ key: z.literal('tutorial') }` and `{ key: z.literal('blog') }` will
63
73
  // raise a single error when `key` does not match:
64
74
  // > Did not match union.
65
75
  // > key: Expected `'tutorial' | 'blog'`, received 'foo'
66
- const typeOrLiteralErrByPath: Map<string, TypeOrLiteralErrByPathEntry> = new Map();
67
- for (const unionError of baseError.unionErrors.map((e) => e.errors).flat()) {
68
- if (unionError.code === 'invalid_type' || unionError.code === 'invalid_literal') {
69
- const flattenedErrorPath = flattenErrorPath(unionError.path);
76
+ const unionErrors = issue.errors.flat();
77
+ const typeOrLiteralErrByPath: Map<string, TypeErrByPathEntry> = new Map();
78
+ for (const unionError of unionErrors) {
79
+ if (unionError.code === 'invalid_type') {
80
+ const flattenedErrorPath = flattenErrorPath([baseErrorPath, ...unionError.path]);
70
81
  if (typeOrLiteralErrByPath.has(flattenedErrorPath)) {
71
82
  typeOrLiteralErrByPath.get(flattenedErrorPath)!.expected.push(unionError.expected);
72
83
  } else {
73
84
  typeOrLiteralErrByPath.set(flattenedErrorPath, {
74
85
  code: unionError.code,
75
- received: unionError.received,
86
+ received: parsedType(issue.input),
76
87
  expected: [unionError.expected],
88
+ message: unionError.message,
77
89
  });
78
90
  }
79
91
  }
@@ -82,83 +94,84 @@ const errorMap: z.ZodErrorMap = (baseError, ctx) => {
82
94
  const details: string[] = [...typeOrLiteralErrByPath.entries()]
83
95
  // If type or literal error isn't common to ALL union types,
84
96
  // filter it out. Can lead to confusing noise.
85
- .filter(([, error]) => error.expected.length === baseError.unionErrors.length)
97
+ .filter(([, error]) => error.expected.length === unionErrors.length)
86
98
  .map(([key, error]) =>
87
99
  key === baseErrorPath
88
100
  ? // Avoid printing the key again if it's a base error
89
- `> ${getTypeOrLiteralMsg(error)}`
90
- : `> ${prefix(key, getTypeOrLiteralMsg(error))}`
101
+ `> ${getTypeErrMsg(error)}`
102
+ : `> ${prefix(key, getTypeErrMsg(error))}`
91
103
  );
92
104
 
93
105
  if (details.length === 0) {
94
106
  const expectedShapes: string[] = [];
95
- for (const unionError of baseError.unionErrors) {
107
+ for (const unionError of issue.errors) {
96
108
  const expectedShape: string[] = [];
97
- for (const issue of unionError.issues) {
109
+ for (const issue of unionError) {
98
110
  // If the issue is a nested union error, show the associated error message instead of the
99
111
  // base error message.
100
112
  if (issue.code === 'invalid_union') {
101
- return errorMap(issue, ctx);
113
+ return errorMap({ ...issue, input: issue.input, path: [baseErrorPath, ...issue.path] });
102
114
  }
103
115
  const relativePath = flattenErrorPath(issue.path)
104
116
  .replace(baseErrorPath, '')
105
117
  .replace(leadingPeriod, '');
106
- if ('expected' in issue && typeof issue.expected === 'string') {
118
+ if (issue.code === 'invalid_type') {
107
119
  expectedShape.push(
108
120
  relativePath ? `${relativePath}: ${issue.expected}` : issue.expected
109
121
  );
110
- } else {
122
+ } else if (issue.code === 'custom') {
111
123
  expectedShape.push(relativePath);
112
124
  }
113
125
  }
114
126
  if (expectedShape.length === 1 && !expectedShape[0]?.includes(':')) {
115
127
  // In this case the expected shape is not an object, but probably a literal type, e.g. `['string']`.
116
128
  expectedShapes.push(expectedShape.join(''));
117
- } else {
129
+ } else if (expectedShape.length > 0) {
118
130
  expectedShapes.push(`{ ${expectedShape.join('; ')} }`);
119
131
  }
120
132
  }
121
133
  if (expectedShapes.length) {
122
134
  details.push('> Expected type `' + expectedShapes.join(' | ') + '`');
123
- details.push('> Received `' + stringify(ctx.data) + '`');
135
+ details.push('> Received `' + stringify(issue.input) + '`');
124
136
  }
125
137
  }
126
138
 
127
- return {
128
- message: messages.concat(details).join('\n'),
129
- };
130
- } else if (baseError.code === 'invalid_literal' || baseError.code === 'invalid_type') {
131
- return {
132
- message: prefix(
133
- baseErrorPath,
134
- getTypeOrLiteralMsg({
135
- code: baseError.code,
136
- received: baseError.received,
137
- expected: [baseError.expected],
138
- })
139
- ),
140
- };
141
- } else if (baseError.message) {
142
- return { message: prefix(baseErrorPath, baseError.message) };
139
+ return messages.concat(details).join('\n');
140
+ } else if (issue.code === 'invalid_type') {
141
+ return prefix(
142
+ baseErrorPath,
143
+ getTypeErrMsg({
144
+ code: issue.code,
145
+ received: parsedType(issue.input),
146
+ expected: [issue.expected],
147
+ message: issue.message,
148
+ })
149
+ );
150
+ } else if (issue.message) {
151
+ return prefix(baseErrorPath, issue.message);
143
152
  } else {
144
- return { message: prefix(baseErrorPath, ctx.defaultError) };
153
+ // By design, the default Zod error may not be provided in Zod 4 error maps. Instead, error
154
+ // maps are supposed to return `undefined` in order to yield control to the next error map in
155
+ // the precedence chain. Unfortunately, this prevents us from prefixing all errors with their
156
+ // paths so we have to manually invoke the default Zod error map here.
157
+ const defaultError = zodErrorMap(issue);
158
+ if (!defaultError) return;
159
+
160
+ return prefix(
161
+ baseErrorPath,
162
+ typeof defaultError === 'string' ? defaultError : defaultError.message
163
+ );
145
164
  }
146
165
  };
147
166
 
148
- const getTypeOrLiteralMsg = (error: TypeOrLiteralErrByPathEntry): string => {
167
+ const getTypeErrMsg = (error: TypeErrByPathEntry): string => {
149
168
  // received could be `undefined` or the string `'undefined'`
150
- if (typeof error.received === 'undefined' || error.received === 'undefined') return 'Required';
169
+ if (typeof error.received === 'undefined' || error.received === 'undefined')
170
+ return error.message ?? 'Required';
151
171
  const expectedDeduped = new Set(error.expected);
152
- switch (error.code) {
153
- case 'invalid_type':
154
- return `Expected type \`${unionExpectedVals(expectedDeduped)}\`, received \`${stringify(
155
- error.received
156
- )}\``;
157
- case 'invalid_literal':
158
- return `Expected \`${unionExpectedVals(expectedDeduped)}\`, received \`${stringify(
159
- error.received
160
- )}\``;
161
- }
172
+ return `Expected type \`${unionExpectedVals(expectedDeduped)}\`, received \`${stringify(
173
+ error.received
174
+ )}\``;
162
175
  };
163
176
 
164
177
  const prefix = (key: string, msg: string) => (key.length ? `**${key}**: ${msg}` : msg);
@@ -166,10 +179,38 @@ const prefix = (key: string, msg: string) => (key.length ? `**${key}**: ${msg}`
166
179
  const unionExpectedVals = (expectedVals: Set<unknown>) =>
167
180
  [...expectedVals].map((expectedVal) => stringify(expectedVal)).join(' | ');
168
181
 
169
- const flattenErrorPath = (errorPath: (string | number)[]) => errorPath.join('.');
182
+ const flattenErrorPath = (errorPath: PropertyKey[]) => errorPath.join('.');
170
183
 
171
184
  /** `JSON.stringify()` a value with spaces around object/array entries. */
172
185
  const stringify = (val: unknown) =>
173
186
  JSON.stringify(val, null, 1).split(newlinePlusWhitespace).join(' ');
174
187
  const newlinePlusWhitespace = /\n\s*/;
175
188
  const leadingPeriod = /^\./;
189
+
190
+ /**
191
+ * In Zod 4, we don't necessarily get a human-readable representation of input data types. For such
192
+ * cases, we use the same logic as Zod's own `parsedType()` function.
193
+ * @see https://github.com/colinhacks/zod/blob/73b071d7d08825dedb6b48b78718739118ee1308/packages/zod/src/v4/locales/en.ts#L5
194
+ */
195
+ const parsedType = (data: unknown): string => {
196
+ const t = typeof data;
197
+
198
+ switch (t) {
199
+ case 'number': {
200
+ return Number.isNaN(data) ? 'NaN' : 'number';
201
+ }
202
+ case 'object': {
203
+ if (Array.isArray(data)) {
204
+ return 'array';
205
+ }
206
+ if (data === null) {
207
+ return 'null';
208
+ }
209
+
210
+ if (data && Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {
211
+ return data.constructor.name;
212
+ }
213
+ }
214
+ }
215
+ return t;
216
+ };
@@ -28,7 +28,7 @@ import type {
28
28
  Route,
29
29
  SidebarEntry,
30
30
  } from './routing/types';
31
- import { localeToLang, localizedId, slugToPathname } from './slugs';
31
+ import { localeToLang, localizedFilePath, slugToPathname } from './slugs';
32
32
  import { isAbsoluteUrl } from './url';
33
33
  import type { StarlightConfig } from './user-config';
34
34
 
@@ -145,7 +145,7 @@ function linkFromInternalSidebarLinkItem(
145
145
  // Astro passes root `index.[md|mdx]` entries with a slug of `index`
146
146
  const slug = item.slug === 'index' ? '' : item.slug;
147
147
  const localizedSlug = locale ? (slug ? locale + '/' + slug : locale) : slug;
148
- const route = routes.find((entry) => localizedSlug === entry.slug);
148
+ const route = routes.find((entry) => localizedSlug === entry.id);
149
149
  if (!route) {
150
150
  const hasExternalSlashes = item.slug.at(0) === '/' || item.slug.at(-1) === '/';
151
151
  if (hasExternalSlashes) {
@@ -170,7 +170,7 @@ function linkFromInternalSidebarLinkItem(
170
170
  const badge = item.badge ?? frontmatter.sidebar?.badge;
171
171
  const attrs = { ...frontmatter.sidebar?.attrs, ...item.attrs };
172
172
  return makeSidebarLink(
173
- slugToPathname(route.slug),
173
+ slugToPathname(route.id),
174
174
  label,
175
175
  getSidebarBadge(badge, locale, label),
176
176
  attrs
@@ -225,12 +225,13 @@ function getBreadcrumbs(path: string, baseDir: string): string[] {
225
225
  return relativePath.split('/');
226
226
  }
227
227
 
228
- /** Return the path of a route relative to the root of the collection, which is equivalent to legacy IDs. */
228
+ /** Return the path of a route relative to the root of the collection. */
229
229
  function getRoutePathRelativeToCollectionRoot(route: Route, locale: string | undefined) {
230
- return project.legacyCollections
231
- ? route.id
232
- : // For collections with a loader, use a localized filePath relative to the collection
233
- localizedId(route.entry.filePath.replace(`${docsCollectionPathFromRoot}/`, ''), locale);
230
+ // Use a localized filePath relative to the collection
231
+ return localizedFilePath(
232
+ route.entry.filePath.replace(`${docsCollectionPathFromRoot}/`, ''),
233
+ locale
234
+ );
234
235
  }
235
236
 
236
237
  /** Turn a flat array of routes into a tree structure. */
@@ -274,7 +275,7 @@ function treeify(routes: Route[], locale: string | undefined, baseDir: string):
274
275
  /** Create a link entry for a given content collection entry. */
275
276
  function linkFromRoute(route: Route, attrs?: LinkHTMLAttributes): SidebarLink {
276
277
  return makeSidebarLink(
277
- slugToPathname(route.slug),
278
+ slugToPathname(route.id),
278
279
  route.entry.data.sidebar.label || route.entry.data.title,
279
280
  route.entry.data.sidebar.badge,
280
281
  { ...attrs, ...route.entry.data.sidebar.attrs }
@@ -300,7 +301,7 @@ function sortDirEntries(dir: [string, Dir | Route][]): [string, Dir | Route][] {
300
301
  // Pages are sorted by order in ascending order.
301
302
  if (aOrder !== bOrder) return aOrder < bOrder ? -1 : 1;
302
303
  // If two pages have the same order value they will be sorted by their slug.
303
- return collator.compare(isDir(a) ? a[SlugKey] : a.slug, isDir(b) ? b[SlugKey] : b.slug);
304
+ return collator.compare(isDir(a) ? a[SlugKey] : a.id, isDir(b) ? b[SlugKey] : b.id);
304
305
  });
305
306
  }
306
307