diff --git a/.changeset/fix-toc-global-config-zod-v4.md b/.changeset/fix-toc-global-config-zod-v4.md new file mode 100644 index 00000000000..a9bca42e5c5 --- /dev/null +++ b/.changeset/fix-toc-global-config-zod-v4.md @@ -0,0 +1,5 @@ +--- +'@astrojs/starlight': patch +--- + +Fixes a regression causing global `tableOfContents` config to be ignored diff --git a/packages/starlight/__tests__/toc-custom-config/toc-custom-config.test.ts b/packages/starlight/__tests__/toc-custom-config/toc-custom-config.test.ts new file mode 100644 index 00000000000..a75b99c27d9 --- /dev/null +++ b/packages/starlight/__tests__/toc-custom-config/toc-custom-config.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test, vi } from 'vitest'; +import { routes } from '../../utils/routing'; +import { getRouteDataTestContext } from '../test-utils'; +import { generateRouteData } from '../../utils/routing/data'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['index.mdx', { title: 'Home Page' }], + ['showcase.mdx', { title: 'ToC Disabled', tableOfContents: false }], + [ + 'environmental-impact.md', + { title: 'Explicit update date', tableOfContents: { minHeadingLevel: 2 } }, + ], + ], + }) +); + +const headings = [ + { depth: 1, slug: 'heading-1', text: 'Heading 1' }, + { depth: 2, slug: 'heading-2', text: 'Heading 2' }, + { depth: 3, slug: 'heading-3', text: 'Heading 3' }, + { depth: 4, slug: 'heading-4', text: 'Heading 4' }, +]; + +describe('custom table of contents config', () => { + test('table of contents heading levels match configuration', () => { + const route = routes[0]!; + const data = generateRouteData({ + props: { ...route, headings }, + context: getRouteDataTestContext(), + }); + expect(data.toc?.minHeadingLevel).toBe(1); + expect(data.toc?.maxHeadingLevel).toBe(4); + }); + + test('table of contents can be disabled by frontmatter', () => { + const route = routes[1]!; + const data = generateRouteData({ + props: { ...route, headings }, + context: getRouteDataTestContext(), + }); + expect(data.toc).toBeUndefined(); + }); + + test('table of contents heading levels can be customised by frontmatter', () => { + const route = routes[2]!; + const data = generateRouteData({ + props: { ...route, headings }, + context: getRouteDataTestContext(), + }); + expect(data.toc?.minHeadingLevel).toBe(2); + expect(data.toc?.maxHeadingLevel).toBe(3); + }); +}); diff --git a/packages/starlight/__tests__/toc-custom-config/vitest.config.ts b/packages/starlight/__tests__/toc-custom-config/vitest.config.ts new file mode 100644 index 00000000000..8e08279bfbb --- /dev/null +++ b/packages/starlight/__tests__/toc-custom-config/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineVitestConfig } from '../test-config'; + +export default defineVitestConfig({ + title: 'custom ToC config', + tableOfContents: { + minHeadingLevel: 1, + maxHeadingLevel: 4, + }, +}); diff --git a/packages/starlight/schema.ts b/packages/starlight/schema.ts index a90220b87e6..746734f3f78 100644 --- a/packages/starlight/schema.ts +++ b/packages/starlight/schema.ts @@ -2,7 +2,7 @@ import { z } from 'astro/zod'; import type { SchemaContext } from 'astro:content'; import { HeadConfigSchema } from './schemas/head'; import { PrevNextLinkConfigSchema } from './schemas/prevNextLink'; -import { TableOfContentsSchema } from './schemas/tableOfContents'; +import { FrontmatterTableOfContentsSchema } from './schemas/tableOfContents'; import { BadgeConfigSchema } from './schemas/badge'; import { HeroSchema } from './schemas/hero'; import { SidebarLinkItemHTMLAttributesSchema } from './schemas/sidebar'; @@ -33,7 +33,7 @@ const StarlightFrontmatterSchema = (context: SchemaContext) => head: HeadConfigSchema({ source: 'content' }), /** Override global table of contents configuration for this page. */ - tableOfContents: TableOfContentsSchema().optional(), + tableOfContents: FrontmatterTableOfContentsSchema(), /** * Set the layout style for this page. diff --git a/packages/starlight/schemas/tableOfContents.ts b/packages/starlight/schemas/tableOfContents.ts index b83ea80b958..918a010f9b3 100644 --- a/packages/starlight/schemas/tableOfContents.ts +++ b/packages/starlight/schemas/tableOfContents.ts @@ -2,18 +2,20 @@ import { z } from 'astro/zod'; const defaults = { minHeadingLevel: 2, maxHeadingLevel: 3 }; -export const TableOfContentsSchema = () => - z - .union([ - z.object({ - /** The level to start including headings at in the table of contents. Default: 2. */ - minHeadingLevel: z.int().min(1).max(6).optional().default(2), - /** The level to stop including headings at in the table of contents. Default: 3. */ - maxHeadingLevel: z.int().min(1).max(6).optional().default(3), - }), - z.boolean().transform((enabled) => (enabled ? defaults : false)), - ]) - .default(defaults) - .refine((toc) => (toc ? toc.minHeadingLevel <= toc.maxHeadingLevel : true), { - error: 'minHeadingLevel must be less than or equal to maxHeadingLevel', - }); +const TableOfContentsBaseSchema = z + .union([ + z.object({ + /** The level to start including headings at in the table of contents. Default: 2. */ + minHeadingLevel: z.int().min(1).max(6).optional().default(2), + /** The level to stop including headings at in the table of contents. Default: 3. */ + maxHeadingLevel: z.int().min(1).max(6).optional().default(3), + }), + z.boolean().transform((enabled) => (enabled ? defaults : false)), + ]) + .refine((toc) => (toc ? toc.minHeadingLevel <= toc.maxHeadingLevel : true), { + error: 'minHeadingLevel must be less than or equal to maxHeadingLevel', + }); + +export const UserConfigTableOfContentsSchema = () => TableOfContentsBaseSchema.default(defaults); + +export const FrontmatterTableOfContentsSchema = () => TableOfContentsBaseSchema.optional(); diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts index 675adae7484..f896b663485 100644 --- a/packages/starlight/utils/user-config.ts +++ b/packages/starlight/utils/user-config.ts @@ -9,7 +9,7 @@ import { PagefindConfigDefaults, PagefindConfigSchema } from '../schemas/pagefin import { SidebarItemSchema } from '../schemas/sidebar'; import { TitleConfigSchema, TitleTransformConfigSchema } from '../schemas/site-title'; import { SocialLinksSchema } from '../schemas/social'; -import { TableOfContentsSchema } from '../schemas/tableOfContents'; +import { UserConfigTableOfContentsSchema } from '../schemas/tableOfContents'; import { BuiltInDefaultLocale } from './i18n'; const LocaleSchema = z.object({ @@ -49,7 +49,7 @@ const UserConfigSchema = z.object({ tagline: z.string().optional(), /** Configure the defaults for the table of contents on each page. */ - tableOfContents: TableOfContentsSchema(), + tableOfContents: UserConfigTableOfContentsSchema(), /** Enable and configure “Edit this page” links. */ editLink: z