How to Use Cloudinary with Astro: Complete Guide
Step-by-step guide to integrating Cloudinary with your Astro website.
Cloudinary is a cloud-based media management platform that handles image and video optimization, transformation, and delivery through a global CDN. Instead of manually resizing images, converting formats, and setting up a CDN yourself, Cloudinary does all of this through URL parameters. Upload an image once, and you can serve it in any size, format, or quality by changing the URL.
For Astro sites where images make up the bulk of page weight, Cloudinary can dramatically improve your Core Web Vitals scores without you writing optimization logic.
There are two solid ways to wire this up. The recommended path, and the one the official Astro docs point to, is the first-party astro-cloudinary package, which ships ready-made .astro components like CldImage and CldOgImage. The second path is a tiny hand-rolled URL helper with no extra runtime dependency, which is useful when you only need static optimized URLs. This guide covers both.
Prerequisites
- Node.js 18 or newer
- An Astro project (
npm create astro@latest). These instructions were checked against Astro 6.4.2. - A Cloudinary account (the free tier gives you 25 monthly credits, where 1 credit equals roughly 1GB of bandwidth, 1GB of storage, or 1,000 transformations, so 25 credits covers about 25GB of bandwidth or 25,000 transformations)
Recommended: The Official astro-cloudinary Package
Cloudinary maintains a dedicated Astro integration. The official Astro media docs recommend it directly, and it gives you typed components instead of string-built URLs.
Install it:
npm install astro-cloudinary
As of this writing the current version is astro-cloudinary 1.3.5. Its only peer dependency is Astro itself, declared as ^3.2.0 || ^4.0.0 || ^5.0.0. See the note in "Common Errors and Fixes" below about using it on Astro 6.
Add your cloud name to .env:
PUBLIC_CLOUDINARY_CLOUD_NAME=your_cloud_name
That single public variable is all you need for delivery (rendering and transforming images). You only add PUBLIC_CLOUDINARY_API_KEY and CLOUDINARY_API_SECRET if you also use the Upload Widget, signed uploads, or Collections. The cloud name is public by design (it appears in every image URL), so keeping it in a PUBLIC_ variable is correct.
Now use the CldImage component in any .astro file. It wraps the Unpic Image component, so you get responsive sizing on top of Cloudinary transformations, and it opts every image into f_auto and q_auto automatically:
---
// src/components/blog/PostImage.astro
import { CldImage } from "astro-cloudinary";
export interface Props {
publicId: string;
alt: string;
width?: number;
height?: number;
}
const { publicId, alt, width = 1200, height = 630 } = Astro.props;
---
<CldImage
src={publicId}
width={width}
height={height}
alt={alt}
loading="lazy"
decoding="async"
/>
The src prop takes a Cloudinary Public ID (the folder path plus the image ID), and width and height describe the rendered size, which keeps Cumulative Layout Shift in check.
For Open Graph and Twitter card images, the package ships CldOgImage, which generates the social meta tags rather than rendering a visible <img>:
---
import { CldOgImage } from "astro-cloudinary";
---
<CldOgImage
src="blog/my-article-hero"
twitterTitle="How to Use Cloudinary with Astro"
alt="A descriptive alt text for the OG image"
/>
The package also exports CldVideoPlayer and CldUploadWidget, plus URL-generating helpers like getCldImageUrl and getCldOgImageUrl for cases where you need a plain string instead of a component.
Alternative: A Manual URL Helper (No Extra Runtime Dependency)
If you prefer not to add an integration, or you only need optimized URLs without the component layer, you can build them yourself with a tiny helper. This approach pairs the Node.js cloudinary SDK (current version 2.10.0, used server-side for uploads if you need them) with hand-built delivery URLs. For pure delivery you do not even need the SDK on the client.
npm install cloudinary
If you want a maintained helper for building delivery URLs without writing the string logic yourself, @cloudinary-util/url-loader (current version 6.2.0) is the same URL builder that powers the official package.
Add your Cloudinary credentials to .env:
PUBLIC_CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
The cloud name is public (it appears in image URLs), but the API key and secret should stay server-side and are only needed if you upload from your own code.
Create a helper for generating Cloudinary URLs:
// src/lib/cloudinary.ts
const CLOUD_NAME = import.meta.env.PUBLIC_CLOUDINARY_CLOUD_NAME;
export function getCloudinaryUrl(
publicId: string,
options: {
width?: number;
height?: number;
format?: "auto" | "webp" | "avif";
quality?: "auto" | number;
crop?: "fill" | "fit" | "scale" | "thumb";
} = {}
) {
const {
width,
height,
format = "auto",
quality = "auto",
crop = "fill",
} = options;
const transforms = [
`f_${format}`,
`q_${quality}`,
width && `w_${width}`,
height && `h_${height}`,
crop && `c_${crop}`,
]
.filter(Boolean)
.join(",");
return `https://res.cloudinary.com/${CLOUD_NAME}/image/upload/${transforms}/${publicId}`;
}
Manual Helper: Basic Usage
Use the helper in your Astro components to serve optimized images:
---
// src/components/blog/PostImage.astro
import { getCloudinaryUrl } from "../../lib/cloudinary";
export interface Props {
publicId: string;
alt: string;
width?: number;
height?: number;
}
const { publicId, alt, width = 800, height = 450 } = Astro.props;
const src = getCloudinaryUrl(publicId, { width, height });
const srcWebp = getCloudinaryUrl(publicId, { width, height, format: "webp" });
const srcAvif = getCloudinaryUrl(publicId, { width, height, format: "avif" });
---
<picture>
<source srcset={srcAvif} type="image/avif" />
<source srcset={srcWebp} type="image/webp" />
<img
src={src}
alt={alt}
width={width}
height={height}
loading="lazy"
decoding="async"
/>
</picture>
Use it in your blog posts or pages:
---
import PostImage from "../components/blog/PostImage.astro";
---
<PostImage
publicId="blog/my-article-hero"
alt="A descriptive alt text for the image"
width={1200}
height={630}
/>
For responsive images with multiple sizes:
---
import { getCloudinaryUrl } from "../../lib/cloudinary";
const publicId = "blog/hero-image";
const sizes = [400, 800, 1200];
const srcset = sizes
.map((w) => `${getCloudinaryUrl(publicId, { width: w, format: "webp" })} ${w}w`)
.join(", ");
---
<img
src={getCloudinaryUrl(publicId, { width: 800, format: "webp" })}
srcset={srcset}
sizes="(max-width: 600px) 400px, (max-width: 1024px) 800px, 1200px"
alt="Responsive hero image"
loading="lazy"
/>
Production Tips
Always use
f_autoandq_auto. These parameters let Cloudinary choose the best format (WebP, AVIF, or JPEG) and quality level based on the requesting browser. This alone can cut image sizes by 40-60%.Set explicit width and height. Providing dimensions in your
<img>tags prevents layout shift (CLS). Cloudinary makes this easy because you control the output size through URL parameters.Use named transformations for consistency. Instead of repeating transform parameters, create named transformations in your Cloudinary dashboard (like "blog_hero" or "thumbnail"). Reference them with
t_blog_heroin the URL.
Serve video through Cloudinary too. Cloudinary handles video optimization with the same URL-based approach. Automatic format selection, adaptive bitrate, and thumbnail generation all work through URL parameters.
Enable fetch mode for external images. You can optimize images from any URL by using Cloudinary's fetch feature: https://res.cloudinary.com/your_cloud/image/fetch/f_auto,q_auto/https://example.com/image.jpg. No upload needed.
A Note on Astro 6 and Rendering Modes
A few older tutorials tell you to set output: 'hybrid' in astro.config.mjs. Do not. That option was removed in Astro 5. Static output is the default now, and you only add output: 'server' plus an adapter when you actually need on-demand rendering. Cloudinary delivery is pure URL construction, so it works in a fully static build with no adapter at all. The official Astro docs do not require any output mode or adapter for Cloudinary.
The astro-cloudinary components and the manual helper both render fine in static, server, and hybrid-style setups, because the optimization happens on Cloudinary's CDN, not in your build.
Common Errors and Fixes
"Cloud name is not configured" or images returning a broken URL. This means
PUBLIC_CLOUDINARY_CLOUD_NAMEis missing or not loaded. The variable must use thePUBLIC_prefix so Astro exposes it to client and component code, and the dev server must be restarted after you edit.env. Confirm the value matches the cloud name shown in your Cloudinary dashboard exactly.astro-cloudinarypeer-dependency warning on Astro 6. As of version 1.3.5, the package declares its Astro peer dependency as^3.2.0 || ^4.0.0 || ^5.0.0, which does not yet list Astro 6. You may see a peer-dependency warning during install on Astro 6.4.2. The components are framework-agnostic.astrofiles, so they work, but if your package manager treats the warning as a hard error, install with your manager's legacy peer-deps flag (for examplenpm install astro-cloudinary --legacy-peer-deps) and watch the package's GitHub releases for an updated peer range.Layout shift (high CLS) on images. Always pass explicit
widthandheight.CldImagesets the correct dimensions automatically when you provide them, and the manual helper relies on you addingwidthandheightattributes to the<img>tag. Omitting them lets the browser reflow once the image loads.Image not optimized (large file in the network tab). Make sure
f_autoandq_autoare present.CldImageopts in automatically; the manual helper applies them through theformat: "auto"andquality: "auto"defaults. If you overrideformatto a fixed value you lose per-browser format negotiation.404 on a Public ID. The
src/publicIdis the path inside your Cloudinary media library, not a filename on your disk. A file uploaded into ablogfolder asmy-article-herohas the Public IDblog/my-article-hero. Do not include the file extension.Secret leaking to the browser. Never give the API secret a
PUBLIC_prefix. Only the cloud name (and, for the Upload Widget, the API key) are safe client-side. The secret stays server-only.
Official Docs and Examples
- Astro's own media guide for Cloudinary: docs.astro.build/en/guides/media/cloudinary
- The
astro-cloudinarydocumentation site, includingCldImage,CldOgImage, and configuration references: astro.cloudinary.dev - Installation page with the exact env-var setup: astro.cloudinary.dev/installation
- The source and example repository (the
docs/directory doubles as a live example site): github.com/cloudinary-community/astro-cloudinary
Alternatives to Consider
- Cloudflare R2 + Images if you want cheaper storage with zero egress fees and basic image resizing through Cloudflare's network.
- ImageKit if you want similar URL-based transformations with a slightly simpler pricing model and built-in DAM features.
- Astro's built-in Image component if your images are local and you want zero external dependencies. Astro optimizes images at build time using Sharp.
Wrapping Up
Cloudinary handles the entire image optimization pipeline so you do not have to. Upload once, serve everywhere, in any size and format. For Astro sites where page speed matters (and it always does), offloading image optimization to Cloudinary is one of the highest-impact improvements you can make with minimal code changes. Reach for the official astro-cloudinary package first, and drop down to the manual URL helper when you want zero extra runtime dependencies.
Sources
All versions and facts below were checked on 2026-05-29.
- astro-cloudinary on npm (registry, version 1.3.5 and peer dependency on Astro)
- cloudinary on npm (registry, version 2.10.0)
- @cloudinary-util/url-loader on npm (registry, version 6.2.0)
- astro on npm (registry, version 6.4.2)
- Astro official media guide for Cloudinary (recommends astro-cloudinary)
- Astro Cloudinary installation and environment variables
- CldImage basic usage and f_auto / q_auto defaults
- CldOgImage component basic usage
- cloudinary-community/astro-cloudinary repository and example docs site
- Cloudinary credit system (1 credit = 1,000 transformations or 1GB storage or 1GB bandwidth)
- Cloudinary pricing (free plan includes 25 monthly credits)
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.