You just published a blog post that has the potential to be the best idea any developer has ever had, so you share it on whatever Twitter is called these days, but then the link doesn’t unfurl because there’s no Open Graph image.
Users scroll right on by your social message and that great idea fades away. Having preview images for a web page gives it an extra layer of polish when the page is shared on social platforms. But generating an image for every page can be tedious work.
Automation to the rescue!
#TL;DR
We’re going to dig into a method of automating the generation of Open Graph images for the pages of a site. We’ll explore how Edge Functions can generate these images for you on-demand, and how the content from your site can be use to populate the images.
This example (and its demo) demonstrates a template you could build once and apply it to all pages on your site.
Just deploy and play
#Links with images get more clicks
Okay, maybe an image won’t make or break the success of a post. But it will add a level of refinement to your web content, adding more clicks (at least that’s what ChatGPT told me).
And yet, adding those images is yet another tedious thing that slows down the publishing process.
#Images can be dynamically generated
But what if it didn’t have to be tedious? What if the images were automatically generated for every page on your site and all you had to do was check the image before publishing?
That’s one way we’ve streamlined our publishing process for this site. I’m going to show you how it works with a trimmed-down example.
#How to generate dynamic images with Edge Functions
Netlify Edge Functions are a powerful way to generate content upon request and serve the appropriate (dynamic) response from the edge (geographically close to the user).
To generate an image response for an Edge Function, we’ll use og_edge, a project from Matt Kane that is built on @vercel/og (which is built on satori). It’s designed to run in Deno, the runtime environment for Edge Functions.
There’s more to how all this works, but we can piece that together along the way. Let’s start building!
#Set up your project
We’re going to focus exclusively on image generation so that it’s easier to take and apply to your project. For that reason, we won’t work with a framework and don’t need much to get started.
#Starting from scratch
If you’re going to follow along, you can start from scratch with the following:
- Basic
package.json
with http-server installed public
directory with a boilerplateindex.html
file- A
dev
script inpackage.json
set tohttp-server --port 3000 ./public
- Netlify CLI installed globally
#Install VS Code Deno recipes
If you’re not used to working in Deno and if you’re working in VS Code, you can use the vscode
recipe to add the appropriate settings to VS Code. Run the following command in your terminal:
You’ll also want to add deno.path
to .vscode/settings.json
and have it set to the local path to the deno
runtime.
In the end, your .vscode/settings.json
should contain five deno
properties:
#Start development server
We can test Edge Functions locally using Netlify Dev. With the Netlify CLI installed globally, run this command:
This will open a new browser window and should serve the public/index.html
file.
Note that yarn dev
and 3000
should be set to the appropriate values for your project.
#Build a basic image generator
Because we’re working with Deno, we don’t have to install any dependencies to get started. However, if you prefer to work in TypeScript (which is what we’ve shown in these examples), you’ll want to install typescript
and add a tsconfig.json
file.
Once you have what you need, add the edge function to netlify/edge-functions/image-preview.tsx
.
This is about as simple as this process gets:
STYLES
sets us up with an organized set of style rules ready to be expanded for the title and description.- The
page
data is hard-coded (for now). - We use
ImageResponse
fromog_edge
to return an image response whenever visiting/preview-image
, which is specified byconfig
.
The result is an unflattering image, but it works!
#Style the image
Let’s step through a few different tasks that will help with styling the image.
#Add style rules to title and description
First, let’s add some CSS to the content by adding new rules to STYLES
and using style
attributes in the markup.
And now we have a bit more styling:
#Add background and logo SVG images
I like to use SVG images to spruce up these images. That lets me do the work in a design program like Figma and then drop into the project as components.
Let’s start with the background image. Feel free to use any image you’d like. Or borrow from the example. In the example project, I added a file for the background image component at netlify/edge-functions/assets/BackgroundImage.tsx
:
And I added a logo file at netlify/edge-functions/assets/Logo.tsx
:
Then we can import those into the function and use them as JSX components:
And now it’s really starting to come together!
#Add fonts to the image
Using custom fonts can be a tricky process. I recommend you follow the example exactly for this one and then veer off on your own when you have the hang of it. (There are gotchas listed at the end of this guide.)
First, add the fonts to your public directory (I put them in public/fonts
). Here are the example fonts.
We’ll add the fonts into the function in four steps:
- Define font attributes
- Add font family to styles
- Use a function to load the font data
- Include the font data in the image response
Refresh, and if everything was configured properly, you should see the new fonts in your image!
#Use dynamic content
Now we have the base in place and have added some styling. All that is left is making the page content dynamic so we can use this function for every page (or some predictable set of pages) on the site.
#Accessing sitemap data
There are several ways to be able to get the content you need to render the function. I’ve tried a few different approaches, but have found the most reliable and efficient approach to be caching sitemap content in a JSON file that can be served statically.
Your mileage may vary as a site scales, but at the time of writing this, we’re using a data cache that comes in at 15 KB (without minifying) and gets the job done.
You can experiment with methods that may work better for your project. However, be cautious about calling external API endpoints for every image request, which may put you at risk for hitting API limits, depending on the service you’re using and volume of content on the site.
#Mock sitemap content
For this example, I added a mock set data for 20 pages. (Thanks, ChatGPT!) Each page has title
, description
, and slug
properties. We’ll use each of these in the function.
Drop the sitemap data in your public
directory if following along —public/sitemap-data.json
:
#Load sitemap content
To load the dynamic content, we’ll replace our static page
object with a call to fetch the sitemap data, put a catch to return 404 when the slug
isn’t present in the sitemap, and then adjust the Edge Function’s route to be dynamic:
After this update, you’ll need to add a slug value to the URL when generating an image. Example: /preview-image/guide-to-netlify-dev
.
And then you’ll see dynamic content!
Notice that with the wrong slug (/preview-image/__WRONG__
) you should get a 404 response.
#Build out the rest of the system
This is just enough to get you started. To prep this for production, you’ll have a few more tasks ahead of you.
#Add meta tags to layouts
You’ll want to make sure that you’re rendering the appropriate meta tag(s) to the <head>
of the appropriate pages or layouts. This will vary from framework to framework, but should resolve to HTML like this:
#Other ideas for improvements
Here are some other considerations for improvement :
- Character limitations for content so that it doesn’t flow outside the bounds of the image
- Dynamic font-sizing based on the length of content
- Optional overrides for title and description so that image content can differ from meta values
- Different layouts/backgrounds for different types of pages
#Limitations and gotchas of this approach
It’s worth noting a few of the limitations and gotchas of this approach. I ran into several hurdles along the way that I hope you can avoid.
#satori is extremely limiting with styling
The engine used to produce these responses is awesome! But it’s also limited and very difficult to debug because it doesn’t tell you when you’ve done something wrong.
Through trial and error, here’s what I can tell you:
- Read through the supported CSS rules and know them well. For example, you can only use
flex
fordisplay
. - Not all font file types are supported.
- Variable fonts are not supported — you must use the explicit font-weight variant.
- HTML can not be deeply nested.
When in doubt, refer to the satori docs. I found that when I received an obscure error, it was usually the result of a satori limitation.
#Be mindful of external requests
I mentioned this in passing above, but it’s important to be mindful of external requests from these edge functions. You may need to come up with clever caching strategies to avoid limitations of external services when using this approach.
#…and have fun!
Most of all, I hope this is productive and fun for you to tinker with. Once I got it working, it was very cool to see these images come to life uniquely for every page on the site. It’s going to save us so much time as we consistently add new content.