← All posts

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

LayerChoiceWhy
FrameworkNext.js (App Router, static export)Pre-renders every page to HTML
ContentMDX files in content/posts/Markdown + React, all in git
StylingTailwind + Typography pluginReadable prose with almost no effort
HostingFirebase HostingGlobal CDN, generous free tier
VideoFirebase StorageSelf-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:

A placeholder banner image

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:

  1. A descriptive <title> and meta description, unique per post.
  2. OpenGraph and Twitter tags, plus an auto-generated social card image.
  3. JSON-LD structured data — BlogPosting for articles, VideoObject for clips.
  4. A sitemap.xml, an rss.xml feed, and a robots.txt that explicitly welcomes AI crawlers like GPTBot and ClaudeBot.
  5. An llms.txt index 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.