The problem: client-side rendering is not so good at SEO 😞
I had just finished creating my first Create React App, a shiny new portfolio site that I had proudly deployed to Firebase Hosting and shared on Twitter. Everything was working well. There was just one problem: despite using React-Helmet to set the appropriate meta tags for blog posts, the Twitter Card Validator couldn't see them.
Why? When I inspected the elements panel in Chrome DevTools, the appropriate meta tags were present. However, the HTML source did not include the necessary meta tags. The problem was that these were inserted client-side with JavaScript by React-Helmet. As of this article's publication, Twitter's robots don't run JavaScript. My efforts to include a fancy Twitter card when I shared my blogs posts had been in vain.
The solution: Gatsby is great at SEO 🥳
I didn't know quite what to do next, but I knew I needed some help. I had just attended the JavaScript and Friends Conference where Michael Richardson gave a great talk on improving a site's SEO. One key takeaway: Google's bots do run JavaScript on the pages they crawl, whereas Twitter's bots do not.
Naturally, I turned to Twitter for guidance:
D22 #100DaysOfCode: applied what I learned yesterday in @AnAccidentalDev's SEO talk to my #react project w/ react-helmet. Feeling good about Google bots, although pretty certain that Twitter bots won't work with JS CSR.
— Nick Smedira (@nsmedira) August 16, 2020
Any tips for Twitter Cards with a CSR React project?
Michael was gracious enough to reply to my tweet with a suggestion:
I love static site generators for things like personal blogs. They offer great user experience, cheap hosting, and almost no run-time surprises.
— Michael Richardson (@AnAccidentalDev) August 16, 2020
I have a good amount of experience with Gatsby and I enjoy using it. A lot of people seem to like Eleventy as well.
And there you have it folks: static site generation! Instead of inserting the meta tags in the client's browser like my Create React App was doing, a static site generator like Gatsby inserts the meta tags during build time. That way, Twitter's JS-unfriendly bots would be able to see what I wanted them to see.
How do you convert a Create React App to Gatsby?
I started by creating a new git branch for my project. My plan was to start by installing Gatsby itself, and then get everything working in the Gatsby mold one thing at a time.
npm install gatsby
But I quickly ran into lots of errors. I knew nothing about the configuration files I would need at the root level, or how routing differed between Create React App and Gatsby, or the various plugins I would want to use. I was a Gatsby noob.
Starters!
Enter Gatsby Starters. Instead of installing Gatsby dependencies one by one, I decided to start with a solid outline of basic Gatsby concepts and technologies and then rebuild my project step by step. This was a worthwhile approach for 2 reasons:
- It helped me learn "the Gatsby way".
- It helped me identify suboptimal code in my Create React App that I was able to improve during the rebuild.
I used the Gatsby Default Starter. After installing Gatsby globally (npm install -g gatsby
), you can start one for yourself with the following command (replace my-gatsby-project
with your project's name):
gatsby new my-gatsby-project https://github.com/gatsbyjs/gatsby-starter-default
With the default starter cloned and basic Gatsby dependencies installed, I was off to the races.
This worked in Create React App! Why doesn't it work now?!
Certain patterns in Gatsby required some changes to my Create React App code. For example, I could use Gatsby's powerful page-based routing to convert react-router-dom
routes to Gatsby Pages.
Pages
One of my favorite things about Gatsby is that you can easily create new routes by adding folders and components to the src/pages
directory.
My Create React App was essentially 6 pages, and so my first step was to convert these from routes using react-router-dom
to page components in Gatsby.
index.js
in Create React App:
import ReactDOM from 'react-dom'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import Navbar from './components/navbar'
ReactDOM.render(
<Router>
<React.StrictMode>
<div className="app">
<Navbar />
<Switch>
<Route exact path='/' component={Home} />
<Route path='/skills' component={Skills}/>
<Route path='/portfolio' component={Portfolio}/>
<Route exact path='/blog' component={Blog}/>
<Route path='/blog/:id' component={Article}/>
<Route path='/about' component={About}/>
</Switch>
</div>
</React.StrictMode>
</Router>,
document.getElementById('root')
)
Gatsby Project Structure:
/
|-- /src
|-- /pages
|-- index.js
|-- skills.js
|-- portfolio.js
|-- blog.js
|-- about.js
|-- /templates
|-- post.js
|-- /posts
|-- how-to-survive-cleveland-winters.md
|-- how-can-there-be-so-many-potholes.md
You will notice that of my 6 Create React App routes, 5 became pages (the home
component became index.js
). The 6th route for the Article
component in Create React App became post.js
in the templates
folder. Instead of storing blog post markup in Firebase Firestore and inserting it into the Article
component, I am now storing posts as markdown files in the posts
folder. Gatsby then converts these into pages at build time.
The Document Head and Body
In Create React App, the entire app runs in a single page, index.html. For this reason, I was able to import several external resources in the <head>
and <body>
tags of index.html, where they would be available to all components in the Create React App.
- Font Awesome
- Materialize CSS
- Google Fonts Material Icons
Gatsby, by contrast, is not a single page application, and so I had to find different ways to use these three resources.
Font Awesome
Create React App's public/index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<!--Import Google Icon Font-->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Import Materialize CSS -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<!-- FONT AWESOME KIT -->
<script src="https://kit.fontawesome.com/<YOUR-KIT-ID>.js" crossorigin="anonymous"></script>
</body>
</html>
In Gatsby, I was able to get my FontAwesome icons working by inserting the above script into the HTML <head>
with React-Helmet in the single component where I needed font-awesome icons (the fixed action button, which is part of the layout component that wraps each page):
<Helmet>
<script src="https://kit.fontawesome.com/<YOUR-KIT-ID>.js" crossorigin="anonymous"></script>
</Helmet>
Materialize CSS
I also used Materialize.CSS in Create React App by importing it in the <head>
tag of index.html
(see above).
In Gatsby, we can add a file gatsby-browser.js
to the root level and import global stylesheets in it. To do this, I downloaded the materialize.css file before importing:
gatsby-browser.js:
// MATERIALIZE
require("./src/styles/materialize.css")
// PRISM.JS
require('./src/styles/prism-themes/xonokai.css')
You can also import stylesheets globally by following the Gatsby design pattern of wrapping each page in a Layout
component and importing the style sheet directly into that component. In layout.js
:
import "../styles/layout.css"
Google Fonts Material Icons
Like Materialize CSS, I was also importing the Material Icons family of Google Fonts inside the <head>
tag of index.html
in Create React App (see Font Awesome section above.)
In Gatsby, I worked around this by importing Google Fonts at the top of my local materialize.css
stylesheet:
@import 'https://fonts.googleapis.com/icon?family=Material+Icons';
What the hell is GraphQL?
GraphQL is a query language for working with APIs, in this case, Gatsby's data layer. All of the data in a Gatsby site, including data from plugins, is structured into a schema that we can query with GraphQL. There are numerous ways you can use GraphQL with Gatsby, but this site uses it in 5 main ways:
- In
pages/blog.js
to query all markdown files wherestatus: 'published'
in the frontmatter, sorting them by date descending. - In
pages/about.js
to query a profile picture in the filesystem. - In
gatsby-node.js
to create pages programmatically from markdown files. See more about the createPages API below. - In
templates/post.js
(a template used bygatsby-node.js
) to query a specific markdown file by slug, including a featured image from the filesystem. - In
components/seo.js
to query site metadata fromgatsby-config.js
.
Queries in Pages
In page components, you can simply import { graphql } from 'Gatsby'
and export your query outside of your default export and pass a data
prop to your page component. An example from my About page:
import React from 'react'
import Img from 'gatsby-image'
const About = ( {data} ) => {
const headshot = data.headshot.childImageSharp.fluid
return (
<Img fluid={headshot} />
)
}
export default About
export const query = graphql`
query {
headshot: file(relativePath: {eq: "headshot.JPG"}) {
childImageSharp {
fluid(maxWidth: 1182) {
...GatsbyImageSharpFluid
}
}
}
}
`
Queries in Components
In non-page components, you need to import { graphql, staticQuery } from 'Gatsby'
and query the data layer from inside the component. See the description of the SEO component below for an example.
Plugins
One of the major attractions of Gatsby is its thriving ecosystem of plugins. This site uses many plugins in order to accomplish various tasks that would be difficult without a plugin, such as using markdown files as a data source (gatsby-transformer-remark).
gatsby-plugin-react-helmet - Used primarily by the SEO component, this plugin allows us to dynamically set site metadata in the document head using React Helmet. Normally, this would occur client-side. However, this plugin allows us to set metadata in the static HTML pages that Gatsby creates during build time.
gatsby-source-filesystem - This plugin allows us to source data from our local filesystem. For example, it will create nodes that we can query using GraphQL from our image, markdown, and JSON files, among others.
You can have several instances of gatsby-source-filesystem in your gatsby-config.js file so that you can source files from multiple directories in your project.
gatsby-image - The gatsby-image plugin will be included in almost all Gatsby starters and represents one of Gatsby's great strengths: speed. It was a bit tricky to understand at first, but it allows us to easily optimize our image loading. It relies on gatsby-plugin-sharp, described below.
gatsby-plugin-sharp - This plugin allows us to easily use functions from the Sharp image processing library.
gatsby-transformer-sharp - This plugin is responsible for creating nodes from images supported by the Sharp image processing library that you can query with GraphQL.
gatsby-plugin-manifest - This plugin enables a web app manifest that allows users to add your site to the home screen on their mobile browser.
gatsby-transformer-remark - This plugin parses markdown files so you can query the data they contain (in frontmatter, for example) and so Gatsby can turn them into HTML pages. See createPages section below.
gatsby-remark-autolink-headers - This plugin allows you to create links in your markdown files to headers in the same file. Any header level element is automatically linkable. The link to the createPages section above is created this way.
gatsby-remark-prismjs - This plugin allows you to add syntax highlighting to code snippets that you include in your markdown files. As you can see, I have used this plugin liberally in this post 😅
gatsby-remark-copy-linked-files - This plugin copies files linked in your markdown files to the static folder during build time so that your readers can download files from your blog posts.
gatsby-remark-images - This plugin allows us to add inline images to our markdown blog posts.
gatsby-plugin-twitter - This plugin allows us to correctly format embedded tweets which would normally rely on client-side JavaScript.
Installing Plugins
One key thing to understand about Gatsby plugins is that you need to configure them in your gatsby-config.js
file and you also need to restart your development server for changes to take effect (gatsby develop
starts the development server and ^c
quits it). A given plugin's page on the Gatsby website will tell you everything you need to know about how to configure it in gatsby-config.js
.
Components
Layout
You can use a reusable layout component to wrap your page components. The layout component for this site is simple. It accepts a children
prop that includes elements from the page components and a location
prop that is used for navigation. It renders a header component, a main component and fixed action button. I wrap every one of my pages in the layout component using the wrapPageElement API. Learn more about the wrapPageElement API below.
layout.js
// PACKAGES
import React from "react"
import PropTypes from "prop-types"
// COMPONENTS
import Header from "./header"
import FixedActionButton from "./fixed-action-button"
// STYLESHEETS
import '../styles/materialize.css'
import "../styles/layout.css"
const Layout = ({ children, location }) => {
return (
<>
<Header pathname={location.pathname}/>
<div className="main-div">
<main>
{children}
</main>
</div>
<FixedActionButton />
</>
)
}
Layout.propTypes = {
children: PropTypes.node.isRequired,
}
export default Layout
SEO
Another component that Gatsby recommends is an SEO component which you can add to any or all of your page or layout components. Here is an example of my SEO component:
seo.js:
import React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
import defaultMetaImage from '../images/memoji.png'
function SEO({ description, lang, meta, title, image, path }) {
const { site } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
title
description
author
siteURL
}
}
}
`
)
const metaDescription = description || site.siteMetadata.description
const siteURL = site.siteMetadata.siteURL
const imageURL = siteURL + ( image || defaultMetaImage )
const titleSiteConcat = `${title !== undefined ? title + ' | ' : '' }${site.siteMetadata.title}`
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${site.siteMetadata.title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: titleSiteConcat,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
property: 'og:image',
content: imageURL
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: site.siteMetadata.author,
},
{
name: `twitter:title`,
content: titleSiteConcat,
},
{
name: `twitter:description`,
content: metaDescription,
},
{
name: 'twitter:site',
content: '@nsmedira'
},
{
name: 'viweport',
content: 'width=device-width, initial-scale=1'
},
{
charset: 'utf-8'
},
{
name: 'theme-color',
content: '#2d728f'
},
{
name: 'google',
content: 'nositelinksearchbox'
},
{
name: 'twitter:image',
content: imageURL
}
].concat(meta)}
>
<link rel="canonical" href={siteURL + path} />
</Helmet>
)
}
SEO.defaultProps = {
lang: `en`,
meta: [],
description: ``,
}
SEO.propTypes = {
description: PropTypes.string,
lang: PropTypes.string,
meta: PropTypes.arrayOf(PropTypes.object),
title: PropTypes.string.isRequired,
}
export default SEO
Gatsby APIs
createPages
The createPages Node API allows us to programmatically create pages from nodes in our GraphQL schema. For example, the gatsby-transformer-remark plugin will create nodes from our markdown files from which we can programmatically generate blog posts using the createPages API. Following is my implementation of the createPages API. It performs a GraphQL query for all markdown files with a status of 'published' and then creates a page for each node that is returned:
gatsby-node.js
exports.createPages = async ({ actions, graphql, reporter }) => {
const { createPage } = actions
const blogPostTemplate = require.resolve(`./src/templates/post.js`)
const result = await graphql(`
{
allMarkdownRemark(
sort: { order: DESC, fields: [frontmatter___date] }, filter: {frontmatter: {status: {eq: "published"}}}
) {
edges {
node {
frontmatter {
slug
}
}
}
}
}
`)
// handle errors
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`)
return
}
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path: node.frontmatter.slug,
component: blogPostTemplate,
context: {
slug: node.frontmatter.slug,
}
})
})
}
wrapPageElement
The wrapPageElement Browser API allows us to wrap each page component in a layout component of our choosing. This makes the job of writing pages easier, as we don't need to manually wrap each one in a layout component. Additionally, the layout component will not unmount and remount during every page change.
Here is a sample implementation of wrapPageElement
. Both files are at the root level:
wrapPageElement.js:
import React from "react"
import Layout from "./src/components/layout"
// Pass all props (hence the ...props) to the layout component so it has access to things like pageContext or location
const wrapPageElement = ({ element, props }) => (
<Layout {...props}>{element}</Layout>
)
export default wrapPageElement
gatsby-browser.js:
// IMPORT WRAP PAGE ELEMENT
import CustomLayout from "./wrapPageElement"
// EXPORT WRAP PAGE ELEMENT
export const wrapPageElement = CustomLayout
Conclusion
My Create React App site was the first website I ever made completely from "scratch" (with quotes because of the obvious reliance on frameworks and libraries). It was fun, but converting it to Gatsby was such a joy that I have since built two more websites in Gatsby and am excited to continue developing with it. The only thing that this article may have aptly demonstrated is how new I am to this tool, and I am looking forward to learning more about it and sharing what I learn with you here.
If you have any questions or comments about this post, please send me a message on Twitter: @nsmedira
Helpful Links
I've compiled some resources that were helpful to me while porting my site to Gatsby. Hopefully you will find them useful as well!
Gatsby Documentation
- Adding an SEO Component
- Adding App and Website Functionality
- Adding Markdown Pages
- Creating Tags Pages for Blog Posts
- Deploying to Firebase Hosting
- Gatsby Tutorials
- gatsby-starter-default
- Layout Components
- Porting from Create React App to Gatsby