How I Built My Portfolio Using NextJS and MDX

June 6, 2024 (5mo ago)

Last year, I was inspired by amazing developers like Josh W Comeau and Kent C Dodds. I decided to make my own portfolio website with a blog to learn in the open and share my journey with the community.

Table of Contents

Inspiration and Initial Research

Seeing how developers like Josh W Comeau and Kent C. Dodds shared their knowledge and projects, I felt motivated to create something similar. I wanted a platform where I could document my learning process and projects. A blog integrated into my portfolio seemed like the perfect idea.

Choosing Next.js and MDX

After some research, I decided to use Next.js for its flexibility and performance. For the blog, I chose MDX because it allowed me to write JSX in Markdown, giving me the power to create interactive content. I opted for next-mdx-remote for MDX compilation.

Setting Up the Project

Here's a step-by-step guide on setting up a Next.js project with MDX, TailwindCSS, and other packages.

Initialize Next.js Project

First, we need to create a new Next.js project:

npx create-next-app@latest my-portfolio
cd my-portfolio

Install Dependencies

Next, we install the necessary dependencies:

npm install @next/mdx next-mdx-remote tailwindcss @tailwindcss/typography sugar-high redis

Setup TailwindCSS

To set up TailwindCSS, follow these steps:

Initialize TailwindCSS:

npx tailwindcss init -p

Configure TailwindCSS:

Update tailwind.config.js:

module.exports = {
  content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}', './posts/**/*.{md,mdx}'],
  theme: {
    extend: {},
  },
  plugins: [require('@tailwindcss/typography')],
};
Add Tailwind Directives:

Update styles/globals.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

Configure MDX with next-mdx-remote

To set up MDX, create a lib/mdx.js file:

import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { serialize } from 'next-mdx-remote/serialize';

const postsDirectory = path.join(process.cwd(), 'posts');

export function getSortedPostsData() {
  const fileNames = fs.readdirSync(postsDirectory);
  const allPostsData = fileNames.map(fileName => {
    const id = fileName.replace(/\.mdx$/, '');
    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');
    const matterResult = matter(fileContents);
    return {
      id,
      ...matterResult.data
    };
  });
  return allPostsData.sort((a, b) => (a.date < b.date ? 1 : -1));
}

export async function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.mdx`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');
  const matterResult = matter(fileContents);
  const mdxSource = await serialize(fileContents);
  return {
    id,
    mdxSource,
    ...matterResult.data
  };
}

Create Blog Page

Create a pages/blog/[id].js file:

import { getPostData, getSortedPostsData } from '../../lib/mdx';
import { MDXRemote } from 'next-mdx-remote';
import Link from 'next/link';

export default function Post({ postData }) {
  return (
    <article className="prose lg:prose-xl mx-auto">
      <h1>{postData.title}</h1>
      <MDXRemote {...postData.mdxSource} />
      <Link href="/blog">← Back to Blog</Link>
    </article>
  );
}

export async function getStaticPaths() {
  const paths = getSortedPostsData().map(post => ({
    params: { id: post.id }
  }));
  return { paths, fallback: false };
}

export async function getStaticProps({ params }) {
  const postData = await getPostData(params.id);
  return {
    props: {
      postData
    }
  };
}

Challenges with the Next.js App Router

Back then, the Next.js App Router was still new, and many packages hadn't been updated to work with it. I faced numerous issues and spent a lot of time on the next-mdx-remote GitHub repo discussing with the community. Eventually, I discovered they had a different implementation for the App Router's React Server Components (RSC).

Styling MDX Content

Creating a blog page was a significant achievement, but styling the MDX content turned out to be tricky. As a newcomer, I found it challenging to make the content look good. After struggling for a while, I decided to pause the project because I needed to focus on my summer project for college.

Using TailwindCSS and Tailwind Typography

TailwindCSS and the Tailwind Typography plugin made styling much easier. By adding prose classes to my blog content, I could quickly achieve a clean and professional look.

For example, wrapping the MDX content with <article className="prose lg:prose-xl mx-auto"> applied beautiful default styles to all text elements.

Implementing Page Views with Redis

To track page views, I used the sugar-high npm package with Redis. Here's a basic implementation:

Install Redis Ensure Redis is installed and running on your machine. For the setup, I used Docker:

docker run --name redis -d -p 6379:6379 redis

Create a lib/redis.js file:

import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

export default redis;

In your API route (e.g., api/view/route.ts), set up the endpoint to count views:

import redis from '../../lib/redis';
import {NextRequest, NextResponse} from 'next';

export default async function POST(req : NextRequest, res : NextResponse) {
  const { id } = req.query;
  const ip = req.headers['x-real-ip'] || req.connection.remoteAddress;

  const hasVisited = await redis.sismember(`views:${id}`, ip);
  if (!hasVisited) {
    await redis.sadd(`views:${id}`, ip);
    await redis.incr(`views-count:${id}`);
  }

  const views = await redis.get(`views-count:${id}`);
  res.json({ views },{staus : 200});
}

On the client side, you can fetch and display the views:

import { useEffect, useState } from 'react';

export default function PageViews({ id }) {
  const [views, setViews] = useState(0);

  useEffect(() => {
    fetch(`/api/views?id=${id}`)
      .then(response => response.json())
      .then(data => setViews(data.views));
  }, [id]);

  return <p>{views} views</p>;
}

Taking a Break

During the break, I continued working with Next.js on various projects, including a cool project called LinkME, you can learn more about it here. This experience gave me a lot more confidence and knowledge.

Back with More Experience

With the additional experience, I felt ready to tackle my portfolio website again. I decided to rebuild it from scratch. This time, everything went much smoother. Although it's not finished yet, I'm developing it iteratively based on feedback.

Current State and Future Plans

The portfolio now includes a Project Page where I post about the projects I've completed. While there's still a lot to do, I'm happy with the progress and look forward to improving it over time.

Thanks for reading about my journey in building my portfolio website. Stay tuned for more updates!