How to Optimize & Dynamically Resize Images in Astro with Cloudinary

What's Inside 🧐

View on YouTube

Astro brings high performance to the web development world, but what about all of your images? How can we reduce the amount of pixels we’re sending to our visitors without sacrificing quality using Cloudinary’s automatic image optimization?

What is Astro?

Astro is a web framework that allows developers to build highly performance websites.

They ship very little JavaScript (if any?) reducing the impact your code has on the browser, making it load faster, and your visitors happy.

Why is optimization and resizing important?

We’re still talking about performance right? Well this comes into play with images and videos as well.

Particularly we’re going to talk about images in this tutorial, where we want to make sure we’re serving as small of images as we can without sacrificing quality including using modern formats (like AVIF) and compressing to the point where it won’t impact how the image looks.

That’s where Cloudinary comes in, where we’ll use Cloudinary inside of an Astro app to deliver our images as performant as we can.

What are we going to build?

We’re going to start off with a new Astro app using one of the templates built in to Astro.

Once we’re up and running, we’ll learn how we can install the Cloudinary SDK to serve those images from Cloudinary, including fetching remote images on the fly and uploading to Cloudinary to serve from there.

We’ll then tackle how we can automatically optimize all of our images as well as dynamically crop and resize them to make sure we’re delivering only the sizes we need.

Step 0: Creating a new Astro app

Getting started, we’re going to be creating a new Astro app.

We can do this pretty easily by running the Astro wizard in our terminal by running the command:

npm create astro@latest

This will start asking you a few questions, such as where do you want to create your new project?

Terminal showing prompt for where to create new project
Creating a new Astro project

I’m going to create my project in my-astro-cloudinary but you can use whatever directory you want (or the default)!

Next we want to select the template. This is an important one to make sure that we’re following along on the same project.

I’m selecting Portfolio, which includes some images that we can get productive with.

Terminal showing prompt for which template to use with Portfolio highlighted
Portfolio template site

Once selected, Astro will copy the template files locally.

For the rest of the questions, I answered:

  • Yes, I want to install the npm dependencies
  • Yes, I want to initialize a new git repository
  • I prefer not to use TypeScript (but it’s okay if you do!)

Once installation has finished, you can navigate into your new directory:

cd my-astro-cloudinary

And start your development server with:

npm run dev

Which will start a new development server (likely http://127.0.0.1:3000/ if you’re on a Mac) which you can open in your browser and see your new Astro app!

Website showing portfolio template and big image
New Astro portfolio site!

If this is your first time using Astro, feel free to take some time to explore. We won’t be digging too deep into Astro itself, but we’ll cover some basic concepts as we learn through using Cloudinary inside Astro.

Follow along with the commit!

Step 1: Creating a new CldImage component in Astro to manage images

Starting off, to make it easier on ourselves, we’re going to create a new image component that will allow us to set this up in one place and use it throughout our application.

Inside src/components create a new file called CldImage.astro and inside add:

---
interface Props {
  src: string;
  width: string;
  height: string;
  alt: string;
}
---

<img loading="lazy" {...Astro.props} />

Tip: if this is the first time you’re using Astro, notice how we write code within the --- barriers. This is where we can write and run JavaScript!

This is giving us a simple wrapper component that allows us to pass through our image details.

Now to use this, while we could start on the homepage, we’re going to start on the About page, which has a big on-page image embedded.

Inside src/pages/about.astro let’s first import our new component at the top between the --- lines:

import CldImage from '../components/CldImage.astro';

Tip: again, seeing the --- which is where we also include our imports like the template included by default.

Next scroll down until you find the <img> tag and simply replace <img with <CldImage.

If we now open up our browser and navigate to the About page (http://127.0.0.1:3000/about/), we should still see the big header image at the top “as is”, but the difference now is we’re using the loading="lazy" attribute which wasn’t there before.

Browser with element inspector showing image loading with lazy attribute
Image loading with Astro

Which means it’s working!

Next we’ll learn how to pull Cloudinary into our component so we can start taking advantage of features like optimization and dynamic resizing.

Follow along with the commit!

Step 2: Installing and configuring the Cloudinary URL Gen SDK in Astro to deliver remote images

For integrating with Cloudinary, we’re going to use the Cloudinary URL Gen SDK.

This allows us to easily set up URLs by passing in our source into an image instance then transform our images on the fly.

Tip: While technically you can use the Node SDK in Astro, as the code should only ever run on a server, there may be instances now or in the future where that might not be supported.

In order to install, we’ll first install the dependency with:

npm install @cloudinary/url-gen

Once finished, we can now import that dependency at the top of our CldImage component:

import { Cloudinary } from '@cloudinary/url-gen';

Next we want to configure a new Cloudinary instance which will allow us to pass in our Cloud Name, which is our Cloudinary account’s name.

const cld = new Cloudinary({
  cloud: {
    cloudName: 'mycloudname'
  }
});

Tip: You can find your Cloudinary Cloud Name right on themain Dashboard of your account

At this point, our component takes in a src prop which will be the URL of the image.

When using Cloudinary to deliver images, we have two base options for how we’ll do so:

  • Passing in a Cloudinary Public ID (image’s ID)
  • Passing in a remote URL and letting Cloudinary “fetch” the image

Because we’re currently delivering our image from a remote URL, let’s start there, where later we’ll cover how to add images already in Cloudinary.

Right now, we’re passing all of our props into our image “as is” but we need to transform how this is happening.

First, still working from within the ---‘s, add the following:

const { src, ...props } = Astro.props;
const cldSrc = cld.image(src).setDeliveryType('fetch');

Here, we’re:

  • Destructuring our src
  • And spreading the rest of our props into the props object
  • Creating a cldSrc value that takes in our source using the cld.image method
  • We set the delivery type to fetch because we’re working with a remote URL

Next we need to update our image to use that new source.

Update the image tag to:

<img src={cldSrc.toURL()} loading="lazy" {...props} />
Tip: you can really do the `toURL` transformation anywhere, but we're doing it here so we can perform any work with our image instance before finally getting the URL.

But now if we head to our browser, we shouldn’t really notice much is different, but if we look in our element inspector, we’ll see the image is now coming from Cloudinary.

Browser with element inspector showing image coming from Cloudinary
Delivering from Cloudinary

Note: Image not working? You may have Fetched Images restricted! You can either follow along to learn how to deliver images from Cloudinary in the next step or head to Settings > Security > Restricted Media Types and uncheck Fetched URL. The restricted image may be cached, so you may have to change an attribute or wait for it to refresh.

Follow along with the commit!

Step 3: Delivering Cloudinary stored images with Astro and the Cloudinary SDK

Now we started off delivering a remote file simply because we already had a remote file, but you may typically have your images already in Cloudinary or pulling your images from a headless source into your app.

For that, you’d be better off using your public ID and passing that right through to the component.

So we’ll look at the src prop passed into our component, look to see if it starts off with https:// meaning it’s a URL, and configure the component to treat it differently based on that.

Starting off, we’re going to turn our cldSrc constant into a let so that we can modify it and apply the chained methods a little differently.

let cldSrc = cld.image(src);

cldSrc.setDeliveryType('fetch');

Now that we’re composing that a bit differently, we can use an if clause to determine what our source looks like:

if ( src.startsWith('https://')) {
  cldSrc.setDeliveryType('fetch');
}

Tip: this is a simple check that we’re doing, where you may run into more complex examples such as finding non-secure http:// sources or full Cloudinary URLs which you can improve on this logic to handle!

And that’s literally all we have to do in our component to dynamically serve a remote image or an image already on Cloudinary, but to test this, we actually need an image on Cloudinary.

If you head into your Cloudinary account and navigate to your Media Library, you should notice whether or not you uploaded anything that Cloudinary starts you off with some sample images.

If you select any of them (or upload a new one and select that), we’ll want to find the Public ID, which is at the top left of the Media Viewer.

Cloudinary Media Viewer with Public ID highlighted
Public ID

Copy that ID and head over to src/pages/about.astro and replace the src attribute of our <CldImage with that value:

<CldImage
  width="1400"
  height="350"
  src="cld-sample"
/>

Once you reload your page, you should now see the new image!

Browser with element inspector showing new image delivered from Cloudinary
Delivering an image from Cloudinary

Follow along with the commit!

Step 4: Automatically optimizing images with the CldImage component and Cloudinary

This next step is nice and quick.

Our goal is to now deliver our images completely optimized.

Cloudinary can automatically optimize your images in two ways:

  • Delivering the most modern and / or efficient format that the browser supports (like AVIF)
  • Compressing it to a point that’s not visually different

And to do that, we’re going to chain two simple additions to our cld.image instance:

let cldSrc = cld.image(src).format('auto').delivery('q_auto');

Where we’re:

  • Setting a format of “auto” (f_auto in the URL)
  • And a quality of “auto” (q_auto in the URL)

We went from 477 kb JPEG image to a 183 kb AVIF image by only making those little tweaks!

Note: AVIF will only be returned to browsers that support it (like Chrome), but if AVIF is not supported, you may see WEBP which will provide benefits on top of JPEG, but just not as big of a difference.

Greatly reduced file size!

Follow along with the commit!

Step 5: Dynamically cropping with face detection and resizing with Cloudinary

Our image is optimized, but looking at the page on a desktop monitor, we’re only using a sliver of the image.

Browser image only showing top of person's head
Head cut off!

Now this image is responsive and as the page shrinks, like on a mobile device, it certainly looks better.

Mobile simulator showing image with different aspect ratio
Different aspect ratio on mobile

But we want it to look great on all sizes!

Starting with desktop, we want to be able to show her face when it’s cropped wide and short.

To do this, we can take advance of Cloudinary face detection, to automate that cropping process.

We’ll use the width and the height already being passed into the component to crop it with that detection.

First let’s do the cropping. Update the cldSrc to:

let cldSrc = cld.image(src)
                .format('auto')
                .delivery('q_auto')
                .resize(`c_crop,w_${props.width},h_${props.height}`);

Here we’re using the resize method passing in a crop of “crop” as well as the width and height from the component.

Note: I also broke down the chain into different lines to be easier to read what’s happening on each line.

Image on webpage showing cropped image of person smiling
Image cropped to center

We’re getting somewhere! But we also want to center it on their face.

So next, we’re going to add a “gravity” which is the anchor point, which we’ll se to “face” meaning, we want the center of the image as best as we can to be the person’s face.

.resize(`c_crop,w_${props.width},h_${props.height},g_face`);

And now once we reload the page, we can see it’s perfectly centered on their face!

Browser showing image cropped to face of person smiling
Cropped image centered to face

But like we said, we want a mobile version too right?

This gets a little trickier in terms of implementation for our modest component, which we’re not going to cover here, but this gets into the question of how you provide responsive imagery, whether:

Needless to say we’re not going to dig into that for this tutorial, but If we simply update the width and height of our component to a different mobile-friendly ratio:

<CldImage
  width="1400"
  height="700"
  src="cld-sample"
/>

We can see our image immediately updates in the browser to the new crop.

Mobile device simulator showing mobile-friendly image
Updated crop for mobile!

Best of all, it’s still centered on the subject’s face automatically using our face detection!

Note: the way the image is embedded on the page with CSS shows only part of the image when on desktop. If you want to see the full image every time, you can remove the max-height CSS property from the .heroImg class inside src/pages/about.astro.

Follow along with the commit!

What else can we do?

Responsive component

Build in responsiveness into the component with dynamic cropping and sizing.

Depending on what responsive solution you want to make use of, where if you’re ONLY resizing maybe a sizes prop is appropriate but if you’re serving different aspect ratios, maybe a separate <picture> component makes sense.

Either way you can generate a new URL for each size.

Update other images with Cloudinary URLs

The main image on the homepage uses background-image to set the image. We can create our Cloudinary URL at the top, just like we did in our component, and pass that to our CSS to make sure we’re optimizing all of our images.

Stay tuned for a Cloudinary integration

While I walked you through building your own custom Cloudinary component, follow me on Twitter and subscribe to my email for announcements and updates of a potential Astro Cloudinary integration!