'+result.html;
if(o.cache)setCache(targetPath,result.html,result.layout,result.layoutGroup);
swap(result.html,targetPath,pushState,targetHash);
}).catch(function(err){
console.error('[router] fetch error:',err);
location.href=url;
}).finally(done);
}
}
function swap(html,url,pushState,hash){
var isFragment=html.indexOf('')===0;
if(isFragment)html=html.slice(''.length);
var currentContent=getContainer();
log('[router] swap: isFragment='+isFragment+' container='+!!currentContent+' tag='+(currentContent&¤tContent.tagName)+' selector='+containerSel+' htmlLen='+html.length);
if(!currentContent){log('[router] no container — falling back');location.href=url;return}
// Fragment mode: server returned just the page content (no document wrapper)
if(isFragment){
if(window.stx&&window.stx._cleanupContainer)window.stx._cleanupContainer(currentContent);
function doFragSwap(){
// Extract scripts from fragment before injecting HTML
var fragScripts=[];
var fragStyles=[];
var fragCrosswindCSS=null;
var cleanFrag=html.replace(new RegExp(']*>([\\s\\S]*?)<\\/scr'+'ipt>','gi'),function(m,code){
if(code&&code.trim())fragScripts.push(code);
return '';
});
cleanFrag=cleanFrag.replace(new RegExp(']*)>([\\s\\S]*?)<\\/sty'+'le>','gi'),function(m,attrs,css){
if(attrs.indexOf('data-crosswind')!==-1){
fragCrosswindCSS=css;
}else{
fragStyles.push({attrs:attrs,css:css});
}
return '';
});
// Remove old page styles (not crosswind — that gets merged)
document.querySelectorAll('style[data-stx-page]').forEach(function(s){s.remove()});
// Merge crosswind CSS from fragment into existing crosswind style
if(fragCrosswindCSS){
var curCrosswind=document.querySelector('head style[data-crosswind]');
if(curCrosswind){
var merged=mergeCrosswindCSS(curCrosswind.textContent||'',fragCrosswindCSS);
if(merged)curCrosswind.textContent=merged;
}else{
var cw=document.createElement('style');
cw.setAttribute('data-crosswind','generated');
cw.textContent=fragCrosswindCSS;
document.head.appendChild(cw);
}
}
// Add new page styles
fragStyles.forEach(function(s){
var el=document.createElement('style');
el.textContent=s.css;
el.setAttribute('data-stx-page','');
document.head.appendChild(el);
});
// Swap content
currentContent.innerHTML=cleanFrag;
// Remove old page scripts
document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
if(pushState!==false)history.pushState({},'',url+(hash||''));
updateNav(url);
updateActiveLinks();
if(o.scrollToTop&&!hash)window.scrollTo({top:0,behavior:'instant'});
else if(hash){var el=document.querySelector(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}}));
// Execute page scripts FIRST — they define setup functions and set _latestSetup
log('[router] frag scripts:', fragScripts.length);
document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
fragScripts.forEach(function(code){
log('[router] exec script len:', code.length, 'has __stx_setup:', code.indexOf('__stx_setup')>-1);
// Skip scripts that were already executed (layout-level partials
// like theme.stx, stores.stx, nav.stx). Their top-level const/let
// declarations would throw "Identifier has already been declared"
// on re-execution. Setup functions (__stx_setup_) must always
// re-execute because each page has its own setup.
var h=hashScript(code);
var isSetup=code.indexOf('__stx_setup_')!==-1;
// Compute isAlreadyScoped first so the dedup check can exempt
// self-contained IIFE scripts. data-stx-scoped scope IIFEs
// (the (function(){ ... })() shape) must re-execute on every
// navigation. They own their own scope and won't collide
// with prior runs, AND _cleanupContainer deleted their
// entry from window.stx._scopes on the way out. Without
// this exemption, navigating away and back leaves the page
// leaf with no scope registered: bindIf/@event never re-bind
// and all :if branches stay visible at once.
// Mirrors the full-doc swap path's runAlways escape below.
var isAlreadyScoped=code.trimStart().charAt(0)==='('||code.trimStart().charAt(0)===';'||code.indexOf('window.stx.mount')>-1;
if(!isSetup&&!isAlreadyScoped&&executedScriptHashes[h]){
log('[router] skipping already-executed script (hash dedup)');
return;
}
executedScriptHashes[h]=1;
var ns=document.createElement('script');
var hasImport=code.indexOf('import ')!==-1;
if(hasImport){
ns.type='module';
}
ns.textContent=(hasImport||isAlreadyScoped)?code:'{'+code+'}';
ns.setAttribute('data-stx-page','');
document.body.appendChild(ns);
});
log('[router] scripts done. _latestSetup:', !!window.stx._latestSetup);
// THEN fire stx:load — now _latestSetup is set and processElement has the right scope
window.dispatchEvent(new Event('stx:load'));
}
if(runViewTransition(doFragSwap)){}
else{currentContent.style.transition='opacity 0.12s ease-out';currentContent.style.opacity='0';setTimeout(function(){doFragSwap();currentContent.style.opacity='1';setTimeout(function(){currentContent.style.transition=''},150)},120)}
return;
}
// Full document mode: parse with DOMParser and extract container content
var parser=new DOMParser();
var doc=parser.parseFromString(html,'text/html');
var newContent=doc.querySelector(containerSel)||doc.querySelector('[data-stx-content]')||doc.querySelector('main');
if(!newContent){location.href=url;return}
// Clean up existing signals/effects
if(window.stx&&window.stx._cleanupContainer){
window.stx._cleanupContainer(currentContent);
}
function doSwap(){
// ── Swap styles ──
// Inject new styles FIRST, then remove old to prevent unstyled flash
var keepIds={'stx-view-transitions':1,'stx-r-css':1};
var curStyles=document.querySelectorAll('head style');
var newStyles=doc.querySelectorAll('head style');
// Merge crosswind styles instead of replacing — persistent elements
// (nav, footer) outside still need their utility classes
var curCrosswind=document.querySelector('head style[data-crosswind]');
var newCrosswind=null;
newStyles.forEach(function(s){if(s.getAttribute('data-crosswind'))newCrosswind=s});
if(curCrosswind&&newCrosswind){
var merged=mergeCrosswindCSS(curCrosswind.textContent||'',newCrosswind.textContent||'');
if(merged)curCrosswind.textContent=merged;
}
var incoming=[];
newStyles.forEach(function(s){
if(!keepIds[s.id]&&!s.getAttribute('data-crosswind')){
var ns=document.createElement('style');
ns.textContent=s.textContent;
ns.setAttribute('data-stx-incoming','');
document.head.appendChild(ns);
incoming.push(ns);
}
});
// If no existing crosswind but new page has one, add it
if(!curCrosswind&&newCrosswind){
var ns=document.createElement('style');
ns.textContent=newCrosswind.textContent;
ns.setAttribute('data-crosswind',newCrosswind.getAttribute('data-crosswind'));
document.head.appendChild(ns);
}
// Remove old styles (except persistent ones, crosswind, and incoming)
curStyles.forEach(function(s){
if(!keepIds[s.id]&&!s.hasAttribute('data-stx-incoming')&&!s.hasAttribute('data-crosswind'))s.remove();
});
incoming.forEach(function(s){s.removeAttribute('data-stx-incoming')});
// ── Swap content ──
// For layout changes: swap the entire to replace layout chrome (nav, footer, etc.)
// For same-layout: swap only the container ()
var newBody=doc.querySelector('body');
var isLayoutChange=false;
if(newBody){
var newMeta=doc.querySelector('meta[name="stx-layout"]');
var curMeta=document.querySelector('meta[name="stx-layout"]');
var curLayout=curMeta?curMeta.getAttribute('content'):'';
var newLayout=newMeta?newMeta.getAttribute('content'):'';
var curGroup=getCurrentLayoutGroup();
var newGroup=getDocLayoutGroup(doc,newLayout);
isLayoutChange=curGroup!==newGroup||(curLayout&&newLayout&&curLayout!==newLayout);
}
if(isLayoutChange&&newBody){
log('[router] full body swap for layout change');
// Replace entire body content — layout chrome and all
var bodyHTML=newBody.innerHTML.replace(new RegExp(']*>[\\s\\S]*?<\\/scr'+'ipt\\s*>','gi'),'');
document.body.innerHTML=bodyHTML;
// Copy body attributes (class, data-stx, etc.)
Array.from(newBody.attributes).forEach(function(attr){document.body.setAttribute(attr.name,attr.value)});
// Update layout meta tag
var oldMeta=document.querySelector('meta[name="stx-layout"]');
var freshMeta=doc.querySelector('meta[name="stx-layout"]');
if(oldMeta&&freshMeta)oldMeta.setAttribute('content',freshMeta.getAttribute('content')||'');
else if(freshMeta){var m=document.createElement('meta');m.name='stx-layout';m.content=freshMeta.getAttribute('content')||'';document.head.appendChild(m)}
var oldGroupMeta=document.querySelector('meta[name="stx-layout-group"]');
var freshGroupMeta=doc.querySelector('meta[name="stx-layout-group"]');
if(oldGroupMeta&&freshGroupMeta)oldGroupMeta.setAttribute('content',freshGroupMeta.getAttribute('content')||'');
else if(freshGroupMeta){var gm=document.createElement('meta');gm.name='stx-layout-group';gm.content=freshGroupMeta.getAttribute('content')||'';document.head.appendChild(gm)}
// Update container reference for script execution below
currentContent=document.querySelector(containerSel)||document.querySelector('main')||document.body;
} else {
// Same layout — swap only container content
var cleanHTML=newContent.innerHTML.replace(new RegExp(']*>[\\s\\S]*?<\\/scr'+'ipt\\s*>','gi'),'');
currentContent.innerHTML=cleanHTML;
}
// ── Load new external scripts ──
var loadedSrcs={};
document.querySelectorAll('head script[src]').forEach(function(s){loadedSrcs[s.src]=1});
var extPromises=[];
doc.querySelectorAll('head script[src]').forEach(function(s){
var src=new URL(s.getAttribute('src'),location.origin).href;
if(loadedSrcs[src])return;
loadedSrcs[src]=1;
extPromises.push(new Promise(function(resolve,reject){
var ns=document.createElement('script');
ns.src=src;
ns.onload=resolve;
ns.onerror=reject;
document.head.appendChild(ns);
}));
});
// ── Script re-execution ──
// Remove previously injected page scripts
document.querySelectorAll('script[data-stx-page]').forEach(function(s){s.remove()});
var scripts=[];
function addScript(text, runAlways){
scripts.push({text:text,runAlways:!!runAlways});
}
if(isLayoutChange){
// Layout change: collect ALL scripts from the new document body
// (layout scripts, component mounts, setup functions — everything)
var newBodyEl=doc.querySelector('body');
if(newBodyEl){
newBodyEl.querySelectorAll('script').forEach(function(s){
var text=s.textContent||'';
if(s.hasAttribute('src'))return;
if(!text.trim())return;
// Skip the signals runtime IIFE — it's already loaded
if(text.indexOf("'use strict';var cloakStyle")!==-1)return;
if(text.indexOf('__stxRouter')!==-1)return;
addScript(text,true);
});
}
// Also collect setup functions from
doc.querySelectorAll('head script').forEach(function(s){
var text=s.textContent||'';
if(s.hasAttribute('src'))return;
if(!text.trim())return;
if(text.indexOf('__stx_setup_')!==-1)addScript(text,true);
});
} else {
// Same layout: collect scripts from the container
newContent.querySelectorAll('script').forEach(function(s){
var text=s.textContent||'';
if(s.hasAttribute('src'))return;
if(!text.trim())return;
addScript(text,true);
});
// Collect page scripts from AND (outside container).
// SSG builds place the setup function in before , not
// in . Without this, SPA navigation on static sites never
// runs the setup and reactive data stays empty.
var seenSetups={};
scripts.forEach(function(entry){var t=entry.text;if(t.indexOf('__stx_setup_')!==-1)seenSetups[t.substring(0,80)]=1});
doc.querySelectorAll('script').forEach(function(s){
var text=s.textContent||'';
if(s.hasAttribute('src'))return;
if(!text.trim())return;
// Skip signals runtime and router (they contain __stx_setup references but aren't setup functions)
if(text.indexOf("'use strict';var cloakStyle")!==-1)return;
if(text.indexOf('__stxRouter')!==-1)return;
if(newContent.contains(s))return;
if(text.indexOf('__stx_setup_')===-1&&!s.hasAttribute('data-stx-scoped')&&!s.hasAttribute('data-stx-page'))return;
var key=text.substring(0,80);
if(seenSetups[key])return;
seenSetups[key]=1;
addScript(text,true);
});
}
// Push history state (before active link updates so location.pathname is current)
if(pushState!==false)history.pushState({},'',url+(hash||''));
// Update active nav links
updateNav(url);
updateActiveLinks();
// Scroll
if(o.scrollToTop&&!hash)window.scrollTo({top:0,behavior:'instant'});
else if(hash){var el=document.querySelector(hash);if(el)el.scrollIntoView({behavior:'smooth'})}
// Update title
var newTitle=doc.querySelector('title');
if(newTitle)document.title=newTitle.textContent;
// Update from the destination doc so screen readers,
// CSS :lang() selectors, and any i18n picker that mirrors
// document.documentElement.lang stay accurate after SPA hops.
if(doc.documentElement&&doc.documentElement.lang){
document.documentElement.lang=doc.documentElement.lang;
}
window.dispatchEvent(new CustomEvent('stx:navigate',{detail:{url:url}}));
// Execute page scripts FIRST — they define setup functions and set _latestSetup
function execScripts(){
scripts.forEach(function(entry){
var text=typeof entry==='string'?entry:entry.text;
var runAlways=typeof entry==='string'?false:entry.runAlways;
// Skip scripts that were already executed (layout-level partials
// like theme.stx). Their top-level const/function declarations
// would throw "Identifier has already been declared" on re-execution.
// Exception: setup functions (__stx_setup_) must always re-execute
// because each page has its own setup, and import statements
// need special handling.
var h=hashScript(text);
var isSetup=text.indexOf('__stx_setup_')!==-1;
var hasImport=text.indexOf('import ')!==-1;
if(!runAlways&&!isSetup&&executedScriptHashes[h])return;
executedScriptHashes[h]=1;
// Wrap scripts with import statements as modules (Bug 3 fix)
var ns=document.createElement('script');
if(hasImport){
ns.type='module';
}
// Wrap in block scope to prevent const/let collisions on re-navigation.
// Top-level const/let in