I Rebuilt My Blog in Vanilla JS and Deleted 847 Dependencies

Six months ago I was shipping a personal blog that required a 3-second CI compile step, crashed on Node 21, and had a node_modules folder with 847 packages for a site that shows text on a screen.

My old blog was built with Next.js 14. I'd followed the standard tutorial, set up MDX for posts, added a few Tailwind classes, and called it done. It worked fine. But every time I wanted to write a post I had to run npm run dev, wait for the dev server to warm up, and deal with occasional hot-reload bugs where my changes didn't show up until I restarted.

The final straw was a security advisory that required updating a transitive dependency I'd never heard of, which bumped another transitive dependency, which broke the build. I spent 45 minutes fixing an issue in a package I don't depend on, for a blog with three posts.

The rewrite

I decided to go back to basics. The requirements for a personal blog are genuinely simple: render HTML with some styling, load fast, and let me write in Markdown without thinking about it. Everything else is optional.

The new stack is exactly this:

  • Static HTML files, written by hand or generated by a 60-line Node.js script
  • One CSS file with custom properties
  • Vanilla JS for the dark mode toggle and reading progress bar
  • No build step — serve . and it works

For Markdown, I use a 60-line build.js script that reads .md files and wraps them in an HTML template. No webpack. No Babel. No TypeScript compiler. No Vite.

import { readFileSync, writeFileSync, readdirSync } from 'fs';
import { marked } from 'marked'; // the one dependency I kept

const template = readFileSync('./src/template.html', 'utf8');

for (const file of readdirSync('./posts').filter(f => f.endsWith('.md'))) {
  const md = readFileSync(`./posts/${file}`, 'utf8');

  // Parse frontmatter
  const [, meta, body] = md.match(/^---\n([\s\S]+?)\n---\n([\s\S]+)$/m);
  const frontmatter = Object.fromEntries(
    meta.split('\n').map(l => l.split(': '))
  );

  const html = template
    .replace('{{TITLE}}',   frontmatter.title)
    .replace('{{CONTENT}}', marked.parse(body))
    .replace('{{DATE}}',    frontmatter.date);

  writeFileSync(`./dist/${file.replace('.md', '.html')}`, html);
  console.log(`built: ${file}`);
}

That's the entire build pipeline. It runs in 200ms. It never breaks. I have one devDependency: marked, which is a Markdown parser with no dependencies of its own.

What I lost

Let me be honest about the tradeoffs. I lost:

Hot reload. I now run build.js && browser-sync in a watch script. It's basically the same, just more explicit. The reload takes 250ms instead of "sometimes instant, sometimes 3 seconds."

JSX and component reuse. I have HTML partials now — a nav component is a string in a components.js file that I interpolate into templates. It's not as ergonomic as JSX, but I have three pages. If I had 50 pages I might reconsider.

TypeScript. I use JSDoc types in my build script for IDE autocomplete. It's not perfect but for 60 lines of build code I don't need a compiler.

Tree shaking. Not relevant when you have no dependencies to tree-shake.

What I gained

The performance improvement was immediate and dramatic. My Lighthouse score went from 82 to 99. Time to First Byte dropped from ~400ms (because Next.js was doing SSR on Vercel cold starts) to ~30ms (static file CDN). My largest contentful paint dropped from 2.1s to 0.4s.

More importantly: I understand everything in my own blog now. If something breaks, I can fix it in 5 minutes because there are only two files that could be broken. The cognitive overhead is zero. I can write a post, push it, and it's live in 30 seconds.

Who this is for

I'm not arguing everyone should build their blog this way. If you're building a blog with 500 posts, multiple authors, a CMS integration, and server-side search, then yes, use a framework — they exist for good reasons.

But a lot of personal blogs are 5-20 posts. For that scale, the dependency overhead of modern static-site generators and JavaScript frameworks is genuinely a liability that outweighs the benefits. You'll spend more time managing the toolchain than writing posts.

The platform has gotten good. ES modules work natively. CSS custom properties, grid, and container queries handle most layout needs without a utility library. fetch, IntersectionObserver, and the History API cover most interaction patterns. The baseline is surprisingly high.

Sometimes the right tool is just a text file.