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:
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
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.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! <FontAwesomeIcon icon={faBlog} />
</h2>
)}{' '}
</Styled.Blog>
)
}
export default Blog
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!
By John Grisham on February 2,
2021.
Top comments (2)
I would like to support you but I (obviously) ran out of free article on Medium for this month. Medium's "writer to writer" business plan is annoying at least, so thanks for publishing it elsewhere.
No worries! You can also support me by downloading the paid template if you enjoyed it!