#548·Instatic

Loop filter mixing {route.query.*} with {currentEntry.*} renders empty

Author: borskyj-symphCreated Sep 19, 2026Updated Sep 19, 2026

Split out of #546 rather than widening it, because the fix is a design change with its own cache and authorization questions.

What happens

A loop cell filter whose cellValue contains a {route.query.*} token is request-dependent, so checkInlineTokens in src/core/publisher/dynamicDetection.ts defers the loop to a Layer C hole. A hole is fetched by node id, and renderHoleFragment in server/handlers/cms/hole.ts seeds an empty entryStack, because the endpoint has no way to know which entry the originating page was rendering.

So a filter that mixes both token kinds loses its entry half. "{currentEntry.slug}-{route.query.region}" on a course entry template, visited at /courses/time-management?region=cz, resolves to -cz, and the loop renders empty.

What works

A filter with only {currentEntry.*}, {page.*}, {site.*}, route.path or route.slug is publish-time deterministic, stays in the baked page, and resolves normally. That covers the feature's main use, which is why this is a follow-up and not a blocker.

Why it renders empty rather than wrong

Before the nested detection landed in #546, such a filter was invisible to detection, so the loop rendered in the full page with the entry in scope and produced correct rows for the first visitor. The page cache key drops unknown query keys, so the next visitor asking for a different region was served the first one's rows. Empty is the safer of the two failures, but neither is right.

What a fix needs

The hole endpoint has the originating page URL, so it can resolve an entry route to its published row the way renderPublishedDataRowTemplate does, and seed entryStack from it. Points to settle:

  • The hole cache key already carries the originating page path, so per-entry caching would be correct without a key change. Worth confirming against HOLE_PATH_PREFIX keying in server/handlers/cms/hole.ts.
  • A hole requested with a path that does not resolve to an entry has to keep rendering with an empty stack rather than erroring.
  • Authorization: the row must be read as the public reader, the same as any published entry route, so an unpublished row cannot be surfaced through a hole.

Documented meanwhile in docs/features/loops.md.