Automating social cards for Gatsby.js
If you share any of my blog posts on Twitter, Facebook, or LinkedIn, you should see something like a branded image card showing up in your share preview. These are commonly known as OG (Open Graph) image sharing cards, and almost every social media platform supports them when sharing links.
Having sharing images when distributing your content around the web can really help with more people noticing your content. A picture pops out more from a text-filled Twitter feed. Social images with brand styling also help capture your audience's attention, as they might recognize your brand colors and fonts better than just a random image in their feed.
You can quite easily add this functionality to your website by altering a few meta tags and finding an accompanying image for every page on your blog. But what if you don't want to use photos? Or perhaps your website has hundreds of pages and finding a good photo for each page—or creating them yourself individually—just isn't a viable strategy. In this case, you can look into creating an automated process to generate these images.
In this tutorial, we're going to look at how to generate social sharing images automatically by building a custom Gatsby plugin that uses Jimp, a Node.js image manipulation library. Our sharing cards will display the title of our blog post, the most relevant tag, and the estimated reading time. The tutorial consists of five parts:
- What are OG-tags and why use them
- Setting React Helmet to support OG-tags and Twitter tags
- Modifying the blog-post template to support tags and reading time
- Creating the social sharing image template
- Building the gatsby-plugin-og-images plugin
What are OG-tags and why use them
OG-tags (Open Graph tags) are meta tags in your page's <head> that social platforms use for link previews. When someone shares a link to your blog post on Facebook, LinkedIn, or other platforms, these tags tell the platform what title, description, and image to display.
Twitter also supports its own set of tags (Twitter Cards), and for maximum compatibility, you'll want to implement both.
The basic OG tags you need are:
og:title— The title of your pageog:description— A brief descriptionog:image— The URL to the preview image (1200×630 pixels is ideal)og:url— The canonical URL of your pageog:type— The type of content (e.g., "article" for blog posts)
Setting React Helmet to support OG-tags and Twitter tags
Let's start by modifying our Gatsby site to support OG-tags and Twitter tags. First, install the required packages:
npm install react-helmet gatsby-plugin-react-helmet
Add the plugin to your gatsby-config.js:
module.exports = {
siteMetadata: {
title: "My Blog",
description: "Thoughts on building things.",
siteUrl: "https://example.com",
twitterUsername: "@yourhandle",
},
plugins: [
`gatsby-plugin-react-helmet`,
// ...other plugins
],
}
Now create or update your SEO component at src/components/seo.js:
import React from "react"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
export default function SEO({
title,
description,
pathname,
image,
article = false,
}) {
const { site } = useStaticQuery(graphql`
query {
site {
siteMetadata {
title
description
siteUrl
twitterUsername
}
}
}
`)
const {
siteUrl,
title: defaultTitle,
description: defaultDescription,
twitterUsername,
} = site.siteMetadata
const seo = {
title: title || defaultTitle,
description: description || defaultDescription,
url: `${siteUrl}${pathname || "/"}`,
image: image ? `${siteUrl}${image}` : `${siteUrl}/og/default.png`,
}
return (
<Helmet title={seo.title} titleTemplate={`%s | ${defaultTitle}`}>
<html lang="en" />
<meta name="description" content={seo.description} />
{/* Open Graph */}
<meta property="og:url" content={seo.url} />
<meta property="og:type" content={article ? "article" : "website"} />
<meta property="og:title" content={seo.title} />
<meta property="og:description" content={seo.description} />
<meta property="og:image" content={seo.image} />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
{twitterUsername && (
<meta name="twitter:creator" content={twitterUsername} />
)}
<meta name="twitter:title" content={seo.title} />
<meta name="twitter:description" content={seo.description} />
<meta name="twitter:image" content={seo.image} />
</Helmet>
)
}
This component handles both Open Graph tags for Facebook/LinkedIn and Twitter Card tags. The image prop will be the path to our auto-generated OG image.
Modifying the blog-post template to support tags and reading time
To display the tag and reading time on our OG images, we need to ensure this data is available. Install the reading time plugin:
npm install gatsby-remark-reading-time
Add it to your gatsby-config.js:
module.exports = {
plugins: [
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [`gatsby-remark-reading-time`],
},
},
],
}
Now update your blog post template at src/templates/blog-post.js:
import React from "react"
import { graphql } from "gatsby"
import SEO from "../components/seo"
export default function BlogPostTemplate({ data, location }) {
const post = data.markdownRemark
const title = post.frontmatter.title
const description = post.frontmatter.description || post.excerpt
const tags = post.frontmatter.tags || []
const primaryTag = tags[0] || "Blog"
const readingTime = post.fields.readingTime?.text || "5 min read"
// This path will be generated by our plugin
const ogImage = post.fields.ogImage
return (
<>
<SEO
title={title}
description={description}
pathname={location.pathname}
image={ogImage}
article
/>
<article>
<h1>{title}</h1>
<p>
{primaryTag} • {readingTime}
</p>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
</>
)
}
export const pageQuery = graphql`
query BlogPostBySlug($id: String!) {
markdownRemark(id: { eq: $id }) {
id
html
excerpt(pruneLength: 160)
frontmatter {
title
description
tags
}
fields {
slug
readingTime {
text
minutes
time
words
}
ogImage
}
}
}
`
Creating the social sharing image template
We want our social sharing cards to follow a consistent theme, with only the text changing between posts. Create a base template image:
- Dimensions: 1200×630 pixels (the standard OG image size)
- Include: Your brand colors, logo, domain name, and any decorative elements
- Leave space: For the title, tag, and reading time text
Save this template image to static/og/template.png in your Gatsby project.
For the text, we'll use Jimp's built-in fonts initially. If you need custom fonts, you can create bitmap fonts (.fnt files) using tools like Hiero or BMFont.
When designing your template, measure the pixel offsets from the edges where you want the text to appear. You'll need these values when positioning text in the plugin.
Building the gatsby-plugin-og-images plugin
Now for the main event: building a custom Gatsby plugin that generates OG images during the build process.
Plugin folder structure
Create the following structure in your Gatsby project:
plugins/
gatsby-plugin-og-images/
package.json
index.js
gatsby-node.js
package.json
{
"name": "gatsby-plugin-og-images",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"jimp": "^0.22.12",
"mkdirp": "^3.0.1"
}
}
Run npm install inside the plugin folder to install the dependencies.
index.js
This file just needs to export an empty object:
module.exports = {}
gatsby-node.js
This is where the magic happens. The plugin will:
- Find all Markdown posts during the build
- Generate an image for each post at
public/og/<slug>.png - Add an
ogImagefield to each post node so templates can query it
const path = require("path")
const fs = require("fs")
const mkdirp = require("mkdirp")
const Jimp = require("jimp")
function safeFileName(input) {
return input
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)+/g, "")
}
async function generateOgImage({
outputPath,
title,
tag,
readingTime,
templatePath,
}) {
const image = await Jimp.read(templatePath)
// Load built-in fonts (you can replace with custom bitmap fonts)
const titleFont = await Jimp.loadFont(Jimp.FONT_SANS_64_BLACK)
const metaFont = await Jimp.loadFont(Jimp.FONT_SANS_32_BLACK)
// Layout constants - adjust these to match your template
const LEFT_MARGIN = 80
const TITLE_TOP = 110
const TITLE_MAX_WIDTH = 1040
const TITLE_MAX_HEIGHT = 240
const META_TOP = 420
// Print the title (with word wrap)
image.print(
titleFont,
LEFT_MARGIN,
TITLE_TOP,
{
text: title,
alignmentX: Jimp.HORIZONTAL_ALIGN_LEFT,
alignmentY: Jimp.VERTICAL_ALIGN_TOP,
},
TITLE_MAX_WIDTH,
TITLE_MAX_HEIGHT
)
// Print the tag and reading time
const metaText = `${tag} • ${readingTime}`
image.print(metaFont, LEFT_MARGIN, META_TOP, metaText)
await image.writeAsync(outputPath)
}
exports.onPostBuild = async ({ graphql, reporter }, pluginOptions) => {
const templatePath =
pluginOptions.templatePath ||
path.resolve(process.cwd(), "static/og/template.png")
if (!fs.existsSync(templatePath)) {
reporter.panic(
`[gatsby-plugin-og-images] Missing template image at: ${templatePath}`
)
}
const result = await graphql(`
{
allMarkdownRemark {
nodes {
id
frontmatter {
title
tags
}
fields {
slug
readingTime {
text
}
}
}
}
}
`)
if (result.errors) {
reporter.panic(
"[gatsby-plugin-og-images] GraphQL query failed",
result.errors
)
}
const posts = result.data.allMarkdownRemark.nodes
const outDir = path.resolve(process.cwd(), "public/og")
await mkdirp(outDir)
reporter.info(
`[gatsby-plugin-og-images] Generating ${posts.length} OG images...`
)
for (const post of posts) {
const title = post.frontmatter.title || "Untitled"
const tag = post.frontmatter.tags?.[0] || "Blog"
const readingTime = post.fields.readingTime?.text || "5 min read"
const slug = post.fields.slug || safeFileName(title)
const fileName = safeFileName(slug.replace(/\//g, "-"))
const outputPath = path.join(outDir, `${fileName}.png`)
try {
await generateOgImage({
outputPath,
title,
tag,
readingTime,
templatePath,
})
} catch (e) {
reporter.warn(
`[gatsby-plugin-og-images] Failed generating image for ${slug}: ${e.message}`
)
}
}
reporter.info(`[gatsby-plugin-og-images] Done.`)
}
exports.onCreateNode = ({ node, actions }) => {
const { createNodeField } = actions
// Add ogImage field to Markdown nodes so templates can query it
if (node.internal.type === "MarkdownRemark") {
const slugField = node.fields?.slug
if (!slugField) return
const fileName = safeFileName(slugField.replace(/\//g, "-"))
createNodeField({
node,
name: "ogImage",
value: `/og/${fileName}.png`,
})
}
}
Adding the plugin to your site
In your main gatsby-config.js, add the plugin:
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-og-images`,
options: {
templatePath: `${__dirname}/static/og/template.png`,
},
},
// ...other plugins
],
}
Now when you run gatsby build, you'll see images generated in public/og/ and your blog posts will automatically include the correct OG image meta tags.
Usage with Contentful
If you're using Contentful instead of Markdown files, you'll need to modify the GraphQL queries. In the onPostBuild function, change the query to:
const result = await graphql(`
{
allContentfulBlogPost {
nodes {
id
title
tags
slug
body {
childMarkdownRemark {
fields {
readingTime {
text
}
}
}
}
}
}
}
`)
And update the onCreateNode function to handle Contentful nodes:
exports.onCreateNode = ({ node, actions }) => {
const { createNodeField } = actions
if (node.internal.type === "ContentfulBlogPost") {
const fileName = safeFileName(node.slug)
createNodeField({
node,
name: "ogImage",
value: `/og/${fileName}.png`,
})
}
}
Testing your OG images
After building your site, test your OG images using these tools:
- Twitter Card Validator — Test how your cards will appear on Twitter
- Facebook Sharing Debugger — Debug and preview Facebook shares
- LinkedIn Post Inspector — Check how LinkedIn will display your links
These platforms cache OG images aggressively, so use the debug tools to force a refresh when testing changes.
Conclusion
Automating OG image generation saves time and ensures consistency across your blog. With this plugin, every new post automatically gets a branded social sharing image without any manual work.
The key benefits:
- Consistency: Every post follows your brand guidelines
- Automation: No manual image creation needed
- Dynamic content: Title, tags, and reading time are always up to date
- SEO improvement: Better social previews lead to higher click-through rates
Feel free to customize the plugin to match your design—adjust the font sizes, positions, colors, and add any additional elements you want on your cards.