Generating OG images with Astro
As I was adding a blog section to the site, I was curious how to possibly generate the OG (Open Graph) images for the blog posts so they give a nice impression when shared via social media. My site is statically generated so I needed a way to create them ideally when the site is build. Turns out it’s super easy to do in Astro via its Endpoint
feature.
This blog post will show you how to create OG images for each of your blog posts and place the generated image next to the blog post.
Endpoints
in Astro
Endpoints
in Astro are well, endpoints. Instead of defining an Astro component or Markdown file to serve at a specific route, you can use them to execute dynamic code. Like your ordinary Astro pages, they are file based. However, when statically building your Astro project, they are executed at build time. Perfect to generate files while building the project, remember we statically build the project!. In this case, we will use it to generate our OG images for each blog post.
That said, Endpoints
are just simple functions that follow a certain format, they are akin to serverless functions.
Basic Example - Generating a json file
To generate a json
file with the following content:
{ "name": "Astro", "url": "https://astro.build/"}
and have it available at /builtwith.json
in your deployed site, all you need to do is create a file called builtwith.json.ts
in your src/pages
folder and add the following code:
export function GET({ params, request }) { return new Response( JSON.stringify({ name: "Astro", url: "https://astro.build/" }) );}
Basically you need to export a function that is named after a HTTP verb (e.g. if you where to use POST
, you could define what should happen when a POST
requests hits the endpoint), in our case though, since we are running it at build time, where we have limited options, the only HTTP verb name that we can use is GET
. In addition we get info about the request and parameters that have been passed to the endpoint. The function returns a Response
object, which is a standard Web API that represents the response to a request.
Armed with that info and a little modification we can use this to generate our OG images.
Placing the OG image next to the blog post
Now that we know that we are using Endpoints to dynamically generate the OG images, we need to tell Astro where to place them. Luckily Endpoints are file route based, just like ordinary Astro pages, we can leverage dynamic routes for that.
Blog posts are generated via the following Astro page:
src/pages/blog/posts/[id].astro
which results in the following URL structure:
https://mysite.com/blog/posts/blog-post-1https://mysite.com/blog/posts/blog-post-2
To have the OG images appear next to the blog post like:
https://mysite.com/blog/posts/blog-post-1/og.pnghttps://mysite.com/blog/posts/blog-post-2/og.png
we need to create a new file [...id].png.ts
, so the new file structure looks like this:
src└── pages └── blog └── posts ├── [id].astro └── [...id].png.ts
[…id] because we need to create a nested route this time.
Generating the OG image
With the file structure in place we can now write the code to generate the OG image.
For the actual image generation we will use the @vercel/og
library. This library will take JSX, which will define the layout of our OG image, render it and return a PNG image.
So in your terminal run:
npm install @vercel/og
Then in our [...id].png.ts
file we need to:
- get all blog posts
- set correct paths
- render the image
- return the image
import fs from "fs";import path from "path";
import type { APIRoute } from "astro";
import { ImageResponse } from "@vercel/og";
import { getBlogPosts } from "../../../util";
// get all blog posts and set correct pathsexport async function getStaticPaths() { const posts = await getBlogPosts(); return posts.map(post => ({ params: { id: post.id + "/og" }, props: { post } }));}
// @vercel/og expects JSX or its AST// since I didn't want to add React to my project, I provide the JSX AST instead// shortened for brevityfunction getHtml(title: string): any { const html = { type: "div", props: { children: [ { type: "div", props: { tw: "shrink flex", children: [ { type: "div", props: { tw: "text-slate-800 font-bold text-6xl", children: title } } ] } } ] } };
return html;}
export const GET: APIRoute = ({ props }) => { // render and return the image return new ImageResponse(getHtml(props.post.data.title), { width: 1200, height: 630 });};
@vercel/og
s ImageResponse
class needs a JSX AST, which will be used to render the image. The JSX AST is provided by the getHtml
function and takes the title of the blog post as a parameter.
Like when rendering ordinary content collections with Astro, we need to define a getStaticPaths
function to tell Astro for which blog posts we want to generate the OG images. Notice the id
that we set: id: post.id + "/og"
, this is important as it will be used to define the location of the OG image. Remeber our Endpoint
file is called [...id].png.ts
. Astro will strip the .ts
extension and will just concatenate the id
that we provide, in this case post.id + "/og"
with the .png
extension. That’s how the destination is defined.
And that’s all you need to do. Now, whenever you build the site, it will go through all blog posts and generate an OG image next to the original blog post. Now all that’s left is to hook it up to your OG meta tags.
Adding the OG image to the blog post
In order for Facebook, X, WhatsApp, etc to find the OG image, we need to add some meta tags to the <head>
section. Which file dependes on your setup but in my case, I have a BlogLayout.astro
component that wraps all blog pages. So I added the following code to it:
---const permalink = new URL(Astro.url.pathname, Astro.site).href;const ogImageURL = new URL(Astro.url.pathname + "/og.png", Astro.site).href;
const { title, description } = post.data;---
{/*-- Open Graph / Facebook --*/} <meta property="og:type" content="website" /> <meta property="og:url" content={permalink} /> <meta property="og:title" content={title} /> <meta property="og:description" content={description} /> <meta property="og:image" content={ogImageURL} /> <meta property="og:image:width" content="1200" /> <meta property="og:image:height" content="630" />
{/*-- X --*/} <meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:url" content={permalink} /> <meta property="twitter:title" content={title} /> <meta property="twitter:description" content={description} /> <meta property="twitter:image" content={ogImageURL} />
Note how ogImageURL
is constructing the URL to the OG image that we just created.
And that’s it! Now your blog posts show up nicely when shared on social media.
Conclusion
While code wise it was super straight forward, there are some things to improve the developer experience. E.g. @vercel/og
needs a JSX AST to render the image. Since I didn’t want to add React to my project, I had to create the JSX AST beforehand manually. Which was a little cumbersome but as I don’t expect to to change the layout of the OG image often, this was a tradeoff I was willing to make.
Likewise the images are recreated every time the site is built. This is not a problem for me as I don’t have many blog posts yet, but could become a problem as I grow the site. Anyhow, both things where not important enough for me to get right, right now.
The goal of this blog post was to show how easily you can get started with generating OG images with Astro. If you have any questions or suggestions, feel free to reach out to me on X (link below).