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
- An Astro site on version 3.0 or newer, that’s when image optimization became built in
- Images currently living in your
publicfolder - Maybe 20 minutes depending on how many images you have to move
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:
- Conversion to webp, which is usually a fraction of the size of the original png or jpg
- Width and height attributes set automatically, which prevents layout shift while the page loads (Google cares about this for Core Web Vitals)
- 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.
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.
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 -->

<!-- To -->
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:
<Image>requires analtattribute and your build will fail without one. Annoying if you’re converting a lot of images at once but it’s good for accessibility and image SEO anyway, so it’s wroth just writing them.- If you get an error that Astro could not find the image, the relative path is wrong. The path in frontmatter or markdown is relative to that file, not to the project root, so a post in
src/content/blog/reachingsrc/assets/needs../../assets/. - Remote images (full
httpurls) aren’t optimized by default. You have to authorize the domain in your astro config and provide dimensions, so this post’s approach really only applies to images you host in the repo. - Not everything should move out of
public. Favicons and anything external services reference by a fixed url should stay there, since optimized image paths get hashed and change between builds. - If you’re using a pagefind search bar, the images in search results will look broken in
astro devafter this change. Don’t worry! They work once deployed, and you can verify locally withastro preview. The search index just gets built on the optimized image paths which change in dev.
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.