{"id":39943,"date":"2026-03-31T07:00:00","date_gmt":"2026-03-31T14:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=39943"},"modified":"2026-03-31T10:21:52","modified_gmt":"2026-03-31T17:21:52","slug":"dynamic-open-graph-images-astro","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro","title":{"rendered":"How to Build Dynamic Open Graph Images With Astro and Cloudinary"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><ul>\n<li>\n<a href=\"https:\/\/astro-cloudinary-og-demo.vercel.app\/\"><strong>Live Demo<\/strong><\/a>\n<\/li>\n<li>\n<a href=\"https:\/\/github.com\/musebe\/astro-cloudinary-og-demo\"><strong>Source Code<\/strong><\/a>\n<\/li>\n<\/ul>\n<p>Open Graph (OG) images are one of those small details that can make your post look professional and credible when you share it across social media. But if you use a generic image, or no image at all, your post can be easy to ignore.<\/p>\n<p>In this guide, we\u2019ll build a simple system using <a href=\"https:\/\/astro.build\/\"><strong>Astro<\/strong><\/a> and <strong>Cloudinary<\/strong>. The goal is to stop creating social images for every single post manually. Instead, we\u2019ll upload one clean base template to Cloudinary and generate unique OG images dynamically using the post title and description.<\/p>\n<p>The stack:<\/p>\n<ul>\n<li>\n<strong>Astro<\/strong> manages the content and page metadata.<\/li>\n<li>\n<strong>Cloudinary<\/strong> handles the image generation via <strong>URL transformations<\/strong>.<\/li>\n<li>\n<strong>One template<\/strong> powers every post on your site.<\/li>\n<\/ul>\n<p>The result is a workflow that\u2019s lightweight, easy to maintain, and much more scalable than exporting static images one by one.<\/p>\n<p>Here\u2019s the <a href=\"https:\/\/astro-cloudinary-og-demo.vercel.app\/\">home page<\/a>, which shows the full idea in action. Each post card displays its own generated OG image. Even though the text is different for every post, they all share a consistent design system.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1774287761\/blog-How_to_Build_Dynamic_Open_Graph_Images_With_Astro_and_Cloudinary-1.png\" alt=\"final home page\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"912\" height=\"630\"\/><\/p>\n<h2>The Reusable Template<\/h2>\n<p>The <a href=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1774287762\/blog-How_to_Build_Dynamic_Open_Graph_Images_With_Astro_and_Cloudinary-2.png\">base image<\/a> stays mostly blank on purpose. This is what makes the system flexible. We\u2019re not \u201cbaking\u201d the title or description into the image file. We\u2019ll leave space for Cloudinary to add those later as <strong>text overlays<\/strong>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1774287762\/blog-How_to_Build_Dynamic_Open_Graph_Images_With_Astro_and_Cloudinary-3.png\" alt=\"Astro x Cloudinary og template\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1536\" height=\"1024\"\/><\/p>\n<h3>One Generated Example<\/h3>\n<p>Once the template is in Cloudinary, Astro passes post data into a transformation URL. Cloudinary then renders the <a href=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1774287761\/blog-How_to_Build_Dynamic_Open_Graph_Images_With_Astro_and_Cloudinary-4.webp\">final social image<\/a> on the fly. This is the core of the whole build.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1774287761\/blog-How_to_Build_Dynamic_Open_Graph_Images_With_Astro_and_Cloudinary-4.webp\" alt=\"og template sample\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1200\" height=\"630\"\/><\/p>\n<h3>The Workflow Shift<\/h3>\n<figure class=\"table-wrapper\"><table>\n<thead>\n<tr>\n<th><strong>Manual Process (The Old Way)<\/strong><\/th>\n<th><strong>Dynamic Process (The New Way)<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>1. Write a post<\/td>\n<td>1. Write a post<\/td>\n<\/tr>\n<tr>\n<td>2. Open Figma or Canva<\/td>\n<td>2. <strong>Done.<\/strong> Astro and Cloudinary handle the rest.<\/td>\n<\/tr>\n<tr>\n<td>3. Manually type the title<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td>4. Export the image<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td>5. Upload to your project<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n<p>This gives you a better developer experience:<\/p>\n<ul>\n<li>\n<strong>Less manual work.<\/strong> No more opening design tools for every blog entry.<\/li>\n<li>\n<strong>Consistent branding.<\/strong> Your design stays the same across every post automatically.<\/li>\n<li>\n<strong>Faster updates.<\/strong> If you change a post title, the OG image updates instantly.<\/li>\n<li>\n<strong>Clean repository.<\/strong> No more bloated folders full of exported PNG files.<\/li>\n<\/ul>\n<p>Instead of spending time on manual exports, you move to a system where your code handles the design logic.<\/p>\n<h2>Project Setup With Astro, Tailwind, and shadcn<\/h2>\n<p>Start with a fresh Astro application. By adding Tailwind CSS and shadcn\/ui, you can build a polished interface quickly without spending hours on custom CSS.<\/p>\n<p>The architecture is straightforward. <strong>Astro<\/strong> handles the routing and content, <strong>Tailwind<\/strong> provides the layout tools, and <strong>shadcn<\/strong> ensures the UI components like cards and buttons look professional from the start.<\/p>\n<h3>Initialize the Project<\/h3>\n<p>Run these commands in your terminal to set up the environment and add the necessary integrations:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-comment\"># Create a new Astro project<\/span>\nnpm create astro@latest astro-cloudinary-og-demo\n\n<span class=\"hljs-comment\"># Navigate into the project<\/span>\ncd astro-cloudinary-og-demo\n\n<span class=\"hljs-comment\"># Add Tailwind and React (required for shadcn)<\/span>\nnpx astro add tailwind\nnpx astro add react\n\n<span class=\"hljs-comment\"># Initialize shadcn\/ui<\/span>\nnpx shadcn@latest init -t astro\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h3>Project Structure<\/h3>\n<p>For this demo, you\u2019ll use a simple directory structure to keep the logic organized:<\/p>\n<ul>\n<li>\n<strong>src\/data\/blog\/<\/strong> stores our Markdown files for the posts.<\/li>\n<li>\n<strong>src\/lib\/<\/strong> contains the Cloudinary helper functions.<\/li>\n<li>\n<strong>src\/pages\/<\/strong> houses the homepage and the individual post routes.<\/li>\n<\/ul>\n<p>At this stage, you should have a basic Astro site running. If you want to follow along with the exact boilerplate code, you can find it here: <a href=\"https:\/\/github.com\/musebe\/astro-cloudinary-og-demo\"><strong>GitHub Repository<\/strong><\/a><\/p>\n<h2>Creating Blog Content With Astro Content Collections<\/h2>\n<p>Once the project is running, the next step is to give Astro real content to work with. You\u2019ll use <strong>Astro Content Collections<\/strong> to manage your blog posts. By storing posts as Markdown files, you can easily validate the data structure using a schema.<\/p>\n<p>Each post contains the specific fields needed for the OG image, such as the title and description.<\/p>\n<h3>Defining the Collection<\/h3>\n<p>In <a href=\"https:\/\/github.com\/musebe\/astro-cloudinary-og-demo\/blob\/main\/src\/content.config.ts\"><code>src\/content.config.ts<\/code><\/a>, you\u2019ll define the blog collection. Use <strong>Zod<\/strong> (integrated into Astro) to validate the shape of each post.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { defineCollection, z } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"astro:content\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { glob } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"astro\/loaders\"<\/span>;\n\n<span class=\"hljs-keyword\">const<\/span> blog = defineCollection({\n  <span class=\"hljs-attr\">loader<\/span>: glob({ <span class=\"hljs-attr\">pattern<\/span>: <span class=\"hljs-string\">\"**\/*.md\"<\/span>, <span class=\"hljs-attr\">base<\/span>: <span class=\"hljs-string\">\".\/src\/data\/blog\"<\/span> }),\n  <span class=\"hljs-attr\">schema<\/span>: z.object({\n    <span class=\"hljs-attr\">title<\/span>: z.string(),\n    <span class=\"hljs-attr\">description<\/span>: z.string(),\n    <span class=\"hljs-attr\">pubDate<\/span>: z.coerce.date(),\n    <span class=\"hljs-attr\">draft<\/span>: z.boolean().default(<span class=\"hljs-literal\">false<\/span>),\n    <span class=\"hljs-attr\">readTime<\/span>: z.string().optional(),\n    <span class=\"hljs-attr\">tags<\/span>: z.array(z.string()).default(&#91;]),\n  }),\n});\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> collections = { blog };\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This configuration is the backbone of the project. The <strong>loader<\/strong> tells Astro where the Markdown files live, while the <strong>schema<\/strong> ensures every post has the required fields the application expects.<\/p>\n<h3>Adding the Post Content<\/h3>\n<p>The individual posts live in <a href=\"https:\/\/github.com\/musebe\/astro-cloudinary-og-demo\/tree\/main\/src\/data\/blog\"><code>src\/data\/blog\/<\/code><\/a>. Each file uses <strong>frontmatter<\/strong> to store its metadata.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">--\ntitle: <span class=\"hljs-string\">\"Build Dynamic OG Images With Astro and Cloudinary\"<\/span>\n<span class=\"hljs-attr\">description<\/span>: <span class=\"hljs-string\">\"Use Astro page data and Cloudinary overlays to generate branded social preview images.\"<\/span>\n<span class=\"hljs-attr\">pubDate<\/span>: <span class=\"hljs-number\">2026<\/span><span class=\"hljs-number\">-03<\/span><span class=\"hljs-number\">-19<\/span>\n<span class=\"hljs-attr\">draft<\/span>: <span class=\"hljs-literal\">false<\/span>\n<span class=\"hljs-attr\">readTime<\/span>: <span class=\"hljs-string\">\"4 min read\"<\/span>\n<span class=\"hljs-attr\">tags<\/span>: &#91;<span class=\"hljs-string\">\"astro\"<\/span>, <span class=\"hljs-string\">\"cloudinary\"<\/span>, <span class=\"hljs-string\">\"open-graph\"<\/span>]\n---\nPost content goes here...\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This is where the dynamic logic begins. The <strong>title<\/strong> and <strong>description<\/strong> are not only used for the text on the page. They also become the raw strings that Cloudinary will place on your social image later.<\/p>\n<h2>Designing the Cloudinary OG Template<\/h2>\n<p>This part of the process is more important than it looks. The template acts as the visual foundation for every generated image. If the design is too busy, the text overlays will fight for space. If it\u2019s too plain, the final image feels empty.<\/p>\n<p>The sweet spot is a clean, branded background with intentional white space for the title and description. For this project, you\u2019ll use one blank template image uploaded to Cloudinary. It stays empty on purpose because Cloudinary adds the post-specific data later through the URL.<\/p>\n<h3>Separating the Template Elements<\/h3>\n<p>To keep the system reusable, you\u2019ll need to distinguish what\u2019s static vs. what\u2019s dynamic.<\/p>\n<figure class=\"table-wrapper\"><table>\n<thead>\n<tr>\n<th><strong>Included in Template (Static)<\/strong><\/th>\n<th><strong>Not Included (Dynamic)<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Soft off-white background<\/td>\n<td>Post title<\/td>\n<\/tr>\n<tr>\n<td>Subtle blue glow or accent<\/td>\n<td>Post description<\/td>\n<\/tr>\n<tr>\n<td>Footer branding band<\/td>\n<td>Post-specific icons<\/td>\n<\/tr>\n<tr>\n<td>Small logo or brand name<\/td>\n<td>Variable screenshots<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n<p>This separation is what makes the system scalable. Instead of \u201cbaking\u201d information into the image, you provide a canvas.<\/p>\n<h3>Uploading to Cloudinary<\/h3>\n<p>Once the base image is ready, upload it to your Cloudinary account. Organizing it into a specific folder helps keep things tidy as your project grows.<\/p>\n<ul>\n<li>Folder Name: <code>astro-cloudinary-og-demo<\/code>\n<\/li>\n<li>Public ID: <code>astro-cloudinary-og-demo\/og-template<\/code>\n<\/li>\n<\/ul>\n<p>That Public ID is crucial. It acts as the unique identifier you\u2019ll use in our code to tell Cloudinary which base image to transform.<\/p>\n<h3>Configuring Environment Variables<\/h3>\n<p>To keep the project portable and secure, store your Cloudinary credentials in your <code>.env<\/code> file. Using the <code>PUBLIC_<\/code> prefix in Astro allows these values to be accessible on the client side if needed.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name\nPUBLIC_CLOUDINARY_OG_TEMPLATE=astro-cloudinary-og-demo\/og-template\n<\/code><\/span><\/pre>\n<ul>\n<li>\n<strong>Cloud Name<\/strong> directs the request to your specific Cloudinary account.<\/li>\n<li>\n<strong>OG Template<\/strong> specifies the exact base image to be used for the transformation.<\/li>\n<\/ul>\n<p>This setup provides a stable visual frame. Astro supplies the changing content, and Cloudinary stitches them together into a final image. You only design the asset once, but you can generate an infinite number of social cards for greater efficiency.<\/p>\n<p>Instead of storing dozens of static files in your repository, you store one template and let the URL do the work.<\/p>\n<h3>The Core Logic<\/h3>\n<p>This is the logic that makes the entire system work. Instead of saving a finished image for each post, you\u2019ll generate the image dynamically through a Cloudinary URL. That URL points to your base template and layers on text overlays for the post title and description.<\/p>\n<h3>The Helper Function<\/h3>\n<p>The helper function takes two pieces of post data (the <strong>title<\/strong> and <strong>description<\/strong>) and combines them with your <strong>Cloudinary cloud name<\/strong>, <strong>template public ID<\/strong>, and a specific set of <strong>text overlay transformations<\/strong>.<\/p>\n<p>In <a href=\"https:\/\/github.com\/musebe\/astro-cloudinary-og-demo\/blob\/main\/src\/lib\/generateOgImage.ts\"><code>src\/lib\/generateOgImage.ts<\/code><\/a>, the helper reads the Cloudinary values from the environment, encodes the text, and returns the final image URL.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">type GenerateOgImageOptions = {\n  <span class=\"hljs-attr\">title<\/span>: string,\n  description?: string,\n};\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">generateOgImage<\/span>(<span class=\"hljs-params\">{\n  title,\n  description = <span class=\"hljs-string\">\"\"<\/span>,\n}: GenerateOgImageOptions<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> cloudName = <span class=\"hljs-keyword\">import<\/span>.meta.env.PUBLIC_CLOUDINARY_CLOUD_NAME;\n  <span class=\"hljs-keyword\">const<\/span> templateId = <span class=\"hljs-keyword\">import<\/span>.meta.env.PUBLIC_CLOUDINARY_OG_TEMPLATE;\n\n  <span class=\"hljs-keyword\">const<\/span> encodedTitle = encodeOverlayText(title);\n  <span class=\"hljs-keyword\">const<\/span> encodedDescription = encodeOverlayText(description);\n\n  <span class=\"hljs-keyword\">const<\/span> transformations = &#91;\n    <span class=\"hljs-string\">\"f_auto\"<\/span>,\n    <span class=\"hljs-string\">\"q_auto\"<\/span>,\n    <span class=\"hljs-string\">\"w_1200,h_630,c_fill\"<\/span>,\n    <span class=\"hljs-string\">`l_text:Arial_54_bold_line_spacing_-10:<span class=\"hljs-subst\">${encodedTitle}<\/span>,co_rgb:0F172A,w_700,c_fit,g_north_west,x_72,y_86`<\/span>,\n    <span class=\"hljs-string\">`l_text:Arial_25:<span class=\"hljs-subst\">${encodedDescription}<\/span>,co_rgb:64748B,w_620,c_fit,g_north_west,x_74,y_245`<\/span>,\n  ].join(<span class=\"hljs-string\">\"\/\"<\/span>);\n\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">`https:\/\/res.cloudinary.com\/<span class=\"hljs-subst\">${cloudName}<\/span>\/image\/upload\/<span class=\"hljs-subst\">${transformations}<\/span>\/<span class=\"hljs-subst\">${templateId}<\/span>.png`<\/span>;\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h3>Breaking Down the Transformations<\/h3>\n<p>The transformation array is the engine of the build. It\u2019s split into two parts: global image settings and text layers.<\/p>\n<h4>1. Global Settings<\/h4>\n<p>These parameters handle the final output of the file:<\/p>\n<ul>\n<li>\n<strong><code>f_auto<\/code><\/strong> automatically picks the best image format for the browser.<\/li>\n<li>\n<strong><code>q_auto<\/code><\/strong> chooses the optimal balance between file size and visual quality.<\/li>\n<li>\n<strong><code>w_1200,h_630,c_fill<\/code><\/strong> resizes the image to the standard Open Graph dimensions.<\/li>\n<\/ul>\n<h4>2. The Title Overlay<\/h4>\n<p>This string creates the primary visual hook:<\/p>\n<ul>\n<li>\n<strong><code>l_text:Arial_54_bold<\/code><\/strong> sets the font, size, and weight.<\/li>\n<li>\n<strong><code>line_spacing_-10<\/code><\/strong> tightens the title lines for a more modern look.<\/li>\n<li>\n<strong><code>co_rgb:0F172A<\/code><\/strong> sets the text color to a deep navy.<\/li>\n<li>\n<strong><code>w_700,c_fit<\/code><\/strong> defines a maximum width of 700px and ensures the text wraps correctly.<\/li>\n<li>\n<strong><code>g_north_west,x_72,y_86<\/code><\/strong> positions the text from the top-left corner.<\/li>\n<\/ul>\n<h4>3. The Description Overlay<\/h4>\n<p>The description uses similar logic but with a different visual hierarchy:<\/p>\n<ul>\n<li>Smaller font size (<code>25<\/code>).<\/li>\n<li>Softer text color (<code>64748B<\/code>).<\/li>\n<li>Lower vertical position (<code>y_245<\/code>).<\/li>\n<\/ul>\n<h3>Encoding the Text<\/h3>\n<p>Before placing text into a URL, you have to encode it. This ensures that spaces, commas, or slashes don\u2019t break the Cloudinary transformation logic.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">encodeOverlayText<\/span>(<span class=\"hljs-params\">text: string<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-built_in\">encodeURIComponent<\/span>(text)\n    .replace(<span class=\"hljs-regexp\">\/,\/g<\/span>, <span class=\"hljs-string\">\"%2C\"<\/span>)\n    .replace(<span class=\"hljs-regexp\">\/\\\/\/g<\/span>, <span class=\"hljs-string\">\"%2F\"<\/span>);\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This small utility is vital. The standard <code>encodeURIComponent<\/code> doesn\u2019t always handle Cloudinary-specific characters, like commas. This function ensures your titles and descriptions render correctly.<\/p>\n<h2>Displaying Generated OG Previews in Astro<\/h2>\n<p>Once the Cloudinary helper is ready, the next step is simple. You\u2019ll read the posts from Astro, generate one OG image per post, and show those previews on the homepage.<\/p>\n<p>Astro gives us the post data, and Cloudinary turns that data into an image URL.<\/p>\n<h3>Fetching the Posts<\/h3>\n<p>Start by loading the blog collection in <a href=\"https:\/\/github.com\/musebe\/astro-cloudinary-og-demo\/blob\/main\/src\/pages\/index.astro\"><code>src\/pages\/index.astro<\/code><\/a>:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">---\n<span class=\"hljs-keyword\">import<\/span> { getCollection } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'astro:content'<\/span>;\n\n<span class=\"hljs-keyword\">const<\/span> posts = (<span class=\"hljs-keyword\">await<\/span> getCollection(<span class=\"hljs-string\">'blog'<\/span>))\n  .filter(<span class=\"hljs-function\">(<span class=\"hljs-params\">post<\/span>) =&gt;<\/span> !post.data.draft)\n  .sort(<span class=\"hljs-function\">(<span class=\"hljs-params\">a, b<\/span>) =&gt;<\/span> b.data.pubDate.getTime() - a.data.pubDate.getTime());\n---\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>You\u2019ll receive a clean list of published posts, sorted by date.<\/p>\n<h3>Generating the OG Image per Post<\/h3>\n<p>Inside the page loop, you\u2019ll call the helper for each post:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">{\n  posts.map(<span class=\"hljs-function\">(<span class=\"hljs-params\">post<\/span>) =&gt;<\/span> {\n    <span class=\"hljs-keyword\">const<\/span> ogImage = generateOgImage({\n      <span class=\"hljs-attr\">title<\/span>: post.data.title,\n      <span class=\"hljs-attr\">description<\/span>: post.data.description,\n    });\n\n    <span class=\"hljs-keyword\">return<\/span> (\n      <span class=\"hljs-comment\">\/\/ render card<\/span>\n    );\n  })\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This is the key handoff. Astro already knows the post title and description, so you\u2019ll just pass those values into the Cloudinary helper.<\/p>\n<h3>Rendering the Preview Card<\/h3>\n<p>Each card uses the generated image URL in a normal <code>img<\/code> tag:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">&lt;img\n  src={ogImage}\n  alt={<span class=\"hljs-string\">`Open Graph preview for <span class=\"hljs-subst\">${post.data.title}<\/span>`<\/span>}\n  <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span><\/span>=<span class=\"hljs-string\">\"aspect-&#91;1200\/630] w-full object-cover bg-muted\"<\/span>\n  loading=<span class=\"hljs-string\">\"lazy\"<\/span>\n\/&gt;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>That means every card shows the final Cloudinary image, not a placeholder.<\/p>\n<p>Below the image, you\u2019ll also render the post metadata and a few actions like opening the OG image, opening the post page, and sharing it.<\/p>\n<p>For the full homepage file, check the GitHub:<\/p>\n<ul>\n<li>\n<a href=\"https:\/\/github.com\/musebe\/astro-cloudinary-og-demo\/blob\/main\/src\/pages\/index.astro\"><code>src\/pages\/index.astro<\/code><\/a>\n<\/li>\n<\/ul>\n<div class='c-callout  c-callout--inline-title c-callout--note'><strong class='c-callout__title'>Note:<\/strong> <p>Make sure to add the actual Open Graph and Twitter meta tags so each article is ready to share.<\/p><\/div>\n<h3>Building the Post Route<\/h3>\n<p>In <code>src\/pages\/posts\/[slug].astro<\/code>, you\u2019ll create a route for each Markdown post with <code>getStaticPaths()<\/code>:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">getStaticPaths<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> posts = <span class=\"hljs-keyword\">await<\/span> getCollection(<span class=\"hljs-string\">'blog'<\/span>, ({ data }) =&gt; !data.draft);\n\n  <span class=\"hljs-keyword\">return<\/span> posts.map(<span class=\"hljs-function\">(<span class=\"hljs-params\">post<\/span>) =&gt;<\/span> ({\n    <span class=\"hljs-attr\">params<\/span>: { <span class=\"hljs-attr\">slug<\/span>: post.id },\n    <span class=\"hljs-attr\">props<\/span>: { post },\n  }));\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This tells Astro to build one page per post.<\/p>\n<h3>Generating the OG Image for the Post<\/h3>\n<p>After receiving the post data, call the same Cloudinary helper:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> ogImage = generateOgImage({\n  <span class=\"hljs-attr\">title<\/span>: post.data.title,\n  <span class=\"hljs-attr\">description<\/span>: post.data.description,\n});\n\n<span class=\"hljs-keyword\">const<\/span> pageUrl = <span class=\"hljs-string\">`https:\/\/astro-cloudinary-og-demo.vercel.app\/posts\/<span class=\"hljs-subst\">${post.id}<\/span>\/`<\/span>;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Now you\u2019ll have both:<\/p>\n<ul>\n<li>the generated OG image<\/li>\n<li>the live page URL<\/li>\n<\/ul>\n<h3>Adding the Meta Tags<\/h3>\n<p>This is the part social platforms care about:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta<\/span> <span class=\"hljs-attr\">property<\/span>=<span class=\"hljs-string\">'og:title'<\/span> <span class=\"hljs-attr\">content<\/span>=<span class=\"hljs-string\">{post.data.title}<\/span> \/&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta<\/span> <span class=\"hljs-attr\">property<\/span>=<span class=\"hljs-string\">'og:description'<\/span> <span class=\"hljs-attr\">content<\/span>=<span class=\"hljs-string\">{post.data.description}<\/span> \/&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta<\/span> <span class=\"hljs-attr\">property<\/span>=<span class=\"hljs-string\">'og:type'<\/span> <span class=\"hljs-attr\">content<\/span>=<span class=\"hljs-string\">'article'<\/span> \/&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta<\/span> <span class=\"hljs-attr\">property<\/span>=<span class=\"hljs-string\">'og:url'<\/span> <span class=\"hljs-attr\">content<\/span>=<span class=\"hljs-string\">{pageUrl}<\/span> \/&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta<\/span> <span class=\"hljs-attr\">property<\/span>=<span class=\"hljs-string\">'og:image'<\/span> <span class=\"hljs-attr\">content<\/span>=<span class=\"hljs-string\">{ogImage}<\/span> \/&gt;<\/span>\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">'twitter:card'<\/span> <span class=\"hljs-attr\">content<\/span>=<span class=\"hljs-string\">'summary_large_image'<\/span> \/&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">'twitter:title'<\/span> <span class=\"hljs-attr\">content<\/span>=<span class=\"hljs-string\">{post.data.title}<\/span> \/&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">'twitter:description'<\/span> <span class=\"hljs-attr\">content<\/span>=<span class=\"hljs-string\">{post.data.description}<\/span> \/&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">meta<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">'twitter:image'<\/span> <span class=\"hljs-attr\">content<\/span>=<span class=\"hljs-string\">{ogImage}<\/span> \/&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>These tags tell X, Facebook, LinkedIn, and other social platforms what to show when the page is shared.<\/p>\n<h3>Updating the Share Buttons<\/h3>\n<p>On the homepage, we also changed the share buttons to use the <strong>post page URL<\/strong>, not the raw Cloudinary image URL.<\/p>\n<pre class=\"js-syntax-highlighted\"><code>const postUrl = `https:\/\/astro-cloudinary-og-demo.vercel.app\/posts\/${post.id}\/`;\n<\/code><\/pre>\n<p>That is important because social platforms expect a page with metadata, not just a direct image link.<\/p>\n<p>For the full implementation, check:<\/p>\n<ul>\n<li>\n<a href=\"https:\/\/github.com\/musebe\/astro-cloudinary-og-demo\/blob\/main\/src\/pages\/posts\/%5Bslug%5D.astro\"><code>src\/pages\/posts\/[slug].astro<\/code><\/a>\n<\/li>\n<li>\n<a href=\"https:\/\/github.com\/musebe\/astro-cloudinary-og-demo\/blob\/main\/src\/pages\/index.astro\"><code>src\/pages\/index.astro<\/code><\/a>\n<\/li>\n<\/ul>\n<h2>Testing and Final Thoughts<\/h2>\n<p>At this point, the full flow is in place. Astro owns the content, Cloudinary generates the social image, and each post page shows the correct metadata when shared across channels.<\/p>\n<p>The last step is ensuring the setup behaves exactly as you expect.<\/p>\n<h3>Testing the Flow<\/h3>\n<p>The most effective way to verify your work is to open a post page and inspect the result. Go down this checklist:<\/p>\n<ul>\n<li>The text in the browser tab matches the text rendered on the social image.<\/li>\n<li>The image URL loads correctly without broken transformations.<\/li>\n<li>The page source has <code>og:image<\/code> and <code>twitter:image<\/code> tags. (These are what social platforms actually read.)<\/li>\n<li>The link preview looks correct for X or LinkedIn when you check it with a tool like the <a href=\"https:\/\/www.opengraph.xyz\/\">Open Graph Visualizer<\/a>.<\/li>\n<\/ul>\n<h3>Final Thoughts and Next Steps<\/h3>\n<p>By moving the design logic into the URL, you remove several friction points from your publishing workflow.<\/p>\n<p>You\u2019ll no longer need to export a new OG image for every single post, maintain a bloated folder of static social assets in your repository, and then open a design tool every time a post title changes. With just one high-quality template, Astro and Cloudinary handle the repetitive work. The OG image becomes a dynamic, generated part of your content system.<\/p>\n<p>This project is a solid foundation, but there\u2019s always room to iterate. Now that you have the core logic working, consider these improvements:<\/p>\n<ul>\n<li>\n<strong>Author profiles.<\/strong> Add the author\u2019s name or avatar as a second overlay.<\/li>\n<li>\n<strong>Dynamic categories.<\/strong> Use different text colors or icons based on the post tags.<\/li>\n<li>\n<strong>Dark mode support.<\/strong> Create a second \u201cDark Template\u201d and toggle the base image ID based on a theme preference.<\/li>\n<li>\n<strong>Featured images.<\/strong> Layer a small thumbnail of the post\u2019s featured image into the corner of the OG card.<\/li>\n<\/ul>\n<p>That\u2019s the beauty of a dynamic system. Once you\u2019ve set the logic, adding a new feature is just a matter of updating a URL string.<\/p>\n<h3>Live Resources<\/h3>\n<p>You can see the final result in action or dive into the underlying code:<\/p>\n<ul>\n<li>\n<a href=\"https:\/\/astro-cloudinary-og-demo.vercel.app\/\"><strong>Live Demo<\/strong><\/a>\n<\/li>\n<li>\n<a href=\"https:\/\/github.com\/musebe\/astro-cloudinary-og-demo\"><strong>Full Source Code on GitHub<\/strong><\/a>\n<\/li>\n<\/ul>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":87,"featured_media":39971,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[370],"class_list":["post-39943","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-image"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.6 (Yoast SEO v26.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>How to Build Dynamic Open Graph Images With Astro and Cloudinary<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How to Build Dynamic Open Graph Images With Astro and Cloudinary\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-03-31T14:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-03-31T17:21:52+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1774641808\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary.jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"2000\" \/>\n\t<meta property=\"og:image:height\" content=\"1100\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"melindapham\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"How to Build Dynamic Open Graph Images With Astro and Cloudinary\",\"datePublished\":\"2026-03-31T14:00:00+00:00\",\"dateModified\":\"2026-03-31T17:21:52+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro\"},\"wordCount\":11,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1774641808\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary.jpg?_i=AA\",\"keywords\":[\"Image\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2026\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro\",\"url\":\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro\",\"name\":\"How to Build Dynamic Open Graph Images With Astro and Cloudinary\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1774641808\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary.jpg?_i=AA\",\"datePublished\":\"2026-03-31T14:00:00+00:00\",\"dateModified\":\"2026-03-31T17:21:52+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1774641808\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1774641808\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary.jpg?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How to Build Dynamic Open Graph Images With Astro and Cloudinary\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\",\"name\":\"melindapham\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"caption\":\"melindapham\"}}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"How to Build Dynamic Open Graph Images With Astro and Cloudinary","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro","og_locale":"en_US","og_type":"article","og_title":"How to Build Dynamic Open Graph Images With Astro and Cloudinary","og_url":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro","og_site_name":"Cloudinary Blog","article_published_time":"2026-03-31T14:00:00+00:00","article_modified_time":"2026-03-31T17:21:52+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1774641808\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary.jpg?_i=AA","type":"image\/jpeg"}],"author":"melindapham","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"How to Build Dynamic Open Graph Images With Astro and Cloudinary","datePublished":"2026-03-31T14:00:00+00:00","dateModified":"2026-03-31T17:21:52+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro"},"wordCount":11,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1774641808\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary.jpg?_i=AA","keywords":["Image"],"inLanguage":"en-US","copyrightYear":"2026","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro","url":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro","name":"How to Build Dynamic Open Graph Images With Astro and Cloudinary","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1774641808\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary.jpg?_i=AA","datePublished":"2026-03-31T14:00:00+00:00","dateModified":"2026-03-31T17:21:52+00:00","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1774641808\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1774641808\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary.jpg?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/dynamic-open-graph-images-astro#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"How to Build Dynamic Open Graph Images With Astro and Cloudinary"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9","name":"melindapham","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","caption":"melindapham"}}]}},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1774641808\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary\/Blog_How_to_Build_Dynamic_Open_Graph_Images_with_Astro_and_Cloudinary.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/39943","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/87"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=39943"}],"version-history":[{"count":1,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/39943\/revisions"}],"predecessor-version":[{"id":39944,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/39943\/revisions\/39944"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/39971"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=39943"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=39943"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=39943"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}