How I Built a Static Blog with 50+ Articles and Zero Frameworks: Pure HTML, CSS, and JavaScript
Every few months, someone asks me the same question: "What framework is your blog built with? Next.js? Astro?" The answer usually surprises them: none. My blog, wuhuajin.com, is a fully static site built with pure HTML, CSS, and vanilla JavaScript — no React, no Vue, no build framework. It has over 50 published articles, and it has held near-perfect PageSpeed scores for over a year.
This is the story of why I went framework-free, how the architecture actually works, and the specific trade-offs you should consider before doing the same.
Why I Dropped Frameworks for a Content Site
Frameworks solve real problems: state management, component re-rendering, routing, and code organization at scale. A blog doesn't have any of those problems. It has one job — serve text and images to readers as fast as possible.
Once I framed it that way, the case for a zero-framework blog became obvious:
- Speed is the feature. No bundle to download, no hydration to wait for, no runtime JavaScript between the user and the content.
- Maintenance is nearly zero. No dependency tree to keep updated, no breaking upgrades every six months, no lockfile churn.
- Total control. Every byte of HTML is mine. I decide how it's structured, how it's cached, and how it's crawled.
- Longevity. A plain HTML file from 2005 still renders today. I want this blog to outlive whatever the frontend ecosystem does next.
The catch: you lose the scaffolding that frameworks give you. So I built a small, custom toolchain to replace only the parts I actually needed.
The Architecture: Content in Markdown, Pages in HTML
The entire site follows one simple rule: write in Markdown, ship as static HTML. No content is rendered on the client — every article is prerendered into its own standalone HTML file at build time.
wuhuajin.github.io/
├── src/
│ ├── components/ # Reusable UI pieces (navigation, footer, sidebar, feed...)
│ ├── template/
│ │ └── posts/ # Article Markdown, organized by category (20+ categories)
│ ├── css/ # Third-party CSS (highlight.js themes, etc.)
│ └── js/ # Third-party JS (marked, highlight.js, APlayer)
├── scripts/ # Custom Node.js build scripts
├── server.js # Local dev server (port 8000)
├── index.html
└── package.json
The src/ directory is the source of truth. Each UI piece is a self-contained component — its own HTML fragment, CSS, and JavaScript. Nothing is shared through a bundler's module graph; components are simply assembled into pages by the build scripts.
The Content Pipeline: Markdown → Prerendered HTML
The heart of the system is the prerendering pipeline. Every article lives as Markdown with YAML front matter:
---
title: "My Article"
date: 2026-01-01
category: ["JavaScript"]
tag: ["es6", "async"]
description: "Hand-written meta description (never auto-overwritten)"
top: true
---
A custom Node.js script parses the front matter, renders the Markdown with marked, applies syntax highlighting with highlight.js, and writes out a complete standalone HTML page — including the table of contents, code-block toolbar, and reward card. The result is a page that renders perfectly even with JavaScript fully disabled.
Why prerender instead of rendering client-side?
- Crawlers see the full content. Googlebot gets the complete article in the initial HTML. There's no "render the page before indexing it" dependency.
- Instant first paint. The browser has nothing to do but paint what's already there.
- Resilience. If a CDN edge or a third-party script fails, the content is still on the page.
The Build System: Custom Node.js Scripts, No Bundler
Instead of Webpack, Vite, or esbuild, the build is a sequence of small, focused .cjs scripts:
| Script | What it does |
|---|---|
build-articles.cjs |
Prerender every Markdown article into posts/<category>/html/
|
build-feed.cjs |
Parse front matter → generate the homepage feed data.json
|
build-home.cjs |
Generate homepage data |
build-sitemap.cjs |
Auto-generate sitemap.xml from the feed |
build-links.cjs |
Build aggregation and resource-navigation pages |
add-front-matter.cjs |
Backfill missing front matter fields |
update-wordcount.js |
Count real words per article for the feed |
watch.cjs |
File watcher + live reload for development |
Each script is idempotent — running it twice produces no diff. That property alone has saved me hours of debugging. And because there's no bundler graph, a broken component fails loudly in the build output instead of as a subtle runtime error in the browser.
For development, a small server.js uses Node's native http module, chokidar for file watching, and a WebSocket connection for live reload. One command (npm run dev) builds once, starts the server at localhost:8000, and hot-reloads on change. No Vite, no webpack-dev-server, no 200MB node_modules.
Bilingual Without an i18n Library
The site is English-first with a one-click Chinese switch. Instead of a heavyweight i18n framework, it uses a lightweight dictionary: a plain JavaScript object mapping English keys to translated strings, plus a runtime function that swaps text nodes and lang attributes when the user switches.
The whole i18n layer is one component (src/components/i18n/) and a few hundred lines. It covers everything a blog actually needs — navigation, footer, buttons, and labels — without the 40KB+ payload of a full i18n solution.
The Performance Numbers
Zero framework doesn't automatically mean fast — you still have to do the work. Here are the results after 20+ rounds of optimization:
| Metric | Before | After | Improvement |
|---|---|---|---|
| Homepage HTML size | 57 KB | 34 KB | −40% |
| Social share image | 576 KB | 262 KB | −55% |
| CLS (layout shift) | 0.37 | < 0.1 | −73% |
| Render-blocking CSS | 11 KB | Async loaded | Eliminated |
| Render-blocking JS | 13 files | All defer
|
Eliminated |
The techniques are mostly boring and reliable: defer on every script, preconnect to the five third-party origins the site talks to, explicit width/height on every image, aspect-ratio locked on cover containers, lazy loading below the fold, and non-critical CSS loaded asynchronously. The biggest single win was prerendering — because the HTML already contains everything, there's almost nothing left to block rendering.
When a Zero-Framework Blog Makes Sense (and When It Doesn't)
Do this if:
- Your site is content-first — articles, documentation, product pages.
- You're comfortable maintaining a few hundred lines of build scripts.
- You want maximum performance with a tiny attack surface.
- You plan to run the site for years, not months.
Don't do this if:
- You need complex interactivity — dashboards, real-time collaboration, or app-like behavior. That's what frameworks are for.
- You want a large team contributing without learning your custom conventions.
- You value ecosystem leverage — prebuilt components, plugins, and hiring familiarity — over raw control.
There's no shame in using a framework. The point is to choose based on what your site actually needs, not on what's popular.
What I'd Do Differently
Two things, looking back. First, I'd write the build scripts with better error messages from day one — "fail loudly" beats "fail mysteriously" every time. Second, I'd add automated checks earlier: a simple script that verifies every article has a meta description and every image has dimensions before deploy. Both would have saved me real debugging time.
This article is based on the real-world architecture of wuhuajin.com, a zero-framework static blog with 50+ articles running on GitHub Pages and Cloudflare CDN. If you're weighing framework-free options for your own content site, feel free to reach out.
Originally published on wuhuajin.com.
Top comments (0)