ISR fallback and App Shell upgrade
generateStaticParams here returns only two of the four regions, and only one station within each. Everything else is an unlisted param — which is the interesting case.
For an unlisted URL, Next.js serves the route’s App Shell immediately, streams the real content, and then re-renders the page in the background with the now-known params. The next visitor to that same URL gets the upgraded result from cache. This is the Cache Components equivalent of fallback: true.
What to check
Listed. Click a prerendered link. Fully rendered, no fallback, stamps dated to the build.
Unlisted, first visit. Click an unlisted link. The region header and station panel arrive as fallbacks first, then stream in.
Unlisted, second visit. Go back and click the same link again. Now it comes from the background upgrade — no fallback. This is the whole mechanism in one click.
Count the fallbacks instead of squinting:
# both params known — no fallbacks, ever curl -s localhost:3000/isr-fallback/europe/london | grep -o 'data-fallback="[^"]*"' | sort -u # known region, unlisted station — PARTIAL shell, station only curl -s localhost:3000/isr-fallback/europe/paris | grep -o 'data-fallback="[^"]*"' | sort -u # neither known — generic shell, both stream curl -s localhost:3000/isr-fallback/africa/nairobi | grep -o 'data-fallback="[^"]*"' | sort -u # then run either unlisted one again: no fallbacks, it upgraded
Prefetching counts as the first visit. In a build, merely scrolling an unlisted link into view starts the upgrade before you click, so the click often lands on the upgraded result. To see the cold path, use curl or type the URL.
Prerendered at build time
Not prerendered
Deep links — the partial shell
Within a prerendered region, only the first station is prerendered. The rest reach the station page with an unlisted [station] param but a known [region] — so the shell they get already has the region header filled in, and only the station streams. That partial shell is the detail worth seeing.