In Part 1, we’ll set up WordPress as a headless CMS with WPGraphQL, grab our public ID data from our specific Cloudinary assets, and create the custom field in WordPress that is necessary to link the Cloudinary asset ID’s to WordPress and Astro.
In Part 2, we’ll put everything together by creating an Astro app and rendering it on the browser.
The first thing we need to do is create a new Astro project.
- Go to your terminal and run these commands:
npm create astro@latest
npm install astro-cloudinary
npm install graphql
Code language: CSS (css)
- Once you do that, create a .env.local file in the root of your project and add these keys and values, and paste the values in from your Cloudinary account accordingly.
PUBLIC_CLOUDINARY_CLOUD_NAME=
PUBLIC_CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=
WORDPRESS_API_URL=
Once you paste those keys into your .env.local
file, you can add the respective values they need. Your Cloudinary values can all be found in your Cloudinary account dashboard and your WP API URL is your WordPress site’s URL.
Now that we have the environment variables setup in our Astro project, the next step is to create some GraphQL requests to fetch data from WordPress. In the src
folder, create another folder called lib
. In this lib
folder, create a file called api.js
. In that file, copy and paste this code block:
const WORDPRESS_API_URL = import.meta.env.WORDPRESS_API_URL;
export async function navQuery() {
const siteNavQueryRes = await fetch(WORDPRESS_API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `
{
menus(where: {location: PRIMARY}) {
nodes {
name
menuItems {
nodes {
uri
url
order
label
}
}
}
}
generalSettings {
title
url
description
}
}
`,
}),
});
const { data } = await siteNavQueryRes.json();
return data;
}
export async function homePagePostsQuery() {
const response = await fetch(WORDPRESS_API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `
query GetPosts {
posts {
nodes {
title
uri
cloudinaryAsset{
cloudinaryPublicId
}
}
}
}
`,
}),
});
const { data } = await response.json();
return data;
}
export async function getNodeByURI(uri) {
const response = await fetch(WORDPRESS_API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `
query GetNodeByURI($uri: String!) {
nodeByUri(uri: $uri) {
__typename
isContentNode
isTermNode
... on Post {
id
title
date
uri
excerpt
content
categories {
nodes {
name
uri
}
}
featuredImage {
node {
srcSet
sourceUrl
altText
mediaDetails {
height
width
}
}
}
}
... on Page {
id
title
uri
date
content
}
... on Category {
id
name
posts {
nodes {
date
title
excerpt
uri
categories {
nodes {
name
uri
}
}
featuredImage {
node {
srcSet
sourceUrl
altText
mediaDetails {
height
width
}
}
}
}
}
}
}
}
`,
variables: {
uri: uri,
},
}),
});
const { data } = await response.json();
return data;
}
export async function getAllUris() {
const response = await fetch(import.meta.env.WORDPRESS_API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `
query GetAllUris {
posts(first: 100) {
nodes {
uri
}
}
pages(first: 100) {
nodes {
uri
}
}
}
`,
}),
});
const { data } = await response.json();
// Combine all URIs from posts and pages
const uris = [...data.posts.nodes, ...data.pages.nodes]
.filter((node) => node.uri !== null)
.map((node) => ({
params: {
uri: node.uri.substring(1), // Remove leading '/' if necessary
},
}));
return uris;
}
Code language: JavaScript (javascript)
This entire code block is all our GraphQL queries needed for this site. Its function is to fetch the post title, URI, and the custom cloudinaryPublicId
field from WordPress. It will also allow us to add a dynamic route file for a single post detail page.
We’re now ready to create a home page that renders Cloudinary assets and matches posts from WordPress to a Cloudinary public ID. In src/pages/index.js
, copy and paste this code block in:
---
import { getCollection } from 'astro:content';
import { CldImage } from 'astro-cloudinary';
import { homePagePostsQuery } from "../lib/api"; // Your WPGraphQL query function
// Fetch the Cloudinary assets
const assets = await getCollection('assets');
// Fetch the posts data from WordPress using the homePagePostsQuery function
const data = await homePagePostsQuery();
const posts = data.posts.nodes;
// Debugging: Log assets and posts to check the structure
console.log("Cloudinary Assets:", assets);
console.log("WordPress Posts:", posts);
// Match posts to assets based on cloudinaryPublicId
const galleryItems = posts.map(post => {
if (!post.cloudinaryAsset || !post.cloudinaryAsset.cloudinaryPublicId) {
return { post, asset: null };
}
// Use the cloudinaryPublicId directly from WordPress (no need to prepend folder)
const fullCloudinaryPublicId = post.cloudinaryAsset.cloudinaryPublicId;
// Log the constructed full public ID for debugging
console.log("Post Full Cloudinary Public ID:", fullCloudinaryPublicId);
// Try to find the matching asset
const matchedAsset = assets.find(asset => asset.data.public_id === fullCloudinaryPublicId);
// Log if a match was found or not
if (matchedAsset) {
console.log(`Match found for post: ${post.title}`);
} else {
console.log(`No match found for post: ${post.title}`);
}
return {
post,
asset: matchedAsset || null // Fallback in case no asset is found
};
});
---
<main>
<h1 class="h1">Headless WP Astro Asset Loader Gallery</h1>
<div class="gallery">
{galleryItems.map(({ post, asset }) => (
<div class="gallery-item" id={post.uri} >
{asset ? (
<a href={post.uri}>
<CldImage
src={asset.data.public_id}
width={300}
height={200}
crop="fill"
gravity="auto"
alt={post.title}
/>
</a>
) : (
<p>No corresponding asset found</p>
)}
<h3><a href={post.uri}>{post.title}</a></h3>
</div>
))}
</div>
</main>
<style>
.h1 {
text-align: center;
font-family: monospace;
}
/* Flexbox layout for gallery */
.gallery {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 20px;
}
/* Style for each gallery item */
.gallery-item {
margin: 10px;
text-align: center;
}
/* Set width to ensure consistent sizing */
.gallery-item img {
max-width: 100%;
height: auto;
border-radius: 8px;
}
/* Ensure h3 and description are styled properly */
.gallery-item h3 {
margin-top: 10px;
font-size: 16px;
font-weight: bold;
}
/* Add styling for the "Read More" links */
a {
display: inline-block;
margin-top: 10px;
color: #003559;
text-decoration: none;
font-weight: bold;
font-size: large;
}
a:hover {
text-decoration: underline;
}
</style>
Code language: JavaScript (javascript)
Let’s break this down since there is a bit of code in this block. At the top of the file, we import the necessary things we need:
import { getCollection } from 'astro:content';
. This imports thegetCollection
function from Astro’s content API, which is used to fetch assets or content collections (like images) stored in Cloudinary.import { CldImage } from 'astro-cloudinary';
. This imports the CldImage component from theastro-cloudinary
package. The CldImage component is responsible for rendering images directly from Cloudinary, with built-in features like responsive image handling and on-the-fly image transformations (e.g., cropping, resizing, etc.).import { homePagePostsQuery } from "../lib/api";
. This imports a custom function from theapi.js
file that queries the WordPress GraphQL API for a list of posts. This data is used to populate the gallery with posts and their related Cloudinary assets.
Next we’ll fetch our data:
// Fetch the Cloudinary assets
const assets = await getCollection('assets');
// Fetch the posts data from WordPress using the homePagePostsQuery function
const data = await homePagePostsQuery();
const posts = data.posts.nodes;
Code language: JavaScript (javascript)
const assets = await getCollection('assets');
. This asynchronously fetches a collection of assets from Cloudinary that were pre-configured in Astro’s content collection system. The'assets'
collection contains the images that will be rendered on the homepage.const data = await homePagePostsQuery();
. This asynchronously fetches the list of posts from the WordPress GraphQL API using the customhomePagePostsQuery
function.const posts = data.posts.nodes;
. This extracts the array of posts from the GraphQL response, which is stored in data.posts.nodes. Each post contains details such as the title, URI, and the associated Cloudinary public ID stored in WordPress.
Following that, we’ll make sure to throw some debugging logs in to check data structure is accurate:
// Debugging: Log assets and posts to check the structure
console.log("Cloudinary Assets:", assets);
console.log("WordPress Posts:", posts);
Code language: JavaScript (javascript)
Then we’ll match Cloudinary assets to WordPress posts like so:
/ Match posts to assets based on cloudinaryPublicId
const galleryItems = posts.map(post => {
if (!post.cloudinaryAsset || !post.cloudinaryAsset.cloudinaryPublicId) {
return { post, asset: null };
}
// Use the cloudinaryPublicId directly from WordPress (no need to prepend folder)
const fullCloudinaryPublicId = post.cloudinaryAsset.cloudinaryPublicId;
// Log the constructed full public ID for debugging
console.log("Post Full Cloudinary Public ID:", fullCloudinaryPublicId);
// Try to find the matching asset
const matchedAsset = assets.find(asset => asset.data.public_id === fullCloudinaryPublicId);
// Log if a match was found or not
if (matchedAsset) {
console.log(`Match found for post: ${post.title}`);
} else {
console.log(`No match found for post: ${post.title}`);
}
return {
post,
asset: matchedAsset || null // Fallback in case no asset is found
};
});
Code language: JavaScript (javascript)
Let’s break down what’s happening:
const galleryItems = posts.map(post => { ... })
. This block of code iterates over the array of posts and attempts to match each post with its corresponding Cloudinary asset using the public ID stored in WordPress.if (!post.cloudinaryAsset || !post.cloudinaryAsset.cloudinaryPublicId) { ... }
. This conditional checks if the post has a cloudinaryAsset and whether the cloudinaryPublicId is present. If either is missing, it returns the post without a corresponding asset (asset: null).const fullCloudinaryPublicId = post.cloudinaryAsset.cloudinaryPublicId;
. This assigns the cloudinaryPublicId from WordPress directly to fullCloudinaryPublicId to be used for matching Cloudinary assets.- const matchedAsset = assets.find(asset => asset.data.public_id === fullCloudinaryPublicId);. This line attempts to find a match between the cloudinaryPublicId from WordPress and the public_id from Cloudinary’s assets. If a match is found, that asset is stored in matchedAsset.
- return { post, asset: matchedAsset || null }. The function returns an object containing the post and the matched asset. If no match is found, the asset will be null.
Once we have that set, we now can have the main content section of our code:
<main>
<h1 class="h1">Headless WP Astro Asset Loader Gallery</h1>
<div class="gallery">
{galleryItems.map(({ post, asset }) => (
<div class="gallery-item" id={post.uri}>
{asset ? (
<a href={post.uri}>
<CldImage
src={asset.data.public_id}
width={300}
height={200}
crop="fill"
gravity="auto"
alt={post.title}
/>
</a>
) : (
<p>No corresponding asset found</p>
)}
<h3><a href={post.uri}>{post.title}</a></h3>
</div>
))}
</div>
</main>
Code language: HTML, XML (xml)
<main>
. This is the main section of the HTML where the content will be rendered.<h1>
. This renders the title of the page, “Headless WP Astro Asset Loader Gallery”.{galleryItems.map(({ post, asset }) => (...)}
. The galleryItems array is mapped over, where each iteration renders a post and its corresponding Cloudinary asset (if available). Each post and asset combination will be rendered inside a div with the class gallery-item.<CldImage src={asset.data.public_id} ... />
. If a corresponding asset is found for the post, it will be rendered using the CldImage component, which handles optimized media delivery from Cloudinary.<h3><a href={post.uri}>{post.title}</a></h3>
. Each post’s title is displayed as a link, which points to the individual post detail page.
And finally we’ll style the component at the bottom.
When you run npm run dev
in your terminal, you should see a page that renders assets from Cloudinary with the corresponding titles those assets are related to from WordPress:
The last thing we need to do is use Astro’s support for dynamic routing to create dynamic pages that display detailed information from each post.
Go to src/pages/[uri].js
and copy and paste this code block in:
---
import { CldImage } from "astro-cloudinary";
import { getNodeByURI, getAllUris } from "../lib/api";
// This function tells Astro which paths to generate at build time
export async function getStaticPaths() {
// Fetch all URIs from WordPress
const uris = await getAllUris();
return uris.map(uri => ({
params: { uri: uri.params.uri }
}));
}
// Get the post's URI from the route parameter
const { uri } = Astro.params;
const data = await getNodeByURI(`/${uri}`);
const post = data.nodeByUri;
// Handle 404 case if no post is found
if (!post) {
return new Response(null, { status: 404 });
}
---
<html lang="en">
<head>
<title>{post.title}</title>
</head>
<body>
<main>
<article>
<h1>{post.title}</h1>
<time>{new Date(post.date).toLocaleDateString()}</time>
{post.featuredImage && (
<CldImage
src={post.featuredImage.node.sourceUrl}
alt={post.featuredImage.node.altText}
width={600}
height={400}
crop="fill"
gravity="auto"
quality="auto"
/>
)}
<div set:html={post.content}></div>
</article>
</main>
</body>
</html>
Code language: JavaScript (javascript)
This code block represents a dynamic route in an Astro project that generates individual post detail pages based on data fetched from WordPress using WPGraphQL and Cloudinary for image handling. Here’s a quick breakdown:
- Imports:
CldImage
fromastro-cloudinary
renders images from Cloudinary, with optimizations like dynamic cropping, resizing, and compression.getNodeByURI
andgetAllUris
from../lib/api
functions that query the WordPress GraphQL API to fetch posts and their associated URIs.
getStaticPaths()
:- This function tells Astro which paths to generate at build time. It fetches all the post URIs from WordPress and maps over them to generate static pages for each post. Each URI becomes a path for a dynamic route.
- Fetching post data:
- The post’s URI is extracted from the route parameter (Astro.params.uri), and getNodeByURI is called with this URI to fetch the corresponding post data from WordPress.
- 404 Handling:
- If no post is found for the given URI, a 404 response is returned to indicate that the page does not exist.
- Rendering the Page:
- The page renders the post title, publication date, featured image (if available), and the post content.
- CldImage is used to display the post’s featured image, pulling the image from Cloudinary and applying transformations (cropping, gravity, quality optimization).
- HTML Structure:
- The HTML structure includes a title, date, and the post content, all of which are dynamically populated based on the post data retrieved from WordPress.
Now when you visit the link on the home page, it should take you to a single post detail page like so:
By following this guide, you’ve successfully created a website using Astro and headless WordPress, enhanced by Cloudinary for efficient asset management. You’ve connected WordPress with Astro using WPGraphQL, pulled in assets from Cloudinary, and displayed them on a dynamic Astro-powered site. This setup ensures your site is fast, responsive, and media-rich — perfect for modern web applications. Sign up for a free Cloudinary account and try it for yourself today.
As always, I look forward to seeing what you build out there on the web and if you have any feedback, please share in the Cloudinary Community forums or the associated Discord.