devGalaktikadevGalaktika

06 Dec, 2024

Build a Blog or Portfolio Website using Next.js and MDX

There are a lot of resources and websites offering you to create a portfolio or a blog. If you're a dev and want to keep things simple as possible and still have a lot of flexibility, Next.js and MDX are the way to go. In this article I describe how you can do that with examples I used on my own website. (this one)


*if you just want to start writing and have blog site like this one, fork the repo here: link to the repo

What are the benefits of using Next.js and MDX?

If you are a developer, you are probably familiar with basic markdown syntax that github uses for README files. MDX is a markdown on steroids, it allows you to use JSX components inside your markdown files. This means you can use React components inside your markdown files, which is great for creating interactive content. And if you know how to use basic GIT commands, you can easily create a portfolio website with Next.js and MDX.

  • You don't have worry about database, since MDX files are stored in your project and live wihin your repo
  • You can have easy and minimalistic design, since you are using markdown files and can focus on writing content
  • You don't have to worry about backend as much
  • All files are static, so you can host your website on Vercel or Netlify for free
  • You don't need any fancy CMS, you can use your favorite code editor to write content

Getting started

Start by creating a new Next.js project, I'll use bun as a package manager, but you can use npm, yarn, or any other package manager you prefer.

bunx create-next-app@latest

We're also gonna use TypeScript and TailwindCSS so you can choose "yes" for both of them when creating a new project:

Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes

Dependencies we're gonna use

Apart from the TypeScript and Tailwind we're gonna need a few more dependencies:

@mdx-js/react,
@next/mdx,
@types/mdx,
next-mdx-remote

and one more dev dependency:

@tailwindcss/typography

All mdx packages are needed for rendering mdx files in Next.js, and @tailwindcss/typography is an TailwindCSS plugin used for styling our headings and paragraphs seamlessly.

Creating articles

Let's create the folder for our articles and inside of it let's create our first mdx file:

root/articles/my-first-article.mdx

Now let's add some content to our file:

---
title: My first Title
description: "This is my first article"
createdAt: 04 Dec, 2024
---

# My first Title

## And my first subtitle

Notice the content within the triple dashes ---, this is called frontmatter and it's used for defining metadata for your article. You can define any metadata you want, but I usually use title, description, and createdAt. You can access that metadata in your components separately from the content.

Fetching the articles

Now let's create a function that will fetch our articles from the /articles folder.

// lib/utils.ts

import fs from "node:fs";
import path from "path";
import matter from "gray-matter";
import { serialize } from "next-mdx-remote/serialize";
import { cache } from "react";

// use path.join to create a path to the articles folder
const getPosts = (blogDirectory = path.join(process.cwd(), `articles`)) => {
  const fileNames = fs.readdirSync(blogDirectory);

  const allBlogPosts = fileNames.map((post) => {
    // create an id from the file name by removing the .mdx extension
    const id = post.replace(/\.mdx$/, "");

    const fullPath = path.join(blogDirectory, post);
    const fileContents = fs.readFileSync(fullPath, "utf-8");

    // Use gray-matter to parse the post metadata defined between triple dashes above
    const matterResult = matter(fileContents);

    return {
      id,
      title: matterResult.data.title,
      createdAt: matterResult.data.createdAt,
      description: matterResult.data.description,
    };
  });

  return allBlogPosts;
};

Now we can use this function to fetch our articles and display them on our homepage for example.

// app/page.tsx

import { getPosts } from "@/lib/utils";

export default function Home() {
  const blogPosts = getPosts();
  ....
}

And now just map through the blogPosts array to render the articles list:

<ul className="col-span-12 flex flex-col gap-y-3 text-lg list-disc list-inside mt-4">
  {blogPosts.map((post) => {
    return (
      <li key={post.title}>
        // Notice I am using the post.id as a path to the article
        <Link href={`/${post.id}`}>{post.title}</Link>
      </li>
    );
  })}
</ul>

This will result in a something like this:


My blog Posts:

  • My first Title

Now to the fun part, rendering the single article page!

Rendering the single article page

  1. First we're gonna write quick uitl function for getting the single article by id:
// lib/utils.ts

const getSingleArticle = (postId: string) => {
  // Just construct the path to the article by appending
  // the mdx extension to the postId
  const blogPost = path.join(process.cwd(), `articles/${postId}.mdx`);

  const fileContents = fs.readFileSync(blogPost, "utf-8");

  // Use gray-matter to parse the post metadata defined between
  // triple dashes as in the previous function
  const matterResult = matter(fileContents);

  return {
    title: matterResult.data.title,
    date: matterResult.data.createdAt,
    description: matterResult.data.description,
    content: matterResult.content,
  };
};
  1. As mentioned above we're gonna use next-mdx-remote to render our article pages. Remember we used the article id as a path to the article, so we can now use that id to fetch the article content.
// pages/[id]/page.tsx
// Since we're rendering the article content directly on server
// side we are importing MDXRemote from next-mdx-remote/rsc
import { MDXRemote } from "next-mdx-remote/rsc";

const BlogPostPage = async ({ params }: PageProps) => {
    // extract the id from the page params
    const { id } = await params;
    const mdxSource = await getSinglePost(slug);

    const mdxSource = getSingleArticle(id);.

    return (
        <section className={`font-[Verdana] grid`}>
            <article>
                <span className="self-end mb-4">{mdxSource.date}</span>
                <h1 className="mb-10 font-bold md:text-3xl text-xl">{mdxSource.title}</h1>

                <article className="prose prose-slate prose-base md:prose-base dark:prose-invert">
                    <h2 className="md:!text-base !text-base mb-10 italic !font-normal">
                        {mdxSource.description}
                    </h2>
                <hr />

                <MDXRemote
                    // pass the mdx content to the source prop
                    source={mdxSource.content}
                    components={{
                    // Here you can define your custom components as described in the next-mdx-remote docs
                    }}
                />
            </article>
        </section>
    );
   };

Important note: Remember to import the MDXRemote component from /rsc directory of next-mdx-remote package, like in the code snippet above since we're rendering the article content directly on the server side, see more here: next-mdx-remote docs

If you are doing client side rendering you can use the MDXRemote component from the main package but you need the serialize the mdx content before being able to render it and make other changes around the MDXRemote component in order for it work. Don't hesitate to reach out via email if you need help with that one.


Congrats! You've just created a fully working blog/portfolio website with Next.js and MDX! Push the code to the github repo and deploy it via Vercel or some cheap VPS and you have yourself a minimalistic/working blog. If you however want to go further and tweak the code or style it to make it prettier you shoul have a pretty good starting point! Hope this helped you in any way!