Image Optimization in Astro
Replacing raw img tags with Astro's built-in Image component for automatic optimization, responsive srcsets, and format conversion.
The Problem
Images were served from public/images/ with raw HTML <img> tags. No optimization, no responsive sizing, no modern format conversion. A 47kB JPEG shipped as-is to every device.
What Changed
Astro provides built-in image optimization through the <Image /> component and the image() content schema helper. No external plugins required — just sharp as a dependency.
Install Sharp
pnpm add sharp
Move Images to Content Directory
Images now live alongside their posts in src/content/blog/images/ — the Astro-recommended pattern:
src/content/blog/
images/
obsidian-nuxt-workflow.jpeg
obsidian-embed-test.svg
obsidian-nuxt-content-workflow-example.md
Update Content Schema
The image() helper validates and imports images as metadata objects:
// src/content.config.ts
const blog = defineCollection({
schema: ({ image }) =>
z.object({
// ... other fields
image: image().optional(),
}),
});
Update Frontmatter
Relative paths from the content file:
image: ./images/obsidian-nuxt-workflow.jpeg
Replace <img> with <Image />
---
import { Image } from "astro:assets";
---
<Image
src={post.data.image}
alt={post.data.title}
width={1200}
height={630}
class="mt-6 w-full rounded-xl object-cover"
loading="eager"
widths={[640, 828, 1200]}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 80vw, 1200px"
/>
OG Images
OG meta tags require URL strings. Use getImage() to process the image:
---
import { getImage } from "astro:assets";
const ogImage = post.data.image
? await getImage({ src: post.data.image, width: 1200, height: 630 })
: undefined;
---
<meta property="og:image" content={ogImage?.src} />
Astro Config
// astro.config.ts
image: {
layout: "constrained",
responsiveStyles: true,
},
The layout: "constrained" setting enables automatic srcset and sizes generation for all <Image /> components.
Results
Build output shows the optimization:
generating optimized images
▶ /_astro/obsidian-nuxt-workflow.xxx.webp (before: 47kB, after: 14kB)
▶ /_astro/obsidian-nuxt-workflow.xxx.webp (before: 47kB, after: 20kB)
▶ /_astro/obsidian-nuxt-workflow.xxx.webp (before: 47kB, after: 31kB)
- 47kB → 14kB (70% reduction) at smallest srcset
- WebP format generated automatically
- 3 responsive sizes via
widthsprop - No CLS — width/height inferred from image metadata
New Posts
For new blog posts, add images to src/content/blog/images/ and reference them in frontmatter:
---
image: ./images/my-cover-photo.jpeg
---
The normalize-obsidian script automatically converts Obsidian embed syntax to relative paths:
![[photo.png]] → 