Add SEO to your Gatsby Website
The Problem
Okay here is what happened. So, I released my portfolio website and announced it on social media through LinkedIn and Twitter and here was the issue:
Both social media platforms had missing thumbnail images within the snippet to represent my webpage :( This is actually the result of poor (albeit non existing) Search Engine Optimisation (SEO) within my site.
What is SEO + Open Graph?
Search Engine Optimisation is the process of improving the meta data of your webpage so that Search Engines eg. Google can analyse your webpage for better search results. In order for the internet to better understand websites there is a <head>
section in the html of each webpage that gives metadata about it. This metadata is stored in the form of <meta>
tags. So in order for your webpage to be prioritised within searches, its just a matter of adding the right metadata as <meta>
tags to your website.
These meta tags are also used for more than just search engine optimisation, they are used for telling browsers how to render your webpage but most importantly they are used within social media platforms to display and share your webpage within their platforms. This is important to get right because having the correct social meta tags can enhance the audience + click through rate of your webpage or blog post (because it looks nice).
The way these platforms are able to understand, categorise and display your webpage is by following the Open Graph Protocol - which was created by Facebook but now widely used by most social platforms (except for Twitter who kind of have their own). According to OGP, the protocol enables webpages to become a rich object in a social graph. Social platforms use this object in order to analyse, display and/or market your webpage within their platforms and throughout the internet.
The Solution: How to Implement Meta Tags in Gatsby
Now that we kind of understand the problem and how to fix it, let's do so within our Gatsby site.
In order to correctly have have social platforms understand and interpret pekevski.com we will need to add certain <meta>
tags to each pages' html. We are also going to have specific <meta>
tags for each blog post so that the thumbnail and heading of each blog post url is contextualised to the content of the post (like this one).
The way we do this is to
- Read the Gatsby documentation on SEO.
Fortunately Gatsby has a really nice tutorial on how to do this with any Gatsby site.
Firstly we want to add the basic meta tags that every webpage should have. Gatsby lets you do this by updating the siteMetadata
tag in the gatsby-config.js
file. So lets update it like so:
// gatsby-config.js
siteMetadata: {
title: 'Daniel Pekevski',
description: "Daniel Pekevski's Portfolio",
url: "https://pekevski.com",
image: "/img/me.png",
twitterUsername: "@pekevski",
}
These values will end up being our default meta tag values for each webpage that is not a blog post. They are stored within our gatsby data source which in this case is GraphQL.
In order to use these values we will make an SEO react component that will query GraphQL for the default siteMetadata and chuck it in the <Helmet />
tag (React Helmet is a reusable react component that lets you modify a webpages <head>
tag and the standard for most react apps)
Note: any nested <Helmet>
definitions in your react app will take the inner most definition and render it in the final <head>
tag of the webpage.
Lets create our SEO component that will be used throughout:
// /components/SEO.js
import React from "react"
import PropTypes from "prop-types"
import logo from '../img/logo.svg'
import { Helmet } from "react-helmet"
import { useLocation } from "@reach/router"
import { useStaticQuery, graphql } from "gatsby"
const SEO = ({ title, titleTemplate, description, image, article }) => {
const { pathname } = useLocation()
const { site } = useStaticQuery(query)
const {
defaultTitle,
defaultDescription,
siteUrl,
defaultImage,
twitterUsername,
} = site.siteMetadata
const seo = {
title: title || defaultTitle,
description: description || defaultDescription,
image: `${siteUrl}${image || defaultImage}`,
url: `${siteUrl}${pathname}`,
}
return (
<Helmet title={seo.title} titleTemplate={titleTemplate}>
<html lang="en" />
<meta name="description" content={seo.description} />
<meta name="image" content={seo.image} />
<link rel="apple-touch-icon" sizes="180x180" href={logo} />
<link rel="icon" type="image/svg" href={logo} sizes="32x32" />
<link rel="icon" type="image/svg" href={logo} sizes="16x16" />
<link rel="mask-icon" href={logo} color="#ff4400" />
<meta name="theme-color" content="#fff" />
// OPG specific meta tags
{(article ? true : null) && <meta property="og:type" content="article" />}
{(article ? false : null) && <meta property="og:type" content="website" />}
{seo.url && <meta property="og:url" content={seo.url} />}
{seo.title && <meta property="og:title" content={seo.title} />}
{seo.description && <meta property="og:description" content={seo.description} />}
{seo.image && <meta property="og:image" content={seo.image} />}
// Twitter specific meta tags
<meta name="twitter:card" content="summary_large_image" />
{seo.url && <meta property="twitter:url" content={seo.url} />}
{seo.title && <meta name="twitter:title" content={seo.title} />}
{seo.description && <meta name="twitter:description" content={seo.description} />}
{seo.image && <meta name="twitter:image" content={seo.image} />}
{twitterUsername && <meta name="twitter:creator" content={twitterUsername} />}
</Helmet>
)
}
export default SEO
SEO.propTypes = {
title: PropTypes.string,
titleTemplate: PropTypes.string,
description: PropTypes.string,
image: PropTypes.string,
article: PropTypes.bool,
}
SEO.defaultProps = {
title: null,
titleTemplate: null,
description: null,
image: null,
article: false,
}
const query = graphql`
query SEO {
site {
siteMetadata {
defaultTitle: title
defaultDescription: description
siteUrl: url
defaultImage: image
twitterUsername
}
}
}
`
Now we have a generic SEO component that will accept a few properties that we pass into it to add meta tags. These inputted props will be used, otherwise, if they are not defined then we use the default values from the database (that we defined in gatsby-config.js
earlier).
Notice that we use react conditional template rendering in order to show the meta tag or not based on the value being present or not. Also all default values will always be rendered.
Lets replace our Helmet content with the new SEO component we just made:
// /components/Layout.js
import React from 'react'
import Footer from './Footer'
import Navbar from './Navbar'
import SEO from './SEO'
import './all.sass'
const TemplateWrapper = ({ children }) => {
return (
<div>
<SEO />
<Navbar />
<div>{children}</div>
<Footer />
</div>
)
}
export default TemplateWrapper
We wont pass any props as we want the page to just use the defaults.
Now we want to update each blog posts' meta tags. We will do this by updating blog-post.js
// /templates/blog-post.js
import React from 'react'
import PropTypes from 'prop-types'
import { kebabCase } from 'lodash'
import { graphql, Link } from 'gatsby'
import Layout from '../components/Layout'
import Content, { HTMLContent } from '../components/Content'
import SEO from '../components/SEO'
import IosTimeOutline from 'react-ionicons/lib/IosTimeOutline'
// ... blog post template component omitted
const BlogPost = ({ data }) => {
const { markdownRemark: post } = data
return (
<Layout>
<BlogPostTemplate
content={post.html}
contentComponent={HTMLContent}
description={post.frontmatter.description}
helmet={
// change made here !
<SEO
titleTemplate={"%s | Blog"}
title={post.frontmatter.title}
description={post.frontmatter.description}
article={true}
image={post.frontmatter.featuredimage.publicURL}
/>
}
tags={post.frontmatter.tags}
title={post.frontmatter.title}
type={post.frontmatter.type}
date={post.frontmatter.date}
timeToRead={post.timeToRead}
/>
</Layout>
)
}
BlogPost.propTypes = {
data: PropTypes.shape({
markdownRemark: PropTypes.object,
}),
}
export default BlogPost
export const pageQuery = graphql`
query BlogPostByID($id: String!) {
markdownRemark(id: { eq: $id }) {
id
html
timeToRead
frontmatter {
date(formatString: "MMMM DD, YYYY")
title
description
tags
type
featuredimage {
publicURL
}
}
}
}
`
Notice that we pass the SEO component to the helmet prop of the blog post template. We also pass in the blog specific title, template, description, article and blog image. These will all render instead of the Helmet content in the Layout component as it is nested.
Now if we inspect the chrome console we can see that any normal page uses the default
tags and any blog page uses the blog specific tags. Great!Now if we edit our post on LinkedIn you will see how the change in meta tags now offers a better display of our webpage and a blog post!
This was a bit of work to just get a nice image and better description for sharing webpages within social media but its worth it :)