Blog posts from markdown with gatsby

John Grisham

Feb 2nd 2021

Learn how to create blog posts from markdown using Remark to format text into valid HTML pages and style them with Styled Components.

Markdown and Blog Posts

Let’s face it building a website is easier than ever. But because we have so many options on how to create a website things can get a little overwhelming. Regardless of where your website is hosted or on what platform one thing is usually the same; blog posts. And those blog posts are almost always formatted in markdown. You can think of markdown as the peanut butter to go with the jelly that is your blog. Despite the name, Markdown is a type of markup language. It’s one of the most lightweight which makes it great for text.

That’s where Gatsby comes in. Gatsby is a React framework for building static websites. You can use Gatsby to create fast and great looking landing pages or blogs. Going from a CMS to Gatsby is even 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 pages, 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 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.js
type 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.js
plugins: [
  `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.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;`

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

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

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

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

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%;
  `

Once again export the styles:

  // blog/post/styles/index.ts
  export * 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.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

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'

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

And don’t forget to export:

  // blog/styles/index.ts
  export * 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.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

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.ts
  export { 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.ts
  export * 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.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

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"

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!