---
title: The MDX-loader bug that shipped for months
description: How a single-character typo (singular vs plural directory names) silently 404ed every MDX-backed dynamic route on niyra.ai for months — and the small fix that brought it all back.
url: /blog/the-mdx-loader-bug-that-shipped-for-months
lastUpdated: 2026-06-11
---

# The MDX-loader bug that shipped for months


I noticed last week that `/features/memory` returned 404 in production. Not "I just deployed something that broke" — I mean, it had been 404ing for *months*.

This is the postmortem.

## What the bug was

`web/src/lib/mdx.ts` is the loader for MDX-backed content. It iterates a `ContentType` enum (`"feature"`, `"doc"`, `"persona"`, etc.) and tries to read the matching directory from `web/src/content/`. The relevant line:

```ts
const dirs = [
  path.join(CONTENT_DIR, t),                  // bug: t = "feature"
  path.join(CONTENT_DIR, "generated", t),
];
```

`t` was the singular enum key. But the actual content directory was `features/` (plural). So the loader scanned `web/src/content/feature/`, which didn't exist, found 0 pages, and returned silently.

`features/[slug]/page.tsx` called `getAllPages("feature")` and got `[]`. `generateStaticParams` returned no params. Every URL like `/features/memory` had no matching pre-rendered route and fell through to 404.

## Why it shipped

Three reasons stacked:

**The loader never threw.** `walk()` had `if (!fs.existsSync(dir)) return;` — designed to be tolerant of missing directories during early bootstrap. The cost of tolerance was that a real bug masqueraded as "no content yet, that's fine."

**No CI test pinned the contract.** Build succeeded. Lighthouse never ran on the affected URLs. Sitemap generation gracefully degraded. Nothing on the CI side asked "does `getAllPages("feature")` return more than zero rows?"

**The hub pages worked.** `/features` (the hub) was a static page with hand-written content, not driven by the loader. So when you clicked around the site, the hub looked fine. The 404s lived in the dynamic-slug layer, which is hard to notice without explicitly visiting `/features/memory`.

## How it surfaced

I was building a vector-search indexer that walks every MDX file and embeds it into pgvector. The first dry run reported "found 0 pages." I assumed the indexer was broken. After ten minutes of debugging the indexer, I realized the loader had the same bug — and the indexer was just faithfully reproducing it.

`curl https://niyra.ai/features/memory` confirmed: 404. In production.

## The fix

```ts
const DIR_NAME: Record<ContentType, string> = {
  feature: "features",
  doc: "docs",
  channel: "channels",
  persona: "personas",
  painState: "pain-states",
  // ... rest
};

const dirs = [
  path.join(CONTENT_DIR, DIR_NAME[t]),
  path.join(CONTENT_DIR, "generated", DIR_NAME[t]),
];
```

A one-time mapping from enum key to actual directory name. Same fix mirrored in the indexer script.

After deploy: `/features/memory` returned 200. Same for `/docs/quickstart` and every other MDX-backed page. Twenty or so pages came back online in the time it took Vercel to deploy.

## The honest lesson

This wasn't a hard bug. It was a tolerant-loader-meets-tolerant-CI bug. The right fix going forward isn't "be more careful with directory names." It's:

1. **Hard-fail when a known type has zero pages.** If the loader iterates `ContentType` and finds 0 entries for a type that should have content, throw at build time. The runtime cost of an extra check is nothing compared to silently shipping a 404.
2. **Add a CI test that hits the dynamic routes.** A 3-line test that fetches `/features/memory` and asserts 200 would have caught this in PR.

Neither was hard. Neither was prioritized. Both are now done.

## What it cost us

Hard to say exactly — we don't have rank-tracking on these URLs from before today. Best estimate: a few months of lost discoverability on the feature pages and a meaningful chunk of "Niyra docs" SEO. Search Console will tell us the recovery curve over the next two weeks.

The lesson I'm keeping: tolerant systems are great until tolerance becomes invisibility. When in doubt, fail loud.
