How to Build a Developer Blog with Gatsby and MDX
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:
# create the project directory
mkdir my-gatsby-blog
# change into the directory
cd my-gatsby-blog
# initialise a package.json file
yarn init -y
# initialise the git repo
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:
# create .gitignore file in my directory
touch .gitignore
# add ignore contents with echo
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
# -p is to create parent directories too if needed
mkdir -p src/pages
# create the index (home) page
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:
# if you're using npm 👇
# $(npm bin)/gatsby develop
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:
# add everything for committing
git add .
# commit to repo
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:
# this creates the folders in the root of the project
mkdir -p content/2021/03/{06/hello-world,07/second-post,08/third-post}
# create individual files
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:
# add changed file for committing
git add .
# commit to repo
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.
Site Metadata
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: [
// configured plugins here
{
// empty for brevity
},
],
};
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: [
// configured plugins here
{
// empty for brevity
},
],
};
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": {}
}
Make a Site Metadata Hook
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:
# make a folder for all the hooks to live
mkdir src/hooks
# creathe the file
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`,
// rest of the module unchanged
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:
# create the folder for the Theme UI theme
mkdir src/gatsby-plugin-theme-ui
# create the theme file
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
:
# create a components folder
mkdir src/components
# create Layout and Header files
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:
# create the route api file
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() ...", // compiled MDX here
"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:
# create gatsby-browser.js and gatsby-ssr.js and root-wrapper.js
touch gatsby-browser.js gatsby-ssr.js root-wrapper.js
The root wrapper file is where I’ll be using the wrapPageElement
function:
// root-wrapper.js
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!
# create the 404.js 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
:
# install the package
yarn add @theme-ui/prism
# create Theme UI components file
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 optional. 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:
# create component file
touch src/components/rainbow-text.js
# install @emotion/react
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
![mdx logo](./mdx-logo.png)
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:
# build the site first
yarn build
# use gatsby serve to run built site locally
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:
- Do you want to embed YouTube videos, Tweets, Strava runs, CoodePens and Codesandbox? Check out gatsby-plugin-mdx-embed.
- Are you using a Google/Bing Search Console? Then you’ll need to generate a sitemap with gatsby-plugin-sitemap.
- Do you want to have your site available offline as a PWA? Add in gatsby-plugin-offline.
- Do you want to have a favicon in the browser tab for your site? Check out gatsby-plugin-manifest.
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.