- How I was writing the content before
I didn't start using MDX from the beginning. When I first built the content for my About page, I stored everything as TypeScript data. It worked, the page looked good, and I could control pretty much everything about how the content was rendered, so at first I didn't really have a reason to change it. But after looking at the code again and adding more content, something started to feel strange. I wasn't really writing my content anymore. I was building a system for describing how my content should be written.
For example, one of my paragraphs looked something like this:
{
type: "paragraph",
content: [
"After high school, I decided to start with frontend development. My first serious learning came from ",
{
text: "Elzero Web School",
emphasis: "strong",
link: "https://elzero.org/"
},
", where I learned ",
{ text: "HTML", emphasis: "strong" },
", ",
{ text: "CSS", emphasis: "strong" },
", and ",
{ text: "JavaScript", emphasis: "strong" },
"."
]
}
There was nothing technically wrong with this. I could make words bold, add links, create quotes, add images, and render everything exactly how I wanted. The problem was that even a simple paragraph had to become a TypeScript structure before I could actually write it. Then I needed another part of my application to understand that structure and turn it into HTML. I had an AboutContentItem, an AboutContentPart, a renderPart() function, a renderEmphasis() function, and more logic to decide what each piece of content was supposed to become.
The more content I added, the more I started asking myself: am I writing content, or am I building a tiny CMS?
- The problem wasn't that the code was wrong
I don't think my original approach was bad. There are definitely situations where representing content as data makes a lot of sense. If you have structured information that needs to be filtered, sorted, transformed, searched, or consumed by different parts of an application, having everything represented as objects can actually be useful. In that case, the fact that your content is data is a feature.
But that wasn't really what I was doing. I was writing stories and explanations. I wanted to sit down and write about how I got into programming, what I learned, and the things I wanted to share. I wasn't trying to build a database of paragraphs. So writing a story as a collection of TypeScript objects started feeling backwards. I had created an abstraction because I wanted more control, but eventually the abstraction itself became something I had to work around.
That was when I started looking for another way to write the content.
- Then I started looking at MDX
This is where things became much simpler. Instead of describing the content using TypeScript, I could just write it:
After high school, I decided to start with frontend development.
My first serious learning came from
[**Elzero Web School**](https://elzero.org/), where I learned
**HTML**, **CSS**, and **JavaScript**.
That's a completely different way of thinking about the same content. There is no object, no type, no content property, and no renderer trying to figure out what I meant. If I want something bold, I use Markdown. If I want a link, I use a Markdown link. If I want a quote, I can just write a quote. The content looks like the content I'm actually trying to write.
That's the part of MDX that really clicked for me. It doesn't force me to choose between Markdown and React. I can write normal content, but I can still bring React into that content whenever I actually need it. MDX lets me use Markdown together with JSX, so instead of inventing my own little language for writing content, I could use something that already existed.
And honestly, that felt like a pretty good deal.
- My paragraphs became paragraphs again
There was another small thing I noticed while moving the content, and this one bothered me more than I expected. My old system encouraged me to create a separate object for every paragraph, so I ended up with things like this:
{
type: "paragraph",
content: "We started with Scratch."
},
{
type: "paragraph",
content: "It was a very simple way to learn programming..."
},
{
type: "paragraph",
content: "One day, our teacher asked:"
}
Again, there was nothing technically wrong with this. But when I was actually writing the story, I didn't want every thought to become its own separate object. Sometimes a few sentences naturally belong together because they're part of the same thought, the same moment, or the same explanation. With MDX, I can simply write those sentences together and decide where the paragraph should end while I'm actually writing.
We started with Scratch. It was a very simple way to learn programming.
Instead of writing lots of code, we had these blocks that we could put
together to make something happen. And I loved it.
That sounds like a small change, but it made a big difference to me. I wasn't thinking, "Okay, this is another AboutContentItem of type paragraph." I was just thinking, "This thought ends here." And that's how I wanted to write in the first place.
- But what about React components?
One of the things I like about MDX is that it isn't just Markdown. I can still use React components when I need something more than normal text. For example, if I want to add an image, I can use the component directly:
<Image src={myimage} alt="" width={300} height={300} />
I don't need to create a special image content type and then teach my renderer what an image is. I can just use the component. The same idea can apply to other components I might want to use inside my content.
That's what makes MDX interesting to me. Content can stay readable like normal writing, while I still have access to React when the content needs something more. I don't have to turn every piece of content into JavaScript data just because it lives inside a React application.
- But how do I style the MDX?
Once I moved the content into MDX, I ran into another question. If MDX gives me normal elements like h2, p, blockquote, and a, how was I supposed to style them without turning every route into a one-off?
My About page and my blog posts both use long-form MDX inside ArticleLayout, so I wanted one consistent typography system for that reading experience, not separate renderers per page. That's when I started using Next.js mdx-components.tsx together with a dedicated mdx-components.module.scss.
The structure in my project looks like this now:
src/
├── mdx-components.tsx
├── mdx-components.module.scss
├── content/
│ ├── about/
│ │ └── about.mdx
│ └── blogs/
│ ├── index.ts
│ └── *.mdx
├── server/
│ └── blog.server.ts
└── app/
├── about/
│ └── page.tsx
└── blog/
├── page.tsx
└── [slug]/
└── page.tsx
Blog metadata (title, description, date) lives in each file's frontmatter and is read by blog.server.ts. The MDX files themselves are just content.
Shared MDX styling lives in one place. For example:
h2: ({ children }) => (
<h2 className={styles["mdx__heading"]}>{`- ${children}`}</h2>
),
p: ({ children }) => <p className={styles["mdx__paragraph"]}>{children}</p>,
blockquote: (props) => (
<blockquote {...props} className={styles["mdx__quote"]} />
),
Each route only decides how to mount the MDX. About imports the file directly; blog posts pick the right component by slug:
import AboutContent from "@/content/about/about.mdx";
<ArticleLayout>
<AboutContent />
</ArticleLayout>;
const Content = blogComponents[slug];
<ArticleLayout header={<ArticleHeader ... />}>
<Content />
</ArticleLayout>
The important part for me was the separation. The .mdx file is responsible for what I'm saying. mdx-components.tsx is responsible for how MDX elements look when they're rendered as articles. The App Router page is responsible for where that content appears (header, layout, metadata).
That separation was what I was missing before.
- So what actually changed?
The biggest change wasn't really the folder structure. It was where I put the responsibility.
Before, my TypeScript data was responsible for describing the content, and then my React code was responsible for figuring out how to render that description. So I had one layer describing the content:
{
type: "paragraph",
content: [...]
}
and another layer interpreting it:
if (item.type === "paragraph") {
// render paragraph
}
Then I needed more logic for emphasis, links, quotes, images, and everything else.
With MDX, the content can describe itself:
## Starting with the frontend
After high school, I decided to start with frontend development.
I learned **HTML**, **CSS**, and **JavaScript**.
There is much less distance between what I want to say and what I actually write. And React only gets involved when I actually need React. That was the biggest change for me.
It's not that my application suddenly became completely different. I just stopped making my written content look like application logic.
- What I actually gained from the change
I didn't choose MDX because my old approach was slow, and I didn't choose it because it was going to magically make my website better either. The biggest improvement for me was how the content is written and maintained.
Before, if I wanted to change a sentence, I was editing a TypeScript object. If I wanted to make one word bold, I needed the right content structure. If I wanted to add a link, I needed another object inside the content array. And if I wanted to add another content type, I had to think about the type, the renderer, and how it should be displayed.
Now I can open an .mdx file and just write.
That's a much smaller mental load for content whose main purpose is being read by people. When I open the content file now, I immediately understand what I'm looking at. It's the actual content, not a data model representing the content.
And I think that's the biggest reason I chose MDX.
- Where I ended up
After all of this, my content structure became much simpler. I don't need to create a custom object every time I want to write a paragraph, and I don't need to create a renderer for every type of content. I can just write:
## Something I learned
This is a normal paragraph with **some bold text** and
[a link](https://example.com).
> And this is a quote.
And when I need React, I can use React.
That's really what I wanted from the beginning.
The funny thing is that I spent time building all of those things before realizing that the thing I wanted was already called MDX. I basically built a small system for writing content and then discovered that I could have just written the content.
And now I'm using the same idea for the content I'm writing here.
This article is written with MDX too.
So I guess I finally took my own advice.
Maybe that's the best test I could have given it.
Sometimes the solution isn't about building a better abstraction. Sometimes it's about realizing that the abstraction you need already exists.
And in my case, it was MDX.
