How I Built a Blazing Fast Blog in a Weekend
Disclaimer & Intro
This post has been made as my notes, even though I attempt to explain what I have built and how, I do not owe anyone any explanation. Do NOT expect anything.
My blog is my garden.
WARNING: No AI was used in the making of this project. Not one token. Just source code, RFCs, and a free weekend.
Most people building personal sites in 2026 reach for a framework before they reach for a spec. The result is a React app serving a blog that publishes twice a year, pulling in 40MB of node_modules to render static HTML. If that describes you, this post is the target audience.
Why I Killed BearBlog
BearBlog is fine. I have nothing against it. But it has one problem that eventually becomes intolerable: you do not own the pipeline. You cannot set your own HTTP headers. You cannot control the caching behaviour. You cannot touch the build process. You are renting, and pretending otherwise.
I kept running into things I could not fix. Font loading I did not choose. Cache headers I did not set. Security headers that were not there at all. None of this is hard to fix. It just requires owning the code.
So one Saturday morning I opened a blank terminal and rebuilt the whole thing from scratch. No AI. No frameworks. No npm install. Zero external dependencies in production. The entire generator fits in three files. The full build runs in under 500ms for any number of posts I will realistically ever write.
What I Actually Read
I want to be specific about this because "reading the spec" needs to mean something concrete, not a vibe.
TCP slow start and the 14kB rule — RFC 5681 defines TCP congestion control. The initial congestion window is 10 segments × 1460 bytes ≈ 14kB. If your initial HTML fits in that window, the browser can start rendering before the second round trip happens. This is not a recommendation. It is physics.
HTTP caching — RFC 9111 (the 2022 revision). Most people copy Cache-Control headers from Stack Overflow without understanding the difference between no-cache and no-store. They are not the same thing. no-cache means revalidate before serving. no-store means never store at all. The stale-while-revalidate extension is now proper IETF, not a Chrome experiment — serve the stale response immediately, revalidate in the background. For a blog, this is free performance.
HSTS — RFC 6797. Once you understand SSL stripping attacks (Section 11 of the RFC), the preload list stops being optional. Browsers that have your domain in the preload list will never attempt HTTP at all — not redirect, not downgrade. Never attempt.
Content Security Policy — W3C CSP Level 3. default-src 'none' with explicit allow-lists is the only baseline that means anything. If you have unsafe-inline or unsafe-eval in production and you think you have a CSP, you do not have a CSP.
CSS Containment — W3C CSS Contain Level 2. content-visibility: auto tells the browser to skip layout and paint for off-screen elements entirely. Combined with contain-intrinsic-size to give the browser a size hint, scrollbars stay accurate and the browser does less work. Not a trick. The spec.
None of this is secret. It is all public. It just requires reading instead of watching a YouTube tutorial.
The Architecture (Three Files, Zero Deps)
src/generator.mjs — the static site generator. ~350 lines. Uses only Node.js built-ins: fs, path, http. No Vite, no webpack, no Rollup, no Babel, no PostCSS, no nothing. It reads Markdown from /posts, runs it through the parser, injects it into an HTML template, and writes static files to /public. Build time for 100 posts: under 500ms. Usually closer to 100ms.
src/markdown.mjs — the Markdown parser. Zero deps. Handles everything I actually use: headings, fenced code blocks with language tags, tables, blockquotes, inline formatting, raw HTML passthrough. Processes a 5kB post in under a millisecond.
src/templates.mjs — HTML templates. All CSS inlined as a template literal. RSS feed, Atom feed, sitemap generators. The entire CSS is ~3kB.
That is the whole thing. No node_modules. No package-lock.json. The supply chain attack surface is zero because there is no supply chain.
The 14kB Budget
The CI pipeline enforces a hard size limit:
MAX_BYTES=14336 # 14kB
for html in public/index.html public/blog/index.html; do
size=$(wc -c < "$html")
if [ "$size" -gt "$MAX_BYTES" ]; then
echo "FAIL: $html is ${size} bytes (limit: ${MAX_BYTES})"
exit 1
fi
done
Not aspirational. Enforced. The build fails if the constraint is violated. Current numbers:
| Page | Size |
|---|---|
index.html | 4.8 kB |
blog/index.html | 6.2 kB |
| Full CSS (inlined) | ~3 kB |
Everything — the HTML shell, the full CSS, every font-face declaration, the navigation — fits in the first TCP window. The browser starts rendering before it has even acknowledged the first packet.
The Security Headers
Cloudflare Pages reads a _headers file at the repo root. This is where HTTP security headers belong — not in HTML meta tags, not in JavaScript, not in your .htaccess copypasta from 2009.
/*
# HSTS — max 1 year, include subdomains, opt into preload list
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# Prevent clickjacking
X-Frame-Options: DENY
# Stop MIME sniffing
X-Content-Type-Options: nosniff
# Minimal referrer info to third-parties
Referrer-Policy: strict-origin-when-cross-origin
# Disable all sensitive browser features
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=(), accelerometer=(), autoplay=(), gyroscope=(), magnetometer=(), midi=(), screen-wake-lock=(), xr-spatial-tracking=()
Content-Security-Policy: default-src 'none'; script-src 'self' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://bear-images.sfo2.cdn.digitaloceanspaces.com; font-src https://bear-images.sfo2.cdn.digitaloceanspaces.com; connect-src https://cloudflareinsights.com https://xer0x.in/cdn-cgi/; media-src 'none'; object-src 'none'; frame-src 'none'; frame-ancestors 'none'; worker-src 'none'; form-action 'self'; base-uri 'none'; manifest-src 'none'; navigate-to 'self'; upgrade-insecure-requests
# Cross-Origin Isolation — prevents other origins getting a reference to this browsing context (Spectre mitigation)
Cross-Origin-Opener-Policy: same-origin
# Cross-Origin Resource Policy — restricts who can load resources from this origin
Cross-Origin-Resource-Policy: same-origin
# Cross-Origin Embedder Policy — blocks cross-origin resources that don't explicitly opt in (enables full isolation)
Cross-Origin-Embedder-Policy: require-corp
# Default cache: no-cache for HTML
Cache-Control: no-cache
Feature-Policy: camera 'none'; microphone 'none'; geolocation 'none'; payment 'none'; usb 'none'
X-DNS-Prefetch-Control: off
Expect-CT: max-age=31536000, enforce
A few things worth explaining:
script-src 'none' or 'self' (cause CF likes to inject some) — this site has zero JavaScript in production. Declaring it in CSP means any injected script is blocked at the browser level, not just absent. That distinction matters. A missing script tag can be added by an attacker. A CSP directive cannot be overridden from within the page.
font-src pointing at my DigitalOcean CDN — I self-host the Comic Code Ligatures fonts I was already using on BearBlog. No Google Fonts. No third-party DNS lookup on every page load. No external timing attack surface. No GDPR implications. Assets served from a domain I control, cached for a week.
Cross-Origin-Opener-Policy: same-origin — a Spectre mitigation. It prevents other origins from getting a reference to your browsing context. Most developers have never heard of it. It costs nothing and you should always set it.
Result: A+ on securityheaders.com. A+ on Mozilla Observatory.
The CI/CD Pipeline
.github/workflows/deploy.yml on every push to main:
- Checkout
node src/generator.mjs— builds the site in under 500ms- 14kB size budget check — fails the build if violated
- HTML structure validation — checks
langattribute, viewport meta - Upload artifact
wrangler pages deploy— ships to Cloudflare's edge
Content workflow: write a .md file, put it in /posts/, commit to main. Pipeline runs. Site updates. No dashboard. No CMS login. No vendor portal.
The Numbers
| Metric | Result |
|---|---|
| Lighthouse Performance | 100 |
| Lighthouse Accessibility | 100 |
| Lighthouse Best Practices | 100 |
| Lighthouse SEO | 100 |
| Security Headers | A+ |
| Mozilla Observatory | A+ |
| Build time (100 posts) | < 500ms |
| npm dependencies (production) | 0 |
| Initial HTML payload | < 6 kB |
The scores are not gamed. They are a consequence of the architecture being correct — zero render-blocking resources, all CSS inlined, no external fonts that can fail, no JavaScript runtime, correct semantic HTML, proper security headers, valid sitemap and RSS feeds.
What Most People Get Wrong
They confuse performance with optimization. Optimization is what you do when your architecture is wrong and you need to paper over it. You do not need to optimize a 5kB HTML file. You need to not ship a 2MB JavaScript bundle in the first place.
The industry has convinced itself that React + Next.js + Vercel is the default for any web property. A blog. A documentation site. A marketing page. Things that are, by definition, static. And then they spend weekends fighting hydration mismatches and bundle size and wondering why their Lighthouse score is 60.
HTML is a document format. CSS is a styling language. Both are understood by every browser since 1995. They do not require a runtime. They do not require a build step that takes 40 seconds. They render directly. This is not controversial — it is just not fashionable.
The real performance gains came from reading the specs, not from running Lighthouse audits in a loop.
Understanding TCP slow start gave me the 14kB target. Understanding RFC 9111 gave me stale-while-revalidate. Understanding CSP Level 3 gave me a header that actually means something. Understanding CSS Containment told me the browser was doing work I never asked for.
None of this is gatekeeping. It is just engineering.
References
- RFC 5681 — TCP Congestion Control
- RFC 9111 — HTTP Caching
- RFC 6797 — HTTP Strict Transport Security
- RFC 9000 — QUIC Transport Protocol
- W3C CSS Containment Level 2
- W3C Content Security Policy Level 3
- tunetheweb — Critical Resources and the First 14kB
- endtimes.dev — Why Your Website Should Be Under 14kB
- Jake Archibald — F1 Performance Series
Final Word
A weekend project is not impressive. What is impressive is that the industry convinced people they need a framework, a bundler, a proprietary runtime, a headless CMS, and a CI/CD tutorial from YouTube just to put text on the internet.
You do not. You need to read the specs. Understand what the browser actually does. Write the simplest thing that satisfies the requirements — then measure it.
Engineering is measurable. Measure it.
Jai Hind.
Wake up. Dig deeper.
