The router emits X-STX-Layout-Group, stamps <meta name="stx-layout">, and implements checkLayoutChange to force a full reload when the group changes. On a cold click (nothing prefetched) the check does not fire, so the destination is fragment-swapped into the previous group's shell and renders with the wrong chrome.
Reproduction
Two pages in different groups:
/compare/sentry→X-STX-Layout: layouts/marketing,X-STX-Layout-Group: marketing(shell includes SiteNav + SiteFooter)/pricing→X-STX-Layout: layouts/default,X-STX-Layout-Group: default(shell includes neither)
Both are served correctly — headers and <meta name="stx-layout"> are right on each hard load.
Click the in-page link from /compare/sentry to /pricing, then measure whether the document was replaced:
// before the click
window.__m = 1
const t0 = performance.timeOrigin
// after the click settles
performance.timeOrigin === t0 // true → same document
window.__m === 1 // true → same document
location.pathname // "/pricing"
document.querySelector('meta[name="stx-layout"]').content // "layouts/default"Both signals say same document: it was a fragment swap. Expected a full document load, because the group changed marketing → default.
Result: /pricing is displayed inside the marketing shell — site nav and footer present — while a hard load of /pricing has neither. The same URL renders differently depending on how the user arrived, and the stx-layout meta now claims layouts/default on a document whose chrome is layouts/marketing.
Measured on the real app: hard-loaded /pricing has 2 data-stx-link anchors and 0 footer links; /compare has 76 and 27. After the SPA hop, /pricing has the marketing set.
Where it looks wrong
packages/router/src/client.ts:497
if(checkLayoutChange(layoutCache[targetPath],url,layoutGroupCache[targetPath])){The guard is called with values from the prefetch cache. On a cold click those entries are undefined, so the decision is made before the response that would carry X-STX-Layout-Group has been read. checkLayoutChange itself (client.ts:416-434) is correct and does compare groups:
if(curGroup!==nextGroup){
log('[router] layout group change:',curGroup,'→',nextGroup,'— full reload to:',targetUrl);
return true;
}so the logic is fine — it is being asked too early, with nothing to compare against.
Suggested fix
Re-check after the response headers arrive, not only before the fetch: the fragment path already reads X-STX-Layout / X-STX-Layout-Group (client.ts:531). If the group differs from getCurrentLayoutGroup() at that point, discard the fragment and fall back to location.assign(url) — the same handoff #1809 added for "fragment needs a runtime this page lacks". Warming layoutGroupCache on prefetch only narrows the window; a first click into an unvisited group is the common case.
Environment
Fork at f539929dfe, rebuilt dist, driven headlessly over CDP against a real app. Content and title were correct after the swap — the defect is confined to chrome and to the layout meta.