🚀 Adding Responsive Images to Astro Blog Posts
Let’s take a look at the built-in Astro Picture component. Astro Content Collections offer a fantastic Developer Experience for building Markdown blog or documentation sites, but did you know how well they dovetail with the Picture component?
The Astro Picture component does the heavy lifting in adding responsive image markup to your content sites. If you have ever had to construct HTML <picture>
tags from scratch, you will know supporting devices with different capabilities and screen-sizes in a performant way is not easy.
Usually with responsive images, we mean serving large, double pixel density images, for users with Retina displays, but dialling down the resolution for a user on a smaller mobile device, which is able to make the image look equally sharp with a fraction of the pixels.
Responsive and Next-gen Images
Sending the right sized image for the user device, reduces the number of bytes streamed. Another popular way of reducing shipped image bytes is using WebP and AVIF images. These next-gen formats should encode images more efficiently, allowing for smaller files, though older devices do not support them.
😕 The Astro Picture Component
The HTML <picture>
tag, essentially, is a way of communicating to the browser which formats you have available on the server and in what sizes, and letting the browser pick the most suitable one. You can imagine, adding this much detail for each image can become cumbersome. And we haven’t even mentioned adding markup to prevent Cumulative Layout Shift (CLS) yet!
The Astro Picture component is designed to help you here, saving you from having to construct that HTML <picture>
tag yourself. In this post, we see how you can work with the Astro Picture component to add a banner image to each post on a blog site. As a bonus, we also look at using Astro Content Collections, to generate a low-resolution placeholder image to display while the banner image is loading.
🧱 What are we Building?
Instead of building a new blog from start-to-finish, we’ll just see the steps you need to add banner images to blog posts to an existing Astro Markdown site with Content Collections. There is a link to the complete project code further down the page.
👮🏽 Content Collections
Content collections are invaluable for making sure your content Markdown front matter has all the fields you need it to have. The schema uses zod
under the hood, so you might already be familiar with the syntax. Here, we will see how you can also use the schema to generate the image metadata needed for use with Astro’s Picture component.
---
postTitle: 'Best Medium Format Camera for Starting Out'
datePublished: '2023-10-07T16:04:42.000+0100'
lastUpdated: '2022-10-23T10:17:52.000+0100'
featuredImage: './best-medium-format-camera-for-starting-out.jpg'
featuredImageAlt: 'Photograph of a Hasselblad medium format camera with the focusing screen exposed'
---
## What is a Medium Format Camera?
If you are old enough to remember the analogue film camera era, chances are it is the 35 mm canisters with the track cut down the side that first come to mind. Shots normally...
The first step is to place the banner image in the content folder, within your Astro project. Here, I copied the image I want to use to the same directory as the Markdown file containing the post content.
Then, above, you can see I added a featuredImage
field to the post front matter, with the path to the image (path is relative to the Markdown file’s own path, src/content/posts/best-medium-format-camera-for-starting-out/index.md
). Equally important is the image alt text (in line 6
), which we also want to feed into the Astro Picture component.
Schema File
The next change is to the schema file (at src/content/config.ts
). A schema is not required for using Content Collections generally, though you will need one to generate the image meta. See zod
docs for explanation of anything that you are not familiar with, or drop a question below in the comments.
import { defineCollection, z } from 'astro:content';
const postsCollection = defineCollection({
type: 'content',
schema: ({ image }) =>
z.object({
postTitle: z.string(),
datePublished: z.string(),
lastUpdated: z.string(),
featuredImage: image(),
featuredImageAlt: z.string(),
ogImage: z.string().optional(),
draft: z.boolean(),
}),
});
export const collections = {
posts: postsCollection,
};
For Astro to generate the image metadata, just include the image function as an argument to the callback in line 5
, and call that same function on the featuredImage
field. If you include multiple image fields in the front matter, you can call image
on each.
Calling image
automatically generates the following fields for the picture, which we use in the next section:
const featuredImage: {
src: string;
width: number;
height: number;
format: "png" | "jpg" | "jpeg" | "tiff" | "webp" | "gif" | "svg" | "avif";
}
🧩 Adding the Astro Picture Component
The final missing piece of the puzzle is pulling in the metadata generated in the schema to an instance of the Astro Picture component. The first step, then, is to extract that metadata within the front matter section of the blog post page template (src/pages/[slug].astro
):
---
import { Picture } from 'astro:assets';
import type { CollectionEntry } from 'astro:content';
import { getCollection } from 'astro:content';
import BaseLayout from '~layouts/BaseLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts.map(
({
data: { featuredImage, featuredImageAlt, postTitle },
render,
slug,
}) => {
return {
params: { slug },
props: {
featuredImage,
featuredImageAlt,
render,
postTitle,
},
};
},
);
}
type PostCollection = CollectionEntry<'posts'>;
type Props = Pick<
PostCollection['data'],
'featuredImage' | 'featuredImageAlt' | 'seoMetaDescription' | 'placeholder' | 'postTitle'
> &
Pick<PostCollection, 'render'>;
const { slug } = Astro.params;
const {
featuredImage,
featuredImageAlt,
render,
postTitle: title,
}: Props = Astro.props;
const { Content } = await render();
const { format, width, height } = featuredImage;
---
Adding the Picture Component to Astro Markup
To finish off, let’s add the Astro Picture component to the markup:
---
// TRUNCATED...
---
<BaseLayout {...seoProps}>
<h1>{title}</h1>
<div class="image-wrapper">
<Picture
src={featuredImage}
alt={featuredImageAlt}
densities={[1.5, 2]}
formats={['avif', 'webp']}
fallbackFormat={format}
loading="eager"
fetchpriority="high"
/>
</div>
<div class="container">
<Content />
</div>
</BaseLayout>
- We imported the Picture from
astro:assets
in line2
of the front matter. - The component has the attributes you expect on a regular HTML
img
tag, such assrc
,alt
andloading
. - For most images,
loading
will belazy
, to optimize page load. Generally, though, for a banner image or other large contentful object initially displayed, you will pickeager
, and also, to setfetchpriority
tohigh
.
Notice, we also have densities
, formats
and fallbackFormat
props. These are used to generate the final HTML <picture>
tag.
-
densities
are the pixel densities used for high resolution screens, like Retina displays, typically1.5
and2
will be just fine here. -
formats
are the next-gen formats you want the images available in. To be safe, you might choose justwebp
as there have been issues with displaying someavif
images in Safari. An alternative is to use content negotiation. -
fallbackFormat
this is the format you want older browsers to use (browsers which do not support WebP or AVIF), and is probably the format of the original image.
Generated HTML Markup
Using the above parameters, the final markup looks something like this:
<picture
><source
srcset="
/_astro/best-medium-format-camera-for-starting-out.8696310b_Zzixeg.avif,
/_astro/best-medium-format-camera-for-starting-out.8696310b_Zzixeg.avif 1.5x,
/_astro/best-medium-format-camera-for-starting-out.8696310b_Zzixeg.avif 2x
"
type="image/avif" />
<source
srcset="
/_astro/best-medium-format-camera-for-starting-out.8696310b_Oo3Ed.webp,
/_astro/best-medium-format-camera-for-starting-out.8696310b_Oo3Ed.webp 1.5x,
/_astro/best-medium-format-camera-for-starting-out.8696310b_Oo3Ed.webp 2x
"
type="image/webp" />
<img
src="/_astro/best-medium-format-camera-for-starting-out.8696310b_1MPGLA.jpg"
srcset="
/_astro/best-medium-format-camera-for-starting-out.8696310b_1MPGLA.jpg 1.5x,
/_astro/best-medium-format-camera-for-starting-out.8696310b_1MPGLA.jpg 2x
"
alt="Photograph of a Hasselblad medium format camera with the focusing screen exposed"
loading="eager"
data-astro-cid-yvbahnfj=""
width="1344"
height="896"
decoding="async"
/></picture>
Notice the image URLs include a hash in the path, which is ideal for cache-busting.
🏆 Level it up: Low Resolution Placeholder
You might not have known it is possible to transform front matter fields within the Content Collection schema file. We can use this feature to generate low resolution placeholders automatically, using the ThumbHash algorithm.
To start, let’s add a placeholder field to the front matter:
---
postTitle: 'Best Medium Format Camera for Starting Out'
datePublished: '2023-10-07T16:04:42.000+0100'
lastUpdated: '2022-10-23T10:17:52.000+0100'
featuredImage: './best-medium-format-camera-for-starting-out.jpg'
featuredImageAlt: 'Photograph of a Hasselblad medium format camera with the focusing screen exposed'
placeholder: 'best-medium-format-camera-for-starting-out/best-medium-format-camera-for-starting-out.jpg'
---
To avoid using undocumented Astro APIs (which might later change), I have included the post folder name in the path here. This is different to how we added the featured image itself.
Next, we can add the transformation to our schema (src/content/config.ts
):
import { image_placeholder } from '@rodneylab/picpack';
import { defineCollection, z } from 'astro:content';
import { readFile } from 'node:fs/promises';
const postsCollection = defineCollection({
type: 'content',
schema: ({ image }) =>
z.object({
postTitle: z.string(),
datePublished: z.string(),
lastUpdated: z.string(),
featuredImage: image(),
featuredImageAlt: z.string(),
ogImage: z.string().optional(),
placeholder: z.string().transform(async (value) => {
const { buffer } = await readFile(`./src/content/posts/${value}`);
const imageBytes = new Uint8Array(buffer);
const { base64 } = image_placeholder(imageBytes);
return base64;
}),
draft: z.boolean(),
}),
});
export const collections = {
posts: postsCollection,
};
Here, I added a Rust WASM helper package (@rodneylab/picpack) to generate the placeholder using ThumbHash. The arrow function in the placeholder
field (lines 15
-19
) reads the bytes from the image and generates a Base64-encoded placeholder. We took the image path and transformed the field to make a placeholder Base64-encoded string available in the Astro page template.
Placeholder Markup
---
// TRUNCATED...
---
<BaseLayout {...seoProps}>
<h1>{title}</h1>
<div class="image-wrapper">
<img class="placeholder" aria-hidden="true" src={placeholder} width={width} height={height} />
<Picture
pictureAttributes={{ class: 'image lazy' }}
src={featuredImage}
alt={featuredImageAlt}
densities={[1.5, 2]}
formats={['avif', 'webp']}
fallbackFormat={format}
loading="eager"
fetchpriority="high"
/>
</div>
<div class="container">
<Content />
</div>
</BaseLayout>
We have an extra <img>
element, which will contain the placeholder. Its src
is the Base64 placeholder, which we just generated in the schema transformation. Notice in line 60
, the pictureAttributes
prop lets us add a class to the generated <picture>
tag.
The final missing piece is a spot of CSS to place the full-scale image on top of the placeholder, so that it displays (over the placeholder) as soon as it loads:
img,
picture {
display: block;
max-width: 100%;
height: auto;
}
.image-wrapper {
display: grid;
.placeholder,
.image {
display: block;
grid-area: 1/1/1/1;
height: auto;
aspect-ratio: 3/2;
}
}
You can go to town on this, adding a touch of JavaScript to blend the placeholder into the full-scale image once it is loaded, though we won’t get into that here.
Astro Picture Component Alternatives
Hopefully, this has given you an idea of how the Astro Picture component works, and whether it will be suitable for your own project. You might also consider the unpic
project as an alternative. It is designed to work with hosting services (such as Imgix), and has an Astro-specific image component.
You can also roll your own. The sharp image plugin is a good starting point. It is performant, and runs in environments other than Node.js: Deno and Bun, for example.
🙌🏽 Astro Picture Component: Wrapping Up
In this post, we saw how you can add responsive banner images to your Astro blog Markdown posts. In particular, we saw:
- how you can automatically generate image metadata using Content Collection schema;
- how you can transform front matter fields to generate Base64 placeholder images from image paths; and
- some options available on the Picture element to control the generation of next-gen AVIF and WebP alternatives for devices of different display sizes.
You can see the full code for this project in the Rodney Lab GitHub repo. I do hope you have found this post useful! Let me know how you add pictures to your site, whether you use the Astro Picture components, go for unpic with a hosting service or build something more manual. Also, let me know about any possible improvements to the content above.
🙏🏽 Astro Picture Component: Feedback
Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also, if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, please consider supporting me through Buy me a Coffee.
Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on X, @rodney@toot.community on Mastodon and also the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Astro as well as SEO. Also, subscribe to the newsletter to keep up-to-date with our latest projects.
Top comments (0)