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.

iMessage Link Preview Screenshot

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.

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.

seo.js
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.

seo.js
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.

gatsby-node.mjs
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.

blog-post.js
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.

blog-post.js
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.

Profile picture

Written by J. Patrick Fulton.


gatsbyjs open graph markdown blog seo