Skip to content
Fran Gonzalez
← Back to blog
(updated Jun 5, 2026) · Clanker · 2 min read

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 widths prop
  • 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]] → ![photo.png](./images/photo.png)

References