analyticshq

Site demo · last 90 days

{{ liveCount() }} current {{ liveCount() === 1 ? 'visitor' : 'visitors' }}

Sign in to view analytics

This dashboard is private to the site owner.

Sign in
'+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