Skip to content

Commit 577d051

Browse files
matthewpclaude
andauthored
Fix bare images used in JSON content collections (#14590)
* fix(content): normalize bare filenames in content layer image() helper Fixes #14456 The image() helper in content layer collections now normalizes bare filenames (e.g., "cover.jpg") to relative paths (e.g., "./cover.jpg") to ensure consistent resolution behavior with markdown frontmatter. Without this normalization, bare filenames were passed directly to Vite's resolver, which treated them as package imports rather than relative file references, causing resolution failures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * test(content): add test for bare filename images in JSON collections Adds a test case to verify that bare filenames (e.g., "cover.jpg") work correctly in JSON content layer collections with the image() helper, ensuring they are resolved the same way as in markdown frontmatter. The test includes: - New rockets.json fixture with bare filename and relative path images - Test assertions for both bare filename and relative path resolution - Updated collections.json.js to include the new collection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * chore: add changeset for image path resolution fix 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(content): exclude aliases and parent paths from normalization Updates the bare filename normalization to properly skip: - Alias paths starting with ~ (e.g., ~/assets/image.jpg) - Alias paths starting with @ (e.g., @images/image.jpg) - Parent directory paths starting with ../ (e.g., ../image.jpg) This prevents breaking existing alias resolution while still normalizing true bare filenames like "cover.jpg" to "./cover.jpg". 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent d5bdbc0 commit 577d051

File tree

7 files changed

+67
-4
lines changed

7 files changed

+67
-4
lines changed

.changeset/neat-images-resolve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fixes image path resolution in content layer collections to support bare filenames. The `image()` helper now normalizes bare filenames like `"cover.jpg"` to relative paths `"./cover.jpg"` for consistent resolution behavior between markdown frontmatter and JSON content collections.

packages/astro/src/content/utils.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,27 @@ export async function getEntryDataAndImages<
184184
schema = schema({
185185
image: () =>
186186
z.string().transform((val) => {
187-
imageImports.add(val);
188-
return `${IMAGE_IMPORT_PREFIX}${val}`;
187+
// Normalize bare filenames to relative paths for consistent resolution
188+
// This ensures bare filenames like "cover.jpg" work the same way as in markdown frontmatter
189+
// Skip normalization for:
190+
// - Relative paths (./foo, ../foo)
191+
// - Absolute paths (/foo)
192+
// - URLs (http://...)
193+
// - Aliases (~/, @/, etc.)
194+
let normalizedPath = val;
195+
if (
196+
val &&
197+
!val.startsWith('./') &&
198+
!val.startsWith('../') &&
199+
!val.startsWith('/') &&
200+
!val.startsWith('~') &&
201+
!val.startsWith('@') &&
202+
!val.includes('://')
203+
) {
204+
normalizedPath = `./${val}`;
205+
}
206+
imageImports.add(normalizedPath);
207+
return `${IMAGE_IMPORT_PREFIX}${normalizedPath}`;
189208
}),
190209
});
191210
}

packages/astro/test/content-layer.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,16 @@ describe('Content Layer', () => {
281281
assert.ok(json.images[1].data.image.startsWith('https://'));
282282
});
283283

284+
it('loads images with bare filenames in JSON', async () => {
285+
assert.ok(json.rockets[0].data.image.src.startsWith('/_astro'));
286+
assert.equal(json.rockets[0].data.image.format, 'jpg');
287+
});
288+
289+
it('loads images with relative paths in JSON', async () => {
290+
assert.ok(json.rockets[1].data.image.src.startsWith('/_astro'));
291+
assert.equal(json.rockets[1].data.image.format, 'jpg');
292+
});
293+
284294
it('renders images from frontmatter', async () => {
285295
assert.ok($('img[alt="Lunar Module"]').attr('src').startsWith('/_astro'));
286296
});

packages/astro/test/fixtures/content-layer/src/content.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,17 @@ const songs = defineCollection({
253253
}),
254254
});
255255

256+
const rockets = defineCollection({
257+
loader: file('src/data/rockets.json'),
258+
schema: ({ image }) =>
259+
z.object({
260+
id: z.string(),
261+
name: z.string(),
262+
manufacturer: z.string(),
263+
image: image(),
264+
}),
265+
});
266+
256267
export const collections = {
257268
blog,
258269
dogs,
@@ -270,6 +281,7 @@ export const collections = {
270281
songs,
271282
probes,
272283
rodents,
284+
rockets,
273285
notADirectory,
274286
nothingMatches
275287
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"id": "falcon-9",
4+
"name": "Falcon 9",
5+
"manufacturer": "SpaceX",
6+
"image": "shuttle.jpg"
7+
},
8+
{
9+
"id": "saturn-v",
10+
"name": "Saturn V",
11+
"manufacturer": "NASA",
12+
"image": "./shuttle.jpg"
13+
}
14+
]
170 KB
Loading

packages/astro/test/fixtures/content-layer/src/pages/collections.json.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export async function GET() {
3434
const nestedJsonLoader = await getCollection('birds');
3535

3636
const csvLoader = await getCollection('plants');
37-
37+
38+
const rockets = await getCollection('rockets');
39+
3840
const numbers = await getCollection('numbers');
3941

4042
const numbersYaml = await getCollection('numbersYaml');
@@ -55,7 +57,8 @@ export async function GET() {
5557
numbers,
5658
numbersYaml,
5759
numbersToml,
58-
images,
60+
images,
61+
rockets,
5962
probes,
6063
yamlLoader,
6164
tomlLoader,

0 commit comments

Comments
 (0)