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
- 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,
};
};
- As mentioned above we're gonna use
next-mdx-remote
to render our article pages. Remember we used the articleid
as a path to the article, so we can now use thatid
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!