Skip to content

fix(publish): scope entry-route runtime assets to their own template - #320

Merged
DavidBabinec merged 1 commit into
mainfrom
fix/entry-route-runtime-scope
Jul 31, 2026
Merged

fix(publish): scope entry-route runtime assets to their own template#320
DavidBabinec merged 1 commit into
mainfrom
fix/entry-route-runtime-scope

Conversation

@DavidBabinec

Copy link
Copy Markdown
Contributor

Every entry route served one arbitrary page's scripts

A runtime script scoped to two pages:

/circuit                           assets/BFqX…/classic/001-circuit.js
/journal                           (none)      ← correct, page not in scope
/rooms/plunge                      assets/BFqX…/classic/001-circuit.js   ← wrong
/posts/the-year-the-boiler-failed  assets/BFqX…/classic/001-circuit.js   ← wrong

All ten entry routes carried it, and all served the same bundle id — /circuit's.

Why

getLatestPublishedSiteSnapshot (repositories/publish.ts) is order by data_rows.created_at asc limit 1 — the first published page row ever created. It exists to supply site_json to routes that are not themselves pages, and its runtime_assets_json rode along by accident; the doc comment even labels it "first published page snapshot (for 404s etc.)".

Entry routes are rendered per request from a template plus a data row, so they have no manifest of their own and took that one verbatim at publicRouter.ts:174. assetScopeAppliesToPage is a correct pageIds.includes(page.id) test, but nothing on the entry-route path ever calls itcollectRuntimeScripts runs only inside publishSite.ts's loop over site.pages. The scope decision made for the oldest page was simply replayed.

Over-inclusion is what surfaced. The same bug under-includes just as easily: had the oldest page carried no scripts, a scoped script would have reached nothing. The 404 route inherited the same manifest, and so did the publish-time bake (bakeDataRows.ts, publishRow.ts) — which is what most requests actually serve.

The fix

getLatestPublishedSiteSnapshot no longer carries runtime assets at all, so the failure mode is "no scripts", never "someone else's". server/publish/entryTemplateSnapshot.ts resolves the manifest from the page that actually renders — the innermost template in the chain — and treats its absence as authoritative too, so a manifest arriving on the snapshot from anywhere else cannot survive onto a route it was never scoped to. Applied at all four sites: the request path, the 404, and both bake paths.

Two more, found on the way

Template-scoped CSS never reached its own routes. composeTemplateChain builds its result field by field and dropped Page.template; assetScopeAppliesToPage gates its templates branch on Boolean(page.template). So a stylesheet scoped to an entry template applied everywhere except the routes it targeted.

Publishing a document stole the editor's active document. applyStatus called updateSelectedEntry for whatever row it published, active or not. Two consequences: it yanked whatever a human had open, discarding their unsaved draft (useContentEntryDraft re-applies on a selectedEntry id change); and it left a tool loop of set_document_fields → set_document_status writing one document behind itself, so every field write after the first was refused. The non-agent paths already guard this (useContentWorkspace.ts:459, :471) — this brings the agent path in line via a new applyEntryUpdate, rather than leaving the two asymmetric. The field tools' descriptions now state the active-document precondition they have always enforced.

Verification

Unit — an entry route takes its template's manifest, ignores a stale one, keeps the site document it resolved against, and falls back cleanly when a table has no template; a composed template keeps its config so a template-scoped asset matches and an unrelated one still does not; publishing another document leaves the active one alone and a following write to it still lands.

That last test is written with one act() per tool call on purpose — batched into a single act() it passes against the unfixed code, because React never commits and the bridge's workspace ref stays stale. I checked it fails without the fix and passes with it.

Live — republished pack 020, which has a genuine page-scoped runtime. The script now appears on / and /circuit only; the harness audit reports routes with a runtime script: /, /circuit, down from twelve, with 0 audit problems across all 19 routes.

Suite: 6490 pass / 0 fail. Lint and tsc --noEmit clean.

Related

Stacks alongside #318 (author bindings publishing the account's email). Independent branches, no overlapping files.

🤖 Generated with Claude Code

Every entry route on a site served one arbitrary page's scripts.

`getLatestPublishedSiteSnapshot` is `order by data_rows.created_at asc limit
1` — the first published page ever created. It exists to carry `site_json`
for routes that are not themselves pages, and its `runtime_assets_json` rode
along by accident. Entry routes took it verbatim, so `assetScopeAppliesToPage`
was never evaluated on that path at all: a script scoped to two pages shipped
on all ten entry routes, and had the oldest page carried no scripts the same
bug would have shipped a scoped script nowhere. The 404 route inherited the
same manifest, and so did the publish-time bake, which is what most requests
actually serve.

That getter no longer carries runtime assets, so a miss now degrades to "no
scripts" rather than "someone else's scripts". `entryTemplateSnapshot.ts`
resolves the manifest from the page that actually renders — the innermost
template in the chain — and treats its absence as authoritative too, so a
manifest arriving from anywhere else cannot survive onto a route it was never
scoped to.

Also fixed, found on the way: `composeTemplateChain` built its result field by
field and dropped `Page.template`, and `assetScopeAppliesToPage` gates its
`templates` branch on `Boolean(page.template)`. A stylesheet scoped to an entry
template therefore never applied to that template's own routes — the one place
it was meant to.

And `applyStatus` called `updateSelectedEntry` for whatever row it published,
active or not, silently retargeting the workspace. That discarded whatever an
author had open and unsaved, and left a tool loop of `set_document_fields →
set_document_status` writing one document behind itself, so every field write
after the first was refused. It now uses `applyEntryUpdate`, which selects only
a row that was already active — matching the guard the non-agent paths have
always had. The field tools' descriptions state the active-document
precondition they have always enforced.

Test: an entry route takes its template's manifest, ignores a stale one, keeps
the site document it resolved against, and falls back cleanly with no
template; a composed template keeps its config so a template-scoped asset
matches; publishing another document leaves the active one alone and a
following write to it still lands.

Verified live: pack 020's page-scoped script now appears on `/` and `/circuit`
only, where it previously appeared on those plus all ten entry routes.
@DavidBabinec
DavidBabinec merged commit d705454 into main Jul 31, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant