Skip to content

Commit 77a1104

Browse files
authored
Fix image metadata validation in StarlightPage schema (#3118)
1 parent 3a087d8 commit 77a1104

File tree

3 files changed

+110
-21
lines changed

3 files changed

+110
-21
lines changed

.changeset/warm-adults-wash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/starlight': patch
3+
---
4+
5+
Fixes passing imported SVGs to the `frontmatter` prop of the `<StarlightPage>` component in Astro ≥5.7.0

packages/starlight/__tests__/basics/starlight-page-route-data.test.ts

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import type { ImageMetadata } from 'astro';
12
import { expect, test, vi } from 'vitest';
2-
import { getRouteDataTestContext } from '../test-utils';
3-
import { generateRouteData } from '../../utils/routing/data';
43
import { routes } from '../../utils/routing';
4+
import { generateRouteData } from '../../utils/routing/data';
55
import {
66
generateStarlightPageRouteData,
77
type StarlightPageProps,
88
} from '../../utils/starlight-page';
9+
import { getRouteDataTestContext } from '../test-utils';
910

1011
vi.mock('virtual:starlight/collection-config', async () =>
1112
(await import('../test-utils')).mockedCollectionConfig()
@@ -523,3 +524,89 @@ test('generates data with a similar root shape to regular route data', async ()
523524

524525
expect(Object.keys(data).sort()).toEqual(Object.keys(starlightPageData).sort());
525526
});
527+
528+
test('parses an ImageMetadata object successfully', async () => {
529+
const fakeImportedImage: ImageMetadata = {
530+
src: '/image-src.png',
531+
width: 100,
532+
height: 100,
533+
format: 'png',
534+
};
535+
const data = await generateStarlightPageRouteData({
536+
props: {
537+
...starlightPageProps,
538+
frontmatter: {
539+
...starlightPageProps.frontmatter,
540+
hero: {
541+
image: { file: fakeImportedImage },
542+
},
543+
},
544+
},
545+
context: getRouteDataTestContext(starlightPagePathname),
546+
});
547+
expect(data.entry.data.hero?.image).toBeDefined();
548+
// @ts-expect-error — image’s type can be different shapes but we know it’s this one here
549+
expect(data.entry.data.hero?.image!['file']).toMatchInlineSnapshot(`
550+
{
551+
"format": "png",
552+
"height": 100,
553+
"src": "/image-src.png",
554+
"width": 100,
555+
}
556+
`);
557+
});
558+
559+
test('parses an image that is also a function successfully', async () => {
560+
const fakeImportedSvg = (() => {}) as unknown as ImageMetadata;
561+
Object.assign(fakeImportedSvg, { src: '/image-src.svg', width: 100, height: 100, format: 'svg' });
562+
const data = await generateStarlightPageRouteData({
563+
props: {
564+
...starlightPageProps,
565+
frontmatter: {
566+
...starlightPageProps.frontmatter,
567+
hero: {
568+
image: { file: fakeImportedSvg },
569+
},
570+
},
571+
},
572+
context: getRouteDataTestContext(starlightPagePathname),
573+
});
574+
expect(data.entry.data.hero?.image).toBeDefined();
575+
// @ts-expect-error — image’s type can be different shapes but we know it’s this one here
576+
expect(data.entry.data.hero?.image!['file']).toMatchInlineSnapshot(`[Function]`);
577+
// @ts-expect-error
578+
expect(data.entry.data.hero?.image!['file']).toHaveProperty('src');
579+
// @ts-expect-error
580+
expect(data.entry.data.hero?.image!['file']).toHaveProperty('width');
581+
// @ts-expect-error
582+
expect(data.entry.data.hero?.image!['file']).toHaveProperty('height');
583+
// @ts-expect-error
584+
expect(data.entry.data.hero?.image!['file']).toHaveProperty('format');
585+
});
586+
587+
test('fails to parse an image without the expected metadata properties', async () => {
588+
await expect(() =>
589+
generateStarlightPageRouteData({
590+
props: {
591+
...starlightPageProps,
592+
frontmatter: {
593+
...starlightPageProps.frontmatter,
594+
hero: {
595+
image: {
596+
// @ts-expect-error intentionally incorrect input
597+
file: () => {},
598+
},
599+
},
600+
},
601+
},
602+
context: getRouteDataTestContext(starlightPagePathname),
603+
})
604+
).rejects.toThrowErrorMatchingInlineSnapshot(`
605+
"[AstroUserError]:
606+
Invalid frontmatter props passed to the \`<StarlightPage/>\` component.
607+
Hint:
608+
**hero.image**: Did not match union.
609+
> Expected type \`file | { dark; light } | { html: string }\`
610+
> Received \`{}\`"
611+
`);
612+
});

packages/starlight/utils/starlight-page.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from 'astro/zod';
2-
import { type ContentConfig, type SchemaContext } from 'astro:content';
2+
import { type ContentConfig, type ImageFunction, type SchemaContext } from 'astro:content';
33
import project from 'virtual:starlight/project-context';
44
import config from 'virtual:starlight/user-config';
55
import { getCollectionPathFromRoot } from './collection';
@@ -179,25 +179,22 @@ export async function generateStarlightPageRouteData({
179179

180180
/** Validates the Starlight page frontmatter properties from the props received by a Starlight page. */
181181
async function getStarlightPageFrontmatter(frontmatter: StarlightPageFrontmatter) {
182-
// This needs to be in sync with ImageMetadata.
183-
// https://github.com/withastro/astro/blob/cf993bc263b58502096f00d383266cd179f331af/packages/astro/src/assets/types.ts#L32
184182
const schema = await StarlightPageFrontmatterSchema({
185-
image: () =>
186-
z.object({
187-
src: z.string(),
188-
width: z.number(),
189-
height: z.number(),
190-
format: z.union([
191-
z.literal('png'),
192-
z.literal('jpg'),
193-
z.literal('jpeg'),
194-
z.literal('tiff'),
195-
z.literal('webp'),
196-
z.literal('gif'),
197-
z.literal('svg'),
198-
z.literal('avif'),
199-
]),
200-
}),
183+
image: (() =>
184+
// Mock validator for ImageMetadata.
185+
// https://github.com/withastro/astro/blob/cf993bc263b58502096f00d383266cd179f331af/packages/astro/src/assets/types.ts#L32
186+
// It uses a custom validation approach because imported SVGs have a type of `function` as
187+
// well as containing the metadata properties and this ensures we handle those correctly.
188+
z.custom(
189+
(value) =>
190+
value &&
191+
(typeof value === 'function' || typeof value === 'object') &&
192+
'src' in value &&
193+
'width' in value &&
194+
'height' in value &&
195+
'format' in value,
196+
'Invalid image passed to `<StarlightPage>` component. Expected imported `ImageMetadata` object.'
197+
)) as ImageFunction,
201198
});
202199

203200
// Starting with Astro 4.14.0, a frontmatter schema that contains collection references will

0 commit comments

Comments
 (0)