< Prev Post | Next Post >

Setting Up a new Dev Blog With Gatsby in 2022

July 20, 2022


Today we will set up a new dev blog. We will build it with Gatsby.

The initial goals are just the basic functionality of an index page with links to individual posts, and some niceties for presenting a lot of code, like syntax highlighting and a copy-to-clipboard button. We will be storing our content in markdown files.


1. Bootstrapping a new Gatsby Project


We could install gatbsy-cli globally or we can run it with NPX, but either way, use the CLI to bootstrap the project:

>_
sh
npx gatsby new

init

Run the CLI and follow the prompts to choose your styling library, plugins, typescript or not, that sort of thing. This time we selected MDX support, responsive images, and React Helmet. For styling, we're going to use plain CSS modules.

The CLI will generate some files, and the first one to know about it is gatsby-config.js/ts:

gatsby-config.ts
ts
import type { GatsbyConfig } from 'gatsby'
const config: GatsbyConfig = {
siteMetadata: {
title: `My New Dev Blog`,
siteUrl: `http://my.blog.local`
},
graphqlTypegen: true,
plugins: [
'gatsby-plugin-image',
'gatsby-plugin-react-helmet',
'gatsby-plugin-mdx',
'gatsby-plugin-sharp',
'gatsby-transformer-sharp',
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'images',
path: './src/images/'
},
__key: 'images'
},
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'pages',
path: './src/pages/'
},
__key: 'pages'
},
]
}
export default config

First, a few things for our initial setup:

Using ES6 syntax in gatsby-node.js

>_
sh
npm i esm
gatsby-node.js
js
require = require('esm')(module)
module.exports = require('./gatsby-node.esm.js')

Add aliases for absolute imports:

gatsby-node.esm.js
js
const path = require('path')
export onCreateWebpackConfig = ({ actions }) => {
actions.setWebpackConfig({
resolve: {
alias: {
'@components': path.resolve(__dirname, 'src', 'components')
}
}
})
}
ts-config.json
json
{
"compilerOptions": {
"paths": {
"@components/*": ["./src/components/*"]
},
...

If your setup is like mine and you're using Typescript + CSS modules, you'll probably also want a definitions file along these lines to prevent the compiler from finding fault with our CSS module imports:

src/global.d.ts.ts
ts
declare module '*.css' {
const content: { [className: string]: string }
export = content
}

2. Configuring a data source


Our posts (as in the mdx files) will be organized by date in subfolders. The mdx behind this post is at ./blog/2022/07/20/setting-up-with-gatsby/index.mdx in the local filesystem, for example.

Our file system will also determine the URL of our post, so the way we organize our files is extremely important. This folder structure is what we want:

πŸ“¦blog
β”” πŸ“‚2022
β”‚ β”” πŸ“‚07
β”‚ β”‚ β”œ πŸ“‚20
β”‚ β”‚ β”‚ β”” πŸ“‚setting-up-with-gatsby
β”‚ β”‚ β”‚ β”‚ β”œ πŸ“œindex.mdx
β”‚ β”‚ β”‚ β”‚ β”” πŸ“œmdx-graphql.png
β”‚ β”‚ β”œ πŸ“‚22
β”‚ β”‚ β”‚ β”” πŸ“‚setting-up-with-gatsby-ii
β”‚ β”‚ β”‚ β”‚ β”œ πŸ“œgithub-pages-settings.png
β”‚ β”‚ β”‚ β”‚ β”” πŸ“œindex.mdx

Make a folder called blog in the project root. This is where we will keep the markdown files that comprise our collection of blog posts.

We tell Gatsby about this blog folder by adding one more item at the bottom of the plugins array in our gatsby-config, just below the other gatsby-source-filesystem definitions.

In effect, this makes the files in the blog folder visible to our GraphQL queries:

gatsby-config.ts
ts
plugins: [
...
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'blog',
path: './blog/'
},
__key: 'blog'
}
]

3. Create the blog index page


Start the development server...

>_
sh
npm start
> dev-blog@1.0.0 start
> gatsby develop -o -p 8000

At localhost:8000/___graphql, you get the GraphQL query interface. Basically this is just a nice UI to query Gatsby's data layer, meaning the things you have configured Gatsby to be able to get data from (like our blog folder). We use the UI to develop the queries we need to power our site.

For example, this query is basically what we will run to get the data to generate the index page. Try running this query while the blog folder is still empty:

graphql
query BlogPosts {
allMdx(
sort: {order: DESC, fields: [frontmatter___date]},
limit: 10
) {
edges {
node {
slug
frontmatter {
date(formatString: "DD MMMM YYYY")
title
}
wordCount {
words
}
}
}
}
}

What happens is that GraphQL will throw an error: Value "frontmatter___date" does not exist in "MdxFieldsEnum" enum, because our query basically says ORDER BY frontmatter___date, which doesn't exist in the GQL schema.

If we had any files with frontmatter___date set, it would have been added to the schema dynamically. If not, GraphQL has no way of knowing that it exists.

However, if we really wanted our query to work in this situation and return an empty result set instead of an error, we could do something like this:

gatsby-node.esm.js
js
export createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
createTypes(`
type Mdx implements Node {
frontmatter: MdxFrontmatter
}
type MdxFrontmatter {
date: Date @dateformat
}
`)
}

This isn't necessary because we don't plan on having an empty blog or inconsistent metadata in our MDX files. It is mentioned just to clarify how GraphQL queries are working with frontmatter in Gatsby.

In any case, we need at least one MDX file with a date to exist in our blog folder before we can continue. Once we have that, our query should work:

mdx gql


Now, to create our index page, replace the contents of src/pages/index.jsx file with the following:

src/pages/index.jsx
jsx
import * as React from 'react'
import { graphql, Link } from 'gatsby'
const Blog = ({ data }) => {
return (
<>
{data.allMdx.nodes.map((node) => (
<article key={node.id}>
<h2>
<Link to={`/${node.slug}`}>
{node.frontmatter.title}
</Link>
</h2>
<p>{node.frontmatter.date}</p>
<p>{node.slug}</p>
</article>
))}
</>
)
}
export const query = graphql`
query {
allMdx(sort: { fields: frontmatter___date, order: DESC }) {
nodes {
frontmatter {
date(formatString: "MMMM D, YYYY")
title
}
slug
id
}
}
}
`
export default Blog

As you have probably noticed, there is some kind of extra something going on here. It's true. Gatsby works a bit of magic, taking the results of the exported query and then passing it into the component as a prop called data.

It is a super-simple pattern where each page component has a query right alongside it, and the fact that GQL syntax resembles ES6 makes it even better, as the shape of the object we are deferencing in our component is made plainly evident just by glancing at the GQL query.


4. Create a Template for Blog Posts


Create a blog post template:

src/pages/{mdx.slug}.jsx
jsx
import * as React from 'react'
import { graphql } from 'gatsby'
import { MDXRenderer } from 'gatsby-plugin-mdx'
const BlogPost = ({ data }) => {
return (
<>
<p>{data.mdx.frontmatter.date}</p>
<MDXRenderer>{data.mdx.body}</MDXRenderer>
</>
)
}
export const query = graphql`
query ($id: String) {
mdx(id: { eq: $id }) {
frontmatter {
title
date(formatString: "MMMM D, YYYY")
}
body
}
}
`
export default BlogPost

The {mdx.slug} in the filename will be automatically expanded to the value returned by the query. For example: 2022/07/20/setting-up-with-gatsby/

Now, we should be able to click the links on the index page and be brought to a page with the full content of the post.


5. Create a Layout


The main thing we are in need of is some kind of navigation control. Eventually we need a header, footer, and that sort of thing. Getting our content nicely centered would also be nice.

We will create a shared Layout component to address these things:

src/components/Layout/index.jsx
jsx
import React from 'react'
import * as styles from './layout.module.css'
const Layout = ({ children }) => (
<div className={styles.pageContainer}>
<nav>
<ul className={styles.navLinks}>
<li className>
<Link to='/'>Home</Link>
</li>
</ul>
</nav>
<div className={styles.innerContainer}>{children}</div>
</div>
)
export default Layout
src/components/Layout/layout.module.css
css
.page-container {
margin: 0 auto;
padding: 0;
box-sizing: border-box;
max-width: 860px;
}
.inner-container {
margin-top: 7em;
padding: 12px;
}
.nav-links {
column-count: 2;
column-gap: 85%;
display: flex;
list-style: none;
padding-left: 0;
}

Speaking of styles, let's also make a global CSS file for rules that need to apply everywhere:

src/styles/global.css
css
body {
margin: 0px;
line-height: 1.6;
text-rendering: optimizeLegibility;
}

That needs to be referenced by a gatsby-browser file, so create one:

gatsby-browser.js
css
import './src/styles/global.css'

We could wrap each of our pages with our new layout component, like this:

src/pages/{mdx.slug}.jsx
jsx
import Layout from '@components/Layout'
const BlogPost = ({ data }) => {
return (
<>
<Layout>
<p>{data.mdx.frontmatter.date}</p>
<MDXRenderer>{data.mdx.body}</MDXRenderer>
</Layout>
</>
)
}
src/pages/index.jsx
jsx
import Layout from '@components/Layout'
const Blog = ({ data }) => {
return (
<>
<Layout>
{data.allMdx.nodes.map((node) => (
<article key={node.id}>
<h2>
<Link to={`/${node.slug}`}>{node.frontmatter.title}</Link>
</h2>
<p>Posted: {node.frontmatter.date}</p>
<p>{node.slug}</p>
</article>
))}
</Layout>
</>
)
}

However, we want every page wrapped, so we can do this instead to acheive the same thing, and new pages we add will be wrapped by default:

Add the following to gatsby-browser.js and export it from gatsby-ssr.js:

gatsby-browser.js
jsx
import * as React from 'react'
import Layout from '@components/Layout'
import './src/styles/global.css'
const wrapPageElement = ({ element, props }) => {
return <Layout {...props}>{element}</Layout>
}
export { wrapPageElement }
gatsby-ssr.js
jsx
export { wrapPageElement } from './gatsby-browser.js'

6. Configure MDX to work with Inline Images


Storing image files directly alongside the markdown files is obviously ideal in our case.

We want to be able to reference images in our Markdown files like this:

markdown
## MDX ❀️
![Alt Text](hello.png)

We need to make a change to the configuration for the gatsby-plugin-mdx plugin due to the way our files are organized.

Replace the existing reference to 'gatsby-plugin-mdx' with a new object definition, like this:

gatsby-config.ts
ts
'gatsby-plugin-mdx',
{
resolve: `gatsby-plugin-mdx`,
options: {
gatsbyRemarkPlugins: [
{
resolve: `gatsby-remark-images`,
}
]
}
},

Upon restarting the development server, we can reference images as described and everything works.

As a bonus, we also now get a neat progressive loading effect, as well as responsive thumbnails.


7. Color Syntax Highlighting with Prism.js


Our use case involves the frequent usage of code blocks, so we might as well make them as easy to digest and as visually pleasing as possible. Prism.js gives us syntax highlighting in almost any language with fairly minimal effort. To get the basic functionality we need only do three things:

Install prism-react-renderer

>_
sh
npm i prism-react-renderer

Create a new component to render code blocks

src/components/Code/index.jsx
jsx
import React from 'react'
import Highlight, { defaultProps } from 'prism-react-renderer'
import theme from 'prism-react-renderer/themes/github'
const Code = ({ codeString, language, ...props }) => {
return (
<Highlight
{...defaultProps}
code={codeString}
language={language}
theme={theme}
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre
className={className}
style={{ ...style, padding: '20px', position: 'relative' }}
>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
)
}
export default Code

Wrap MDXRenderer with provider

src/pages/{mdx.slug}.jsx
jsx
import * as React from 'react'
import { graphql } from 'gatsby'
import { MDXRenderer } from 'gatsby-plugin-mdx'
import { MDXProvider } from '@mdx-js/react'
import Code from '@components/Code'
import Layout from '@components/Layout'
const BlogPost = ({ data }) => {
return (
<Layout>
<MDXProvider components={components}>
<p>{data.mdx.frontmatter.date}</p>
<MDXRenderer>{data.mdx.body}</MDXRenderer>
</MDXProvider>
</Layout>
)
}
export const query = graphql`
query ($id: String) {
mdx(id: { eq: $id }) {
frontmatter {
title
date(formatString: "MMMM D, YYYY")
}
body
}
}
`
const components = {
pre: preProps => {
if (preProps.children?.props?.mdxType !== 'code') {
return <pre {...preProps} />
}
const {
children: codeString,
className = '',
...props
} = preProps.children.props
const match = className.match(/language-([\0-\uFFFF]*)/)
return (
<Code
{...props}
codeString={codeString.trim()}
className={className}
language={match != null ? match[1] : ''}
/>
)
}
}
export default BlogPost

That's basically it. This looks pretty nice once it is working, but since presenting code is a fundamental part of a dev blog, we're going to want to improve some things. Line numbers, for example, are something we will want to add.

Given the endless possibilities we will have to have a post entirely dedicated to prism.js and advanced options for it. For now, we'll just be happy about how nice and colorful things already are.


8. Copy to Clipboard Button


If you've made it to this point, you should have a pretty spiffy looking setup coming together. As far as our initial list of desired features, the only thing we have yet to do is the Copy-to-Clipboard button. This is the last task of our initial setup. We need to change only one file to get this working. Add the following code to it:

src/components/Code/index.jsx
jsx
import React from 'react'
import Highlight, { defaultProps } from 'prism-react-renderer'
import theme from 'prism-react-renderer/themes/github'
import * as styles from './code.module.css'
const CopyButton = props =>
<button className={styles.button} {...props} />
const Code = ({ codeString, language, ...props }) => {
const [isCopied, setIsCopied] = React.useState(false)
return (
<Highlight
{...defaultProps}
code={codeString}
language={language}
theme={theme}
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre
className={className}
style={{ ...style, padding: '20px', position: 'relative' }}
>
<CopyButton
onClick={() => {
navigator.clipboard.writeText(codeString)
setIsCopied(true)
setTimeout(() => setIsCopied(false), 4000)
}}
>
{isCopied ? <i>Copied!</i> : 'Copy'}
</CopyButton>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
)
}
export default Code
src/components/Code/code.module.css
css
.button {
position: absolute;
top: 0;
right: 0;
margin: 8px;
padding: 8px 12px;
border-radius: 8px;
border-width: 0;
background: deepskyblue;
}

9. Wrapup


We now drop markdown and image files into our blog folder and know that when our app gets built and deployed, our files will be processed into blazing fast pre-built static pages, looking good and ready to be served.

Markdown and React go good together. Gatsby's data layer is a perfect use case for GraphQL. These things are really meant for each other, and it is not surprising that this stack is a popular way to build websites.

For the second part of this post, we will concern ourselves with getting our new Gatsby blog deployed.