Back to Blog
Web Development
9 min read

How I Implemented the Blog Section in My Portfolio Website Using MDX

July 26, 2025
Next.jsMDXBlogTypeScriptPortfolioTutorial

How I Implemented the Blog Section in My Portfolio Website Using MDX

Building a blog for your portfolio website doesn't have to be complicated. In this comprehensive guide, I'll walk you through exactly how I implemented a file-based blog system using Next.js, TypeScript, and markdown files. This approach gives you complete control over your content while maintaining excellent performance and SEO.

Why Choose a File-Based Blog System?

Before diving into the implementation, let me explain why I chose this approach:

  • Content Ownership: Your blog posts are markdown files in your repository
  • Version Control: Track changes to your posts with Git
  • Performance: Static generation means lightning-fast loading
  • Flexibility: Complete control over styling and functionality
  • No Database: Simpler deployment and maintenance

Project Structure Overview

Here's how I organized the blog system in my Next.js portfolio:

src/
├── app/
│   └── blog/
│       ├── page.tsx              # Blog listing page
│       └── [slug]/
│           └── page.tsx          # Individual blog post page
├── components/
│   └── MarkdownRenderer.tsx      # Renders markdown content
├── content/
│   └── posts/                    # All blog posts live here
│       ├── my-first-post.md
│       └── another-post.md
└── lib/
    └── blog/
        ├── types.ts              # TypeScript interfaces
        ├── markdown.ts           # File parsing utilities
        └── utils.ts              # Blog helper functions

Step 1: Installing Required Packages

First, let's install the necessary dependencies for our blog system:

npm install gray-matter next-mdx-remote
npm install -D @types/mdx

Here's what each package does:

  • gray-matter: Parses frontmatter from markdown files
  • next-mdx-remote: Renders MDX content in Next.js (optional but powerful)
  • @types/mdx: TypeScript support for MDX files

Step 2: Setting Up TypeScript Interfaces

I started by defining the data structures for blog posts:

// src/lib/blog/types.ts

export interface BlogPost {
  slug: string
  title: string
  date: string
  excerpt: string
  tags: string[]
  readTime: string
  category: string
  content?: string
  author?: {
    name: string
    bio: string
  }
}

export interface BlogPostMeta {
  slug: string
  title: string
  date: string
  excerpt: string
  tags: string[]
  readTime: string
  category: string
}

Step 3: Creating the Markdown Parser

The heart of the system is the markdown parser that reads files and extracts frontmatter. This component handles the heavy lifting of converting your markdown files into structured data that your application can work with.

The parser performs several critical functions:

  • File Discovery: Scans the posts directory for markdown files
  • Frontmatter Extraction: Uses gray-matter to separate metadata from content
  • Slug Generation: Creates URL-friendly slugs from filenames
  • Read Time Calculation: Estimates reading time based on word count
  • Content Validation: Ensures required fields are present

Here's the core parsing logic:

// src/lib/blog/markdown.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const POSTS_DIRECTORY = path.join(process.cwd(), 'src/content/posts')

function parseMarkdownFile(filename: string): BlogPost | null {
  const fullPath = path.join(POSTS_DIRECTORY, filename)
  const fileContents = fs.readFileSync(fullPath, 'utf8')
  const { data, content } = matter(fileContents)
  
  // Generate slug and calculate read time
  const slug = data.slug || filename.replace(/\.(md|mdx)$/, '')
  const wordCount = content.split(/\s+/).length
  const readTime = Math.ceil(wordCount / 200)
  
  // Validate and return structured data
  if (!data.title || !data.date) return null
  
  return {
    slug, title: data.title, date: data.date,
    excerpt: data.excerpt || extractExcerpt(content),
    tags: data.tags || [], readTime: `${readTime} min read`,
    category: data.category || 'Uncategorized',
    content, author: data.author
  }
}

The parser also includes an extractExcerpt function that automatically generates post previews by cleaning markdown syntax and extracting the first meaningful paragraph. This ensures every post has a preview even if you forget to add an excerpt in the frontmatter.

Step 4: Creating Utility Functions

The utility functions serve as a clean interface between your components and the markdown parsing logic. This abstraction layer makes it easy to change the underlying implementation without affecting your UI components.

I created wrapper functions that handle the most common blog operations:

// src/lib/blog/utils.ts
import { getAllMarkdownPostsMeta, getMarkdownPostBySlug } from './markdown'

export function getAllPosts(): BlogPostMeta[] {
  return getAllMarkdownPostsMeta()
}

export function getPostBySlug(slug: string): BlogPost | null {
  return getMarkdownPostBySlug(slug)
}

export function getFeaturedPosts(): BlogPostMeta[] {
  return getFeaturedMarkdownPosts(3)
}

These utility functions provide several benefits:

  • Simplified API: Components don't need to know about the markdown parsing details
  • Consistent Naming: Clean, predictable function names throughout the app
  • Easy Testing: Functions can be easily mocked for unit tests
  • Future-Proofing: Easy to switch from file-based to database-based content later

Step 5: Building the Markdown Renderer

The markdown renderer is the component that transforms your raw markdown text into beautifully formatted HTML. This is where the magic happens - converting simple text syntax into rich, styled content.

// src/components/MarkdownRenderer.tsx
export default function MarkdownRenderer({ content }: { content: string }) {
  const renderMarkdown = (text: string) => {
    return text
      .replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) => 
        `<pre><code class="language-${lang}">${code.trim()}</code></pre>`
      )
      .replace(/`([^`]+)`/g, '<code>$1</code>')
      .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
      .replace(/^## (.*$)/gim, '<h2>$1</h2>')
  }

  return (
    <div className="prose prose-lg max-w-none" 
         dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }} />
  )
}

The renderer handles the most common markdown elements:

  • Code Blocks: Converts triple backticks to properly styled <pre><code> elements
  • Inline Code: Transforms backtick-wrapped text into styled code spans
  • Bold Text: Converts <BOLD>text</BOLD> to <strong> elements
  • Headers: Transforms ## syntax into proper heading elements

The prose classes from Tailwind CSS provide excellent typography without additional styling work, making your blog posts look professional instantly.

Step 6: Creating the Blog Pages

With all the backend logic in place, it's time to create the user-facing pages. The blog system needs two main pages: a listing page showing all posts and individual post pages.

Blog Listing Page

The listing page displays all your blog posts in an attractive grid layout:

// src/app/blog/page.tsx
import { getAllPosts } from '@/lib/blog/utils'

export default function BlogPage() {
  const posts = getAllPosts()

  return (
    <div className="max-w-6xl mx-auto px-8 py-20">
      <h1 className="text-4xl font-bold mb-12">Blog</h1>
      <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
        {posts.map((post) => (
          <Link key={post.slug} href={`/blog/${post.slug}`}>
            <article className="border rounded-lg p-6 hover:shadow-lg">
              <h2 className="text-xl font-semibold mb-2">{post.title}</h2>
              <p className="text-gray-600 mb-4">{post.excerpt}</p>
              <div className="text-sm text-gray-500">
                {post.date} • {post.readTime}
              </div>
            </article>
          </Link>
        ))}
      </div>
    </div>
  )
}

This page fetches all posts using the utility function and displays them in a responsive grid. Each post card shows the title, excerpt, date, and read time.

Individual Blog Post Page

The individual post page displays a single blog post with proper Next.js 15 async params handling:

// src/app/blog/[slug]/page.tsx
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = getPostBySlug(slug)
  
  if (!post) {
    notFound()
  }

  return (
    <article className="max-w-4xl mx-auto px-8 py-20">
      <header className="mb-8">
        <h1 className="text-4xl font-bold">{post.title}</h1>
        <div className="text-gray-600 mt-2">
          {post.date} • {post.readTime}
        </div>
      </header>
      <MarkdownRenderer content={post.content} />
    </article>
  )
}

This page automatically generates static pages for all blog posts at build time using generateStaticParams. The async params handling ensures compatibility with Next.js 15.

Step 7: Writing Your First Blog Post

The beauty of this system is its simplicity. To create a new blog post, simply create a markdown file in the src/content/posts/ directory:

---
title: "My First Blog Post"
date: "2025-01-26"
excerpt: "Welcome to my blog! Here's what I learned building this system."
tags: ["First Post", "Web Development"]
category: "Tutorial"
---

# Welcome to My Blog

This is my first blog post using the new markdown-based system I built. 

## What I Learned

Building this blog system taught me several important concepts:
- File-based content management
- TypeScript type safety
- Next.js server-side rendering
- Responsive design principles

The system automatically handles:
- **Frontmatter parsing** for metadata
- **Slug generation** from filenames  
- **Read time calculation** based on word count
- **Responsive styling** with Tailwind CSS

Once you save this file, it automatically appears on your blog listing page. The system handles everything else - parsing the frontmatter, generating the URL slug, calculating read time, and rendering the content beautifully.

Key Features of the System

The blog system I built includes several powerful features that make content creation and management seamless:

1. **Frontmatter Support**

Every blog post can include metadata at the top:

---
title: "Post Title"          # Required
date: "2025-01-26"          # Required  
excerpt: "Post summary"      # Auto-generated if not provided
tags: ["tag1", "tag2"]       # Optional
category: "Category"         # Optional
---

2. **Automatic Features**

  • Slug Generation: URLs created from filenames automatically
  • Read Time Calculation: Estimated based on word count (200 words/minute)
  • Excerpt Extraction: Auto-generated from first paragraph if not provided
  • Responsive Design: Looks great on all devices

3. **Developer Experience**

  • Type Safety: Full TypeScript support with custom interfaces
  • Hot Reloading: Changes appear instantly during development
  • Error Handling: Graceful 404 pages for missing posts
  • Clean URLs: SEO-friendly post URLs

Deployment & Performance

The system generates static pages at build time, resulting in:

  • Fast Loading: No database queries needed
  • SEO Optimization: Proper meta tags and structured data
  • Cost Efficiency: Host anywhere that serves static files
  • Version Control: All content tracked with Git

Conclusion

This file-based blog system provides the perfect balance of simplicity and power. Writing new posts is as easy as creating a markdown file, while the system handles all the complex rendering and optimization automatically.

The modular architecture makes it easy to extend with new features like search, categories, or commenting systems. Most importantly, you maintain complete control over your content and presentation without relying on external services.

The entire system is scalable, maintainable, and provides an excellent developer experience. Whether you're building a personal blog or a company tech blog, this approach offers the perfect balance of simplicity and power.

Ready to start writing? Just create a new .md file in your src/content/posts/ directory and watch your content come to life! 🚀

Built by Jishnu Khargharia