How To Optimize Your Gatsby Blog For SEO

Last updated Apr 17th, 2019
Gatsby is a great tool for creating static websites, landing pages, and blogs. This post will show you how you can optimize your blog for SEO using a simple config.

Originally published June 18th, 2018.

I've re-written the way I do SEO in my blog currently to be a lot more clean in 2019. You can view the current source here.

Man, this tool is really awesome 🔥

I've been using Gatsby for about a month now and after taking the time to learn it, I've really been enjoying it. So far, I've put together this blog, a landing page for a pet project, a portfolio website for a client and a blog for my business using Gatsby and I have a strong feeling that I'll be using it for a lot more projects. As a full-stack JavaScript-er, any day I don't have to use WordPress is a good day 😃 (just kidding, WordPress is actually pretty lit).

One of the most important parts of building a blog is making sure that each of your blog posts has the appropriate meta tags in them so that when your content is shared and indexed by search engines, it shows the correct title, description and keywords. The Gatsby documentation points to using the gatsby-plugin-react-helmet plugin to add meta tags to your blog, but doesn't give a nice concrete example as to how you might set up a nice reusable component to do this. That's what I'm going to share with you today.

We're going to create a simple reusable component that you can use in your blog post template.

Hooking it all up

1. Create a config file

First, we'll want to create a config file like so:


const Config = {
  title: 'Khalil Stemmler - Software Developer / Designer',
  twitter: '',
  url: '',
  logo: ''

export default Config

For each blog post, we'll want to have the title of the blog post show up as the title. But for every other route that's not a blog post, we can configure the title to use as a fallback.

Hook up your logo, twitter and website url as well here.

2. Create the reusable component

This file takes in the isBlogPost, postData, and postImage props and builds a component describing the data in JSON-LD format. It also injects your blog post data into the meta tags. Thank you to Jason Lengstorf for sharing this snippet.


import path from 'path';
import React from 'react';
import Helmet from 'react-helmet';
import PropTypes from 'prop-types';
import * as config from '../config';

const getSchemaOrgJSONLD = ({
}) => {
  const schemaOrgJSONLD = [
      '@context': '',
      '@type': 'WebSite',
      name: title,
      alternateName: config.title,

  return isBlogPost
    ? [
          '@context': '',
          '@type': 'BreadcrumbList',
          itemListElement: [
              '@type': 'ListItem',
              position: 1,
              item: {
                '@id': url,
                name: title,
          '@context': '',
          '@type': 'BlogPosting',
          name: title,
          alternateName: config.title,
          headline: title,
          image: {
            '@type': 'ImageObject',
            url: image,
          author: {
            '@type': 'Person',
            name: 'Khalil Stemmler',
          publisher: {
            '@type': 'Organization',
            url: '',
            logo: config.logo,
            name: 'Khalil Stemmler',
          mainEntityOfPage: {
            '@type': 'WebSite',
            '@id': config.url,
    : schemaOrgJSONLD;

const SEO = ({ postData, postImage, isBlogPost }) => {
  const postMeta = postData || {};

  const title = postMeta.title || config.title;
  const description =
    postMeta.description || postData.excerpt || config.description;
  const image = `${config.url}${postImage}` || config.image;
  const slug = postMeta.slug;
  const url = postMeta.slug
    ? `${config.url}${postMeta.slug}`
    : config.url;
  const datePublished = isBlogPost ? : false;

  const schemaOrgJSONLD = getSchemaOrgJSONLD({
  return (
      {/* General tags */}
      <meta name="description" content={description} />
      <meta name="image" content={image} />

      {/* tags */}
      <script type="application/ld+json">

      {/* OpenGraph tags */}
      <meta property="og:url" content={url} />
      {isBlogPost ? <meta property="og:type" content="article" /> : null}
      <meta property="og:title" content={title} />
      <meta property="og:description" content={description} />
      <meta property="og:image" content={image} />

      {/* Twitter Card tags */}
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:creator" content={config.twitter} />
      <meta name="twitter:title" content={title} />
      <meta name="twitter:description" content={description} />
      <meta name="twitter:image" content={image} />

SEO.propTypes = {
  isBlogPost: PropTypes.bool,
  postData: PropTypes.shape({
    frontmatter: PropTypes.any,
    excerpt: PropTypes.any,
  postImage: PropTypes.string,

SEO.defaultProps = {
  isBlogPost: false,
  postImage: null,

export default SEO;

The coolest part about Helmet is that you can use it multiple times in any nested component structure but the result will always come from the very last instance of it. So if we used Helmet earlier in our layout/index.js, when we hook it up in our app/templates/blog-post.js, it will take the result from Helmet in our blog post template. That's because Gatsby renders our layout first before rendering our template files.

If you want to learn more about JSON-LD, you can check this out.

3. Hook it up in your blog post template file

The final thing to do is to actually hook it up in your template file for your blog posts. Here's what mine looks like.


import React from 'react'
import PropTypes from 'prop-types'
import { kebabCase } from 'lodash'
import Helmet from 'react-helmet'
import Link from 'gatsby-link'
import Content, { HTMLContent } from '../components/Content'
import ReactDisqusComments from 'react-disqus-comments';
import SEO from '../components/SEO';

import helpers from '../helpers'

import styles from '../styles/Blog.module.css'

function getUniquePageIdentifier () {
  return typeof window !== 'undefined' && window.location.href
      ? typeof window !== 'undefined' && window.location.href
      : ''

export const BlogPostTemplate = ({
}) => {
  const PostContent = contentComponent || Content

  return (
      {helmet || ''}
          <div style= {{ margin: '0 auto'}} className="column is-10">
            <h1 className="title is-size-2 has-text-weight-bold is-bold-light">
            <h4 className={}>in <Link className={styles.category} to={`/blog/categories/${kebabCase(category)}/`}>{category}</Link></h4>

              <img src={image}/>

            <PostContent content={content} />
            {tags && tags.length ? (
              <div style={{ marginTop: `4rem` }}>
                <ul className="taglist">
                  { => (
                    <li key={tag + `tag`}>
                      <Link to={`/blog/tags/${kebabCase(tag)}/`}>{tag}</Link>
            ) : null}
              identifier={ getUniquePageIdentifier() }
              url={ getUniquePageIdentifier() }

BlogPostTemplate.propTypes = {
  content: PropTypes.string.isRequired,
  contentComponent: PropTypes.func,
  description: PropTypes.string,
  title: PropTypes.string,
  helmet: PropTypes.instanceOf(Helmet),

const BlogPost = ({ data }) => {
  let { markdownRemark: post } = data;

  post = Object.assign({}, post, post.fields, post.frontmatter)

  return (

BlogPost.propTypes = {
  data: PropTypes.shape({
    markdownRemark: PropTypes.object,

export default BlogPost

export const pageQuery = graphql`
  query BlogPostByID($id: String!) {
    markdownRemark(id: { eq: $id }) {
      fields {
      frontmatter {
        date(formatString: "MMMM DD, YYYY")

That's it

Easy right? That's how you create a reusable SEO component with Helmet in Gatsby. Now you should be able to use something like Facebook's URL Debugger and test it out to see if it works properly. If you have other types of collections and templates like tags, categories, products, services, etc- you could extend this so that each of those pages are SEO optimized as well. I'll be doing that in the near future.

I hope this was helpful to you! Let me know if you have any questions in the comments.

Keep building cool things.


Liked this? Sing it loud and proud 👨‍🎤.


Commenting has been disabled for now. To ask questions and discuss this post, join the community.

4 years ago

Any ideas how one would do something like this but using WordPress as the data source? I'm specifically looking how to add OG:image as anyone sharing my site on FB or wherever currently gets no image preview which sucks.

Khalil Stemmler
4 years ago

Several different ways you can do that. I think by default if you use the plugin, it has the ability to fetch `featured_media`?



allWordpressPost {

edges {

node {







featured_media // this one







You should be able to feed that through as props.

4 years ago

I want to know how you're managing your site? Is this site use some kind of data source like Wordpress

Khalil Stemmler
4 years ago

Hey Ali,

With GatsbyJS, you can use pretty much anything as a data source. For me, my main data sources are markdown files.

From my markdown files, I can turn them into Graphql nodes to model an `article` or a `wiki` entry.

That's pretty much all I'm doing on the site right now.

Eventually, I'll hook it up to more APIs like Spotify in order to show what I'm currently listening to (it's always the same 3 albums anyways, so maybe not the most useful idea for me) :p

4 years ago

Awesome post thanks for the help!

3 years ago

Khalil, frist of all thanks for this amazing article!

Everything for me it's clear but regarding the keywords, as you explain it's important but I can't see in the SEO component the using of this. Actually, just in the starter of Gatsby I can see that they use the keywords but in general nobody use it (as I can see so far).

Best regards,


Fabricio Ziliotti
3 years ago

Hi, thanks for the post!!

I put this blogpost on

And a I discovered an error. You just have to pass the url atribute to JSON LD BlogPosting =)

Fabricio Ziliotti
3 years ago

On GetSchemaOrgJSONLD, the correct @context is '' =)

3 years ago

Great article! Thanks for sharing.

subhan akram
3 years ago

sir, which one is more seo friendly wordpress or gatsby website?

Belajar Setiawan
8 months ago

Thank you khalil.

This article is quite helpful. But I use the HUGO generator.

I wrote about hugo here. Maybe that will help too.

Stay in touch!

About the author

Khalil Stemmler,
Software Essentialist ⚡

I'm Khalil. I turn code-first developers into confident crafters without having to buy, read & digest hundreds of complex programming books. Using Software Essentialism, my philosophy of software design, I coach developers through boredom, impostor syndrome, and a lack of direction to master software design and architecture. Mastery though, is not the end goal. It is merely a step towards your Inward Pull.

View more in Web Development

You may also enjoy...

A few more related articles

How to Build a Related Posts Component with Gatsby.js
Here's how to build a "Related Posts" component for your Gatsby.js site.
How to Prerender Comments | Gatsbyjs Guide
Prerendering dynamic data can have several advantages. For Gatsby blogs with high engagement, comments can positively impact SEO, ...
Over $85 billion spent on fixing bad code [bootcamps, junior devs, JavaScript, and software design principles]
More and more money is being spent by companies on maintaining bad JavaScript code. Here's where I think our industry needs to cha...
TypeScript vs. JavaScript [Pros / cons]
TypeScript is arguably one of the best things to come to the JavaScript world helping developers comfortably create complex enterp...

Want to be notified when new content comes out?

Join 15000+ other Software Essentialists learning how to master The Essentials of software design and architecture.

Get updates