Managing blog and documentation with Gatsby is one
of many perk the open source frontend framework provides. In this tutorial
we will explore how we've added open graph and other search engine optimization
related head
tags in our website.
Pre-requisites for Gatsby SEO tags
I will try to be as abstract as possible. There are bunch of ways to render pages in gatsby and I will not cover them here. Instead we will focus on creating react components which will be responsible for plugging our SEO tags to pages.
We will need the following packages first.
- gatsby-plugin-react-helmet - For adding things to the
head
of our website. - gatsby-plugin-image - For getting images to put inside
og:image
tags.
Let's install them.
1npm install gatsby-plugin-react-helmet \2 react-helmet \3 gatsby-plugin-image \4 gatsby-plugin-sharp \5 gatsby-source-filesystem \6 gatsby-transformer-sharp
Copied!
The plugin gatsby-transformer-sharp
is needed because we will be using a
fallback image for og:image
tag in our SEO component.
Preparing SEO strategy
Let's assume we have images like this our directory.
1my-website/2├── src/3│ ├── images/4│ │ └── featured-image.jpg5│ └── components/6│ └── Seo.tsx7└── gatsby-config.js
Copied!
Our goal is to make a SEO component that will:
- Render basic SEO tags like
title
,meta:description
etc. - Render all needed open graph tags like
og:image
,og:type
,og:description
,og:title
etc. - Add twitter specific tags.
- Must be extensible, i.e, should accept props so that it can be used on other page templates with sane defaults.
- Finally if no image is passed to the component, it will use
src/images/featured-image.jpg
for rendering a default featured image.
Make sure your open graph image is at-least 1200px wide and has a ratio of
1.91/1
. So the minimum dimension should be 1200px X 627px.
We are going to need a few things in our gatsby config.
- Base website URL, because gatsby doesn't care what domain your website lives, we will need to be specific and tell it through the config file.
- Default value of title, description and other og tags would live in our config file for easier access.
- Setup the plugins we need to dynamically generate images.
For the rest of the tutorial, I will be writing TypeScript code. The types come automatically from the gatsby-plugin-typegen plugin.
Adding data and plugin to gatsby-config.js file
Let's add the following data to our gatsby-config.js
file.
1module.exports = {2 siteMetadata: {3 // add general title and description4 title: 'WPEForm No-Code Drag and Drop WordPress Form Builder',5 description:6 'Fast and modern WordPress no-code form builder for payments, quotation, quizzes, conversations & feedback.',7 // site URL, no trailing slash8 siteUrl: 'https://www.wpeform.io',9 // other default open graph data10 og: {11 siteName: 'WPEForm - WordPress Form Builder',12 twitterCreator: '@swashata',13 },14 },15 plugins: [16 'gatsby-plugin-image',17 'gatsby-plugin-react-helmet',18 {19 resolve: 'gatsby-plugin-sharp',20 options: {21 defaults: {22 quality: 70,23 formats: ['auto', 'webp', 'avif'],24 placeholder: 'tracedSVG',25 },26 },27 },28 `gatsby-remark-images`,29 'gatsby-transformer-sharp',30 {31 resolve: 'gatsby-source-filesystem',32 options: {33 name: 'images',34 path: './src/images/',35 },36 __key: 'images',37 },38 {39 resolve: 'gatsby-source-filesystem',40 options: {41 name: 'pages',42 path: './src/pages/',43 },44 __key: 'pages',45 },46 ],47};
Copied!
The things inside siteMetadata
is what we will query later in our SEO
component. The rest of the plugins are there to make sure we get our image
transformed by gatsby.
Preparing the SEO component
Now let's create a file src/components/Seo.tsx
and have the following code.
1import { useStaticQuery, graphql } from 'gatsby';2import * as React from 'react';3import { Helmet } from 'react-helmet';4import { useLocation } from '@reach/router';56type ImageDataType = {7 images: {8 fallback: {9 src: string;10 };11 };12 width: number;13 height: number;14};1516export type SeoProps = {17 /**18 * Meta description. If not given, the one from the gatsby-config will be used.19 */20 description?: string;21 /**22 * Page title. If not given, the one from the gatsby-config will be used.23 */24 title?: string;25 /**26 * Meta keywords, will only be used if given.27 */28 keywords?: string;29 /**30 * Featured image, the data type should match the return type of `gatsbyImageData`.31 * If not given, the default image will be used.32 */33 featuredImage?: ImageDataType;34};35export default function Seo(props: SeoProps) {36 // first get our default data from gatsby config and default featured image37 const { site, featuredImage } =38 useStaticQuery<GatsbyTypes.SeoMetaDataQuery>(graphql`39 query SeoMetaData {40 site {41 siteMetadata {42 title43 description44 siteUrl45 og {46 siteName47 twitterCreator48 }49 }50 }51 featuredImage: file(52 absolutePath: { glob: "**/src/images/featured-image.png" }53 ) {54 childImageSharp {55 gatsbyImageData(layout: FIXED, width: 1200)56 }57 }58 }59 `);6061 // determine the featured image from props62 const ogImage =63 props.featuredImage ??64 (featuredImage?.childImageSharp65 ?.gatsbyImageData as unknown as ImageDataType);6667 // determine title and description68 const title = props.title ?? site?.siteMetadata?.title;69 const description = props.description ?? site?.siteMetadata?.description;7071 // Use the location hook to get current page URL72 const location = useLocation();7374 // construct the meta array for passing into react helmet.75 const metas = [76 // basic seo77 {78 name: 'description',79 content: description,80 },81 {82 name: 'og:image',83 content: ogImage.images.fallback.src,84 },85 {86 name: 'og:image:width',87 content: `${ogImage.width}`,88 },89 {90 name: 'og:image:height',91 content: `${ogImage.height}`,92 },93 {94 name: 'og:type',95 content: 'website',96 },97 {98 name: 'og:title',99 content: title,100 },101 {102 name: 'og:description',103 content: description,104 },105 {106 name: 'og:site_name',107 content: site!.siteMetadata!.og!.siteName,108 },109 {110 name: 'og:url',111 content: `${site?.siteMetadata?.siteUrl}${location.pathname}`,112 },113 {114 name: 'twitter:card',115 content: 'summary_large_image',116 },117 {118 name: 'twitter:description',119 content: description,120 },121 {122 name: 'twitter:title',123 content: title,124 },125 {126 name: 'twitter:image',127 content: ogImage.images.fallback.src,128 },129 {130 name: 'twitter:creator',131 content: site!.siteMetadata!.og!.twitterCreator,132 },133 ];134135 // If we have keywords, then add it136 if (props.keywords) {137 metas.push({138 name: 'keywords',139 content: props.keywords,140 });141 }142143 return (144 <Helmet>145 <html lang="en" />146 <meta charSet="utf-8" />147 <title>{title}</title>148 {metas.map(meta => (149 <meta key={meta.name} name={meta.name} content={meta.content} />150 ))}151 </Helmet>152 );153}
Copied!
A few things to note in the component are:
- The current path comes from the
@reach/router
which gatsby uses under the hood. - We use
useStaticQuery
for getting data fromgatsby-config.js
. The plugin gatsby-plugin-typegen automatically generates the type of the data. - We combine
${site.siteMetadata.siteUrl}${ogImage.images.fallback.src}
to print http URLs for the open graph images. - In the static query, we have a
file
query with filterabsolutePath: { glob: "**/src/images/featured-image.png" }
to get the fallback featured image. This image will be used if the component is rendered without afeaturedImage
prop. If your image lives elsewhere, be sure to change the glob match pattern.
If you're not using typescript, the JavaScript version of the code can be found below.
1import { useStaticQuery, graphql } from 'gatsby';2import * as React from 'react';3import { Helmet } from 'react-helmet';4import { useLocation } from '@reach/router';5export default function Seo(props) {6 // first get our default data from gatsby config and default featured image7 const { site, featuredImage } = useStaticQuery(graphql`8 query SeoMetaData {9 site {10 siteMetadata {11 title12 description13 siteUrl14 og {15 siteName16 twitterCreator17 }18 }19 }20 featuredImage: file(21 absolutePath: { glob: "**/src/images/featured-image.png" }22 ) {23 childImageSharp {24 gatsbyImageData(layout: FIXED, width: 1200)25 }26 }27 }28 `);29 // determine the featured image from props30 const ogImage =31 props.featuredImage ?? featuredImage?.childImageSharp?.gatsbyImageData;32 // determine title and description33 const title = props.title ?? site?.siteMetadata?.title;34 const description = props.description ?? site?.siteMetadata?.description;35 // Use the location hook to get current page URL36 const location = useLocation();37 // construct the meta array for passing into react helmet.38 const metas = [39 // basic seo40 {41 name: 'description',42 content: description,43 },44 {45 name: 'og:image',46 content: ogImage.images.fallback.src,47 },48 {49 name: 'og:image:width',50 content: `${ogImage.width}`,51 },52 {53 name: 'og:image:height',54 content: `${ogImage.height}`,55 },56 {57 name: 'og:type',58 content: 'website',59 },60 {61 name: 'og:title',62 content: title,63 },64 {65 name: 'og:description',66 content: description,67 },68 {69 name: 'og:site_name',70 content: site.siteMetadata.og.siteName,71 },72 {73 name: 'og:url',74 content: `${site?.siteMetadata?.siteUrl}${location.pathname}`,75 },76 {77 name: 'twitter:card',78 content: 'summary_large_image',79 },80 {81 name: 'twitter:description',82 content: description,83 },84 {85 name: 'twitter:title',86 content: title,87 },88 {89 name: 'twitter:image',90 content: ogImage.images.fallback.src,91 },92 {93 name: 'twitter:creator',94 content: site.siteMetadata.og.twitterCreator,95 },96 ];97 // If we have keywords, then add it98 if (props.keywords) {99 metas.push({100 name: 'keywords',101 content: props.keywords,102 });103 }104 return (105 <Helmet>106 <html lang="en" />107 <meta charSet="utf-8" />108 <title>{title}</title>109 {metas.map(meta => (110 <meta key={meta.name} name={meta.name} content={meta.content} />111 ))}112 </Helmet>113 );114}
Copied!
Using the SEO component in your pages
Let's say we have called pages/about.tsx
. There we use the newly created
component like this.
1import * as React from 'react';2import { graphql, PageProps } from 'gatsby';3import Seo from '../components/Seo';45export default function Features(6 props: PageProps<GatsbyTypes.AboutPagesDataQuery>7) {8 const { data } = props;9 return (10 <>11 <Seo12 title="About page title"13 description="About page description"14 featuredImage={data.aboutPageFeatured.childImageSharp.gatsbyImageData}15 />16 {/** Other page stuff goes here */}17 </>18 );19}2021export const query = graphql`22 query AboutPagesData {23 aboutPageFeatured: file(24 absolutePath: { glob: "**/src/images/about-us-image.png" }25 ) {26 childImageSharp {27 gatsbyImageData(layout: FIXED, width: 1200)28 }29 }30 }31`;
Copied!
In the page query, we specifically query for the file
src/images/about-us-image.png
and pass the data to our SEO component for
rendering a different featured image.
If you are using markdown frontmatter data, then you'd need to pass the image from frontmatter instead. The exact implementation will differ, but the concept remains the same.
Bonus: Adding canonical URLs to all your gatsby pages
We can use a plugin gatsby-plugin-canonical-urls for that. Simply install the plugin
1npm install gatsby-plugin-canonical-urls
Copied!
And add it to your gatsby-config.js
file.
1module.exports = {2 // ... previous things3 plugins: [4 {5 resolve: `gatsby-plugin-canonical-urls`,6 options: {7 siteUrl: `https://www.wpeform.io`,8 },9 },10 // ... other plugins11 ],12};
Copied!
Now check the output of your pages, it should have something like this
1<link2 rel="canonical"3 href="https://www.wpeform.io/"4 data-baseprotocol="https:"5 data-basehost="www.wpeform.io"6/>
Copied!
So we've now created a handy SEO component which can be used anywhere within your gatsby site for instant SEO tags. If you've liked this tutorial, please follow me on twitter and give a shoutout.