← Back to Articles

Building a Personal Blog with Next.js and Markdown

Code

I've always wanted to have a personal blog where I could share my thoughts and experiences as a developer. After trying several platforms, I decided to build my own using Next.js and Markdown. It gives me complete control over the design and functionality, and I can write posts in a format I'm comfortable with.

Why Next.js and Markdown?

When I started thinking about building a blog, I wanted something that was:

  • Easy to write in
  • Fast and performant
  • SEO-friendly
  • Customizable

Markdown ticked all the boxes for writing—it's simple, readable, and I can focus on content rather than formatting. Next.js provided the framework I needed for performance and SEO.

Setting up the project structure

I started with a basic Next.js project and organized it like this:

my-blog/
├── app/
│   ├── blog/
│   │   ├── [slug]/
│   │   │   └── page.tsx
│   │   └── page.tsx
│   └── layout.tsx
├── content/
│   └── blog/
│       ├── post-1.md
│       ├── post-2.md
│       └── ...
├── lib/
│   └── markdown.ts
└── components/
    └── BlogPost.tsx

Processing Markdown files

The key to making this work is processing Markdown files into HTML. I used a few libraries:

  • gray-matter for parsing frontmatter
  • remark and rehype for processing Markdown
  • remark-html for converting to HTML
  • remark-prism for syntax highlighting
// lib/markdown.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
import prism from 'remark-prism';

export async function getPostData(slug: string) {
  const fullPath = path.join(process.cwd(), 'content/blog', `${slug}.md`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  const matterResult = matter(fileContents);

  const processedContent = await remark()
    .use(prism)
    .use(html)
    .process(matterResult.content);

  return {
    slug,
    content: processedContent.toString(),
    ...matterResult.data,
  };
}

Frontmatter for metadata

Each Markdown file starts with frontmatter that contains metadata:

---
title: "My First Blog Post"
date: "2025-03-05"
excerpt: "This is my first blog post about..."
tags: ["nextjs", "markdown", "blog"]
---

# My First Blog Post

This is the content of my blog post...

Dynamic routing for blog posts

Next.js App Router makes dynamic routing straightforward:

// app/blog/[slug]/page.tsx
import { getPostData, getAllPostSlugs } from '@/lib/markdown';

export async function generateStaticParams() {
  const slugs = getAllPostSlugs();
  return slugs.map((slug) => ({ slug }));
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const postData = await getPostData(params.slug);

  return (
    <article>
      <h1>{postData.title}</h1>
      <p className="text-gray-600">{postData.date}</p>
      <div dangerouslySetInnerHTML={{ __html: postData.content }} />
    </article>
  );
}

SEO optimization

SEO is crucial for a blog. I made sure each post has proper metadata:

// app/blog/[slug]/page.tsx (continued)
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const postData = await getPostData(params.slug);

  return {
    title: postData.title,
    description: postData.excerpt,
    openGraph: {
      title: postData.title,
      description: postData.excerpt,
      type: 'article',
      publishedTime: postData.date,
    },
  };
}

Adding syntax highlighting

Code blocks look much better with syntax highlighting. I used Prism.js:

// lib/markdown.ts (updated)
import prism from 'remark-prism';

const processedContent = await remark()
  .use(prism, { plugins: ['line-numbers'] })
  .use(html)
  .process(matterResult.content);

Styling the blog posts

I wanted a clean, readable design. I used Tailwind CSS with some custom components:

// components/BlogPost.tsx
export function BlogPost({ postData }) {
  return (
    <article className="max-w-3xl mx-auto">
      <header className="mb-8">
        <h1 className="text-4xl font-bold mb-4">{postData.title}</h1>
        <p className="text-gray-600 text-lg">{postData.excerpt}</p>
        <div className="flex items-center gap-4 mt-4 text-sm text-gray-500">
          <time>{formatDate(postData.date)}</time>
          <div className="flex gap-2">
            {postData.tags.map(tag => (
              <span key={tag} className="bg-gray-100 px-2 py-1 rounded">
                {tag}
              </span>
            ))}
          </div>
        </div>
      </header>

      <div
        className="prose prose-lg max-w-none"
        dangerouslySetInnerHTML={{ __html: postData.content }}
      />
    </article>
  );
}

Adding a blog index page

The blog index shows all posts:

// app/blog/page.tsx
import { getAllPosts } from '@/lib/markdown';
import Link from 'next/link';

export default async function BlogIndex() {
  const posts = await getAllPosts();

  return (
    <div className="max-w-4xl mx-auto">
      <h1 className="text-4xl font-bold mb-8">Blog</h1>

      <div className="space-y-8">
        {posts.map((post) => (
          <article key={post.slug} className="border-b pb-8">
            <h2 className="text-2xl font-bold mb-2">
              <Link href={`/blog/${post.slug}`} className="hover:text-blue-600">
                {post.title}
              </Link>
            </h2>
            <p className="text-gray-600 mb-2">{post.excerpt}</p>
            <div className="flex items-center gap-4 text-sm text-gray-500">
              <time>{formatDate(post.date)}</time>
              <div className="flex gap-2">
                {post.tags.map(tag => (
                  <span key={tag} className="bg-gray-100 px-2 py-1 rounded text-xs">
                    {tag}
                  </span>
                ))}
              </div>
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

Performance considerations

To keep the blog fast, I implemented a few optimizations:

  • Static generation for all posts
  • Image optimization with Next.js Image component
  • Lazy loading for long blog posts
  • Minimal JavaScript bundle

Deployment and hosting

I deployed the blog on Vercel, which works perfectly with Next.js. The build process automatically generates all the static pages, and the blog loads quickly.

My experience

Building my own blog has been incredibly rewarding. I have complete control over the design and functionality, and writing in Markdown feels natural. The performance is excellent, and SEO works great. If you're thinking about starting a blog, I highly recommend building it yourself with Next.js and Markdown.

About the author

Rafael De Paz

Full Stack Developer

Passionate full-stack developer specializing in building high-quality web applications and responsive sites. Expert in robust data handling, leveraging modern frameworks, cloud technologies, and AI tools to deliver scalable, high-performance solutions that drive user engagement and business growth. I harness AI technologies to accelerate development, testing, and debugging workflows.

Tags:

Share:

Building a Personal Blog with Next.js and Markdown - Rafael De Paz