
Free JavaScript Book!
Write powerful, clean and maintainable JavaScript.
RRP $11.95
You can easily publish your ideas to sites like Dev.to, Hashnode or Medium, but the ideal is to have full control over your own content. Thereās an ever-growing list of tools for building your own website and controlling your own content. In this extensive tutorial, Iāll be covering how you can make your content shine using Gatsby, with the added bells and whistles you get with such an ecosystem.
I originally used Jekyll to publish my blog, but then switched to Gatsby, using the Lumen template. Iāve been using Gatsby since version 0, around May 2017.
Iāll be going from a Hello, World!
Gatsby project through to a coding blog with code syntax highlighting and a theme toggle for that dark mode goodness.
Thereās a rich ecosystem of plugins, starters and themes available for Gatsby to get you up and running quickly, but I want to take a progressive disclosure approach to presenting Gatsby, focusing on the basics of how a Gatsby project works.
Why Gatsby?
Gatsby is a static site generator, so thereās no dynamic generation of pages when the pages are requested. The built output for a Gatsby site can be hosted on a CDN, making it globally available and super scalable.
Gatsby can use Markdown files to create pages in a site project. Gatsby will read the Markdown files into the Gatsby file system and transform the Markdown to HTML and then when building the site create static pages.
The end result is a super fast site with little latency when requesting the pages.
Markdown and MDX
Iāve been documenting my development journey since 2016 in Markdown. Markdown offers a way to enable simple editing in plain text files that can be converted to HTML.
MDX (or Markdown JSX) is a tool that lets you write JSX in your Markdown documents, sort of like this:
import { RainbowText } from './components/rainbow';
## A Markdown Heading
<RainbowText>Wheeeeeeee</RainbowText>
Gatsby is by far the best framework Iāve used for working with Markdown and MDX, as the thereās no special notation needed above using frontmatter on your posts.
What Do I need?
If youāre going to follow along, thereās a few things youāll need:
- a basic web development setup: Node, terminal (bash, zsh or fish)
- a text editor
- a basic understanding of React
If you donāt have any of these, thereās both StackBlitz and GitHub Codespaces where you can create an empty GitHub repository and get started with a development environment from there.
Iāll be using VS Code as my text editor and Yarn as my preferred package manager in the examples below. If you prefer npm, thatās cool. š
You can also find the complete code for this tutorial on GitHub.
Okay, itās time to get started!
Hello, World!
Itās time to spin up a Gatsby project. Iām going to do the majority of this from the command line to begin with:
mkdir my-gatsby-blog cd my-gatsby-blog yarn init -y git init
Cool. Now, before going anywhere else with this, Iām going to need to add a .gitignore
file before installing any npm modules:
touch .gitignore echo "# Project dependencies
.cache
node_modules # Build directory
public # Other
.DS_Store
yarn-error.log" > .gitignore
Now I can install all the npm goodness I need to without VS Code Git screaming at me about too many active changes. Letās now install some dependencies to get up and running with Gatsby:
yarn add gatsby react react-dom mkdir -p src/pages touch src/pages/index.js
Next, weāll add the first React component (of many) for the project. Iāll add the following to the index.js
file I created:
import React from "react"; export default function IndexPage() { return <h1>Hello, World!</h1>;
}
Iām now ready to run the Gatsby develop
command from the command line:
yarn gatsby develop
This will spin up the Gatsby dev sever and say that my project is available to view in the browser on port 8000 (the default Gatsby port). The URL is http://localhost:8000/.
Using the Gatsby binary commands directly from the command-line interface (CLI) is totally doable, but most people will add the available commands to the scripts
section on the package.json
file, like this:
"scripts": { "build": "gatsby build", "dev": "gatsby develop", "serve": "gatsby serve", "clean": "gatsby clean"
},
As an added bonus, thereās a few extras that can be added to the Gatsby scripts here.
If we donāt want to run the project on the same port each time, it can be changed with the -p
flag, and and a port specified after that. For example, gatsby develop -p 8945
.
If we want to open the browser tab once the project is ready, we can add -o
to the script.
Iāll do the same with the serve
script, so I know when Iāve built a project itās on a different port to the development one:
"scripts": { "build": "gatsby build", "dev": "gatsby develop -p 8945 -o", "serve": "gatsby serve -p 9854 -o", "clean": "gatsby clean"
},
And with that, the mandatory āHello, World!ā welcome is complete and I can move on with the rest of this post! š¤
Lastly Iāll commit the changes Iāve made so far:
git add . git commit -m 'init project'
Content for the Blog
Okay, thereās not a great deal going on with the project right now, so first up Iāll add in some content, from the command line again:
mkdir -p content/2021/03/{06/hello-world,07/second-post,08/third-post} touch content/2021/03/06/hello-world/index.mdx
touch content/2021/03/07/second-post/index.mdx
touch content/2021/03/08/third-post/index.mdx
Iāll be using these throughout the examples Iām making.
Youāll notice the file extension .mdx
. This is an MDX file.
Front matter
Before I add some content for the blog, Iāll need to talk about front matter.
Front matter is a way to store information about the file that can be used by Gatsby when building the pages from them. For now, Iāll add a title
of the post and a date
. Iāll also add some content to them. Hereās our first post:
---
title: Hello World - from mdx!
date: 2021-03-06
--- My first post!! ## h2 Heading Some meaningful prose ### h3 Heading Some other meaningful prose
Hereās our second post:
---
title: Second Post!
date: 2021-03-07
--- This is my second post!
A third post:
---
title: Third Post!
date: 2021-03-08
--- This is my third post! > with a block quote! And a code block: ```js
const wheeeeee = true;
```
Thatās it for the posts for now, because these posts arenāt yet recognized by Gatsby as pages. Iāll need to let Gatsby know where to find content to add to the project. To do this, Iām going to add a configuration file to Gatsby.
Letās commit the changes Iāve made to Git:
git add . git commit -m 'add markdown files'
Gatsby Config
Gatsby config is whatās used to define and configure the many Gatsby plugins you can use. More on the Gatsby plugin eco system in a bit. For now, Iām going to create the file, again in the terminal:
touch gatsby-config.js
This creates the gatsby-config.js
at the root of the project so I can start configuring Gatsby to read the .mdx
files I created earlier.
Gatsby Plugins
Now I can install and configure the plugins Gatsby needs to source and display the files I created. Iāll install them all now and briefly detail what theyāre for:
yarn add gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react gatsby-source-filesystem
A quick look at the package.json
now shows that I have the following dependency version installed:
"dependencies": { "@mdx-js/mdx": "^1.6.22", "@mdx-js/react": "^1.6.22", "gatsby": "^3.1.1", "gatsby-plugin-mdx": "^2.1.0", "gatsby-source-filesystem": "^3.1.0", "react": "^17.0.1", "react-dom": "^17.0.1"
},
One thing to note is that, in Gatsby, thereās no need to import React in your components with React 17. But for the sake of completeness, and to avoid any confusion, Iāll be including it in these examples.
Now I need to configure gatsby-plugin-mdx
and gatsby-plugin-mdx
. In the gatsby-config.js
file, Iāll add this:
module.exports = { plugins: [ `gatsby-plugin-mdx`, { resolve: `gatsby-source-filesystem`, options: { path: `${__dirname}/content`, name: `content`, }, }, ],
};
Commit changes up to now:
git add .
git commit -m 'add gatsby plugins'
Gatsby GraphQL
Now itās time to see where Iām at with the files in Gatsby by using the Gatsby GraphQL client, GraphiQL. You may have noticed, if youāre following along, that the CLI indicates two URL locations to view the project:
You can now view my-gatsby-blog in the browser.
ā http://localhost:8000/
ā
View GraphiQL, an in-browser IDE, to explore your site's data and schema
ā http://localhost:8000/___graphql
Iām going to be using the ___graphql
(three underscores) route now to see the files in the file system.
If this seems a bit intimidating, Iāll attempt to cover all the parts that may not seem to make much sense. If youāre following along, you should be fine copying the examples into the GraphiQL explorer.
When I open up the GraphiQL explorer, I have several Explorer panels. This is all available data to explore in the project and is dependent on what Iāve configured in the gatsby-config.js
file.
The GraphiQL query panel and the results are next to that. This is where Iāll be writing GraphQL queries to retrieve the data I need. Thereās also a QUERY VARIABLES section at the bottom of the query panel, and Iāll come onto that later on.
Over on the far right is the GraphQL Documentation Explorer. Because of GraphQLās strict typing, this means that itās able to generate its own documentation on its data. But thatās outside the scope of this post.
Query Local Files with GraphQL
Next, Iām going to query for the files I added earlier in the GraphiQL query panel. In this query, Iām querying the title and date defined in the font matter of the files:
{ allMdx { nodes { frontmatter { title date } } }
}
If we pop that into the query panel press the big play button, we get back some data in the results panel. We can also use the Explorer in the left panel to pick out the data. Hereās what I get after running the query:
{ "data": { "allMdx": { "nodes": [ { "frontmatter": { "title": "Hello World - from mdx!", "date": "2021-03-06T00:00:00.000Z" } }, { "frontmatter": { "title": "Second Post!", "date": "2021-03-07T00:00:00.000Z" } }, { "frontmatter": { "title": "Third Post!", "date": "2021-03-08T00:00:00.000Z" } } ] } }, "extensions": {}
}
This is a big JSON object with the relevant information we requested in the query. Weāll look at how to use this soon. For now, this means that we can use this data in the Gatsby project to make pages.
In the gatsby-config.js
file, thereās also an option to specify site metadata. Site metadata is for when I want to reuse common data like the site title and description.
This is will be useful further down the road when I want to add meta tags to the site for search engine optimization (SEO). (Again, more on that later.) For now, Iām going to define some basic information about the site in the gatsby-config.js
with the siteMetadata
object.
I could define the site metada directly in the module.exports
like so:
module.exports = { siteMetadata: { title: `My Gatsby Blog`, description: `This is my coding blog.`, }, plugins: [ { }, ],
};
The site metadata object can get a bit large, and Iāve found keeping it in its own object can make it a bit simpler to reason about, so instead Iām going to define it separately:
const siteMetadata = { title: `My Gatsby Blog`, description: `This is my coding blog.`,
};
Then add the siteMetadata
object to the Gatsby config file:
const siteMetadata = { title: `My Gatsby Blog`, description: `This is my coding blog.`,
}; module.exports = { siteMetadata, plugins: [ { }, ],
};
Now I can hop over to the GraphiQL explorer again and query that site metadata with the following query:
{ site { siteMetadata { title description } }
}
Itās always a good idea to stop and restart the development server if youāre making changes to the gatsby-config.js
file, so Iāll do that (Ctrl + c, then yarn develop
), then in the GraphiQL explorer refresh the page and run the query again to get the data back:
{ "data": { "site": { "siteMetadata": { "title": "My Gatsby Blog", "description": "This is my coding blog." } } }, "extensions": {}
}
Now that I have the site metadata in the Gatsby file system, I can query it wherever I want to use it with the Gatsby static query hook useStaticQuery
. Iām going to kill off the dev server and restart after Iāve added the following to the src/pages/index.js
file:
import { graphql, useStaticQuery } from "gatsby";
import React from "react"; export default function IndexPage() { const { site: { siteMetadata }, } = useStaticQuery(graphql` { site { siteMetadata { title description } } } `); console.log("====================="); console.log(siteMetadata); console.log("====================="); return <h1>Hello World!</h1>;
}
A quick note on some of the notation there: const { site: { siteMetadata }, }
is quick way to get to the data in the site
query, where Iām pulling the siteMetadata
from the site
object. This is referred to as destructuring.
Now, after Iāve started the dev server again, I can go over to the browser console (Control + Shift + J in Windows/Linux, Command + Option + J on macOS) and see the siteMetadata
object in the console output.
I get the following console output:
=====================
{title: "My Gatsby Blog", description: "This is my coding blog."} description: "This is my coding blog." title: "My Gatsby Blog" __proto__: Object
=====================
Donāt worry about the console warning for a missing 404 page not found (net::ERR_ABORTED 404 (Not Found)
). Iāll make that later.
To avoid having to write this query each time, I want to use it in a component. Iām going to abstract this out into its own hook:
mkdir src/hooks touch src/hooks/use-site-metadata.js
Now Iāll add in a hook to the newly created src/hooks/use-site-metadata.js
file to get the site metadata on demand:
import { graphql, useStaticQuery } from "gatsby";
export const useSiteMetadata = () => { const { site } = useStaticQuery( graphql` query SITE_METADATA_QUERY { site { siteMetadata { title description } } } ` ); return site.siteMetadata;
};
You may have noticed that this query isnāt the same as the one from from the GraphiQL explorer:
+ query SITE_METADATA_QUERY {
site { siteMetadata { title description } }
}
This is to name the query. Because Iāll be using a lot of queries in the project, it makes sense to give them meaningful names.
Now Iāll implement the new hook into the src/pages/index.js
file:
import React from "react";
import { useSiteMetadata } from "../hooks/use-site-metadata"; export default function IndexPage() { const { title, description } = useSiteMetadata(); return ( <> <h1>{title}</h1> <p>{description}</p> </> );
}
Thatās a lot less verbose, and Iām able to pick and choose what items I want from the SITE_METADATA_QUERY
.
Itās time to commint the changes made so far:
git add .
git commit -m 'add site metadata and metadata hook'
Styling with Theme UI
To style this project, Iām going to be using Theme UI, because of its speed with implementing layouts and features like dark mode. Iāll be detailing whatās relevant to what Iām doing and reasons for that, although this wonāt be a guide on how to use Theme UI.
Thereās a few additional dependencies to add for Theme UI, which are:
yarn add theme-ui gatsby-plugin-theme-ui @theme-ui/presets
With those installed, Iāll need to add the gatsby-plugin-theme-ui
to the gatsby-config.js
plugin array:
module.exports = { siteMetadata, plugins: [ `gatsby-plugin-theme-ui`, `gatsby-plugin-mdx`, { resolve: `gatsby-source-filesystem`,
Now, if I stop and restart the dev server I have a slightly different looking site! Itās all gone a bit blue ā or periwinkle, to be precise! This is the gatsby-plugin-theme-ui
doing its thing and that color is the default.
The Gatsby plugin for Theme UI offers a lot of configuration options, some of which Iāll cover in more detail when needed. For now, Iām going to create a folder and define a theme object for Theme UI to use:
mkdir src/gatsby-plugin-theme-ui touch src/gatsby-plugin-theme-ui/index.js
In the src/gatsby-plugin-theme-ui/index.js
file, Iām going to add in a couple of the Theme UI presets, define the theme object, and spread in the swiss
preset to the theme
, to the theme
colors
, and to the styles
.
For dark mode, Iām using the deep
Theme UI preset and spreading that into the modes
object for dark
. (More on this soon.) For now, know that this is going to take care of a lot of the theming for me:
import { deep, swiss } from "@theme-ui/presets"; const theme = { ...swiss, colors: { ...swiss.colors, modes: { dark: { ...deep.colors, }, }, }, styles: { ...swiss.styles, p: { fontFamily: "body", fontWeight: "body", lineHeight: "body", fontSize: 3, }, },
}; export default theme;
Now if I restart the dev server (again, yes, youāll learn to deal with it) it will look a bit more acceptable with the Swiss theme being applied. At the time of writing, Theme UI sometimes doesnāt refresh the localhost
page, so itās necessary to do a browser page refresh.
Commit the changes so far to Git:
git add .
git commit -m 'add Theme UI and configure presets'
Time to add some React components!
Layout Component
Gatsby doesnāt have a specific layout, giving that responsibility to the developer. In this case, Iām making a layout for the whole site. Itās possible to incorporate many layouts for use in a Gatsby project, but for this example Iāll be using just one.
Now Iām going to refactor what I have currently so that everything is wrapped by a Layout
component. What I have currently in src/pages/index.js
can be used for a Header
component, so Iām going to make a couple of files now for Layout
and Header
:
mkdir src/components touch src/components/header.js src/components/layout.js
Now to move the title and description from src/pages/index.js
to the newly created src/components/header.js
component.
Rather than have the useSiteMetadata
used in the Header
component, Iāll pass the useSiteMetadata
props I need to the header from the Layout
component, which is where the header is going to live. (More on that shortly.) First up, hereās the header component, which lives in src/components/header.js
:
import { Link as GatsbyLink } from "gatsby";
import React from "react";
import { Box, Heading, Link } from "theme-ui"; export const Header = ({ siteTitle, siteDescription }) => { return ( <Box as="header" sx={{ bg: "highlight", mb: "1.45rem" }}> <Box as="div" sx={{ m: "0 auto", maxWidth: "640px", p: "1.45rem 1.0875rem", }} > <Link as={GatsbyLink} to="/"> <Heading>{siteTitle}</Heading> </Link> <Box as="p" variant="styles.p"> {siteDescription} </Box> </Box> </Box> );
};
Iāve added in some basic styles using the Theme UI layout elements. This looks a bit different from before: Box
, Link
, Heading
⦠what? These are all Theme UI components that can be used for layouts, form elements and more.
You may notice the as={GatsbyLink}
link prop added to the Link
component. This uses the as
prop in Theme UI and lets the component being passed in take on Theme UI styles.
Thereās a great post from Paul Scanlon explaining in more detail how this is done in Theme UI. For a really comprehensive explanation of Theme UI, thereās also āUnderstanding Theme UIā by the same author.
Thereās also the sx
and variant
props from Theme UI. sx
enables additional styles to be passed to the component. Think of it as an equivalent to the JSX style={{}}
prop. The variant
prop allows a group of predefined styles to be applied from the theme to the component being used.
Now for the Layout
component, which is located in src/components/layout.js
:
import React from "react";
import { Box } from "theme-ui";
import { useSiteMetadata } from "../hooks/use-site-metadata";
import { Header } from "./header"; export const Layout = ({ children }) => { const { title, description } = useSiteMetadata(); return ( <> <Header siteTitle={title} siteDescription={description} /> <Box as="div" sx={{ margin: "0 auto", maxWidth: "640px", padding: "0 1.0875rem 1.45rem", }} > <Box as="main">{children}</Box> </Box> </> );
};
Here Iām keeping the useSiteMetadata
hook and passing the props the Header
component needs, again with the sx
prop to add some basic styles for alignment to the main containing div. Then Iām creating a main
wrapper for the children
.
The children
prop is to return anything the Layout
component encapsulates, which will include anything I want to apply the layout to. For example:
<Layout> <h1>This is wrapped</h1>
</Layout>
This will return everything in the Layout
component and what itās wrapping. In in the example above, that will currently be the header and the H1 wrapped by the Layout
component.
As an example, Iāll go back to the index page (src/pages.index.js
) and add the following:
import React from "react";
import { Layout } from "../components/layout"; export default function IndexPage() { return ( <> <Layout> <h1>This is wrapped</h1> </Layout> </> );
}
The result is the header, provided in the Layout
component and the H1 This is wrapped
.
Index Page Posts Query
Now itās time to get the posts I created at the beginning and display them on the index page as a list of clickable links.
To get the post information, Iāll recreate the query I made in the section on querying local files with GraphQL with a couple of extra bits:
{ allMdx(sort: { fields: [frontmatter___date], order: DESC }) { nodes { id slug excerpt(pruneLength: 250) frontmatter { title date(formatString: "YYYY MMMM Do") } } }
}
Iāve added in the id
of the node and the slug
. This is the file path to the .mdx
files.
The excerpt
is using a Gatsby function to get the first 250 characters from the post body, also adding some formatting to the date
with another built-in Gatsby function.
Then as a way to order the posts in date descending order, Iāve added a sort: allMdx(sort: { fields: [frontmatter___date], order: DESC }) {
. This is sorting on the date in the posts front matter.
Adding that to the GraphiQL explorer gives me this result:
{ "data": { "allMdx": { "nodes": [ { "id": "2bed526a-e5a9-5a00-b9c0-0e33beafdbcf", "slug": "2021/03/08/third-post/", "excerpt": "This is my third post! with a block quote! And a code block:", "frontmatter": { "title": "Third Post!", "date": "2021 March 8th" } }, { "id": "89ea266b-c981-5d6e-87ef-aa529e98946e", "slug": "2021/03/07/second-post/", "excerpt": "This is my second post!", "frontmatter": { "title": "Second Post!", "date": "2021 March 7th" } }, { "id": "75391ba1-3d6b-539f-86d2-d0e6b4104806", "slug": "2021/03/06/hello-world/", "excerpt": "My first post!! h2 Heading Some meaningful prose h3 Heading Some other meaningful prose", "frontmatter": { "title": "Hello World - from mdx!", "date": "2021 March 6th" } } ] } }, "extensions": {}
}
Now I can use that query in the src/pages/index.js
file to get that data for use in the index page. In the IndexPage
function, Iāll destructure data
from the props given to the component via the GraphQL query:
import { graphql, Link as GatsbyLink } from "gatsby";
import React from "react";
import { Box, Heading, Link } from "theme-ui";
import { Layout } from "../components/layout"; export default function IndexPage({ data }) { return ( <> <Layout> {data.allMdx.nodes.map(({ id, excerpt, frontmatter, slug }) => ( <Box key={id} as="article" sx={{ mb: 4, p: 3, boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)", border: "1px solid #d1d1d1", borderRadius: "15px", }} > <Link as={GatsbyLink} to={`/${slug}`}> <Heading>{frontmatter.title}</Heading> <Box as="p" variant="styles.p"> {frontmatter.date} </Box> <Box as="p" variant="styles.p"> {excerpt} </Box> </Link> </Box> ))} </Layout> </> );
} export const query = graphql` query SITE_INDEX_QUERY { allMdx(sort: { fields: [frontmatter___date], order: DESC }) { nodes { id excerpt(pruneLength: 250) frontmatter { title date(formatString: "YYYY MMMM Do") } slug } } }
`;
This uses the components previously detailed. Note that the excerpt
, frontmatter
, and slug
are being destructured from data.allMdx.nodes
:
{data.allMdx.nodes.map(({ excerpt, frontmatter, slug }) => (
Clicking on the links will take me to the Gatsby.js development 404 page. Thatās because I havenāt made the pages for the .mxd
files yet. Thatās next.
Iāll commit what Iāve done so far before moving on:
git add .
git commit -m 'add Header and Layout components'
Using the Gatsby File System Route API with MDX
Iām going to be using the Gatsby File System Route API to get the file paths of the posts I created earlier on. The File System Route API is a way to programmatically create pages from my GraphQL data.
This approach has a special file notation for the page thatās going to be targeted when Gatsby generates the file system data at build time. The file indicates the node and the slug. Iāll create the file first, then detail where the data is coming from:
touch src/pages/{mdx.slug}.js
In the file, Iāll define a GraphQL query for the data I want to include in this template:
import { graphql } from "gatsby";
import { MDXRenderer } from "gatsby-plugin-mdx";
import React from "react";
import { Box } from "theme-ui"; export default function PostPage({ data }) { const { body, frontmatter: { title }, } = data.mdx; return ( <> <Box as="h1" variant="styles.h1" fontSize="4xl"> {title} </Box> <MDXRenderer>{body}</MDXRenderer> </> );
} export const query = graphql` query POST_BY_SLUG($slug: String) { mdx(slug: { eq: $slug }) { id slug body frontmatter { date title } } }
`;
Now thatās a lot of code, so Iāll break it down. Itās mainly to do with the GraphQL query:
query POST_BY_SLUG($slug: String) { mdx(slug: { eq: $slug }) { id slug body frontmatter { date title } }
}
The start of the query is taking in a slug
with POST_BY_SLUG($slug: String)
, and the main node is mdx
, so Iām using mdx.slug
like the filename {mdx.slug}.js
.
If I take that query and paste it into my GraphiQL explorer and press the play button, I get this:
{ "data": { "mdx": null }, "extensions": {}
}
Thatās because thereās no variable defined for $slug
in the GraphiQL explorer. If you look to the bottom of the query panel, youāll see thereās a Query Variables section. Clicking this will expand it. In here is where I need to add a variable for slug
. Iāll define it in curly braces with the path of one of the files:
{ "slug": "2021/03/08/third-post/"
}
Running the query again, Iāll get all the data for that file. Iāve commented out the body
output for readability:
{ "data": { "mdx": { "id": "105a5c78-6a36-56e8-976c-d53d8e6ca623", "slug": "2021/01/08/third-post/", "body": "function _extends() ...", "frontmatter": { "date": "2021-03-08T00:00:00.000Z", "title": "Third Post!" } } }, "extensions": {}
}
What the File System Route API is doing is passing the individual file paths into the page query in src/pages/{mdx.slug}.js
and returning the data to the page from that query in the ({ data })
prop being passed to the page.
In this file, you may notice Iāve destructured the body
from the data being returned, and then title
from from the frontmatter
, in a two-level destructure:
const { body, frontmatter: { title },
} = data.mdx;
An alternative way to do it would be:
const body = data.mdx.body;
const title = data.mdx.frontmatter.title;
Using destructuring makes it a lot less verbose.
One last thing to note is the MDXRenderer
wrapping the body
of the post. This is everything included in the .mdx
file after the front matter block. The compiled MDX from the GraphiQL query, which was commented out, is what needs to be wrapped in the MDXRenderer
:
<MDXRenderer>{body}</MDXRenderer>
Iāll commit the changes now:
git add .
git commit -m 'create file route API file'
Root Wrapper Concept
Now clicking on one of the links on the index page will take me to the desired .mdx
page, but it looks a bit different from the index page, right?
Thatās because thereās no layout wrapping it yet. This is where I can use the Gatsby browser API and use the wrapPageElement
function to wrap all the page elements. Itās also recommended that I use the same function in Gatsby SSR.
To avoid duplicating the same code in two files, Iāll create a third file with the actual code Iām going to use and import that into the two gatsby-*
files mentioned.
First up, Iāll create the files needed:
touch gatsby-browser.js gatsby-ssr.js root-wrapper.js
The root wrapper file is where Iāll be using the wrapPageElement
function:
import React from "react";
import { Layout } from "./src/components/layout"; export const rootWrapper = ({ element }) => { return <Layout>{element}</Layout>;
};
Then, in both the gatsby-browser.js
and gatsby-ssr.js
files, Iāll add this:
import { rootWrapper } from "./root-wrapper"; export const wrapPageElement = rootWrapper;
If there are any changes needed to the wrapPageElement
function, I can do it in the one file root-wrapper.js
.
Time to stop and restart the dev server again to see the changes take effect!
Because the layout component is being used here to wrap all the page elements on the site, thereās no need to keep it on the index page anymore, so Iām going to remove that from src/pages/index.js
:
import { graphql, Link as GatsbyLink } from "gatsby";
import React from "react";
import { Box, Heading, Link } from "theme-ui";
- import { Layout } from "../components/layout";
export default function IndexPage({ data }) {
return ( <>
- <Layout>
{data.allMdx.nodes.map(({ id, excerpt, frontmatter, slug }) => ( <Box key={id} as="article" sx={{ mb: 4, p: 3, boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)", border: "1px solid #d1d1d1", borderRadius: "15px", }} > <Link as={GatsbyLink} to={`/${slug}`}> <Heading>{frontmatter.title}</Heading> <Box as="p" variant="styles.p"> {frontmatter.date} </Box> <Box as="p" variant="styles.p"> {excerpt} </Box> </Link> </Box> ))}
- </Layout>
</> );
};
// rest unchanged
Iāll commit the changes so far before moving on:
git add .
git commit -m 'add root wrapper to Gatsby Browser and SSR'
404 Page
Time to make that 404 page!
touch src/pages/404.js
In the src/pages/404.js
file, Iāll and add a message:
import React from "react";
import { Box, Heading } from "theme-ui"; export default function NotFound() { return ( <> <Heading variant="styles.h1"> Page not found! <span role="img" aria-label="crying face"> š¢ </span> </Heading> <Box as="h2" variant="styles.h2"> It looks like that page doesn't exist </Box> </> );
}
Now I can directly navigate to the 404 page to check it out: http://localhost:8000/404
.
Note that, when developing using gatsby develop
, Gatsby will continue to use the default 404 page that overrides your custom 404 page.
Commit this and move on to the next part:
git add .
git commit -m 'add 404 page'
Dark Theme Toggle
Dark mode is an essential feature of coding blogs. (Iām saying that half jokingly, in case you werenāt sure!) Iām going to use the Theme UI color mode hook useColorMode
and do a simple toggle between the two modes I defined in the theme
object earlier. Hereās whatās getting added to src/components/header.js
:
import { Link as GatsbyLink } from "gatsby";
import React from "react";
+ import { Box, Button, Heading, Link, useColorMode } from "theme-ui";
export const Header = ({ siteTitle, siteDescription }) => {
+ const [colorMode, setColorMode] = useColorMode();
return ( <Box as="header" sx={{ bg: "highlight", mb: "1.45rem" }}> <Box as="div" sx={{ m: "0 auto", maxWidth: "640px", p: "1.45rem 1.0875rem", }} > <Link as={GatsbyLink} to="/"> <Heading>{siteTitle}</Heading> </Link> <Box as="p" variant="styles.p"> {siteDescription} </Box>
+ <Button
+ onClick={(e) => {
+ setColorMode(colorMode === "default" ? "dark" : "default");
+ }}
+ >
+ {colorMode === "default" ? "Dark" : "Light"}
+ </Button>
</Box> </Box> );
};
But that doesnāt look great, so Iāll wrap the container with the Theme UI Flex
component and shift the button over to the right:
import { Link as GatsbyLink } from "gatsby";
import React from "react";
+import { Box, Button, Flex, Heading, Link, useColorMode } from "theme-ui";
export const Header = ({ siteTitle, siteDescription }) => {
const [colorMode, setColorMode] = useColorMode(); return ( <Box as="header" sx={{ bg: "highlight", mb: "1.45rem" }}> <Box as="div" sx={{ m: "0 auto", maxWidth: "640px", p: "1.45rem 1.0875rem", }} >
+ <Flex>
+ <Box sx={{ flex: "1 1 auto", flexDirection: "column" }}>
<Link as={GatsbyLink} to="/"> <Heading>{siteTitle}</Heading> </Link> <Box as="p" variant="styles.p"> {siteDescription} </Box>
+ </Box>
<Button onClick={(e) => { setColorMode(colorMode === "default" ? "dark" : "default"); }} > {colorMode === "default" ? "Dark" : "Light"} </Button>
+ </Flex>
</Box> </Box> );
};
Git commit before moving to the next section:
git add .
git commit -m 'add theme toggle to header'
Code Blocks
The code blocks look a bit meh at the moment, so Iām going to add in some syntax highlighting with one of the many handy-dandy Theme UI packages. The one Iām using for this is Prism.
Iāll need to install the package and create a component in the gatsby-plugin-theme-ui
folder called components.js
:
yarn add @theme-ui/prism touch src/gatsby-plugin-theme-ui/components.js
In that file, Iāll need to define where I want to apply the Prism styles to, which is all pre
and code
tags:
import Prism from "@theme-ui/prism"; export default { pre: (props) => props.children, code: Prism,
};
With that defined, Iāll also need to define in the theme
object which Prism theme I want to use:
// scr/gatsby-plugin-theme-ui/index.js import { deep, swiss } from "@theme-ui/presets";
+ import nightOwl from "@theme-ui/prism/presets/night-owl.json";
const theme = {
...swiss, colors: { ...swiss.colors, modes: { dark: { ...deep.colors, }, }, },
styles: { ...swiss.styles,
+ code: {
+ ...nightOwl,
+ },
// remainder of the file unchanged
Another stop and start of the dev server is needed to see the changes take effect!
Commit the changes and move onto the next section:
git add .
git commit -m 'add Prism package and update theme object'
Add Components to the MDX
This next bit is ptional. Markdown JSX allows React (JSX) components to be included in the Markdown. To demonstrate this, Iām going to add a RainbowText
component that will animate some colors on an animation cycle. Thereās an additional dependency I need for the animation: keyframes
from @emotion/react
. Iāll install that now:
touch src/components/rainbow-text.js yarn add @emotion/react
This will probably trash the dev server if itās running, so Iāll stop it for now.
In the src/components/rainbow-text.js
file, Iāll be adding this component:
import { keyframes } from "@emotion/react";
import React from "react";
import { Box } from "theme-ui"; export const RainbowText = ({ children }) => { const rainbow = keyframes({ "0%": { backgroundPosition: "0 0", }, "50%": { backgroundPosition: "400% 0", }, "100%": { backgroundPosition: "0 0", }, }); return ( <Box as="span" variant="styles.p" sx={{ fontWeight: "heading", cursor: "pointer", textDecoration: "underline", ":hover": { background: "linear-gradient(90deg, #ff0000, #ffa500, #ffff00, #008000, #0000ff, #4b0082, #ee82ee) 0% 0% / 400%", animationDuration: "10s", animationTimingFunction: "ease-in-out", animationIterationCount: "infinite", animationName: `${rainbow}`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", }, }} > {children} </Box> );
};
As this is optional, I wonāt be going into detail on whatās going on here. Just know that itās a nice CSS effect to have on hover.
With that component created, I can import it into any .mdx
file I want to use it in. In this example, Iām adding it to content/2021/03/third-post/index.mdx
. Hereās the diff of the file now that Iāve added the component:
---
title: Third Post!
date: 2021-03-08
--- + import { RainbowText } from "../../../../../src/components/rainbow-text";
This is my third post! > with a block quote!
+ <RainbowText>Wheeeeeeee</RainbowText>
And a code block: ```js
const wheeeeee = true;
```
After starting up the dev server again, I can go to the post where that component has been added, and when I hover over the text being wrapped in <RainbowText>Wheeeeeeee</RainbowText>
, I can see that animation in effect.
Youāll probably be grimacing at that import: ../../../
. On and on! Thereās a way to go around this, however, using the root wrapper concept I detailed earlier and using the MDXProvider
which will ā ahem! ā provide MDX with any components you pass to it.
Going back to the root wrapper (root-wrapper.js
), I can wrap the page element
with the MDXProvider
and pass the RainbowText
component to the MDXProvider
:
import { MDXProvider } from "@mdx-js/react";
import React from "react";
import { Layout } from "./src/components/layout";
import { RainbowText } from "./src/components/rainbow-text"; const MDXComponents = { RainbowText,
}; export const rootWrapper = ({ element }) => { return ( <Layout> <MDXProvider components={MDXComponents}>{element}</MDXProvider> </Layout> );
};
Now I can remove the import from the .mdx
file:
---
title: Third Post!
date: 2021-03-08
--- - import { RainbowText } from "../../../../../src/components/rainbow-text";
This is my third post! > with a block quote!
<RainbowText>Wheeeeeeee</RainbowText>
And a code block: ```js
const wheeeeee = true;
```
After stopping and restarting the dev server, I can go to this post and still see the RainbowText
working. The extra advantage of adding components directly to the MDXProvider
is that thereās no need to import a component into the .mdx
document when you want to use it. Itās available via the provider for all MDX documents.
Iāll commit this now:
git add .
git commit -m 'add component for mdx'
Markdown Images
If I want to add images to my blog posts, I can include them in the MDX files, something like this:
---
title: Hello World - from mdx!
date: 2021-03-06
--- My first post!! ## h2 Heading  Some meaningful prose ### h3 Heading Some other meaningful prose
The ./mdx-logo.png
is a file Iāve added to the content/2021/03/06/hello-world
folder, and Iām referencing it as a relative file. Thatās not it for this, though. If I go to the hello world post, the image being displayed is broken. Iām going to need to add gatsby-remark-images
as a plugin to gatsby-plugin-mdx
so it knows what to do with the image files:
yarn add gatsby-remark-images gatsby-plugin-sharp
Iāll then need to configure the plugins in gatsby-config.js
:
const siteMetadata = {
title: `My Gatsby Blog`, description: `This is my coding blog.`,
}; module.exports = {
siteMetadata, plugins: [ `gatsby-plugin-theme-ui`,
+ `gatsby-plugin-sharp`,
+ {
+ resolve: `gatsby-plugin-mdx`,
+ options: {
+ gatsbyRemarkPlugins: [
+ {
+ resolve: `gatsby-remark-images`,
+ options: {
+ maxWidth: 640,
+ },
+ },
+ ],
+ },
+ },
+ {
+ resolve: `gatsby-source-filesystem`,
+ options: {
+ path: `${__dirname}/content/`,
+ },
+ },
{ resolve: `gatsby-source-filesystem`, options: { path: `${__dirname}/content`, name: `content`, }, }, ],
};
The additional gatsby-source-filesystem
object is letting Gatsby know where to look for the images to be processed.
Commit this now:
git add .
git commit -m 'add and configure images'
SEO
SEO is quite important if I want to have my content found on the Internet by search engines, so Iāll need to add the relevant meta tags to my blog here. It can be quite an involved process defining all the relevant tags needed, so to save time, Iāve created a React SEO Component for use in Gatsby for generating all the meta tags needed.
Iām going to yarn add
the component along with the dependencies needed for it to work:
yarn add react-seo-component react-helmet gatsby-plugin-react-helmet
Iāll need to add the gatsby-plugin-react-helmet
to the gatsby-config.js
plugin array:
module.exports = {
siteMetadata, plugins: [
+ `gatsby-plugin-react-helmet`,
`gatsby-plugin-theme-ui`, `gatsby-plugin-sharp`, { // rest unchanged
Then itās a case of using the SEO
component throughout the site where I need to have meta tags.
The component takes quite a few props, many of which are defined once throughout the site, so the best place to add these would be in the siteMetadata
object. Then I can pull out what I need with the useSiteMetadata
hook.
Iām going to add several more properties to the siteMetadata
object:
const siteMetadata = {
title: `My Gatsby Blog`, description: `This is my coding blog.`,
+ lastBuildDate: new Date(Date.now()).toISOString(),
+ siteUrl: `https://dummy-url-for-now.com`,
+ authorName: `Author McAuthorson`,
+ twitterUsername: `@authorOfPosts`,
+ siteLanguage: `en-GB`,
+ siteLocale: `en_gb`,
};
If youāre following along, you can change these as needed. The siteUrl
can be a dummy URL for now. Thatās to help with pointing to any images needed for use in Open Graph protocol, and itās the image you see when sharing a post you have made on Twitter, Facebook, LinkedIn and Reddit, for example.
Now that those additional properties are on the siteMetadata
object, Iāll need to be able to query them. Currently the useSiteMetadata
hook only has title
and description
, so Iāll add the rest in now:
// src/hooks/use-site-metadata.js import { graphql, useStaticQuery } from "gatsby";
export const useSiteMetadata = () => {
const { site } = useStaticQuery( graphql` query SITE_METADATA_QUERY { site { siteMetadata { title description
+ lastBuildDate
+ siteUrl
+ authorName
+ twitterUsername
+ siteLanguage
+ siteLocale
} } } ` ); return site.siteMetadata;
};
Iāll add the SEO component to all the pages. First up, Iāll do the posts pages in the src/pages/{mdx.slug}.js
page. This is one of the most involved, so Iāll dump out the difference here and detail whatās going on:
import { graphql } from "gatsby";
import { MDXRenderer } from "gatsby-plugin-mdx";
import React from "react";
+ import SEO from "react-seo-component";
import { Box } from "theme-ui";
+ import { useSiteMetadata } from "../hooks/use-site-metadata";
export default function PostPage({ data }) {
const { body,
+ slug,
+ excerpt,
+ frontmatter: { title, date },
} = data.mdx;
+ const {
+ title: siteTitle,
+ siteUrl,
+ siteLanguage,
+ siteLocale,
+ twitterUsername,
+ authorName,
+ } = useSiteMetadata();
return ( <>
+ <SEO
+ title={title}
+ titleTemplate={siteTitle}
+ description={excerpt}
+ pathname={`${siteUrl}${slug}`}
+ article={true}
+ siteLanguage={siteLanguage}
+ siteLocale={siteLocale}
+ twitterUsername={twitterUsername}
+ author={authorName}
+ publishedDate={date}
+ modifiedDate={new Date(Date.now()).toISOString()}
+ />
<Box as="h1" variant="styles.h1" fontSize="4xl"> {title} </Box> <MDXRenderer>{body}</MDXRenderer> </> );
} export const query = graphql`
query POST_BY_SLUG($slug: String) { mdx(slug: { eq: $slug }) { id slug body
+ excerpt
frontmatter { date title } } }
`;
The siteUrl
, slug
and excerpt
are needed for the canonical link (very important in SEO) and the excerpt
is for the meta description.
Iām using the siteMetadata
hook to get the rest of the information the component needs. title
and titleTemplate
are used to make up what you see in the browser tab.
The article
Boolean is for the component, so it can create the breadcrumb list in JSONLD format. The rest of the props are to help identify the author and published date. š
That was a lot. I hope some of it made sense! For the scope of this post, Iāll leave it there, but thereās a lot more to dig into on this subject, and I mean a lot!
Thankfully the src/pages/index.js
page is a bit simpler!
import { graphql, Link as GatsbyLink } from "gatsby";
import React from "react";
+ import SEO from "react-seo-component";
import { Box, Heading, Link } from "theme-ui";
+ import { useSiteMetadata } from "../hooks/use-site-metadata";
export default function IndexPage({ data }) {
+ const {
+ title,
+ description,
+ siteUrl,
+ siteLanguage,
+ siteLocale,
+ twitterUsername,
+ } = useSiteMetadata();
return ( <>
+ <SEO
+ title={`Home`}
+ titleTemplate={title}
+ description={description}
+ pathname={siteUrl}
+ siteLanguage={siteLanguage}
+ siteLocale={siteLocale}
+ twitterUsername={twitterUsername}
+ />
{data.allMdx.nodes.map(({ id, excerpt, frontmatter, slug }) => (
// rest of component unchanged
Iāve intentionally left out the image from both examples. If youāre interested in making your own Open Graph images to use in this component, check out the post āOpen Graph Images with Gatsby and Vercelā for how to do this with a serverless function. š„
Now I can build the site (almost ready for production), and once itās built I can check out the page source for the meta tags:
yarn build yarn serve
Once the build has finished, I can use yarn serve
to have the built site served locally on localhost:9000
. In the browser, I can view the page source with the keyboard shortcut Ctrl + u. From here, I can check for the canonical
meta tag, which will be the dummy URL used in the metadata.
Alrighty! Commit this to Git and move on:
git add .
git commit -m 'add SEO component :sweat_smile:'
Push It to GitHub
You may be wondering why Iāve been making Git commits at the end of each section. Thatās because Iām going to push the project up to GitHub now.
Iāll log in to my GitHub account and select the plus +
icon next to my avatar image on the top right corner and select New repository.
In the Repository name, Iāll add in the project name my-gatsby-blog
but leave the rest of the defaults and click Create repository.
The next screen gives me the terminal commands I need to push my local project to GitHub:
git remote add origin https://github.com/spences10/my-gatsby-blog
git branch -M main
git push -u origin main
Once youāve put all those into the terminal and hit Enter, refresh the GitHub page to see the new project!
Deploy
Time to put this baby on the Web! There are many ways to do this. Because Gatsby builds to a flat file structure, you can host a Gatsby site on any file server with access to the Internet.
There are many services out there that offer hosting on a CDN, many for free! Services like Netlify, Vercel and Render will allow you to push your built site to their CDNs via a CLI, GitHub integration, or, in the case of Netlify, a straight up drag and drop!
Vercel
To deploy with Vercel, youāll need a GitHub, GitLab or Bitbucket account to authenticate with. Then youāll be prompted to install the Vercel CLI:
yarn global add vercel
I already have it installed, so now itās a case of running the CLI command:
vc
Iām then prompted to set up and deploy the new project. Iām going to answer the default to all the questions with Enter:
Set up and deploy ā~/repos/my-gatsby-blogā? [Y/n]
Which scope do you want to deploy to?
Link to existing project? [y/N]
Whatās your projectās name? (my-gatsby-blog)
In which directory is your code located? ./
> Upload [====================] 99% 0.0sAuto-detected Project Settings (Gatsby.js):
- Build Command: `npm run build` or `gatsby build`
- Output Directory: public
- Development Command: gatsby develop --port $PORT
? Want to override the settings? [y/N]
Thatās it. Iām then given a deployment URL where I can watch the build of the site on Vercel.
From the Vercel dashboard I can configure the domain, and also buy one from Vercel if I want. I personally use Namecheap.com, but itās an option.
Netlify
Deploying with Netlify via the CLI is much the same as with Vercel, but Iām going to do the drag-and-drop creation.
For authentication, Iāll need one of GitHub, GitLab, Bitbucket or email account. Once Iāve authenticated and logged in, I can select Sites in the menu bar, then thereās a drop area Want to deploy a new site without connecting to Git? Drag and drop your site output folder here. Iām going to navigate in my file explorer to the root of my project and drag and drop the public
folder to the drop area.
Netlify will build the files and deploy them to a generated URL for inspection. Much the same as with Vercel, Netlify will let you purchase a domain there and deploy to it.
Render
Render doesnāt have a CLI or drop option and instead uses a GitHub integration. To authenticate, Iāll need a GitHub, GitLab or Google account. Once Iāve authenticated and logged in, Iām on the services section. From here, I can select New Static Site then enter my GitHub URL for the project I pushed to GitHub earlier.
On the next page, Iāll give it the following settings:
- Name:
my-gatsby-blog
- Branch: the default value
- Build command:
yarn build
- Publish directory:
./public
Then click Create Static Site.
Wait for Render to do its thing, and then click the link below the project name to see the site live.
Render also has the option to set your own custom domain for the site!
Optional Gatsby plugins
There are many more Gatsby plugins to choose from for adding additional functionality. Iāll leave these to you if you want to add more. For example:
Analytics
If youāre interested in knowing how popular your site is, there are analytics options. I stopped using Google Analytics a while back on my own projects, and I now prefer more privacy-focused alternatives. One I recommend is Fathom Analytics. (I have an affiliate link if you want to get $10 off your first monthās subscription.)
Another alternative is Plausible, which Iāve also heard good things about.
To implement Fathom Analytics on a Gatsby site, Iāll need to add an additional script tag to the head of my site. What does that mean? Well, first up Iāll need to create the site on my Fathom dashboard, then go to https://app.usefathom.com/#/settings/sites, scroll to the bottom of the list, add in my new site (my-gatsby-blog
), then click Get site code. I then get a popup modal with the site code. Iāll need that for the script Iām going to add to the head of my Gatsby project. Hereās what the script looks like:
<script src="https://cdn.usefathom.com/script.js" data-spa="auto" data-site="ABCDEF" defer
></script>
Hereās the diff of root-wrapper.js
:
import { MDXProvider } from "@mdx-js/react";
import React from "react";
+import { Helmet } from "react-helmet";
import Layout from "./src/components/layout";
import RainbowText from "./src/components/rainbow-text"; const MDXComponents = {
RainbowText,
}; export const wrapPageElement = ({ element }) => {
return (
+ <>
+ <Helmet>
+ <script
+ src="https://cdn.usefathom.com/script.js"
+ spa="auto"
+ data-site="ABCDEF"
+ defer
+ ></script>
+ </Helmet>
<Layout> <MDXProvider components={MDXComponents}>{element}</MDXProvider> </Layout>
+ </>
);
};
Wrap!
Thatās it from me. Thank you so much for making it to the end. š
I hope you got what you needed from this quite extensive guide on setting up a Gatsby project from scratch!
If you want to reach out and say hi, the best place to get me is on Twitter.