Why a blog?
I wanted a place to write about things I'm building and learning. A personal site felt like the right home — no algorithmic feed, no engagement metrics, just writing.
The goal was to keep it simple: Markdown files on disk, no CMS, no database.
The stack
npm install gray-matter react-markdown remark-gfm rehype-highlight highlight.js
- gray-matter parses YAML frontmatter from
.mdfiles - react-markdown turns the raw Markdown string into React elements
- remark-gfm adds GitHub Flavored Markdown (tables, strikethrough, task lists)
- rehype-highlight runs
highlight.json fenced code blocks
How it works
Each post lives in content/blog/*.md. The filename becomes the URL slug — no date prefix, no slugify gymnastics. A small content loader in lib/content/blog.ts reads the directory with Node's fs module, parses frontmatter with gray-matter, and computes a reading time estimate.
import fs from "fs";
import path from "path";
import matter from "gray-matter";
const BLOG_DIR = path.join(process.cwd(), "content/blog");
export function getAllPostsMeta() {
if (!fs.existsSync(BLOG_DIR)) return [];
return fs
.readdirSync(BLOG_DIR)
.filter((f) => f.endsWith(".md"))
.map((filename) => {
const slug = filename.replace(/\.md$/, "");
const raw = fs.readFileSync(path.join(BLOG_DIR, filename), "utf-8");
const { data, content } = matter(raw);
const wordCount = content.trim().split(/\s+/).length;
return { slug, ...data, readingTime: Math.ceil(wordCount / 200) };
})
.sort((a, b) => b.date.localeCompare(a.date));
}
Because this only runs in Server Components, there's no client bundle cost.
Styling without @tailwindcss/typography
The project uses Tailwind v4, which doesn't yet have a stable @tailwindcss/typography plugin. Instead, PostBody maps each Markdown element to Tailwind classes directly via react-markdown's component prop:
<ReactMarkdown
components={{
h2: ({ children }) => (
<h2 className="border-border mt-8 mb-3 border-b pb-1 text-xl font-bold tracking-tight">
{children}
</h2>
),
code: ({ children, className }) => {
const isBlock = !!className;
return isBlock ? (
<code className={className}>{children}</code>
) : (
<code className="bg-muted rounded px-1.5 font-mono text-sm">
{children}
</code>
);
},
}}
>
{content}
</ReactMarkdown>
What's next
- Adding more posts as I build things
- Maybe an RSS feed someday
Thanks for reading.
