Architecture January 28, 2026

The Routing Paradox: Debugging Static Site Architectures

By Kahraman Bayraktar

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:

  1. The File Match: The server saw blog.html and said, "Aha! Serve this file."
  2. The Folder Match: The server also saw the /blog directory 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 .html extensions, 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).

Enjoyed the read?

Let's Talk Concepts
KB