When I decided to add a blog to my website, the obvious path was to do what everyone else does: install a popular MDX package, connect a headless CMS, or just publish on Medium or Substack.
I decided not to do any of those.
I wanted something fast, completely self-contained, and integrated directly into the site I already built. I didn't want external databases, extra monthly subscriptions, or a dozen third-party libraries fighting my layout.
Here is the approach I took, why I skipped the usual tools, and how the system works.

1. Why I Skipped the Usual Options
Before writing any code, I looked closely at the alternatives.
Hosted Platforms (Medium, Substack, Dev.to)
Publishing on an external platform is easy, but you lose control. Your articles sit behind someone else's domain, their algorithms decide who sees your work, and readers get hit with popups, sign-up banners, and paywalls. I wanted my writing to live on my own domain, under my own design.
Headless CMSs (Sanity, Contentful, Strapi)
A headless CMS gives you an online dashboard to edit articles and sends the content to your site through an API.
For a team of non-technical writers, that makes sense. For a developer writing alone, it is unnecessary overhead. You have to manage an extra service, configure API keys, worry about rate limits, and make network requests just to build a page. If their servers go down, your site builds fail.
Off-the-Shelf MDX Libraries
The standard setup in the React and Next.js world is MDX with a collection of plugins (remark, rehype, syntax highlighters, and slug generators).
While powerful, this stack comes with real baggage: - It pulls in dozens of npm packages just to turn text into HTML. - It frequently causes version headaches whenever you update Next.js or React. - Most plugins come with their own opinions about styling and layout, forcing you to fight their CSS overrides to match your own site.
I didn't need a massive generic engine. I just needed a clean way to write Markdown files and turn them into my site's existing components.
2. My Approach: Plain Files in Git
The core philosophy of this blog is simple: my content lives in git, right next to my code.
Every article is a single .md file inside a content/blog/ folder. When I want to write a new post: 1. I create a new Markdown file. 2. I add a few lines of details at the top (title, description, date, tags). 3. I write the post in my everyday code editor. 4. I run git push, and the site builds automatically.
There is no database, no admin dashboard, and no internet connection required while drafting. If git ever goes away, I still have a folder of human-readable Markdown files that I can take anywhere.
3. How the Engine Actually Works
Instead of bringing in heavy external libraries, I wrote a small custom parser in TypeScript that converts Markdown into structured components.
Here is the entire journey from a text file to a live web page:
Step 1: Breaking the Text into Blocks
When the site builds, the parser reads the Markdown file line by line. Instead of converting text straight into raw HTML strings, it breaks the article down into clean "content blocks": - A line starting with ## becomes a Heading Block. - A paragraph of text becomes a Paragraph Block. - A fenced code block becomes a Code Block, pulling out details like the filename and lines to highlight. - An image tag becomes an Image Block, ensuring an alt text description is present.
Step 2: Mapping Blocks to Real Components
Once the article is broken into blocks, each block is handed to a matching React component that already uses my site's dark palette, typography, and styling: - Headings automatically generate anchor links so readers can copy links to specific sections. - Code blocks get clean file headers, line numbering, and a one-click copy button. - Images get wrapped in a responsive container with caption support and a click-to-zoom view.
Step 3: Generating Static Pages Upfront
Because the content is already local, Next.js generates static HTML pages at build time.
When a reader visits an article, their browser receives ready-to-render HTML. There are no database queries, no API waterfalls, and no waiting for client-side JavaScript before the text appears.
4. Handling Things Markdown Can't Do
Standard Markdown is great for simple formatting, but technical writing often needs richer elements: callout boxes, benchmark stats, video clips, and diagrams.
Normally, people solve this by embedding messy raw HTML or React imports into their Markdown. I didn't want that—it clutters the document and breaks plain-text readability.
Instead, I used a clean block syntax marked by three colons (:::):
Callout Boxes
For important tips or warnings:
Keep your content format decoupled from third-party platforms. A text file in git will still open in thirty years; a hosted CMS dashboard might not exist in five.
Benchmarks and Stats
When writing post-mortems or system reviews, numbers tell the story better than long paragraphs. The :::metrics directive lets me display stats cleanly:
:::metricsitems: - label: "Game-ready load" value: "5.87s" - label: "Average FPS" value: "~60 FPS":::Clean Architecture Diagrams
Technical posts often need diagrams. Rather than taking screenshots that get blurry on high-resolution screens and clash with dark backgrounds, I use :::mermaid blocks. The diagram code is written directly in the text file and drawn cleanly in the browser using the site's dark color scheme.
5. Thoughtful Details on Embeds
Embedding external media usually comes with unwanted baggage: slow third-party tracking scripts, cookie consent banners, and slow page loads. I handled these with deliberate choices:
- YouTube: Instead of loading an iframe immediately, the post shows a crisp preview image with a play button. The actual player only loads when a reader clicks it, keeping the initial page load lightweight.
- X / Twitter: Tweets can be viewed in three different ways: as a native lightweight card, as the official interactive post, or directly with the attached video player. Readers can toggle between these modes right inside the card.
- Catching Typos: If I mistype a directive name late at night (like writing
:::youtubez), the parser catches it and suggestsDid you mean ":::youtube"?instead of failing silently.
6. What I Learned
Building this myself took a bit of upfront planning, but the payoff was worth it:
- Zero bloat: The entire parsing and rendering system is roughly 800 lines of straightforward TypeScript.
- Speed: Articles compile in milliseconds, and pages load instantly.
- Total ownership: Everything lives in one git repository, styled with my own theme, without relying on third-party services.
Now when I want to share a project post-mortem or technical breakdown, I just open my editor, create a .md file, and write.
