Blog Platform with Next.js, Markdown, and Dynamic Routing

Develop a Next.js blogging system with Markdown posts, dynamic routes, and fast static pages

Time to implement the project: ~ 20-32 hours

  • Next.js
  • Dynamic Routing
  • Static Site Generation
  • Markdown Processing
  • Content Rendering
  • Chakra UI
  • SEO-Friendly Pages

In this intermediate Next.js project, you will build a blogging platform that loads articles from Markdown files and renders them as structured web pages. Each article should be accessible through a unique URL generated using Next.js dynamic routing. The system must read content files, convert Markdown into HTML, and display formatted posts with headings, code blocks, images, and metadata.

The platform should also include a homepage that lists blog posts with previews and navigation links. Next.js static generation should be used so pages load quickly and remain search-engine friendly. Chakra UI will help you design consistent typography, responsive article layouts, and readable content sections without requiring custom CSS architecture.

Project Purpose and Key Learning Areas

This project demonstrates how Next.js can power a modern content platform while keeping the architecture simple and efficient. Instead of storing content in a database, Markdown files act as the data source. This approach is widely used for documentation sites, technical blogs, and static content platforms.

By implementing this system, you will learn how dynamic routes map content to pages, how build-time data generation works, and how content rendering fits into the Next.js lifecycle. The result is a lightweight publishing platform that still reflects professional development patterns.

Prerequisites Before Building This Platform

Since this is an intermediate project, you should already understand the basics of React and have experience running a Next.js development environment. You should also be comfortable reading file content and transforming it before rendering.

  • Understanding of React components and JSX structure
  • Basic knowledge of Next.js page system and routing
  • Familiarity with Markdown syntax
  • Experience working with JavaScript arrays and objects
  • Ability to organize reusable UI components
  • Basic knowledge of responsive layouts using Chakra UI

Functional Requirements for the Blog Platform

The platform should behave like a real publishing website. Visitors must be able to browse posts, open individual articles, and navigate between entries smoothly. These requirements emphasize predictable routing, content formatting, and performance rather than complicated backend logic.

Requirement Explanation
Markdown-based blog posts Articles should be stored as Markdown files that are parsed and rendered dynamically.
Dynamic routes for posts Each article must be generated through Next.js dynamic routing based on file metadata.
Homepage with article previews The homepage should list posts with title, description, and navigation links.
Static page generation Using Next.js static generation ensures fast page loading and SEO-friendly output.
Post metadata support Articles should include information such as title, publication date, and author.
Readable article layout Chakra UI typography components should make long-form content comfortable to read.
Responsive navigation Users should be able to browse posts easily on both desktop and mobile devices.
Organized content structure Posts and components must be arranged logically within the Next.js project.

Implementation Tips for a Scalable Blog Architecture

Start by defining a clear folder structure for content and pages. Markdown files should live in a dedicated directory, while page components should focus only on rendering. Use Next.js data-fetching methods such as getStaticProps and getStaticPaths to transform content into pages at build time. Chakra UI should define the layout grid and article typography so all pages remain visually consistent. Separating content, layout, and routing logic keeps your Next.js project maintainable.

  • Store Markdown files in a dedicated content folder
  • Create reusable components for post previews
  • Extract metadata using frontmatter parsing
  • Keep article layout separate from page logic
  • Use Chakra UI containers for consistent content width
  • Ensure code blocks and images render correctly in Markdown
  • Sort blog posts by date for chronological display

Common Mistakes When Building a Blog Platform with Next.js

1. Hardcoding blog posts instead of creating a real content structure

A blog platform should not be built like a normal landing page where every post preview is written manually inside JSX. This may work for two demo posts, but it breaks the purpose of the project. A real Markdown blog needs a repeatable content structure: one file per article, metadata for listings, and reusable logic that can read posts without rewriting page components every time.

Problematic approach:


          export default function BlogPage() {
            return (
              <main>
                <article>
                  <h2>How to Learn React</h2>
                  <p>A short article about learning React.</p>
                  <a href="/blog/how-to-learn-react">Read more</a>
                </article>

                <article>
                  <h2>Next.js Routing Basics</h2>
                  <p>A short article about routing.</p>
                  <a href="/blog/nextjs-routing-basics">Read more</a>
                </article>
              </main>
            );
          }

This creates a static page that only looks like a blog. The content is not portable, the post list is not generated from files, and adding new articles requires editing the component directly.

Better content file:


          ---
          title: "Next.js Routing Basics"
          description: "Learn how dynamic routes work in a Markdown blog."
          date: "2026-06-17"
          author: "ReadyToDev"
          slug: "nextjs-routing-basics"
          ---

          # Next.js Routing Basics

          Dynamic routes allow every Markdown file to become a separate blog post page.

Better post-loading helper:


          import fs from "fs";
          import path from "path";
          import matter from "gray-matter";

          const postsDirectory = path.join(process.cwd(), "content/posts");

          export function getAllPosts() {
            const fileNames = fs.readdirSync(postsDirectory);

            return fileNames.map((fileName) => {
              const slug = fileName.replace(/\.md$/, "");
              const fullPath = path.join(postsDirectory, fileName);
              const fileContent = fs.readFileSync(fullPath, "utf8");
              const { data } = matter(fileContent);

              return {
                slug,
                title: data.title,
                description: data.description,
                date: data.date,
                author: data.author
              };
            });
          }

Pay attention to: A blog platform should be content-driven. Store articles in a dedicated content folder and generate post previews from metadata instead of manually writing every card.

2. Building dynamic routes without validating the slug

Dynamic routes are one of the key parts of this project, but they must be handled carefully. A common mistake is taking the slug from the URL and immediately using it to read a file. This can lead to broken pages, confusing errors, or unsafe file-path behavior if the slug is not validated.

Problematic code:


          export async function getPostBySlug(slug) {
            const filePath = `content/posts/${slug}.md`;
            const fileContent = fs.readFileSync(filePath, "utf8");

            return matter(fileContent);
          }

This code assumes the slug is always valid and the file always exists. If someone opens /blog/missing-post, the page can crash instead of showing a proper 404.

Better approach:


          export function getPostSlugs() {
            return fs
              .readdirSync(postsDirectory)
              .filter((fileName) => fileName.endsWith(".md"))
              .map((fileName) => fileName.replace(/\.md$/, ""));
          }

          export function getPostBySlug(slug) {
            const availableSlugs = getPostSlugs();

            if (!availableSlugs.includes(slug)) {
              return null;
            }

            const fullPath = path.join(postsDirectory, `${slug}.md`);
            const fileContent = fs.readFileSync(fullPath, "utf8");
            const { data, content } = matter(fileContent);

            return {
              slug,
              metadata: data,
              content
            };
          }

Pages Router example:


          export async function getStaticPaths() {
            const slugs = getPostSlugs();

            return {
              paths: slugs.map((slug) => ({
                params: { slug }
              })),
              fallback: false
            };
          }

          export async function getStaticProps({ params }) {
            const post = getPostBySlug(params.slug);

            if (!post) {
              return {
                notFound: true
              };
            }

            return {
              props: { post }
            };
          }

Pay attention to: Generate valid slugs from the content folder and only render posts that actually exist. Missing or invalid posts should result in a clean 404, not a server error.

3. Rendering Markdown without controlling components and unsafe content

Markdown rendering is not just converting text into HTML. Blog posts may contain headings, links, images, lists, code blocks, and sometimes raw HTML. If you render Markdown without controlling how elements appear, the article layout can become inconsistent. If you allow raw HTML carelessly, you can also introduce avoidable security risks.

Problematic approach:


          import ReactMarkdown from "react-markdown";

          export default function PostContent({ content }) {
            return (
              <ReactMarkdown>
                {content}
              </ReactMarkdown>
            );
          }

This may render basic Markdown, but it gives you little control over article typography, external links, images, and code blocks.

Better approach:


          import ReactMarkdown from "react-markdown";

          export function PostContent({ content }) {
            return (
              <ReactMarkdown
                components={{
                  h2: ({ children }) => (
                    <h2 className="article-heading">{children}</h2>
                  ),
                  p: ({ children }) => (
                    <p className="article-paragraph">{children}</p>
                  ),
                  a: ({ href, children }) => (
                    <a
                      href={href}
                      target={href?.startsWith("http") ? "_blank" : undefined}
                      rel={href?.startsWith("http") ? "noreferrer" : undefined}
                    >
                      {children}
                    </a>
                  ),
                  img: ({ src, alt }) => (
                    <img
                      src={src}
                      alt={alt || ""}
                      className="article-image"
                      loading="lazy"
                    />
                  )
                }}
              >
                {content}
              </ReactMarkdown>
            );
          }

Code block example:


          const markdownComponents = {
            code({ inline, className, children }) {
              if (inline) {
                return <code className="inline-code">{children}</code>;
              }

              return (
                <pre className="code-block">
                  <code className={className}>{children}</code>
                </pre>
              );
            }
          };

Pay attention to: Markdown needs a design system too. Define how headings, paragraphs, links, images, and code blocks should render so every post feels consistent and readable.

4. Sorting blog posts by formatted date strings

A blog homepage usually shows the newest articles first. A common mistake is sorting posts by the date after it has already been formatted for display. Human-readable dates like June 17, 2026 or 17/06/2026 are good for readers, but not ideal as the main sorting source.

Problematic code:


          const posts = [
            {
              title: "Post A",
              date: "June 17, 2026"
            },
            {
              title: "Post B",
              date: "May 02, 2026"
            }
          ];

          posts.sort((a, b) => {
            return b.date.localeCompare(a.date);
          });

This can produce incorrect ordering because the strings are sorted alphabetically, not chronologically.

Better metadata:


          ---
          title: "How Static Generation Works"
          description: "A practical introduction to static pages in Next.js."
          date: "2026-06-17"
          ---

Better sorting logic:


          export function sortPostsByDate(posts) {
            return [...posts].sort((a, b) => {
              return new Date(b.date).getTime() - new Date(a.date).getTime();
            });
          }

          export function formatPostDate(date) {
            return new Intl.DateTimeFormat("en", {
              year: "numeric",
              month: "long",
              day: "numeric"
            }).format(new Date(date));
          }

Homepage usage:


          const posts = sortPostsByDate(getAllPosts());

          return (
            <section>
              {posts.map((post) => (
                <PostPreview
                  key={post.slug}
                  title={post.title}
                  date={formatPostDate(post.date)}
                  href={`/blog/${post.slug}`}
                />
              ))}
            </section>
          );

Pay attention to: Store dates in a sortable format such as YYYY-MM-DD. Format dates only when rendering them to users.

5. Forgetting SEO metadata for generated article pages

A blog platform is content-driven, so each article should have its own title, description, canonical URL, and social preview data. Beginners often create dynamic pages that render correctly in the browser but still use a generic site title when shared or indexed. This makes every post look the same outside the website.

Problematic metadata:


          export const metadata = {
            title: "My Blog",
            description: "A blog built with Next.js"
          };

This is acceptable for the homepage, but not enough for individual posts. Each article should generate metadata from its own frontmatter.

Better App Router example:


          export async function generateMetadata({ params }) {
            const post = getPostBySlug(params.slug);

            if (!post) {
              return {
                title: "Post not found"
              };
            }

            return {
              title: `${post.metadata.title} | ReadyToDev Blog`,
              description: post.metadata.description,
              openGraph: {
                title: post.metadata.title,
                description: post.metadata.description,
                type: "article",
                publishedTime: post.metadata.date,
                authors: [post.metadata.author || "ReadyToDev"],
                images: [
                  {
                    url: post.metadata.image || "/blog/default-og.png",
                    width: 1200,
                    height: 630,
                    alt: post.metadata.title
                  }
                ]
              }
            };
          }

Frontmatter with SEO fields:


          ---
          title: "How to Build a Markdown Blog with Next.js"
          description: "Learn how Markdown files become static blog pages."
          date: "2026-06-17"
          author: "ReadyToDev"
          image: "/blog/markdown-nextjs-og.png"
          ---

Pay attention to: Treat metadata as part of the post content model. A blog is not only read on your site; it is also previewed in search results, social cards, messengers, and bookmarks.

By completing this project, you'll gain hands-on experience building a content-driven website with Next.js. You will practice dynamic routing, static generation, and Markdown content processing while creating a fully functional blog platform. The project strengthens your understanding of Next.js architecture and demonstrates how lightweight content systems can power real developer websites and technical blogs.

Reference Implementations Worth Studying

Beginner-friendly Markdown blog reference:
andresz74 - Next.js Markdown Blog

This is the most beginner-friendly reference from the list because it keeps the idea focused: a blog that uses Next.js and react-markdown. It is useful for understanding the simplest version of the project, where Markdown content is the main source and the frontend is responsible for turning that content into readable pages.

Pay particular attention to:

  • How Markdown files are used as the content source instead of a database or CMS.
  • How react-markdown turns article text into renderable React output.
  • How a small static blog can stay portable and easy to deploy.
  • How static export settings can affect deployment to static hosting platforms.
  • What you would improve with stronger frontmatter validation, post sorting, and SEO metadata.

Use this repository as the simplest practical baseline. It is especially helpful if you want to understand the core Markdown-to-page flow before adding richer typography, tags, search, author pages, or MDX components.

More structured file-based blog reference:
MoazIrfan - Next Blog App

This implementation is useful because it focuses on creating and managing blog posts through Markdown files, with routes generated automatically based on file names. That is very close to the core learning goal of this project: the file system becomes the content layer, and Next.js routes turn those files into navigable article pages.

When studying the code, focus on:

  • How Markdown file names can become URL slugs for individual posts.
  • How automatic route generation reduces manual page creation.
  • How the post list should connect to the same content source as the post detail pages.
  • How you could extend the project with categories, tags, featured posts, and better post previews.

What makes this reference valuable is the route-generation idea. Study it to understand how a blog can scale from a few files into a maintainable publishing system without manually creating every route.

Alternative App Router and MDX implementation:
emanuelefavero - Next Markdown Blog App Router

This repository is a strong alternative direction because it uses the Next.js App Router and MDX. Compared with plain Markdown, MDX lets articles include richer React-style content when needed. The project is also designed as a static site and includes guidance for testing static generation and serving the exported output.

While reviewing this project, examine:

  • How the App Router changes the structure compared with older Pages Router examples.
  • How MDX can make articles more flexible than plain Markdown posts.
  • How static generation and static export are tested before deployment.
  • How content rendering can stay simple while still supporting richer article components.

Use this implementation as the advanced comparison point. For the main project, plain Markdown is enough, but studying an App Router + MDX version helps you understand where a Next.js blog can evolve as the content becomes more interactive and technical.

© 2026 ReadyToDev.Pro. All rights reserved.

Methodology

Privacy Policy

Terms & Conditions