Skip to content
Astro as images 4 min read

Images in Markdown & MDX

Most of the images on a documentation or blog site live inside written content, not hand-authored components. Astro treats images referenced from Markdown and MDX as first-class assets: any relative image path is run through the same optimization pipeline as the <Image /> component, so you get hashed filenames, modern formats, and dimension metadata without writing a single line of build config. This page explains how that pipeline kicks in, when it does not, and how to size images so the browser never reflows your layout.

How relative images are optimized

When you use standard Markdown image syntax with a path relative to the source file, Astro intercepts it during the build, imports the underlying asset, and rewrites the output URL to a processed, content-hashed file.

![A diagram of the request lifecycle](../../assets/lifecycle.png)

At build time Astro will:

  • Copy the image into the output with a hashed filename for long-term caching.
  • Infer the intrinsic width and height and emit them on the <img> tag so the browser reserves space (no layout shift).
  • Optionally transcode it to an optimized format depending on your image service settings.

This only applies to local relative paths. Two other path types behave differently:

Path styleExampleOptimized?Notes
Relative local![](../assets/hero.png)YesImported and processed by the assets pipeline.
Absolute from public/![](/images/hero.png)NoServed verbatim, no hashing or sizing.
Remote URL![](https://cdn.site/hero.png)NoLeft untouched; you control sizing.

Files in public/ are intentionally left alone. Use them for things that must keep a stable, predictable URL — favicons, social-card images referenced by external tools, or assets you optimize yourself.

Storing images for content collections

For Markdown managed by a content collection, keep images alongside the content rather than in public/. A common layout co-locates each post with its assets:

src/content/blog/
  my-post/
    index.md
    hero.png

Then reference the file relatively from index.md:

![Cover art](./hero.png)

You can also validate and import images through the collection schema using the image helper, which is useful when an image is metadata (like a cover) rather than inline body content.

import { defineCollection, z } from "astro:content";

const blog = defineCollection({
  type: "content",
  schema: ({ image }) =>
    z.object({
      title: z.string(),
      cover: image(),
      coverAlt: z.string(),
    }),
});

export const collections = { blog };

The image() helper returns a validated ImageMetadata object — the same shape an import produces — so you can pass entry.data.cover straight into the <Image /> component in your layout.

Sizing images: Markdown vs MDX

Plain Markdown cannot set attributes like width, but because Astro already reads the intrinsic dimensions, the rendered <img> carries correct width and height automatically. If you need explicit control, switch the file to MDX, where you can mix components and JSX expressions directly into prose.

---
title: "Using the Image component in MDX"
---

import { Image } from "astro:assets";
import diagram from "../assets/architecture.svg";

Here is the architecture at a fixed display size:

<Image src={diagram} width={640} alt="Service architecture" />

You can still use plain Markdown images in the same file:

![Inline screenshot](./screenshot.png)

In MDX you import the image as a module and hand the resulting metadata object to <Image />. Astro uses the imported intrinsic size to compute the omitted dimension, so passing only width preserves the aspect ratio.

Always provide a meaningful alt. Astro’s <Image /> requires the alt attribute and will throw a build error if it is missing — a deliberate accessibility guardrail.

Customizing the Markdown image renderer

If you want every Markdown image to render through <Image /> with shared defaults, MDX lets you override the img element via the components map.

---
// src/components/MarkdownImage.astro
import { Image } from "astro:assets";
const { src, alt } = Astro.props;
---
<Image src={src} alt={alt} width={800} class="rounded-lg" />
import MarkdownImage from "../components/MarkdownImage.astro";

export const components = { img: MarkdownImage };

![This now renders through the Image component](./photo.png)

This keeps your prose authoring simple — plain ![]() syntax — while every image still flows through the optimization pipeline with your styling and sizing rules applied. Because Astro ships zero JavaScript for static images, none of this adds client-side weight; the work happens entirely at build time.

Best Practices

  • Co-locate content images with their Markdown file and reference them with relative paths so they pass through the assets pipeline.
  • Reserve public/ for assets that need stable, unhashed URLs; everything else belongs in src/.
  • Use the content-collection image() schema helper for cover/metadata images so paths are validated at build time.
  • Reach for MDX when you need explicit sizing, class names, or to swap in <Image /> per image.
  • Always write descriptive alt text — <Image /> enforces it, and Markdown images should follow the same bar.
  • Prefer letting Astro infer dimensions; only override width/height when the display size genuinely differs from the source.
Last updated June 14, 2026
Was this helpful?