The Routing Paradox: Debugging Static Site Architectures
The Routing Paradox: Debugging Static Site Architectures
What started as a simple task—"Serve a few HTML files locally"—turned into a masterclass on web server behavior, routing conflicts, and the importance of clean architecture.
In this post, I’ll dissect a frustrating "Endless Redirect Loop" issue we faced while deploying this portfolio, and how we solved it not by writing more code, but by restructuring our folders.
The Setup
We have a simple Static Site:
index.html(Home)blog.html(The Blog Page)blog/(A folder containing Markdown files and JSON data)
The Problem: "Smart" Servers vs. Reality
We initially used Vercel's serve package. It’s a fantastic, modern tool that defaults to "Clean URLs." This means if you visit /blog, it automatically tries to serve blog.html and hides the .html extension.
However, we also had a folder named /blog.
The Conflict
When a user (or the browser) requested http://localhost:3000/blog:
- The File Match: The server saw
blog.htmland said, "Aha! Serve this file." - The Folder Match: The server also saw the
/blogdirectory and said, "Wait, this is a folder. I should list its contents (Directory Listing) or look for an index.html inside it."
This ambiguity caused chaos.
- Production Environment (Nginx) treated it one way (404 Not Found, needing explicit extensions).
- Local Environment (serve) tried to be smart, continuously stripping
.htmlextensions, causing a redirect loop when we tried to force them back for Nginx compatibility.
Attempt 1: Configuration Hell
We tried to patch this by configuring the server (serve.json). We attempted to force rewrites, disable clean URLs, and manually map routes.
// The "Patchwork" Approach
{
"cleanUrls": false,
"rewrites": [
{ "source": "/blog", "destination": "/blog.html" }
],
"directoryListing": false
}
Why it failed: It was fragile. Every time we fixed one edge case (e.g., local routing), we broke another (e.g., production logic or query parameters). We were fighting the tool's default nature.
Attempt 2: Changing the Tool
We switched to http-server, a simpler, "dumber" tool.
npx http-server . -c-1
This helped because it didn't try to strip extensions. But the core problem remained: The Naming Collision. Even a dumb server gets confused when a file and a folder share the same name stem contextually.
The Real Solution: Architectural Cleanup
The root cause wasn't the server software; it was our data structure. Having data files (blog/posts.json) inside a folder named the same as a primary page (blog.html) is an architectural anti-pattern.
We solved it by moving the data to where it belongs: an assets folder.
Before (Ambiguous)
/root
├── blog.html <-- The Conflict
└── blog/ <-- The Conflict
└── posts.json
After (Clean)
/root
├── blog.html <-- Unique Entry Point
└── assets/
└── data/
└── blog/ <-- Isolated Data
└── posts.json
Conclusion
We spent hours trying to configure routing rules, but the fix took 30 seconds of moving files.
The Lesson: If you find yourself fighting your tools (writing complex configs for simple tasks), stop. You likely have an architectural conflict, not a configuration error. Separate your Content (HTML) from your Data (JSON/MD).