withastro/astro · error · AstroError

ImageNotFound

ImageNotFound

Error message

Could not find requested image `${base}`. Does it exist?

What it means

Content collection images — frontmatter fields typed with image() and markdown images collected by the remark plugin — are resolved through a Vite plugin. When this.resolve() cannot find the referenced file from the importing entry, Astro throws ImageNotFound with the raw specifier.

Solutions

  1. Open the entry named in the error and verify the image path resolves relative to that file; fix typos and letter case.
  2. Ensure the image is committed to git and not excluded by .gitignore.
  3. Use relative paths from the entry to the asset.
  4. On case-insensitive systems, verify exact-name casing before pushing to Linux CI.

Example fix

# before (file on disk is cover.png)
cover: ../../assets/Cover.png

# after
cover: ../../assets/cover.png
Defensive patterns

Strategy: validation

Validate before calling

// Pre-build: verify every image() frontmatter path resolves relative to its entry
import { readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
for (const file of await collectEntries('src/content')) {
  const text = await readFile(file, 'utf8');
  for (const [, rel] of text.matchAll(/^(?:cover|image|hero|ogImage):\s*['"]?([^'"\s]+)['"]?\s*$/m)) {
    if (!existsSync(resolve(dirname(file), rel))) console.error(`missing image ${rel} in ${file}`);
  }
}

Prevention

When it happens

Trigger: A frontmatter image path that does not resolve relative to the entry (../../assets/missing.png); a typo'd filename; a case mismatch (Cover.png vs cover.png) that resolves on macOS/Windows but not Linux CI.

Common situations: Case-sensitivity differences between local dev and Linux CI; images not committed because .gitignore excludes them; assets moved during refactors; absolute paths (/assets/...) instead of relative ones.

Related errors


AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18). Data as JSON: /api/errors/9f8c55dd09880438. Report an issue: GitHub.

Appendix: source

Thrown at packages/astro/src/content/vite-plugin-content-assets.ts:57

		name: 'astro:content-asset-propagation',
		enforce: 'pre',
		resolveId: {
			filter: {
				id: new RegExp(`(?:\\?|&)(?:${CONTENT_IMAGE_FLAG}|${CONTENT_RENDER_FLAG})(?:&|=|$)`),
			},
			async handler(id, importer, opts) {
				if (hasContentFlag(id, CONTENT_IMAGE_FLAG)) {
					const [base, query] = id.split('?');
					const params = new URLSearchParams(query);
					const importerParam = params.get('importer');

					const importerPath = importerParam
						? fileURLToPath(new URL(importerParam, settings.config.root))
						: importer;

					const resolved = await this.resolve(base, importerPath, { skipSelf: true, ...opts });
					if (!resolved) {
						throw new AstroError({
							...AstroErrorData.ImageNotFound,
							message: AstroErrorData.ImageNotFound.message(base),
						});
					}
					// Preserve the content image flag in the resolved ID so that downstream plugins
					// (e.g. astro:assets:esm) can detect content collection images and avoid creating
					// full SVG components, which would import from the server runtime and cause a
					// circular module dependency deadlock when combined with top-level await (TLA).
					resolved.id = `${resolved.id}?${CONTENT_IMAGE_FLAG}`;
					return resolved;
				}
				if (hasContentFlag(id, CONTENT_RENDER_FLAG)) {
					const base = id.split('?')[0];

					for (const { extensions, handlePropagation = true } of settings.contentEntryTypes) {
						if (handlePropagation && extensions.includes(extname(base))) {
							return this.resolve(`${base}?${PROPAGATED_ASSET_FLAG}`, importer, {
								skipSelf: true,

View on GitHub (pinned to 52e6c34790)