·6 min read·Updated March 10, 2026

How I Built This Blog

The stack behind this blog — MDX, Shiki, Sandpack playgrounds, and the authoring pipeline that ties it all together.

nextjsmdxdeveloper-experience

Every developer eventually builds their own blog. It's a rite of passage — part portfolio piece, part creative outlet, part excuse to yak-shave for weeks on tooling instead of actually writing.

This is mine. It's built with Next.js, MDX, and a handful of custom widgets that make writing technical posts more fun (and hopefully more useful to read). This post walks through the pieces and doubles as a living demo of everything the system can do.


The stack#

The blog runs on:

  • Next.js 16 with the App Router — each post is a statically generated route at build time via generateStaticParams
  • MDX via next-mdx-remote/rsc — posts are plain .mdx files compiled on the server as React Server Components
  • Shiki (through rehype-pretty-code) — syntax highlighting with dual light/dark themes that match the site's color mode
  • Sandpack — embedded, runnable React playgrounds powered by CodeSandbox's browser bundler
  • Custom componentsAside callouts, CodeSnippet blocks with copy support, and Playground wrappers with multi-file and dependency support

Posts live as .mdx files in a content/blog/ directory. Each one starts with a frontmatter block:

snippetyaml
---
title: "My Post"
description: "One-sentence summary for cards and SEO metadata."
publishedAt: "2026-03-10"
updatedAt: "2026-03-10"
tags: ["nextjs", "mdx"]
draft: false
slug: "optional-custom-slug"
---

Only title, description, and publishedAt are required. The draft flag hides work-in-progress posts from production while keeping them visible in development, and slug defaults to the filename if omitted. Every post automatically gets OpenGraph metadata, a sitemap entry, and an RSS feed entry.

Code snippets#

Any fenced code block gets syntax highlighting via Shiki and a copy button for free. The language tag determines the theme:

snippetts
export const normalizeSlug = (value: string) =>
  slugify(value, { lower: true, strict: true, trim: true });

Shiki runs at build time, so there's zero client-side JavaScript for highlighting — just pre-rendered HTML with inline styles. It supports dual themes (github-light and github-dark) that swap automatically with the site's color mode.

snippettsx
export function Greeting({ name }: { name: string }) {
  return <h1 className="text-2xl font-bold">Hello, {name}!</h1>;
}

Asides#

For callouts, tips, and warnings, the Aside component renders a toned callout box. It supports info, warning, success, and danger tones, and you can nest any markdown content inside — including code blocks.

Playgrounds#

This is the feature I'm most excited about. For React examples, the Playground component embeds a live Sandpack editor directly in the post. You can edit the code and see the result immediately:

Gradient card

tsx
Loading interactive playground...

Multi-file examples#

For anything more complex, playgrounds support multiple files with tabs. Each file is wrapped in a PlaygroundFile component:

Multi-file components

tsx
Loading interactive playground...

External dependencies#

Playgrounds can pull in npm packages too. Sandpack installs them on the fly:

With npm dependency

tsx
Loading interactive playground...

For non-JS/TS languages, the playground falls back to a static code snippet with an optional link to an external sandbox:

Python example

Inline playground execution is limited to JavaScript/TypeScript (React). For languages like Python, Rust, or Go, pass an externalUrl and keep this snippet as a readable fallback.

Open external playground
fallbackpython
def greet(name: str) -> str:
    return f"hello {name}"
 
print(greet("world"))

Machine-readable by default#

Every post on this blog supports content negotiation. Request a post with Accept: text/markdown (or just use curl) and you'll get the raw markdown instead of HTML:

snippetbash
curl https://datta.me/blog/how-i-built-this-blog

The proxy layer detects CLI user agents and markdown-preferring clients automatically, so the same URL serves HTML to browsers and markdown to agents, scrapers, and CLI tools. If you're building something that consumes blog content programmatically — an LLM agent, a feed reader, a research tool — every post is already accessible as plain text without any scraping.

Diagrams with Mermaid#

Sometimes a diagram says more than a paragraph. Fenced code blocks with the mermaid language tag render as interactive diagrams that adapt to light and dark mode:

What's next#

There's more I want to add — a table of contents sidebar, view count tracking, and maybe some post-specific interactive widgets beyond the generic playground. But the foundation is solid enough to start writing, and that's the point. The best blog engine is the one that gets out of the way and lets you publish.