Migrate personal blog to Next.js + MDX

June 30, 2024


I built my previous blog site in 2023 May using Jekyll, a Ruby plugin (Gem) for static site generation using the So Simple Template.

The UI looked nice with Medium-like author info on the side, tagging/searching feature built in so it's easy to retrieve specific blogs. The downside is that I need to set up a Ruby environment on WSL2 and learn how to work with Ruby and Liquid, which are two fundametnal components where Jekyll is built on.

Figure 1: The old blog design

After a busy start in 2024, I decided to spend some time on a lovely Saturday during the Canada Day long weekend to migrate my blog site from Jekyll to Next.js with MDX. There were some plugins I had to extend to support Latex math rendering in HTML and GitHub flavoured Markdown syntax. However, the overall experience was very smooth and I managed to set up, extend the original template, and deploy the new blog site using Vercel all within a few hours.

Why Next.js + MDX?

  • Next.js (Web framework)
    • Built on top of React therefore solid React integration
    • File-based routing - Routes are automatically created based on the file structure
    • Support server-side rendering (SSR) and static site generation (SSG) (PS: Jekyll does not support SSR)
    • MDX integration
  • MDX (Markdown -> HTML)
    • React integration - Allows you to use JSX in Markdown. That means using custom React components in Markdown
      • Method 1: Using defined JSX tag like <Component prop1="Text" prop2="Path" ...></Component> in Markdown
      • Method 2: Binding a React component to Markdown syntax like # heading
    • Overall, good for adding dynamic interactivity and embed React components

Next.js & MDX Integration

  • For sourcing Markdown data from local files. Use @next/mdx package.
  • For sourcing Markdown data from remote or other folders. Use next-mdx-remote
    • This is what the Next.js Portfolio Starter Kit template used
  • Under the hood, MDX uses remark and rehype to transform Markdown plaintext into HTML

See an example here:

import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeSanitize from 'rehype-sanitize'
import rehypeStringify from 'rehype-stringify'
async function main() {
  const file = await unified()
    .use(remarkParse) // Convert into Markdown AST
    .use(remarkRehype) // Transform to HTML AST
    .use(rehypeSanitize) // Sanitize HTML input
    .use(rehypeStringify) // Convert AST into serialized HTML
    .process('Hello, Next.js!')
  console.log(String(file)) // <p>Hello, Next.js!</p>

High-level Walkthrough

Here's a high-level walkthrough of what I did. For more details on implementation, see Technical Details section below.

  1. Use Vercel to deploy the Next.js portfolio template
  2. Git clone the new code repository created during the deployment from my GitHub account
  3. Install dependencies locally using either pnpm or npm
  4. Migrate blog files (.md) from the old blog repo to the new repo + tweak Markdown syntax to resolve any error
  5. Tweak the template code to extend with more features
  6. Push to a development branch and check the preview
  7. Merge to main
  8. DONE!

Technical Details

Choice of Template

I used Next.js Portfolio Starter Kit with MDX and Markdown support. It also has other features out of the box:

  • Optimized for SEO
  • Dynamic Open Graph (OG) images
    • This is the thumbnail image you see when someone post on social medias
  • Syntax highlighting through sugar-high
  • Vercel Speed Insights
  • Built-in web analytics through Vercel
  • Tailwind CSS

Dependency Management

The default used by the template is pnpm as it's a more memory-efficient version of npm, kind of like conda to venv, as pnpm maintains "a global index of dependencies" and reuses them into different projects.

I ended up switching to npm because deploying using pnpm-lock.json gave me an error. pnpm install --frozen-lockfile was failing for some reason. In the future, this can be one enhancement.

Extend the Code

  • MDX has lots of Remark and Rehype plugins. I used:
  • Other tweaks
    • Custom CSS class for styling <blockquote> tag rendered through GFM plugin (both light and dark mode)
    • Add caption prop to <Image> component in mdx.tsx
    • Add "cv" in nav bar and link it to the internal static PDF file stored in the /public directory


  • Simply push to whatever development branch (e.g. dev) and raise a PR to merge to main
  • A preview will be built by Vercel
  • Once satisfied with the preview, merge the PR and deployment to Prod will start automatically

Future Enhancements

  • Replace title OG image with an actual image in app/og/route.tsx with frontmatter
  • Dark/light mode toggle
  • Add more tabs in the nav bar like projects, film list, reading list
  • Create CSS in app/global.css for better table styling
  • Debug math rendering
    • Using $$ ... $$ creates an error with math plugins right now. Need to use blockquote syntax with math like
# Need to use
E = mc^2
# Instead of
E = mc^2


The project uses Remote MDX to fetch Markdown files from app/blog/posts directory and render into HTML.