Tristen.

Building My Personal Blog with Next.js and MDX

August 19, 2024 (3mo ago)

Building My Personal Blog with Next.js and MDX

Building My Personal Blog with Next.js and MDX

When I set out to build my personal blog, I wanted a modern, clean, and fast solution to showcase my thoughts, projects, and experiences. After researching different approaches, I chose to build the blog using Next.js with MDX for flexibility, CSS modules for custom styling, and a minimal reliance on external dependencies. This post covers the systems design choices I made, challenges I encountered, and the lessons learned while building the blog.


Why Next.js?

Next.js is a versatile framework that offers server-side rendering (SSR), static site generation (SSG), and dynamic rendering. For my blog, Next.js stood out due to its performance optimizations, such as incremental static regeneration (ISR), which allows pages to update without a full redeployment. The developer experience is also top-notch, with fast refresh, built-in routing, and the ability to mix static and dynamic content easily.


Embedding Custom Components in MDX with Next.js

Another key reason I chose Next.js for this blog is its seamless support for MDX. MDX allows me to combine the simplicity of Markdown with the power of React, enabling me to embed custom components like videos, code blocks, and charts directly into my blog posts.

Using next-mdx-remote, I can load and render MDX content dynamically. Here's how I set it up to include custom components like CodeBlock and Video:

import { MDXRemote } from 'next-mdx-remote/rsc'
import CodeBlock from './CodeBlock'
import Video from './Video'

// Define custom components
const components = {
  code: CodeBlock,
  Video,
}

// Render MDX content with custom components
export function CustomMDX(props) {
  return <MDXRemote {...props} components={components} />
}

With this setup, whenever I write a blog post in MDX format, I can easily embed a React component using simple markdown syntax.

When rendered, the MDXRemote component dynamically inserts the Video component, allowing it to function as part of the blog content.

Video Component Example:

import React from 'react'
import styles from './Video.module.css'

const Video = ({ src }) => {
  return (
    <div className={styles.videoWrapper}>
      <video controls>
        <source src={src} type="video/mp4" />
        Your browser does not support the video tag.
      </video>
    </div>
  )
}

export default Video

This flexibility is incredibly useful for creating more engaging blog posts that go beyond plain text and images. For example, in this very blog post, I’m embedding a video of an octopus from my recent dive trip.

<Video src="https://tristenwallace.sirv.com/content/Projects/nextjs-blog/octopus.mov" />

This approach not only makes the blog more dynamic and interactive, but also allows me to reuse components across different posts. Whenever I need to insert code snippets or video content, I don’t need to leave the markdown structure—everything integrates smoothly into the blog via MDX.

This also reflects my ongoing effort to minimize dependencies, allowing Next.js and MDX to do most of the heavy lifting for managing content while keeping the codebase simple and efficient.

Benefits of Next.js:

  • Static Site Generation (SSG): Static pages are pre-rendered at build time, which improves loading speed and SEO.
  • Dynamic Routing: Simplified routing with file-based routing, perfect for creating dynamic content pages like blogs and category archives.
  • Server-side Rendering (SSR): For more dynamic parts of the site, I can opt for server-side rendering, ensuring data freshness and personalization where needed.
  • MDX Support: The ability to write content in MDX means I can integrate React components seamlessly into my markdown files, enabling interactive and dynamic content.

CSS Modules: A Better Approach Than Tailwind CSS

While Tailwind CSS has been a popular choice for many developers, I opted for CSS Modules for this project. The main reason behind this choice is control and maintainability. Tailwind offers utility classes, but I wanted to focus on crafting more custom and scalable styles without relying on utility-first design. CSS Modules ensure that styles are scoped locally by default, reducing the chances of conflicts across components and making it easier to manage as the blog grows.

Benefits of CSS Modules:

  • Scoped CSS: CSS is scoped to the component by default, preventing any global overrides or conflicts.
  • Maintainability: Each component gets its own stylesheet, keeping the styling modular and easy to manage.
  • Custom Design: CSS Modules allow for more granular control over the design without the need for long utility class strings.

Challenges Faced and Solutions

1. Syntax Highlighting

Getting syntax highlighting to work across multiple languages was a challenge. Initially, I experimented with Prism.js, but it introduced performance bottlenecks and was difficult to implement smoothly within the SSR framework of Next.js. Ultimately, I opted for Highlight.js. It offered better flexibility and was much easier to integrate without needing complicated React hooks.

2. Styling highly variable content

When managing a growing number of post categories, keeping track of layouts, styles, and metadata became an organizational challenge. I solved this by implementing Layouts in the frontmatter of each MDX file, allowing me to dynamically choose a layout (e.g., PostSimple, PostBanner) based on the needs of the post.

//app/blog/[slug]/page.tsx
export default function BlogPage({ params }) {
  let post = getBlogPosts().find((post) => post.slug === params.slug)

  if (!post) {
    notFound()
  }

  const Layout = layouts[post.metadata.layout || 'PostSimple']

  return <Layout post={post} />
}

3. Content delivery

Initially, I tried using Contentlayer to manage the content of my blog. However, during the process, I discovered that Contentlayer was no longer actively maintained. This posed a major challenge, as I didn’t want to rely on outdated or unsupported dependencies.

Instead, I opted to use Next.js’s built-in fs module for file system-based routing and data handling, coupled with custom functions to parse frontmatter in markdown and MDX files. This decision allowed me to maintain control over the content pipeline while reducing my reliance on external libraries.

Example: Reading content from the file system:

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

export function getBlogPosts() {
  const postsDirectory = path.join(process.cwd(), 'content')
  const filenames = fs.readdirSync(postsDirectory)

  return filenames.map((filename) => {
    const filePath = path.join(postsDirectory, filename)
    const fileContents = fs.readFileSync(filePath, 'utf8')
    const { data: metadata, content } = matter(fileContents)

    return {
      metadata,
      slug: filename.replace(/\.mdx?$/, ''),
      content,
    }
  })
}

By leveraging Next.js’s file system routing, we simplified content management, improved performance, and ensured our stack remains future-proof.

Future Plans: Scaling the Blog and Learning New Technologies

As I continue to develop my blog, I aim to gradually introduce more advanced features while keeping the overall design clean and fast. Some potential improvements include:

  1. Dynamic Content: I plan to integrate more dynamic content, like real-time updates on what I’m working on or live previews of code snippets.
  2. SEO Improvements: Although the blog performs well, there’s always room to improve SEO by refining metadata and adding schema markup to pages.
  3. Interactive Components: As I learn more about modern front-end frameworks, I want to explore building more interactive and data-driven components for my blog.

In terms of my personal growth, I’m focusing on expanding my knowledge in backend development and cloud infrastructure. AWS and containerization technologies like Docker are at the top of my list for deepening my understanding of scalable systems.


Building this blog has been a rewarding experience, from selecting technologies to overcoming unexpected challenges. Moving forward, I’m excited to continue refining the site and sharing my knowledge with the world.