For Tailwind CSS users, HTML is a faster design tool than Sketch and Figma. Better, too. Use the design system that powers your website to generate its own static assets.

Dynamically generating images is so hot right now. Problem is services like Imgix have super complex parameters. Especially when it comes to orchestrating text. Just look at the length of the tutorial on multiline text!

A simpler method is to use a service that generates screenshots of your website. Then you can design your image using the CSS, fonts and colours of your website.

So let's create dedicated pages for Open Graph images, and dynamically generate screenshots of them.

Preview This Page's Image

(Note: You can totally do all this without Gatsby and Tailwind CSS, but that's what this tutorial will be using.)


In gatsby-node.js you probably have some code like this. For the query above this function, you're generating a page for every result.

path: path,
component: require.resolve("./src/templates/PostSingle.js"),
context: { id: id, slug: path },

Let's duplicate this to also create our Screenshot page. We've added an additional context key slug, with the original URL path. More on that later.

path: `${path}/screenshot`,
component: require.resolve("./src/templates/Screenshot.js"),
context: { id: id, slug: path },

Now for every page in this query, if you add /screenshot on the end of the URL you'll be served a page generated by Screenshot.js.

And now in your Screenshot.js you have access to all the same content of the original page, but in another component in which you can design your share image.

I've made mine 1200×630px. Absolutely positioned to the top of the window and centred. So when we take a snap we can crop to this size.

className="absolute inset-0 mx-auto flex overflow-hidden"
style={{ maxWidth: 1200, maxHeight: 630 }}
// ...design with your title, image, publish date, etc

Side quest: Add a nofollow,noindex meta tag to your /screenshot page. Also set the canonical <link> and URL meta tags to the original page, by removing /screenshot off the current path.


Now we're creating dynamic pages for our images, we need to generate an image. There's a bunch of services to do this but I found API Flash super easy to setup.

Once you've got your access key, go to your original PageSingle.js component.


  1. Share image URLs need to be fully qualified – https://example.com/image.jpg – not relative – /image.jpg
  2. These URLs need to be present in the build, and load with JavaScript disabled, not after hydration. So building the image URL using location from Reach Router is out. As is window.location. These won't exist during gatsby build.

This is why we passed along the extra slug key in as context. Page context variables are present during build.

// PageSingle.js
const PostSingle = ({ data, pageContext }) => {
// Your Gatsby site should have the production URL stored in gatsby-config.js
const { siteUrl } = data.site.siteMetadata;
// Then we get the slug of the page from pageContext on build
const { slug } = pageContext;
// This page's full URL
const buildTimeUrl = `${siteUrl}${slug}`;
// This page's screenshot page URL
const buildTimeUrlScreenshot = `${buildTimeUrl}/screenshot`;
// URL to our image
const metaImage = `https://api.apiflash.com/v1/urltoimage?access_key=YOUR_ACCESS_KEY&url=${buildTimeUrlScreenshot}&format=png&height=630&width=1200&response_type=image`;
// ... return, etc

The parameters we're passing to API Flash will create a PNG, crop it to the right side and ensure the response type returned is an image.

Pass the long metaImage variable into your SEO/React Helmet component and you're done!

At a minumum, make sure you're generating these meta tags along with your generated URL above.

<meta property="og:image" content="https://api.apiflash..." />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />

Facebook's Sharing Debugger is a good tool to make sure your deployed share image is registering as expected.

Again, checking that the meta tag is present after JavaScript has loaded will not be sufficient. If your share images aren't appearing in the debugger, disable JavaScript and load the page to make sure the URL is being loaded.


On API Flash the image should be cached the first time it is retrieved. But you could add additional parameters to make sure it refreshes when you publish updates.

The free plan gives you up to 100 screenshots a month, but it works so well it's worth upgrading for a larger website.