...
Back

Your 404 Is a Routing Bug, and the Index Page Is Where It Hides

Localized routes break in a specific place: the page with no slug. Everything under it works, which is exactly why nobody finds it.

Your 404 Is a Routing Bug, and the Index Page Is Where It Hides

Your 404 Is a Routing Bug 🚧

A pattern we have now hit twice, in two different codebases, with the same root cause:

Every article page works. Every document page works. The index of that section 404s, and only in the non-default language.

It survives review because reviewers click links. Links go to articles. Nobody types the bare section URL, so nobody sees it until a user does — or until someone shares the section landing page and it lands on a 404.


Why the index specifically

Localized content routing usually looks something like /[lang]/docs/[[...slug]], with content files keyed by a path that already encodes the language: docs/en/getting-started, docs/zh/getting-started.

Lookup for an article is unambiguous — you have a language and a slug, you join them, you find the file.

The index is where it falls apart, because the index has no slug, and the code that computes "the path of this document" has to produce something for a document that is only a language. Two things then collide:

  1. In the default locale, the framework often omits the language prefix entirely, so the index's computed path is the empty string.
  2. In every other locale, the index's computed path is the language code itselfzh, not zh/ and not empty.

Write the lookup for one of those cases and the other 404s. Write it for articles only and both 404. The bug is not in the route; it is in the one input value that has a different shape from all the others.

The shape of the fix:

// The slug is empty: we are on the section index.
if (!slugPath) {
  // Non-default locales: the index document's path *is* the locale.
  if (lang !== defaultLocale) {
    return docs.find((d) => d.published && d.slugAsParams === lang) ?? null;
  }
  // Default locale: the prefix is stripped, so the index path is "".
  return docs.find((d) => d.published && d.slugAsParams === "") ?? null;
}

Seven lines. The work was never in writing them.


The general class

This is an instance of something broader, worth naming because it recurs far outside routing:

The empty case has a different shape than the general case, and the general case was written first.

Path joining where one component is empty. Pagination where the first page has no cursor. A filter list where "no filters" is not an empty filter object but an absent one. A breadcrumb trail of length zero. In each, the code handles N and quietly mis-handles 0, and 0 is rare enough in testing to go unnoticed and common enough in production to matter.


How to actually catch it

Three things, cheapest first.

Enumerate your routes and request them. If your content pipeline can list documents, it can generate the URL list, and a loop over that list with a status check is a smoke test that would have caught both of our occurrences. Crucially it must include the section roots, not just the documents — which means generating them deliberately, since they are not documents in the same way.

for url in $(cat routes.txt); do
  code=$(curl -s -o /dev/null -w '%{http_code}' "$BASE$url")
  [ "$code" = "200" ] || echo "FAIL $code $url"
done

Test the boundary, not the middle. For any list-shaped input, the cases that find bugs are zero, one, and the one with a different shape from its neighbors. The middle of the range almost never does.

Check every locale, not just two. A bug that depends on "default vs non-default" shows up if you check English and one other. A bug that depends on a specific locale's own quirk — a region subtag, a script variant, a language whose code is a prefix of another — only shows up if you check all of them. Enumerating is cheap; sampling is what lets these through.


The part worth internalizing

When one page in a section 404s and the rest work, do not start by reading the route handler top to bottom. Ask what is different about the input for that one page. In localized routing, the answer is almost always that its slug is empty, or is the locale, and the lookup was written for neither.