Building This Blog From Scratch

Every developer eventually builds their own blog. It's practically a rite of passage. The interesting part isn't the blog itself — it's the decisions made along the way.

Here's how this one was built, and why.

The requirements

Before picking tools, I wrote down what I actually needed:

  • Easy to write in — no CMS, no dashboards, just files I can edit in my editor
  • Fast to serve — static HTML, no server-side rendering, no runtime
  • Easy to deploy — the output should be a folder I can drop anywhere
  • Easy to understand — if something breaks at 11pm, I want to fix it in minutes, not hours

That last point ruled out a lot of things.

Why not an existing SSG?

The obvious choices were Hugo, Eleventy, and Astro. All are excellent. All would have worked. But each comes with tradeoffs.

Hugo is fast and has great defaults, but it's written in Go. Customising the build pipeline means learning Go templating and Hugo's conventions — a non-trivial investment for a personal blog.

Eleventy is the most flexible JavaScript SSG and has a healthy ecosystem. But flexibility has a cost: there are five ways to do everything, and the docs reflect that. Configuration sprawl is real.

Astro is impressive for component-heavy sites. It's overkill for a text blog, and it pulls in a lot of tooling that doesn't pay for itself here.

The alternative: write a build script. A static site generator is just a program that reads some files and writes some other files. For a blog, that's not much code.

const posts = loadPosts();         // read markdown from posts/
buildPosts(template, posts);       // write public/posts/slug/index.html
buildIndex(template, posts);       // write public/index.html
buildTagPages(template, posts);    // write public/tags/tag/index.html
buildRSS(posts);                   // write public/feed.xml
buildSitemap(posts, tagMap);       // write public/sitemap.xml
buildSearchIndex(posts);           // write public/search-index.json

That's the entire pipeline. It runs in under a second.

The stack

Markdown + frontmatter

Posts are Markdown files with a YAML header:

---
title: My Post Title
date: 2026-04-03
description: Shown on the home page and in search results.
tags: [tag1, tag2]
draft: true   # omit from build until ready
---

Post content here...

gray-matter parses the header. marked converts the Markdown body to HTML. Both are small, well-maintained, and do exactly one thing.

Syntax highlighting

highlight.js runs at build time inside the marked renderer:

marked.use({
  renderer: {
    code({ text, lang }) {
      const validLang = lang && hljs.getLanguage(lang);
      const highlighted = validLang
        ? hljs.highlight(text, { language: lang }).value
        : hljs.highlightAuto(text).value;
      return `<pre><code class="hljs">${highlighted}</code></pre>`;
    },
  },
});

The key decision here: highlighting happens at build time, not in the browser. The page ships pre-highlighted HTML. No JavaScript bundle, no flash of unstyled code on load.

Search

Client-side search using Fuse.js. At build time, a search-index.json is generated containing the title, description, tags, and body text of every post. The search page fetches this file and queries it locally.

The tradeoff: the index grows with the number of posts. For a personal blog that likely never exceeds a few hundred posts, this is fine — the index will stay well under a megabyte. At larger scale, you'd want Algolia or a dedicated search service.

A single HTML template

There's one template.html file. It uses {{variable}} placeholders filled in by a small render() function:

function render(template, vars) {
  return template
    .replace(/\{\{#if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
      (_, key, inner) => vars[key] ? inner : '')
    .replace(/\{\{(\w+)\}\}/g,
      (_, key) => vars[key] ?? '');
}

No templating library. Thirty characters of regex. This is either elegant or reckless depending on your perspective — but it's also completely transparent and trivially debuggable.

The pros

You own everything. There's no abstraction between you and the output. Want to change how tags render? Edit build.js. Want to add an /about page? Add a function that writes public/about/index.html. The mental model never gets complicated.

The output is pure HTML. No JavaScript frameworks, no hydration, no virtual DOM. Each page is a self-contained HTML file. It loads fast on any connection and works without JavaScript (except search and the theme toggle).

Zero runtime dependencies. Once built, the public/ folder is inert. It can be served by anything: Nginx, Cloudflare Pages, GitHub Pages, an S3 bucket, a Raspberry Pi.

Trivially fast builds. The full build — including syntax highlighting, RSS, sitemap, tag pages, and search index — completes in under a second. No caching, no incremental builds, no build graphs. Just run it.

The cons

No ecosystem. With Hugo or Eleventy you get themes, plugins, and community solutions to common problems. Here, if you want something, you write it.

Scaling has a ceiling. The template system is intentionally simple. A site with complex layouts — per-section templates, nested partials, dynamic data sources — would outgrow this quickly. At that point, reaching for Eleventy or Astro makes sense.

No incremental builds. Every build rebuilds everything. For hundreds of posts this is still fast, but it won't scale to tens of thousands.

You maintain it. Dependencies get outdated. Marked will have a breaking change eventually. You're responsible for keeping things working.

What was built

Beyond the basic post-to-HTML pipeline, the blog has:

  • Tag index pages at /tags/tag/ listing all posts with that tag
  • Prev/Next navigation between posts
  • RSS feed at /feed.xml for feed readers
  • Sitemap at /sitemap.xml for search engines
  • Open Graph meta tags for link previews on social platforms
  • Reading time estimates calculated from word count
  • Dark/light mode toggle with prefers-color-scheme as the default, persisted in localStorage
  • Scroll progress bar on post pages
  • Copy buttons on code blocks
  • Draft support — add draft: true to frontmatter to exclude a post from builds
  • Live reload dev server that rebuilds on file changes and reloads the browser via Server-Sent Events

All of this fits in one build script, one template, one CSS file, and one dev server. The entire source — excluding node_modules — is a handful of files.

The verdict

For a personal tech blog, building your own is a reasonable choice. The scope is small enough that the custom code stays manageable, and you get exactly what you want without negotiating with a framework.

The moment your requirements grow beyond what fits in a few hundred lines, that calculus changes. But that's a good problem to have.