How to Create Blog Posts From Markdown With Gatsby in 2021

squashbugler

John Grisham

Posted on February 26, 2021

How to Create Blog Posts From Markdown With Gatsby in 2021

If you want to support me please check out the original post on Medium:
How to create blog posts from Markdown with Gatsby in 2021


Let’s face it building a website is easier than ever; with plenty of platforms to choose from.

But Regardless of where your website is hosted or the platform one thing is usually the same; blog posts.

Then introduce Gatsby which is perfect for building static websites. And
moving from another platform to Gatsby is easier when your blog posts are in Markdown which luckily they usually are!

I’m going to show you how to take markdown files in Gatsby and turn them into generated HTML blog posts, so let’s get started.


Setting up the project

For this tutorial, I’m going to be using the free Peanut Butter & Jelly
Gatsby template I created. The complete version is also available if you
like the template and want to support me by purchasing it.

You can check out the template demo here:

PB&J Demo

And you can download it here:
PB&J Gumroad

or

Clone the repo:

https://github.com/JohnGrisham/PB-JPlain.git

This will give you the same project to work from as the one I used to
set up my landing page. To get this template up and running, in a
terminal go into the directory you put the project in and run:

yarn
Enter fullscreen mode Exit fullscreen mode

This will download all the dependencies required to get going and once
that’s done run:

yarn develop
Enter fullscreen mode Exit fullscreen mode

This will start development and you should be able to navigate to
localhost:8000 to see the landing page.

If you haven’t done so already go ahead and open up the project in a
text editor of your choice, I use Vscode.

Take a few minutes to note the file structure, everything that’s
included is documented in the readme.

We’ll need a few more packages to get started so run this command in a
separate terminal.

yarn add gatsby-transformer-remark rehype-react
Enter fullscreen mode Exit fullscreen mode

Generating types and configuration

This template uses a development tool to generate Typescript types from
Graphql schemas. If this is all Greek to you that’s fine, I handle most
of the setup for you. All you need to know is that we’ll need the types
for the new transformer we added. But first, we need to do some
configuration. In the codegen.yml file at the root of the project add
this line under documents.

// codegen.yml  - node_modules/gatsby-transformer-remark/!(node_modules)/**/*.js
Enter fullscreen mode Exit fullscreen mode

This will add the new types for Remark to our generated types file. This
works fine for most uses but we need to extend the ‘frontmatter’ field
to add some extra props such as slug. So open the typedefs.js file in
src/graphql/typedefs.js to include these new types.

// src/grapql/typedefs.jstype

MarkdownRemarkFrontmatter {  
    author: AttributedUser
    title: String!
    slug: String!
    date: String
    featuredImage: String
}

type MarkdownRemark implements Node {  
    frontmatter: MarkdownRemarkFrontmatter
}
Enter fullscreen mode Exit fullscreen mode

The last thing we need to do before generating types is update the
gatsby-config with the plugin we added. So somewhere in the plugins
array add this:

// gatsby-config.js

plugins: [`gatsby-transformer-remark`]
Enter fullscreen mode Exit fullscreen mode

Then stop and restart your development process and run:

yarn generate-types
Enter fullscreen mode Exit fullscreen mode

Gatsby templates with styled components

Now we will need to tell Gatsby to generate the HTML files for our
markdown. We’ll want control over how each of these pages looks but we
also want them to all function the same. That’s where Gatsby templates
come in.

You can see an example of this in Gatsby’s docs:

Creating Pages from Data Programmatically

We’re going to create our own template and use it for layout and styling
on our posts. In the src folder add a templates folder. And inside it
add a styles folder with article-template.styled.tsx and index.ts files.
Inside of the article-template.styled.tsx file add these styles.

// templates/styles/article-template.styled.tsx

import styled from 'styled-components'

export const Article = styled.article`
  background-color: white;
  color: ${({ theme }) => theme.colors.mediumGray};
  display: flex;
  flex-direction: column;
  padding: 2em 10vw 2em 10vw;`

export const Author = styled.div`
  display: flex;
  justify-content: center;`

export const AfterTitle = styled.div`
  margin: auto auto;
  width: 100%;

  @media all and (min-width: 700px) {
       width: 70%;
    }`

export const Content = styled.div`
   display: flex;
   flex: 1;
   margin-top: 2em;
   max-width: 100vw;
   overflow: hidden;
   word-wrap: break-word;

   div {
       max-width: 100%;
     }`

export const Heading = styled.h1`
  font-size: 2em;`

export const List = styled.ul`
  list-style-type: disc;`

export const Paragraph = styled.p`
  font-size: 1.2em;
  line-height: 1.5;`

export const SubHeading = styled.h2`
  font-size: 1.5em;`

export const Title = styled.h1`
  font-size: 3em;
  text-align: center;`
Enter fullscreen mode Exit fullscreen mode

And export all the styles from the index.ts file like so:

// templates/styles/index.ts

export * from './article-template.styled'
Enter fullscreen mode Exit fullscreen mode

Finally, create an article-template.tsx file at the root of templates:

// src/templates/article-template.tsx
import * as React from 'react'
import * as Styled from './styles'
import { Avatar, Image, Layout } from '../components'
import { ImageType } from '../enums'
import { Query } from '../interfaces'
import RehypeReact from 'rehype-react'
import { format } from 'date-fns'
import { graphql } from 'gatsby'

export const query = graphql`
    query($slug: String!) {
        allMarkdownRemark(filter: { frontmatter: { slug: { eq: $slug } } }) {
            edges {
                node {
                    frontmatter {
                        author {
                            avatar
                            name
                        }
                        date
                        featuredImage
                        title
                    }
                    excerpt
                    htmlAst
                }
            }
        }
    }
`

const articleTemplate: React.FC<{ data: { allMarkdownRemark: Query['allMarkdownRemark'] } }> = ({ data }) => {
    if (!data) {
        return null
    }

    const {
        allMarkdownRemark: {
            edges: [
                {
                    node: { frontmatter, htmlAst }
                }
            ]
        }
    } = { ...data }

    const renderAst = new (RehypeReact as any)({
        components: { h1: Styled.Heading, h2: Styled.SubHeading, p: Styled.Paragraph, ul: Styled.List },
        createElement: React.createElement
    }).Compiler

    return (
        <Layout>
            {' '}
            <Styled.Article>
                {' '}
                {frontmatter && (
                    <>
                        {' '}
                        <Styled.Title>{frontmatter.title}</Styled.Title>{' '}
                        {frontmatter.author && (
                            <Styled.Author>
                                {frontmatter.author.avatar && <Avatar avatar={frontmatter.author.avatar} />}{' '}
                                <Styled.SubHeading> {frontmatter.author.name} </Styled.SubHeading>{' '}
                            </Styled.Author>
                        )}{' '}
                        {(frontmatter.featuredImage || frontmatter.date) && (
                            <Styled.AfterTitle>
                                {' '}
                                {frontmatter.featuredImage && (
                                    <Image
                                        src={frontmatter.featuredImage}
                                        type={ImageType.FLUID}
                                        style={frontmatter.date ? { marginBottom: '10px' } : undefined}
                                    />
                                )}{' '}
                                {frontmatter.date && (
                                    <Styled.SubHeading style={{ textAlign: 'center' }}>
                                        {' '}
                                        {format(new Date(frontmatter.date), 'MMM do yyyy')}{' '}
                                    </Styled.SubHeading>
                                )}{' '}
                            </Styled.AfterTitle>
                        )}
                    </>
                )}{' '}
                <Styled.Content>{renderAst(htmlAst)}</Styled.Content>{' '}
            </Styled.Article>{' '}
        </Layout>
    )
}

export default articleTemplate
Enter fullscreen mode Exit fullscreen mode

This may look complicated but all we’re doing is querying all the
markdown and filtering it by the slug. The slug is used to determine the
URL of the post and the front matter are fields like featured image and
author. After we have the correct post we will render all the
frontmatter I mentioned. Then use Rehype React to turn the raw HTML
string into a component. Each of the defined basic HTML elements we
specified get converted to styled-components. By doing so we have more
control over the style of our posts.


Creating Pages as Blog Posts

Here’s where the magic happens.

We will be using the create pages hook provided by Gatsby to query our
markdown into pages using the template we made. In the gatsby-config.js
file add the hook.

// gatsby-config.js

exports.createPages = async ({ actions: { createPage }, graphql }) => {
    const {
        data: { allMarkdownRemark, errors }
    } = await graphql(
        `
            {
                allMarkdownRemark {
                    edges {
                        node {
                            frontmatter {
                                slug
                            }
                        }
                    }
                }
            }
        `
    )

    if (!allMarkdownRemark || errors) {
        console.log('Error retrieving data', errors || 'No data could be found for this query!')
        return
    }

    const articleTemplate = require.resolve('./src/templates/article-template.tsx')

    allMarkdownRemark.edges.forEach((edge) => {
        createPage({
            component: articleTemplate,
            context: { slug: edge.node.frontmatter.slug },
            path: `/blog/${edge.node.frontmatter.slug}/`
        })
    })
}

Enter fullscreen mode Exit fullscreen mode

Navigating Posts

We could just navigate manually to the URL in each of our posts but the
user will need to be able to find and navigate to our posts. So first
off create a blog folder in components and inside that folder create a
post folder. From there create a styles folder and populate it with
post.styled.tsx and index.ts files.

// blog/post/styles/post.styled.tsx

import { Card } from '@material-ui/core'
import { Image } from '../../../image'
import { Link } from 'gatsby'
import styled from 'styled-components'

export const AuthorInfo = styled.div`
  align-items: center;
  display: flex;
  flex-direction: column;
  justify-content: center;

    h4 {
        margin: 0px;
   }`

export const FeaturedImage = styled(Image).attrs(() => ({ 
  aspectRatio: 21 / 11,
  type: 'fluid'
}))`flex: 1;`

export const Info = styled.div`
  align-items: center;
  display: flex;
  flex-direction: column;
  margin-top: 1em;`

export const PostItem = styled(Card).attrs({ raised: true })`
  align-items: center;
  display: flex;
  flex-direction: column;
  text-align: center;`

export const PostContent = styled.span`padding: 1em;`

export const PostContentUpper = styled.div`
  margin-bottom: 10px; 

    h3 { 
        margin: 0px;
    }`

export const PostLink = styled(Link)`
  color: ${({ theme }) => theme.colors.black};
  display: flex;
  flex: 1;
  flex-direction: column;
  text-decoration: none;
  width: 100%;`

Enter fullscreen mode Exit fullscreen mode

Once again export the styles:

// blog/post/styles/index.ts

export * from './post.styled'
Enter fullscreen mode Exit fullscreen mode

Now let’s make the actual post component. We’ll need to pass along the
‘frontmatter’ of each post in order to give the reader a taste of what
the post is about.

// blog/post/post.tsx

import * as React from 'react'
import * as Styled from './styles'
import { MarkdownRemark, MarkdownRemarkFrontmatter } from '../../../interfaces'
import { Avatar } from '../../avatar'
import { CardProps } from '@material-ui/core'
import { GatsbyLinkProps } from 'gatsby'
import { format } from 'date-fns'

interface Post extends MarkdownRemarkFrontmatter, Omit<CardProps, 'title' | 'onClick'> {
    excerpt: MarkdownRemark['excerpt']
    onClick?: GatsbyLinkProps<Record<string, unknown>>['onClick']
}

const Post: React.FC<Post> = ({ author, className, date, excerpt, featuredImage, onClick, slug, title }) => {
    return (
        <Styled.PostItem className={className}>
            <Styled.PostLink to={`/blog/${slug}`} onClick={onClick}>
                {' '}
                {featuredImage && <Styled.FeaturedImage src={featuredImage} />}{' '}
                <Styled.PostContent>
                    {' '}
                    <Styled.PostContentUpper>
                        {' '}
                        <h3>{title}</h3>
                        <Styled.Info>
                            {author && (
                                <Styled.AuthorInfo>
                                    {' '}
                                    {author.avatar && <Avatar avatar={author.avatar} />} <h4>{author.name}</h4>{' '}
                                </Styled.AuthorInfo>
                            )}{' '}
                            {date && <h5>{format(new Date(date), 'MMM do yyyy')}</h5>}{' '}
                        </Styled.Info>{' '}
                    </Styled.PostContentUpper>
                    <p>{excerpt}</p>
                </Styled.PostContent>{' '}
            </Styled.PostLink>
        </Styled.PostItem>
    )
}

export default Post

Enter fullscreen mode Exit fullscreen mode

We might want to use this component in other places on our site so go
ahead and export it from the root of the post folder with another
index.ts file.

// blog/post/index.ts

export { default as Post } from './post'
Enter fullscreen mode Exit fullscreen mode

We’ll need a component to display our yummy posts in, so go ahead and
make a styles folder at the root of components/blog. Just like the post
example, you’ll create a blog.styled.tsx file and an index.ts file
inside the styles folder.

// blog/styles/blog.styled.tsx

import styled from 'styled-components'

export const Blog = styled.div`
  align-items: center;
  background-color: ${({ theme }) => theme.colors.white};
  display: flex;   justify-content: center;
  min-height: 100vh;
  padding: 1em 0 1em 0;`

Enter fullscreen mode Exit fullscreen mode

And don’t forget to export:

// blog/styles/index.ts

export * from './blog.styled'
Enter fullscreen mode Exit fullscreen mode

If our posts are peanut butter inside the sandwich of the blog page then
the blog component is the jelly. It uses a grid component I provided to
hold posts together in a simple but effective manner on the page.

// blog/blog.tsx

import * as React from 'react'
import * as Styled from './styles'
import { MarkdownRemark, MarkdownRemarkFrontmatter } from '../../interfaces'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Grid } from '../grid'
import { Post } from './post'
import { faBlog } from '@fortawesome/free-solid-svg-icons'

interface BlogProps {
    posts: MarkdownRemark[]
}

const Blog: React.FC<BlogProps> = ({ posts }) => {
    const blogItems = React.useMemo(() => {
        const postsWithFrontMatter = posts.filter(({ frontmatter }) => frontmatter)

        if (postsWithFrontMatter.length <= 0) {
            return null
        }

        return postsWithFrontMatter.map(({ frontmatter, excerpt, id }) => (
            <Post key={id} {...(frontmatter as MarkdownRemarkFrontmatter)} excerpt={excerpt} />
        ))
    }, [posts])

    return (
        <Styled.Blog>
            {' '}
            {blogItems ? (
                <Grid items={blogItems} style={{ width: '90%' }} />
            ) : (
                <h2>
                    No blog posts yet but check back soon!&nbsp; <FontAwesomeIcon icon={faBlog} />
                </h2>
            )}{' '}
        </Styled.Blog>
    )
}

export default Blog

Enter fullscreen mode Exit fullscreen mode

And this is the final time I’ll have you export something from another
file I promise. In the index.ts file at the root of the components
folder add this line at the top.

// components/index.ts

export * from './blog'
Enter fullscreen mode Exit fullscreen mode

If you took a look at the demo I gave earlier for this template you’ll
have noticed that the latest post section included a familiar article.
In this tutorial, I won’t go into creating this latest post section on
but I will have you export the Blog and Post components so they can be
used elsewhere.


Putting it all together

Now we’re done with the hard part. We have the pieces needed for
displaying our brilliant posts all that’s left is to create the page to
display them and at least one sample post to try it out. Find the pages
folder at src/pages and add a blog.tsx file. This will be the page that
displays our blog component and posts.

// src/pages/blog.tsx

import * as React from 'react'
import { Blog, Layout, SEO } from '../components'
import { Query } from '../interfaces'
import { graphql } from 'gatsby'

export const query = graphql`
    query {
        allMarkdownRemark {
            totalCount
            edges {
                node {
                    id
                    frontmatter {
                        author {
                            avatar
                            name
                        }
                        slug
                        title
                        date
                        featuredImage
                    }
                    excerpt
                }
            }
        }
    }
`

const BlogPage: React.FC<{ data: { allMarkdownRemark: Query['allMarkdownRemark'] } }> = ({
    data: {
        allMarkdownRemark: { edges }
    }
}) => {
    return (
        <Layout>
            {' '}
            <SEO title="Blog" /> <Blog posts={edges.map(({ node }) => node)} />
        </Layout>
    )
}

export default BlogPage

Enter fullscreen mode Exit fullscreen mode

This page will look for all of our markdown files and pass them along to
the blog component as posts. If you go to
localhost:8001/blog you should see an
empty blog page with a no posts message.

Now is the moment of truth, we need to make a sample post to make sure
this is all working. Go ahead and create a folder in src/content called
posts and inside it create a what-time-is-it.md file. We’ll be using the
lyrics to ‘Peanut Butter Jelly Time’ as a fitting test.

---

author: { avatar: 'bannans.png', name: 'Buckwheat Boyz' }
title: 'What time is it?'
slug: 'what-time-is-it'
date: '2/1/2021'
---

It's peanut butter jelly time!
Peanut butter jelly time!
Peanut butter jelly time!

<!-- endexcerpt -->

Now Where he at?
Where he at?
Where he at?
Where he at?

NowThere he go
There he go
There he go
There he go

## Peanut butter jelly [x4]

Do the Peanut butter jelly
Peanut butter jelly
Peanut butter jelly with a baseball bat

Do the Peanut butter jelly
Peanut butter jelly
Peanut butter jelly with a baseball bat

## Chorus

Now break it down and freeze
Take it down to your knees
Now lean back and squeeze
Now get back up and scream

## Chorus

Now sissy walk
Sissy walk
Sissy walk
Sissy walk

Now sissy walk
Sissy walk
Sissy walk
Sissy walk

## Chorus

Now walk walk walk walk
Stomp stomp stomp stomp
Slide slide slide slide
Back it up one more time

Now walk walk walk walk
Stomp stomp stomp stomp

Peanut butter jelly break it down
Throw the ball up swing that bat

Turn your head back and see where it at
Throw the ball up swing that bat
Turn you head back and see where it at

Palm beachpeanut butter
Dade countyjelly
Orlandopeanut butter
Tallahasse jelly

Hold on hold on hold on hold on
"Hey chip man what time is it?"

"I don't know what time it is ray low"

"It's peanut butter jelly time"

Enter fullscreen mode Exit fullscreen mode

You should see our what-time-is-it blog post appear on the blog page and
clicking it will, in fact, tell you what time it is.


Conclusion

You should now understand the concepts behind querying markdown files
and changing them into HMTL pages. To recap, we added and generated
types for the Remark transformer in Gatsby. Then we made a template to
use for our markdown that converts each file into valid HTML with
styles. We then set up a create pages hook that uses a template to
render our posts. And finally, we made a page with blog and post
components to display those posts for site visitors to enjoy.

I hope you enjoyed this tutorial and learned a few things along the way.
This is my first attempt at creating a Gatsby website template and would
love feedback.

If you got lost or didn’t have the time to follow along you can get the
$5 version of the template at the link I listed at the beginning of
this tutorial. It includes all the code I went over here as
well as a few more features such as the latest post section.

But most importantly, what’s the best kind of peanut butter; crunchy or
smooth? Let the debate ensue in the comments section, thanks!

By John Grisham on February 2,
2021
.

💖 💪 🙅 🚩
squashbugler
John Grisham

Posted on February 26, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related