Context
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.
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
- Method 1: Using defined JSX tag like
- Overall, good for adding dynamic interactivity and embed React components
- React integration - Allows you to use JSX in Markdown. That means using custom React components in Markdown
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
andrehype
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'
main()
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.
- Use Vercel to deploy the Next.js portfolio template
- Git clone the new code repository created during the deployment from my GitHub account
- Install dependencies locally using either pnpm or npm
- Migrate blog files (.md) from the old blog repo to the new repo + tweak Markdown syntax to resolve any error
- Tweak the template code to extend with more features
- Push to a development branch and check the preview
- Merge to
main
- 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:
- GitHub flavored Markdown (GFM)
- Math plugins
- Used
remark-math
for math Markdown detection combined withrehype-katex
for HTML rendering
- 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 inmdx.tsx
- Add "cv" in nav bar and link it to the internal static PDF file stored in the
/public
directory
- Custom CSS class for styling
Deploy!
- 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
- Using
# Need to use
```math
E = mc^2
```
# Instead of
$$
E = mc^2
$$
Miscellaneous
The project uses Remote MDX to fetch Markdown files from app/blog/posts
directory and render into HTML.