How to Optimize Images in Astro Markdown and Content Collections

Jul 26, 2024 Updated Jul 5, 2026

One of the easiest wins on an Astro site is moving images out of public and into the built-in image pipeline. That’s what this post covers: regular images, blog images in content collections, and the handful of path/schema changes you need to make it all work. Once it’s set up, Astro handles WebP conversion, dimensions, and lazy loading for you at build time.

If you’re just interested in the code here it is as a template website.

What You Need

Why Use Astro’s Built-In Optimization?

The public folder contents are all directly copied over into the build folder without any pre-processing, so anything in there is served exactly as you saved it. A 2MB screenshot stays a 2MB screenshot. By moving images into src and using Astro’s <Image> component you get, at build time:

  1. Conversion to webp, which is usually a fraction of the size of the original png or jpg
  2. Width and height attributes set automatically, which prevents layout shift while the page loads (Google cares about this for Core Web Vitals)
  3. Lazy loading and async decoding by default

You could do this with manual Sharp scripts or an image CDN like Cloudinary, but Astro’s built-in path is simpler and fits a static site better. No external service, no runtime work, and no extra moving parts once the migration is done.

Statically Linked Images

First, we need to move all of our images that we’re serving into src instead of in the public folder.

Anywhere in your code that you have statically defined links to images like

<img src="/public/assets/my-image.png" alt="My alt" />

After moving the image from public/assets into src/assets we can replace it with the following.

---
import { Image } from "astro:assets";
import localBirdImage from '../../assets/my-image.png';
---
<Image src={localBirdImage} alt="My alt">

By using <Image> Astro will automatically optimize your image for the web at build-time, and that’s all you have to do for statically linked images.

With Dynamic Images From Blogs

This part is a little trickier because blog images aren’t imported the same way as images in a normal component, but Astro gives you a clean way to handle it.

In your src/content/config.ts you might have something like this.

src/content/config.ts
const blog = defineCollection({
  schema: z.object({
    heroImage: z.string().optional(), // a url like /public/assets/heroImage.png
    // ... other fields
  }),
});

For us to be able to use the image in an <Image> component, we have to update the schema to be of the image type. This tells Astro to treat the field as an image import instead of a plain string.

src/content/config.ts
const blog = defineCollection({
  schema: ({ image }) =>
    z.object({
      heroImage: image().optional(),
      // ... other fields
    }),
});

Next for each of your blogs update your metadata to point to the image now in the src folder.

# From
heroImage: "/public/assets/blog-placeholder-3.jpg"
# To
heroImage: "../../assets/blog-placeholder-3.jpg"

Then update any references to images in your markdown itself. Once these are relative paths into src, Astro optimizes them automatically with no extra work.

<!-- From -->
 
![blog placeholder](/public/assets/blog-placeholder-about.jpg)
 
<!-- To -->
 
![blog placeholder](../../assets/blog-placeholder-about.jpg)

After that, as we did before, update any references that were using <img> to use <Image>.

<!-- From -->
<img
    src={article.data.heroImage}
    alt=""
    data-pagefind-meta="image[src], image_alt[alt]"
/>
<!-- To -->
<Image
    src={article.data.heroImage}
    alt=""
    data-pagefind-meta="image[src], image_alt[alt]"
>

Then, Astro will automatically optimize all of your images!

Troubleshooting

Some things that might trip you up along the way:

And that’s it, your images should now be a lot smaller. Here’s a template website with optimized images and a search bar. If you’re building out an Astro blog I’ve also written about adding interactive charts and adding Giscus comments.

Related posts

← Back to blog More in Building with Astro · 7 posts →