Building a Blog From Markdown in Gatsby — Complete Guide
Learn how to build a complete blog in Gatsby using Markdown files, create pages, add tags, and implement pagination for a production-ready blog.
In this lesson, you'll combine source plugins, transformer plugins, and programmatic page creation to build a full blog from Markdown.
What You'll Learn
How to structure Markdown files, configure transformers, create post pages programmatically, add tag archives, and implement pagination.
Why It Matters
A Markdown blog is the most common Gatsby use case. Understanding this workflow gives you a foundation for any content-driven Gatsby site.
flowchart TD
A[Markdown Files] --> B[Source Plugin]
B --> C[Transformer Plugin]
C --> D[gatsby-node.js]
D --> E[Post Pages]
D --> F[Tag Pages]
D --> G[Pagination]
style A fill:#639,color:#fff
style D fill:#4a148c,color:#fff
Markdown File Structure
---
title: "Getting Started with Gatsby"
date: 2026-06-28
tags: ["gatsby", "react", "tutorial"]
featured: true
author: "DodaTech"
image: ./hero.jpg
---
Welcome to this tutorial on Gatsby. In this post, we'll explore...
## What is Gatsby?
Gatsby is a React-based static site generator...
### Why Static Sites?
Static sites are fast, secure, and scalable...
Organize files:
content/blog/
├── getting-started-gatsby/index.md
├── gatsby-vs-nextjs/index.md
├── gatsby-plugins-guide/index.md
└── deploying-gatsby/index.md
Each post in its own directory with an index.md file and associated assets.
Slug Creation in gatsby-Node.js
Generate slugs from file paths:
// gatsby-node.js
const path = require('path');
const { createFilePath } = require('gatsby-source-filesystem');
exports.onCreateNode = ({ node, getNode, actions }) => {
const { createNodeField } = actions;
if (node.internal.type === 'MarkdownRemark') {
const slug = createFilePath({ node, getNode, basePath: 'content/blog' });
createNodeField({ node, name: 'slug', value: slug });
}
};
Output: A file at content/blog/getting-started-gatsby/index.md gets a slug /getting-started-gatsby/.
Creating Post Pages
// gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
const result = await graphql(`
query {
allMarkdownRemark(sort: { frontmatter: { date: DESC } }) {
nodes {
fields { slug }
frontmatter { tags }
}
}
}
`);
const posts = result.data.allMarkdownRemark.nodes;
// Create individual post pages
posts.forEach((post, index) => {
const prev = index < posts.length - 1 ? posts[index + 1] : null;
const next = index > 0 ? posts[index - 1] : null;
createPage({
path: post.fields.slug,
component: path.resolve('./src/templates/blog-post.js'),
context: {
slug: post.fields.slug,
prevSlug: prev?.fields.slug || null,
nextSlug: next?.fields.slug || null
}
});
});
};
Output: Each Markdown file becomes a blog post with prev/next navigation passed through context.
Post Template with Navigation
// src/templates/blog-post.js
import { graphql, Link } from 'gatsby';
import React from 'react';
export default function BlogPost({ data, pageContext }) {
const post = data.markdownRemark;
return (
<article>
<h1>{post.frontmatter.title}</h1>
<p>{post.frontmatter.date} · {post.timeToRead} min read</p>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
<nav>
{pageContext.prevSlug && (
<Link to={pageContext.prevSlug}>← Previous Post</Link>
)}
{pageContext.nextSlug && (
<Link to={pageContext.nextSlug}>Next Post →</Link>
)}
</nav>
</article>
);
}
export const query = graphql`
query BlogPostBySlug($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
timeToRead
frontmatter { title date(formatString: "MMMM D, YYYY") }
}
}
`;
Output: Each post displays with navigation to adjacent posts.
Tag Pages
Create archive pages for each tag:
// gatsby-node.js (add to createPages)
const tags = new Set();
posts.forEach(post => {
post.frontmatter.tags?.forEach(tag => tags.add(tag));
});
tags.forEach(tag => {
createPage({
path: `/tags/${tag}/`,
component: path.resolve('./src/templates/tag-page.js'),
context: { tag }
});
});
// src/templates/tag-page.js
export const query = graphql`
query TagPage($tag: String!) {
allMarkdownRemark(
filter: { frontmatter: { tags: { in: [$tag] } } }
sort: { frontmatter: { date: DESC } }
) {
nodes {
frontmatter { title date }
fields { slug }
excerpt
}
}
}
`;
Output: Each tag gets a page at /tags/gatsby/ listing all posts with that tag.
Pagination
Create paginated archive pages:
exports.createPages = async ({ graphql, actions }) => {
const postsPerPage = 6;
const result = await graphql(`query { allMarkdownRemark { totalCount } }`);
const totalPages = Math.ceil(result.data.allMarkdownRemark.totalCount / postsPerPage);
Array.from({ length: totalPages }).forEach((_, i) => {
createPage({
path: i === 0 ? '/blog' : `/blog/${i + 1}`,
component: path.resolve('./src/templates/blog-list.js'),
context: { limit: postsPerPage, skip: i * postsPerPage, currentPage: i + 1, totalPages }
});
});
};
Common Mistakes
- Not creating slugs with
createFilePath: The helper generates clean slugs from file paths. Manual slug creation is error-prone. - Forgetting to sort posts: Without
sort: { frontmatter: { date: DESC } }, posts appear in arbitrary order. - Not passing prev/next navigation context: Navigation between posts requires passing adjacent post slugs through
createPagecontext. - Using
dangerouslySetInnerHTMLwithout sanitization: Markdown from trusted sources is safe. For user-generated content, sanitize first. - Missing
gatsby-remark-imagesfor inline images: Images in Markdown aren't optimized without this plugin.
Practice Questions
How do you generate slugs from file paths? Answer: Use the
createFilePathhelper fromgatsby-source-filesystemin theonCreateNodeAPI.How do you create tag archive pages? Answer: Collect all unique tags from posts, then call
createPagefor each tag with a template that filters posts by that tag.What is the purpose of prev/next navigation context? Answer: It passes the slug of the previous and next post to the template, enabling navigation between adjacent posts.
How do you paginate the blog listing? Answer: Calculate total pages from post count, loop with
Array.from, and passskipandlimitto the page query.
Challenge
Extend the blog with: image galleries in posts (using gatsby-remark-images), related posts based on shared tags, an RSS feed with gatsby-plugin-feed, and reading progress indicator.
Mini Project
Build a complete blog with: 20+ Markdown posts, 5+ tag categories, paginated archive (6 per page), prev/next navigation, tag pages, featured posts section, and an RSS feed.
FAQ
What's Next
Learn about Gatsby File System Route API for a simpler approach to creating pages without manual gatsby-node.js configuration.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro