How I Built My Portfolio Using NextJS and MDX
Tags: webdev, nextjs, howto, tutorial
Published: 2024-06-10 04:42:10

Last year, I was inspired by amazing developers like [Josh W Comeau](https://joshwcomeau.com/) and [Kent C Dodds](https://kentcdodds.com/). I decided to make my own portfolio website with a blog to learn in the open and share my journey with the community. You can check it out [here](https://beginarjun.vercel.app)

## Table of Contents
- [Inspiration and Initial Research](#inspiration-and-initial-research)
- [Choosing Next.js and MDX](#choosing-nextjs-and-mdx)
- [Setting Up the Project](#setting-up-the-project)
- [Challenges with the Next.js App Router](#challenges-with-the-nextjs-app-router)
- [Styling MDX Content](#styling-mdx-content)
- [Using TailwindCSS and Tailwind Typography](#using-tailwindcss-and-tailwind-typography)
- [Implementing Page Views with Redis](#implementing-page-views-with-redis)
- [Taking a Break](#taking-a-break)
- [Back with More Experience](#back-with-more-experience)
- [Current State and Future Plans](#current-state-and-future-plans)

## Inspiration and Initial Research

Seeing how developers like [Josh W Comeau](https://www.joshwcomeau.com/) and [Kent C. Dodds](https://www.kentcdodds.com) 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:

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

### Install Dependencies
Next, we install the necessary dependencies:

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

### Setup TailwindCSS
To set up TailwindCSS, follow these steps:

#### Initialize TailwindCSS:

```bash
npx tailwindcss init -p
```

#### Configure TailwindCSS:

Update tailwind.config.js:

```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:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

### Configure MDX with next-mdx-remote
To set up MDX, create a lib/mdx.js file:

```js
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:

```js
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:

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

Create a `lib/redis.js` file:

```js
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:

```js
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:

```js
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](https://beginarjun.vercel.app/projects/linkme). 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!