I use Astro's Mizuki theme (forked version of Fuwari) and I try to migrate to use Content Loader API
and I found problem with ImageWrapper.astro
Due to Astro v5 and the deprecation of how content was implicitly handled, the cover image for the guide post located at src/content/posts/guide/index.md was not loading correctly. Namely, ImageWrapper was looking for ../../content/posts/cover.jpeg instead of ../../content/posts/guide/cover.jpeg
example: I set image: in frontmatter as image: "./cover.jpeg" (same folder as main markdown file for post src/content/posts/guide/index.md) script not detect image but when I set to image: "./guide/cover.jpeg" or "/guide/cover.jpeg" script detect image
#migrate component for support nested content structures
22 messages · Page 1 of 1 (latest)
Hi @little valve. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
The issue you're encountering is related to how Astro's Content Collections and the Content Loader API resolve image paths in frontmatter, especially after migrating to Astro v5.
How Astro Resolves Image Paths in Content Collections
When you specify an image in your frontmatter like this:
image: "./cover.jpeg"
Astro expects the path to be relative to the Markdown file itself. So, for a file at src/content/posts/guide/index.md, the path ./cover.jpeg should resolve to src/content/posts/guide/cover.jpeg—which is the behavior you want.
If your component or script is looking for ../../content/posts/cover.jpeg instead, it suggests that the path resolution logic is not using the Markdown file's directory as the base, but perhaps the project root or another directory. This is a common pitfall when migrating from legacy content handling to the new Content Loader API.
Correct Usage with Content Loader API
Astro's recommended approach is:
-
In your Markdown frontmatter, use a path relative to the Markdown file:
image: "./cover.jpeg" -
In your
content.config.ts, use theimage()helper in your schema:import { defineCollection, z } from "astro:content"; const posts = defineCollection({ schema: ({ image }) => z.object({ title: z.string(), image: image(), // ...other fields }), });This ensures Astro will resolve and import the image correctly, turning it into metadata you can use in your components.
-
In your Astro component, use the imported image metadata:
<Image src={post.data.image} alt={post.data.title} />
Troubleshooting
-
If
./cover.jpegdoes not work but./guide/cover.jpegdoes:
This suggests your component or loader is not using the correct base directory for resolving the image path. Double-check that you are using theimage()helper in your schema, as it handles this resolution for you.
If you are manually resolving image paths, you should stop and let Astro's schema helpers do the work. -
Do not use absolute or project-root-relative paths in frontmatter.
Always use paths relative to the Markdown file for local images in content collections.
Reference
Summary
To fix your issue, ensure:
- Your frontmatter uses
image: "./cover.jpeg"(relative to the Markdown file). - Your content collection schema uses the
image()helper. - You are not manually resolving image paths in your components—let Astro handle it.
If you follow these steps and still encounter issues, double-check that your migration to the Content Loader API is complete and that your content.config.ts is set up as shown above. If the problem persists, there may be a custom script or logic in the Mizuki/Fuwari theme that needs updating to align with Astro v5's content handling.
If you need to see a full example, let me know!
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@little valve Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@coral pebble in collection schema I setup like this
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const postsCollection = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/posts/" }),
schema: z.object({
title: z.string(),
published: z.date(),
updated: z.date().optional(),
draft: z.boolean().optional().default(false),
description: z.string().optional().default(""),
image: z.string().optional().default(""),
tags: z.array(z.string()).optional().default([]),
category: z.string().optional().nullable().default(""),
lang: z.string().optional().default(""),
pinned: z.boolean().optional().default(false),
author: z.string().optional().default(""),
sourceLink: z.string().optional().default(""),
licenseName: z.string().optional().default(""),
licenseUrl: z.string().optional().default(""),
/* Page encryption fields */
encrypted: z.boolean().optional().default(false),
password: z.string().optional().default(""),
/* For internal use */
prevTitle: z.string().default(""),
prevSlug: z.string().default(""),
nextTitle: z.string().default(""),
nextSlug: z.string().default(""),
}),
});
const specCollection = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/spec/" }),
schema: z.object({}),
});
export const collections = {
posts: postsCollection,
spec: specCollection,
};
how should I update?
Let me check the knowledge base for you again. One moment! :books:
To fully support local images in your content collection (e.g., for a field like image in your frontmatter), you should replace the image: z.string().optional().default("") field with Astro’s image() helper in your schema. This allows Astro to resolve and import the image file, enabling you to use it directly in your components (such as with the <Image /> component), and ensures correct path resolution for nested content structures.
Here’s how you should update your postsCollection schema:
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const postsCollection = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/posts/" }),
schema: ({ image }) => z.object({
title: z.string(),
published: z.date(),
updated: z.date().optional(),
draft: z.boolean().optional().default(false),
description: z.string().optional().default(""),
image: image().optional(), // <-- Use the image() helper here
tags: z.array(z.string()).optional().default([]),
category: z.string().optional().nullable().default(""),
lang: z.string().optional().default(""),
pinned: z.boolean().optional().default(false),
author: z.string().optional().default(""),
sourceLink: z.string().optional().default(""),
licenseName: z.string().optional().default(""),
licenseUrl: z.string().optional().default(""),
encrypted: z.boolean().optional().default(false),
password: z.string().optional().default(""),
prevTitle: z.string().default(""),
prevSlug: z.string().default(""),
nextTitle: z.string().default(""),
nextSlug: z.string().default(""),
}),
});
const specCollection = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/spec/" }),
schema: z.object({}),
});
export const collections = {
posts: postsCollection,
spec: specCollection,
};
**Key changes:**
- The schema is now a function: `schema: ({ image }) => z.object({ ... })`
- The `image` field uses `image().optional()` instead of `z.string().optional().default("")`
**Why?**
This change ensures that when you specify `image: "./cover.jpeg"` in your Markdown frontmatter, Astro will resolve the path relative to the Markdown file and import the image correctly, even for nested structures. This is the recommended approach in Astro v3+ and v5 for handling images in content collections [Images in content collections](<https://docs.astro.build/en/guides/images/#images-in-content-collections>) [Update Content Collections schemas](<https://docs.astro.build/en/guides/upgrade-to/v3/#update-content-collections-schemas>).
If you need to support remote images or images as plain strings, you can add a separate field for that, but for local images, always use the `image()` helper.
If you have further custom requirements, let me know!
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
@little valve Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@coral pebble image: image().optional().default(""), is work too?
Let me check the knowledge base for you again. One moment! :books:
No, you should not use .default("") with image() in your schema. The knowledge sources do not mention or recommend using .default("") with the image() helper. The correct usage is image().optional(), which allows the field to be omitted or undefined if not present in the frontmatter.
Using .default("") is appropriate for string fields, but image() returns image metadata, not a string, so setting a default of an empty string is not valid and may cause type or runtime errors.
The recommended approach is:
image: image().optional()
This is shown in the official documentation for images in content collections and in schema examples using the image() helper.
If you want the field to be optional, use only .optional()—do not add .default("") to image() fields.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@little valve Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@coral pebble found error
[GenerateContentTypesError] `astro sync` command failed to generate content collection types: image is not defined
Hint:
This error is often caused by a syntax error inside your content, or your content configuration file. Check your src/content.config.ts file for typos.
Let me check the knowledge base for you again. One moment! :books:
The error message
[GenerateContentTypesError] `astro sync` command failed to generate content collection types: image is not defined
Hint:
This error is often caused by a syntax error inside your content, or your content configuration file. Check your src/content.config.ts file for typos.
indicates that there is a problem with how the image helper is being used in your src/content.config.ts file.
According to the knowledge sources, the image helper is not imported directly. Instead, it is provided as an argument to the schema function. Your schema should look like this:
schema: ({ image }) => z.object({
// ...
image: image().optional(),
// ...
})
If you try to use image() outside of the schema function or forget to use the function form for the schema, you will get an "image is not defined" error. Make sure you are not importing image directly or referencing it outside the schema function context Removed: image from astro:content in content collections schema Images in content collections.
Summary:
- Do not import
imagefrom"astro:content". - Use the schema as a function:
schema: ({ image }) => z.object({ ... }). - Reference
image()only inside that function.
This should resolve the error you are seeing GenerateContentTypesError.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@little valve Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@coral pebble found another error
20:58:03 ▶ src/pages/posts/[...slug].astro
20:58:03 ├─ /posts/markdown-tutorial/index.html (+91ms)
20:58:03 ├─ /posts/encrypted-post/index.html (+667ms)
20:58:04 ├─ /posts/markdown-extended/index.html (+20ms)
20:58:04 ├─ /posts/guide/index.htmlsrc.startsWith is not a function
Stack trace:
at file:///D:/symlink/happiness2script/vue/Mizuki/dist/chunks/MainGridLayout_DeCOG63l.mjs:2588:25
at AstroComponentInstance.init (file:///D:/symlink/happiness2script/vue/Mizuki/dist/chunks/astro/server_x7q2dT5S.mjs:5888:29)
at Object.render (file:///D:/symlink/happiness2script/vue/Mizuki/dist/chunks/astro/server_x7q2dT5S.mjs:6287:23)
at file:///D:/symlink/happiness2script/vue/Mizuki/dist/chunks/astro/server_x7q2dT5S.mjs:5406:18
at createBufferedRenderer (file:///D:/symlink/happiness2script/vue/Mizuki/dist/chunks/astro/server_x7q2dT5S.mjs:1068:10)
ELIFECYCLE Command failed with exit code 1.
Let me check the knowledge base for you again. One moment! :books:
The error src.startsWith is not a function suggests that somewhere in your code, a value expected to be a string (with a .startsWith method) is actually an object or another type. In the context of Astro content collections and the image() helper, this often happens if you treat the image field as a string, but it is actually an image metadata object when using the image() schema helper.
What’s happening:
- When you use
image: image().optional()in your schema, the value ofimagein your frontmatter is no longer a string path, but an object containing image metadata. - If your component or template code still expects
imageto be a string (e.g., tries to callimage.startsWith(...)), you will get this error.
How to fix:
- Update your component code to use the image metadata object as required by Astro. For example, if you are passing
imageto the<Image />component, you should pass the whole object, not just a string path.
Example usage in your Astro component:
---
// Assuming post.data.image is the image metadata object
import { Image } from 'astro:assets';
---
{post.data.image && (
<Image src={post.data.image} alt={post.data.title} />
)}
Do not use post.data.image.src.startsWith(...) or similar unless you have confirmed post.data.image is a string (which it is not when using the image() helper).
Summary:
- The
imagefield is now an object, not a string. - Update your code to use it as an object, especially when passing to
<Image />or similar components.
If you need to check if the image exists, use a simple truthy check (if (post.data.image)), not string methods.