Why I wrote this: When I edit aijsons.com, every page uses absolute paths like /css/styles.css and /js/navbar.js. Opening index.html directly in a browser breaks navigation. Live Server and python -m http.server fix that — but they do not simulate Cloudflare redirect rules, trailing-slash normalization, or the security headers in our _headers file. I pushed a blog card link to a slug that existed only in sitemap-blog.xml, not on disk, and nothing failed locally because I never clicked that card. Docker gave me a reproducible local environment that behaves enough like production to catch those mistakes before git push.
What Local Preview Must Simulate
A static site on GitHub Pages + Cloudflare is not just HTML files in a folder. For aijsons.com, production behavior includes:
- Directory URLs with trailing slashes —
/blog/not/blog - Content redirects — merged tool pages like
/tools/json-diff/→/tools/json-compare/ - Security headers —
X-Content-Type-Options,Referrer-Policy, etc. from_headers - Correct MIME types —
ads.txtmust serve astext/plain, nottext/html - SPA-style tool pages — client-side JS that assumes it runs under
/tools/json-formatter/, notfile:///D:/...
A bare static file server satisfies only the last item partially. That gap is why I run nginx inside Docker for final manual QA.
Why Not Just Use Live Server?
VS Code Live Server is excellent for iterative CSS work. I still use it daily. But it cannot:
- Parse Cloudflare
_redirectssyntax (301 chains, splat patterns,200!force rules) - Apply per-path cache and security headers from
_headers - Simulate the apex → www redirect at the DNS edge
- Run the same port and hostname assumptions as production (
www.aijsons.com)
Real incident: I tested a redirect rule with pattern /tools/json-diff (no trailing slash). Cloudflare Pages matched paths differently than I expected; the live site returned 404 while my local Live Server test looked fine because I manually navigated to the destination URL. A local nginx config that mirrors the redirect table would have caught it immediately.
The Docker Setup
Two files in the repo root are enough. I keep them in a docker/ folder so they do not clutter the GitHub Pages root.
docker-compose.yml
services:
preview:
image: nginx:1.25-alpine
ports:
- "8080:80"
volumes:
- ..:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./redirects.map:/etc/nginx/redirects.map:ro
nginx.conf (minimal production-like behavior)
worker_processes 1;
events { worker_connections 256; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
map_hash_bucket_size 128;
map $request_uri $redirect_target {
include /etc/nginx/redirects.map;
default "";
}
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Security headers (subset of _headers)
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
# ads.txt content type
location = /ads.txt {
default_type text/plain;
}
# Apply redirect map
if ($redirect_target != "") {
return 301 $redirect_target;
}
# Trailing slash for directories
location ~ ^(/[^.]*[^/])$ {
try_files $uri $uri/ $uri/index.html @slash;
}
location @slash {
return 301 $uri/;
}
location / {
try_files $uri $uri/ $uri/index.html =404;
}
}
}
redirects.map (maintained manually from _redirects)
I translate the highest-risk rules from _redirects into nginx map entries — not all 90+ rules, just the ones I am actively changing:
"/tools/json-diff/" "/tools/json-compare/";
"/tools/json-diff" "/tools/json-compare/";
"/blog/json-formatter-guide-developers/foo/" "/blog/json-formatter-complete-guide-2026/";
Full automation is possible (a script that converts Cloudflare syntax to nginx), but for a solo-maintainer site, translating 5–10 rules before each deploy takes two minutes and prevents the most common regressions.
Running the Preview
cd docker
docker compose up
# Open in browser
# http://localhost:8080/
# http://localhost:8080/tools/json-formatter/
# http://localhost:8080/tools/json-diff/ → should 301 to json-compare
Verify redirects with curl the same way I test production:
curl -sI http://localhost:8080/tools/json-diff/ | findstr /i "HTTP location"
curl -sI http://localhost:8080/ads.txt | findstr /i "content-type"
On Linux/macOS, swap findstr for grep -i.
Three-Layer Local Workflow
I do not replace Live Server — I stack three layers with different jobs:
| Layer | Tool | Purpose |
|---|---|---|
| 1. Fast edit | Live Server / python -m http.server | CSS, tool UI, instant reload |
| 2. Structure check | GitHub Actions CI | Sitemap ↔ disk, XML validity, redirect count — see our CI guide |
| 3. Pre-push QA | Docker + nginx | Redirects, headers, directory URLs, ads.txt MIME |
Layer 2 catches missing files. Layer 3 catches routing behavior. Together they prevented a repeat of the sitemap/blog-index drift incident described in our deployment guide.
What Docker Preview Still Cannot Simulate
Be honest about limits — overstating local parity creates false confidence:
- Cloudflare Worker trailing-slash logic (
worker-redirect-v2.js) runs at the edge, not in nginx. I still verify Worker changes in Cloudflare's dashboard preview or staging route. - Cache behavior —
Cache-Control: immutableon JS/CSS only shows up in DevTools after a real Cloudflare fetch. - GitHub Pages build quirks — Jekyll is disabled via
.nojekyll, so this is a non-issue for us, but other static hosts may inject build steps. - AdSense ad slots — Google does not serve real ads on
localhost; I only verify the ad script loads without console errors.
Alternative: Caddy with the Cloudflare Adapter
If you already use Caddy, the cloudflare snippet community projects parse a subset of _redirects directly. For aijsons.com I stayed on nginx because the map directive is explicit — when a redirect fails, I can read one file and know exactly what nginx evaluated. Fewer magic layers means faster debugging at 11pm before a deploy.
Checklist Before Every Deploy
- Run CI locally or wait for GitHub Actions green on the PR
docker compose up— click every new blog card and tool link added in this commitcurl -sIon any new or changed redirect rule- Confirm
/ads.txtreturnstext/plainwith publisher ID intact - Check browser console on one tool page — no 404s for
/js/or/css/assets
This checklist takes about eight minutes. The deploy that skips it is the one that sends me back to Search Console fixing crawl errors.
Key Takeaways
- Absolute-path static sites need an HTTP server locally — never rely on
file://testing. - Live Server is for speed; Docker + nginx is for production-like routing and headers.
- Maintain a small
redirects.mapsynced with your highest-risk_redirectsrules. - Pair local Docker preview with CI sitemap checks — structure and behavior are different failure modes.
- Know what local preview cannot simulate (Cloudflare Workers, edge cache, real ads).