How to Create Blog Posts From Markdown With Gatsby in 2021

Photo by Freddy G on Unsplash

Markdown and Blog Posts

The modern man’s equivalent to the ’60s nuclear family is a website; and today, building one is easier than ever. With plenty of platforms to choose from, it’s not difficult to find which one suits your needs — but once you’ve found it, there’s still work to do.

Regardless of where your site is hosted or what platform it’s built on, the one thing they all have in common is blog posts. That’s something to think about before moving from another host and changing platforms as well.

Fortunately, Gatsby has your back with their perfect static website builder for blogs using Markdown formatting for those blog posts which means an easy transition from another host and site builder!

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:

Gatsby Template

And you can download it here: https://gum.co/pbj-template

or

Clone the repo:

JohnGrisham/PB-JPlain

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

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

yarn develop

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

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

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}

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.jsplugins: [     `gatsby-transformer-remark`]

Then stop and restart your development process and run:

yarn generate-types

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.tsximport 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;`

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

// templates/styles/index.tsexport * from './article-template.styled'

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

// src/templates/article-template.tsximport * 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   }).Compilerreturn (  <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

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.jsexports.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}/`     })  })}

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.tsximport { 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%;`

Once again export the styles:

// blog/post/styles/index.tsexport * from './post.styled'

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.tsximport * 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

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.tsexport { default as Post } from './post'

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.tsximport 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;`

And don’t forget to export:

// blog/styles/index.tsexport * from './blog.styled'

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.tsximport * 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

As with every other component in the components folder, I’ll export it with an index file to make using groups of components easier in the project.

// components/blog/index.tsexport { default as Blog } from './blog'export { Post } from './post'

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.tsexport * from './blog'

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.tsximport * 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

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 goThere he goThere he goThere he go## Peanut butter jelly [x4]Do the Peanut butter jellyPeanut butter jellyPeanut butter jelly with a baseball batDo the Peanut butter jellyPeanut butter jellyPeanut butter jelly with a baseball bat## ChorusNow break it down and freezeTake it down to your kneesNow lean back and squeezeNow get back up and scream## ChorusNow sissy walkSissy walkSissy walkSissy walkNow sissy walkSissy walkSissy walkSissy walk## ChorusNow walk walk walk walkStomp stomp stomp stompSlide slide slide slideBack it up one more timeNow walk walk walk walkStomp stomp stomp stompPeanut butter jelly break it downThrow the ball up swing that batTurn your head back and see where it atThrow the ball up swing that batTurn you head back and see where it atPalm beachpeanut butterDade countyjellyOrlandopeanut butterTallahasse jellyHold 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"

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!

Powered By Swish