Skip to content

A Static Folder Was Shadowing the Sports Route

Adityo Guni Waluyo

The OLAHRAGA rubric rendered empty while every other rubric matched. A static folder outranked the dynamic segment, and a fetch cache default kept new rows out.

TL;DR

A weekly parity check caught OLAHRAGA showing zero public announcements while the admin feed had several. Root cause: a static page at informasi/olahraga won Next.js route precedence over the dynamic template, then default fetch caching blocked new rows. Fix mirrored the announcements component in both pages and set cache: "no-store".

The weekly parity check that compares the KotaPortal announcement feed against the public pages came back with one empty row. The OLAHRAGA rubric showed zero announcements on the public side. The admin feed listed several for the same rubric, and every other rubric matched within seconds.

The check only compares rendered output, so it pointed at nothing specific. The dynamic route under informasi/[kategori] looked like the obvious suspect, because OLAHRAGA is one of its parameters. That guess held for about ten minutes: rendering locally with the same category key produced a full announcements list, and no other category broke.

Short hunt. Then the route tree explained it.

frontend/src/app/(public)/informasi/
├── olahraga/page.tsx
└── [kategori]/page.tsx

A static page existed at informasi/olahraga. Next.js resolves a request against the most specific segment it can find, and the official routing docs state the rule plainly: predefined routes take precedence over dynamic routes [1]. The archived snapshot of those docs keeps the same point as an explicit caveat for exactly this case [2]. Requests for the sports rubric never reached the dynamic template. They landed on the static folder, whose page rendered a category header and nothing else.

A static page won route resolution

The static page predates the announcements section. It was written as a landing page for that rubric with its own layout and sidebar modules, which is why deleting the folder and letting informasi/[kategori] handle everything was not the fix: those modules would have disappeared with it.

The fix was structural parity. The announcements section now renders in both places, pulled from the same component, so the static page cannot drift from the dynamic template again.

Two options were rejected on the way there. A redirect from the static path to the dynamic one would hide the precedence rule instead of respecting it, and a rewrite in the routing config would move the same problem into a file even fewer people read. Mirroring one component is dumber and easier to verify.

The fetch default that kept old rows

Parity went green for existing rows. New announcements still did not appear on the public side, and again nothing failed loudly. The fetch that fills the section lives in the endpoints module:

fetch(url, { cache: "no-store" })

The cache option is the fix, not part of the original code. On Next.js 14, fetch responses are cached by default and the request behaves like force-cache [3], so the section kept rendering whatever the first build had stored. The project pins Next.js ^14.2.0, which means the default still applies and the explicit option still matters. Next.js 15 changed the default so fetch is no longer cached unless asked [4]; the upgrade notes are only relevant once the project actually moves.

Verifying both fixes

Verification ran in two passes. First, a test announcement was published in the OLAHRAGA rubric and the parity check was run again: the public page and the feed showed the same set, including the new row. Second, the announcement body was edited and the check was rerun without a redeploy. The edited text showed up, which is the part that proves cache: "no-store" is doing the work rather than a lucky rebuild. One non-sports rubric was checked for regression and still matched.

Both failures shared one property: no error was raised. Route precedence and fetch caching are decisions made outside the code written for the feature, and neither one files a complaint when it silently wins. A comparison of rendered output against the source feed caught both, where unit tests and error logs had nothing to say.

Sources

[1] [1] source

[2] [2] source

[3] [3] source

[4] [4] source

Related articles