Adding Open Graph Images to a Gatsby Site
July 09, 2023 - 9 min read (1719 words)
The Open Graph Protocol specifies a series of tags that
may be included in the header of an HTML
document to describe its content.
Among them are the og:image
tag which is used to render images associated
with a page in
iMessage link previews,
Twitter cards
and previews on other platforms. Most Gatsby starters
include several of these SEO tags but omit preview image support. This article
describes an implementation to add them to Gatsby website or blog.
The evolving GitHub repository storing this blog and its implementation can be found here.
Table of Contents
Key Package Versions in the Current Implementation
This site implementation uses fairly current versions of
Gatsby, its plugins and React at the time of this writing. The relevant
versions are highlighted below and are taken from package.json
.
"dependencies": {
...
"gatsby": "^5.11.0", "gatsby-plugin-google-gtag": "^5.11.0",
"gatsby-plugin-image": "^3.11.0", "gatsby-plugin-local-search": "^2.0.1",
"gatsby-plugin-manifest": "^5.11.0",
"gatsby-plugin-mdx": "^5.11.0", "gatsby-plugin-offline": "^6.11.0",
"gatsby-plugin-react-helmet": "^6.11.0",
"gatsby-plugin-sass": "^6.11.0",
"gatsby-plugin-sharp": "^5.11.0", "gatsby-plugin-sitemap": "^6.11.0",
"gatsby-plugin-styled-components": "^6.11.0",
"gatsby-plugin-typography": "^5.11.0",
"gatsby-remark-autolink-headers": "^6.11.0",
"gatsby-remark-code-titles": "^1.1.0",
"gatsby-remark-copy-linked-files": "^6.11.0",
"gatsby-remark-images": "^7.11.0",
"gatsby-remark-prismjs": "^7.11.0",
"gatsby-remark-responsive-iframe": "^6.11.1",
"gatsby-remark-smartypants": "^6.11.0",
"gatsby-source-filesystem": "^5.11.0",
"gatsby-transformer-sharp": "^5.11.0",
"prismjs": "^1.29.0",
...
"react": "^18.2.0", ...
"react-dom": "^18.2.0",
...
},
"devDependencies": {
"gatsby-cli": "^5.11.0", "gatsby-plugin-root-import": "^2.0.9",
"prettier": "^2.8.8",
"resolve-url-loader": "^5.0.0"
},
The Desired HEAD Tag Contents
Taken from the generated output
of a previous post,
the following HTML
from the HEAD
tag represents the desired content. The
highlighted lines are the target of this post’s exercise. The majority of the
other open graph and general purpose META
tags were already in place from the
starter version of the seo.js
component.
The missing elements, not included in the starters, were the og:image
and
twitter:image
meta tags. These elements drive the preview image used on
link previews in iMessage, Twitter Cards and other platform representations
of link previews.
Note: The current implementation of this blog still uses the
react-helmet
component. However,
the Gatsby HEAD API
was introduced in modern Gatsby versions and replaces that functionality. It is likely
that in a future pull request, I will modernize the implementation to use that
API and eliminate a warning on the topic when running in gatsby develop
mode.
<head>
...
<meta
name="description"
content="Quite painfully, the VS Code ms-dotnettools.csharp extension debugger
binaries do not work out-of-the-box on modern macOS versions (v12+). This article
outlines the steps that are necessary to get those debugger binaries working
macOS Monterey and beyond."
/>
<meta
property="og:title"
content="Fixing C# Debugging in Visual Studio Code on macOS"
/>
<meta
property="og:description"
content="Quite painfully, the VS Code ms-dotnettools.csharp extension debugger
binaries do not work out-of-the-box on modern macOS versions (v12+). This
article outlines the steps that are necessary to get those debugger binaries
working macOS Monterey and beyond."
/>
<meta property="og:type" content="website" />
<meta property="og:image" content="https://www.jpatrickfulton.dev/static/066d0dbddfa7c84427d75eda7696a50e/c0a1b/vscode.png" /> <meta property="twitter:image" content="https://www.jpatrickfulton.dev/static/066d0dbddfa7c84427d75eda7696a50e/c0a1b/vscode.png" /> <meta name="twitter:card" content="summary" />
<meta name="twitter:creator" content="jpatrickfulton" />
<meta name="twitter:site" content="jpatrickfulton" />
<meta
name="twitter:title"
content="Fixing C# Debugging in Visual Studio Code on macOS"
/>
<meta
name="twitter:description"
content="Quite painfully, the VS Code ms-dotnettools.csharp extension debugger
binaries do not work out-of-the-box on modern macOS versions (v12+). This
article outlines the steps that are necessary to get those debugger binaries
working macOS Monterey and beyond."
/>
<meta
name="keywords"
content="Visual Studio Code, csharp, macOS, debugging"
/>
...
</head>
Add a Default Open Graph Image
My objective was to ensure that all pages in the site have an open graph image
associated with them which may be overridden at the page level. To accomplish this,
the first step was to modify the seo.js
component.
Modify the SEO.js Component’s GraphQL Static Query
This component already used a static query to pull elements of data needed for
the other SEO header tags from GraphQL. I modified the static query to include
a new node: openGraphDefaultImage
that leveraged the
gatsby-plugin-image
and gatsby-plugin-sharp
plugins.
The new node is based on a file
query for an image (code.png
) which was placed
in the /src/images/open-graph/
folder of the site. My intention was to store images
in this folder that might be reused as open graph images across multiple pages.
In this case, the code.png
image is intended to be the default fallback image
for pages that have not declared an override image. The height
and width
parameters
here are of special importance. The specified height and width are the target dimensions
of the image that will be transformed by the sharp plugin and match the recommended
size of images used for these SEO tags.
The existence of the node will cause the plugins to process the image and then add data to the GraphQL site metadata that may be used in the component JavaScript as seen in the next section.
const { site, openGraphDefaultImage } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
title
description
author
siteUrl
social {
twitter
}
}
}
openGraphDefaultImage: file( relativePath: { eq: "open-graph/code.png" } ) { childImageSharp { gatsbyImageData(layout: FIXED, height: 580, width: 1200) } } }
`
);
Implement the Open Graph Image Tags within SEO.js
Once the static GraphQL queries has been modified, the rest of the component can be altered to support the use case.
On line 1, a new parameter to the component has been added to supply an
optional override image to replace the default image. Lines 5-6 show the
override logic. Two new meta tags are then added to the meta
array in the
embedded Helmet
component.
Note: The URLs pointing to images in the Open Graph tags must be fully qualified
to be used my most previewers (e.g. iMessage) and cannot be relative links. The
constructUrl
function concatenates the base site url to the paths queried from
GraphQL to create the fully qualified URL and handles several error conditions that
might arise from missing data.
function Seo({ description, lang, meta, keywords, title, openGraphImageSrc }) {
...
const imagePath = constructUrl( site.siteMetadata.siteUrl, openGraphImageSrc ?? openGraphDefaultImage.childImageSharp.gatsbyImageData.images.fallback.src ); ...
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${site.siteMetadata.title}`}
meta={[
...
{ property: `og:image`, content: imagePath, }, { property: `twitter:image`, content: imagePath, }, ...
]}
...
/>
);
}
function constructUrl(baseUrl, path) { if (baseUrl === "" || path === "") return ""; return `${baseUrl}${path}`;}
export default Seo;
With these steps complete, the component is prepared to accept override images and a default image is in place.
Allow Page-level Overrides to the Default Image
Each post in this blog implementation is generated from an MDX (markdown including React components) file, the objective is to allow each file to optionally provide a element in its frontmatter to specify a custom open graph image for the post.
Example MDX Frontmatter Configuration
Line 6 shows this example markdown frontmatter section setting a open graph image
for this post. Omission of the openGraphImage
declaration would cause the
default image to be included in the post’s meta tags.
---
title: "Fixing C# Debugging in Visual Studio Code on macOS"
date: 2023-07-08
description: "Quite painfully, the VS Code ms-dotnettools.csharp extension debugger binaries do not work out-of-the-box on modern macOS versions (v12+). This article outlines the steps that are necessary to get those debugger binaries working macOS Monterey and beyond."
keywords: ["Visual Studio Code", "csharp", "macOS", "debugging"]
openGraphImage: ../../../src/images/open-graph/vscode.png---
Extending the GraphQL Frontmatter Definition
The Gatsby Node API provides a hook for customizing the GraphQL schema. In this step, it is used to customize the frontmatter type definitions. This schema customization is required to support the GraphQL query expansions made in later steps.
Note: This gatsby-node.js
file was converted in an earlier series of commits
to ES module syntax
for better compatibility with the
MDX plugin
in Gatsby v5+. The extension of the file was changed to .mjs
as a result.
export const createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions;
createTypes(`
type Mdx implements Node {
frontmatter: MdxFrontmatter
}
type MdxFrontmatter @infer {
openGraphImage: File @fileByRelativePath
}
`);
};
Modifying the blog-post.js Template
The src/templates/blog-post.js
file is the template used by the
Gatsby Node API createPages hook
to render each MDX file into a blog page. It is composed of a number of components.
Among them is the seo.js
component modified in previous steps. Additionally,
the template hosts a dynamic pageQuery
to pull data from GraphQL to render
the post.
Alter the Dynamic GraphQL Page Query
With the GraphQL schema customizations made above in place, the pageQuery
can be altered to use the new frontmatter field: openGraphImage
. Again,
the
gatsby-plugin-image
and gatsby-plugin-sharp
plugins are used to process the image and add new data to the query result
about the processed image for use in the template.
export const pageQuery = graphql`
query BlogPostBySlug($slug: String!, $keywords: [String]!) {
site {
siteMetadata {
title
author
}
}
mdx(fields: { slug: { eq: $slug } }) {
id
excerpt(pruneLength: 160)
frontmatter {
title
date(formatString: "MMMM DD, YYYY")
description
keywords
openGraphImage { childImageSharp { gatsbyImageData(layout: FIXED, height: 580, width: 1200) } } }
...
}
...
}
`;
Alter the Template to Support Overrides
In a final step, we utilize the new data available from the GraphQL
dynamic page query to access the relative link to the processed
open graph image and pass it to the seo.js
component.
function BlogPostTemplate({
location,
pageContext,
data: { mdx, site, allMdx },
children,
}) {
const post = mdx;
...
const openGraphImageSrc = post.frontmatter.openGraphImage?.childImageSharp.gatsbyImageData.images .fallback.src; ...
return (
<Layout location={location} title={siteTitle}>
<Seo
title={post.frontmatter.title}
description={post.frontmatter.description || post.excerpt}
keywords={post.frontmatter.keywords}
openGraphImageSrc={openGraphImageSrc} />
...
</Layout>
);
}
At this point in the implementation, each MDX file may optionally specify an override to the default open graph image for the site by modifying its frontmatter configuration.
Appendix: Key Commits
The following are the commits made for this implementation in reverse chronological order. The bulk of the work was done in 04da7d5 and the rest were either supporting changes or refactoring.
- 92132ab - Rename ogDefaultImage to openGraphDefaultImage.
- b57fcaa - Rename featuredImage to openGraphImage.
- bb13f79 - Modernize image generation API.
- 48c080d - Use image local to blog folder for featured image.
- 04da7d5 - Add support for open graph images.
- d49ebfb - Add default open graph image.
Written by J. Patrick Fulton.