How I Built a Fully-Custom Blog with MDX and Next.js

8 min read · 2025-11-21


I always wanted to build my own blog and start posting things I find interesting. However, I kept delaying this task for so long that I ended up not doing it. I finally decided to give it a try and I'm happy to say that it's been a great experience (to some degree).

When building this blog, I only had one thing in mind: I wanted a markdown style blog. In this post, I will go over the journey and the challenges I faced along the way.

A Little Back Story..

Posting my thoughts and ideas on the internet was something I wanted to do: things I learned, topics that interests me, learned lessons, educational content, goofy random shit I did.. you get the idea. But, I've told myself if I ever was going to do that, I wanted to publish it in my own blog.

Why you must ask? Current platforms are highly limited in terms of customization and features, other platforms might require a subscription to access certain features (or to even read an article...)

If I wanted to publish my thoughts, I want to publish it in a way that I can easily control how the content is delivered to the reader. I want to be able to customize the design, the layout, the font, the color scheme, etc..

Day Job

I've also recently graduated from college and was lucky to get hired right out of college. I've been working for a few months now and it took some adjusting for the demanding nature of my role, especially after my probation period ended and the requirement of understanding highly complex parts of the system.

So this pushed the blog idea even more to the back of my mind as I couldn't really focus on anything other than the job. Now, after understanding my role and becoming more comfortable with my job, and being able to deal with the stress and pressure better than before, all of this allowed me to come back to the unfinished ideas, though I'm starting simple, which is creating my own blog (that can't be that hard, right?).

Additionally, There are a lot of cool things I did in my job that I want to share with you, but I'm not going to do that now.

I still haven't reached the level of 'customization' yet with this application. I had a lot of ideas but because I kept delaying building this app for so long, I lost sight of what I wanted to do with it. After building a simple version, I can finally focus on regaining my vision of what this should look like.

Transitioning from being a student to a full-time job needs it own post, but I'll leave that to another day.

Now, let us get a bit technical.

Tech Stack

Image

This blog application is a part of a monorepo that is currently hosting both the portfolio and the blog (too much I know). This website is written in Next.js and uses MDX for the blog posts. The blog posts are written in markdown and compiled to React components using remark.

for styling I used Tailwind CSS and @tailwindcss/typography plugin to style things like headings, paragraphs, lists, etc..

MDX is the most critical part of this entire system so without it this would've required a lot of work to get this blog up and running. So, much appreciation for the hard work of the people behind this beautiful piece of wonder.

Finally, I used Vercel to deploy this website.

Database

I DID NOT use a database to store information about the blog posts (Definetly not because I didn't want to pay for it).

Some would argue that I should've used sqlite or a free database but honestly I did not want to overcomplicate this project because I wanted to get this project out of 'wishlist' bucket.

SOO... I used a much simpler system to store information about the blog posts, which is a custom script that loops over the mdx files and collects necessary info (extracted by gray-matter) into an object. This object is then saved to a typescript file that is used by the application to render the blog post information like title, date, etc..

The main rational behind this is that I did not want my app to do any sort of heavy processing (like reading files) in runtime, so I I moved all of this processing to compile time because the data doesn't really change until the next time I build the app. so it made sense to do it that way.

Here is a small snippet of the JSON object that is generated:

// THIS FILES IS AUTO GENERATED BY compile-mdx-data SCRIPT, DO NOT EDIT
export const blogPostsObject: Record<string, BlogPost> = {
"mdx-nextjs-blog-setup": {
"title": "My MDX + Next.js Blog Setup",
"description": "This is a description",
"tags": [
"nextjs",
"mdx",
"tailwind"
],
"date": "2025-11-21",
"readingTime": "5 min"
},
};

This script always runs before the build script.

Blog Main Page

The blog main page which contains all the blog posts also uses the blogPostsObject that was auto generated by the script to render the posts.

This allows me to keep things consistent and avoid having to manually update the blog posts information in multiple places.

Image

Code Snippets

as you have noticed, I used a code snippet to highlight the code blocks. I'm using Expressive Code to do this. this is a very cool plugin that allows you to add syntax highlighting to your code blocks, but there is much more to it than that.

as an example, you can add a file tab to the code block like this:

server.ts
const port = 3000;
app.listen(port, () => {
console.log(`Server running on ${port}`);
});

there is also a show line numbers option that you can use to show the line numbers of the code block, as an example I want to highlight the line that contains the port variable:

server.ts
const port = 3000;
app.listen(port, () => {
console.log(`Server running on ${port}`);
});

even cooler than that, you can display a code diff like this, highlighting what has been added and removed:

line-markers.js
function demo() {
console.log('this line is marked as deleted')
// This line and the next one are marked as inserted
console.log('this is the second inserted line')
return 'this line uses the neutral default marker type'
}

If you would like to see more examples of code snippets, you can check out the Expressive Code website.

TOC (Table of Contents)

This one was pretty hard to figure out as the documentation wasn't very clear (or because I was working on this after my day j*b..)

Either way, I ended up using rehype-extract-toc to generate the table of contents.

The plugin is pretty simple to use and it generates a table of contents based on the headings in the blog post. the withTocExport function is what is triggers exporting the toc data.

import withToc from "@stefanprobst/rehype-extract-toc";
import withTocExport from "@stefanprobst/rehype-extract-toc/mdx";
const withMdx = nextMdx({
options: {
...
rehypePlugins: [
withToc,
[withTocExport, { name: 'toc' }],
]
}
})

The toc variable is then exported from the mdx import itself that we are currently passing to the Toc component to render the table of contents tree:

const { default: Post, toc } = await import(`@/app/(blog)/mdx/${slug}.mdx`);
return (
<div>
<Toc toc={toc} />;
<Post />
</div>
);

Post is the component that will be rendered and toc is the variable that contains the table of contents. toc is then passed to the TOC component which is responsible for rendering the table of contents.

Closing Thoughts

I'm really happy with how this blog turned out. Even though there is still much work to be done, what I have finished deserves to stop a bit and appreciate the hard work that went into it.

I'm really excited for what comes next into this little project, and I hope you enjoyed reading this post! sorry for any mistakes or typos, I'm still learning how to write this kind of stuff 😅

Last Updated: 2025-11-22


© 2025 Malek Shawahneh, All rights reserved.