How I Built This Blog
- nextjs
- firebase
- seo
- mdx
I wanted a personal blog that I fully control: no platform lock-in, no monthly CMS bill, and pages that load instantly. This post walks through how it works — and doubles as a demo of everything the site can render.
The goals
A few non-negotiables shaped every decision:
- Own the content. Posts live in this repo as plain text, versioned in git.
- Fast and cheap. Pre-rendered HTML on a CDN, no server to pay for or babysit.
- Indexable. Both search engines and AI crawlers should understand each page.
- Video, self-hosted. No third-party embeds tracking my readers.
The stack
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js (App Router, static export) | Pre-renders every page to HTML |
| Content | MDX files in content/posts/ | Markdown + React, all in git |
| Styling | Tailwind + Typography plugin | Readable prose with almost no effort |
| Hosting | Firebase Hosting | Global CDN, generous free tier |
| Video | Firebase Storage | Self-hosted, streamed on demand |
Because the whole site is exported as static files, there's no server doing work when you load this page — your browser just receives finished HTML.
Writing a post is just a file
Each post is an MDX file with a little frontmatter on top:
---
title: "How I Built This Blog"
description: "A tour of the stack behind this site."
date: "2026-06-28"
tags: ["nextjs", "firebase"]
---
I wanted a personal blog that I fully control...The filename becomes the URL, the frontmatter feeds the SEO tags, and the body is whatever I want to write.
Code blocks are highlighted
Fenced code gets syntax highlighting at build time, so there's no JavaScript shipped to color it in:
type Post = {
slug: string;
title: string;
date: string;
};
function sortByNewest(posts: Post[]): Post[] {
return [...posts].sort((a, b) => (a.date < b.date ? 1 : -1));
}Images just work
Drop in a Markdown image and it lazy-loads automatically:
Video is a first-class citizen
This is the part most static blogs punt on. I upload a clip to Firebase Storage
and reference it with a <Video> component. It streams on demand, supports
seeking, and emits structured data so the clip itself is indexable:
<Video
src="videos/intro.mp4"
poster="videos/intro-poster.jpg"
title="A short hello"
duration="PT1M30S"
uploadDate="2026-06-28"
/>Until a real video is uploaded, the snippet above stays commented out — but the component is wired up and ready.
What makes it indexable
Every page ships with the things crawlers look for:
- A descriptive
<title>and meta description, unique per post. - OpenGraph and Twitter tags, plus an auto-generated social card image.
- JSON-LD structured data —
BlogPostingfor articles,VideoObjectfor clips. - A
sitemap.xml, anrss.xmlfeed, and arobots.txtthat explicitly welcomes AI crawlers like GPTBot and ClaudeBot. - An
llms.txtindex summarizing the site for language models.
Wrapping up
The result is a blog that costs essentially nothing to run, loads as fast as the network allows, and stays entirely in my control. Every post — including this one — is just a file I can edit and commit.
That's the whole point.