stx-router 0.2.706 → 0.2.708

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/client.js +44 -4
  2. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -590,7 +590,38 @@ function routerSource() {
590
590
  // Apply a page title captured from the fragment response's X-STX-Title
591
591
  // header (URI-encoded) so SPA swaps keep document.title in sync with the
592
592
  // page \u2014 full-document swaps set the title from the <title> tag directly.
593
- function applyTitle(t){if(t){try{document.title=decodeURIComponent(t)}catch(e){document.title=t}}}
593
+ function applyTitle(t,html){
594
+ // The parsed metadata payload already set the title with the browser's
595
+ // complete entity decoder. A legacy header must not overwrite it.
596
+ if(html&&/^<!--stx-fragment(?: rt=[01])?--><template data-stx-page-head="1">/.test(html))return;
597
+ if(t){try{document.title=decodeURIComponent(t)}catch(e){document.title=t}}
598
+ }
599
+ // Only these page-owned tags are reconciled. Assets, security tokens,
600
+ // theme metadata and arbitrary attributes never enter the contract (#2121).
601
+ function pageMetadataTag(node){
602
+ var tag=node.tagName.toLowerCase(),out;
603
+ if(tag==='meta'){
604
+ var kind=node.hasAttribute('property')?'property':'name';
605
+ var name=(node.getAttribute(kind)||'').toLowerCase();
606
+ if(name!=='description'&&!/^(og|twitter):[\\w:]+$/.test(name))return null;
607
+ out=ce('meta');out.setAttribute(kind,name);out.setAttribute('content',node.getAttribute('content')||'');return out;
608
+ }
609
+ if(tag==='link'){
610
+ var rel=(node.getAttribute('rel')||'').toLowerCase(),lang=node.getAttribute('hreflang');
611
+ if(rel!=='canonical'&&!(rel==='alternate'&&lang))return null;
612
+ out=ce('link');out.setAttribute('rel',rel);out.setAttribute('href',node.getAttribute('href')||'');
613
+ if(rel==='alternate')out.setAttribute('hreflang',lang);return out;
614
+ }
615
+ return null;
616
+ }
617
+ function pageMetadataNodes(root){return Array.prototype.slice.call(root.querySelectorAll('meta,link'))}
618
+ function capturePageHead(){return pageMetadataNodes(document.head).map(function(node){var tag=pageMetadataTag(node);return tag?tag.outerHTML:''}).join('')}
619
+ function applyPageHead(html){
620
+ var doc=new DOMParser().parseFromString('<html><head>'+html+'</head><body></body></html>','text/html');
621
+ pageMetadataNodes(document.head).forEach(function(node){if(pageMetadataTag(node))node.remove()});
622
+ pageMetadataNodes(doc.head).forEach(function(node){var tag=pageMetadataTag(node);if(tag)document.head.appendChild(tag)});
623
+ var title=doc.head.querySelector('title');if(title)document.title=title.textContent;
624
+ }
594
625
  // \u2500\u2500 Container attributes \u2500\u2500
595
626
  // A fragment carries only the container's INNER content, so any layout that
596
627
  // lives on the destination's <main> itself (e.g. a centered auth page whose
@@ -1355,7 +1386,7 @@ function routerSource() {
1355
1386
  var loading=beforeSwap(cached,url);
1356
1387
  return (loading?loading.then(function(ok){if(!ok){if(!stale())recoverUnbound(url,ctx);return false}return swapCached()}):Promise.resolve(swapCached())).then(function(swapped){
1357
1388
  if(swapped===false)return false;
1358
- applyTitle(titleCache[targetPath]);
1389
+ applyTitle(titleCache[targetPath],cached);
1359
1390
  // Shown from the cache at once; asked again behind it, so a page that
1360
1391
  // changed since says so (stx:updated) instead of staying stale.
1361
1392
  revalidate(targetPath,url);
@@ -1431,7 +1462,7 @@ else {
1431
1462
  if(swapped===false)return false;
1432
1463
  // Fragment swaps carry no <head>; apply the title from the header.
1433
1464
  // Full-document swaps already set document.title from the <title> tag.
1434
- if(result.isFragment)applyTitle(result.title);
1465
+ if(result.isFragment)applyTitle(result.title,result.html);
1435
1466
  return true;
1436
1467
  });
1437
1468
  }).catch(failed).finally(done);
@@ -1727,6 +1758,11 @@ else {
1727
1758
  var isFragment=!!fragMark;
1728
1759
  var declaredRuntime=fragMark&&fragMark[1]?fragMark[1]:'';
1729
1760
  if(isFragment)html=html.slice(fragMark[0].length);
1761
+ var incomingHead=null;
1762
+ if(isFragment){
1763
+ var headPayload=/^<template data-stx-page-head="1">([\\s\\S]*?)<\\/template>\\n?/.exec(html);
1764
+ if(headPayload){incomingHead=headPayload[1];html=html.slice(headPayload[0].length)}
1765
+ }
1730
1766
  if(!isFragment&&!isStxDocument(html)){log('[router] non-stx document \u2014 full navigation to:',url);location.href=url;return Promise.resolve(false)}
1731
1767
  var ctx=pendingCtx||{direction:pushState==='replace'?'replace':pushState==='tab'?'tab':pushState===false?'pop':'push'};
1732
1768
  pendingCtx=null;
@@ -1974,6 +2010,7 @@ else {
1974
2010
  setDirection(ctx.direction);
1975
2011
  if(useScreens){scr=screenBegin(host,ctx,html,pushState!==false);currentContent=scr.el}
1976
2012
  beginPageSwap();
2013
+ if(incomingHead!==null)applyPageHead(incomingHead);
1977
2014
  doFragSwap();
1978
2015
  if(scr)screenEnd(scr);
1979
2016
  resolve(true);
@@ -2330,6 +2367,7 @@ else {
2330
2367
  // Update title
2331
2368
  var newTitle=doc.querySelector('title');
2332
2369
  if(newTitle)document.title=newTitle.textContent;
2370
+ applyPageHead(doc.head.innerHTML);
2333
2371
 
2334
2372
  // Update <html lang> from the destination doc so screen readers,
2335
2373
  // CSS :lang() selectors, and any i18n picker that mirrors
@@ -2586,6 +2624,7 @@ else {
2586
2624
  function saveEntry(e){
2587
2625
  var stx=window.stx||{};
2588
2626
  e.title=document.title;
2627
+ e.head=capturePageHead();
2589
2628
  e.setup=stx._latestSetup;
2590
2629
  e.data=window.__STX_DATA__;
2591
2630
  e.config=window.__STX_RUNTIME_CONFIG__;
@@ -2595,6 +2634,7 @@ else {
2595
2634
  function restoreEntry(e){
2596
2635
  var stx=window.stx||{};
2597
2636
  if(e.title)document.title=e.title;
2637
+ if(typeof e.head==='string')applyPageHead(e.head);
2598
2638
  if(e.setup)stx._latestSetup=e.setup;
2599
2639
  window.__STX_DATA__=e.data||{};
2600
2640
  window.__STX_RUNTIME_CONFIG__=e.config||{};
@@ -3418,7 +3458,7 @@ else {
3418
3458
  // 'replace' so re-rendered validation errors do not stack one history
3419
3459
  // entry per attempt.
3420
3460
  return Promise.resolve(swap(marked,cacheKey(action),'replace','')).then(function(){
3421
- applyTitle(r.headers.get('X-STX-Title')||'');
3461
+ applyTitle(r.headers.get('X-STX-Title')||'',marked);
3422
3462
  });
3423
3463
  });
3424
3464
  }).catch(function(err){
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stx-router",
3
3
  "type": "module",
4
- "version": "0.2.706",
4
+ "version": "0.2.708",
5
5
  "description": "File-based router for STX — .stx template discovery, nested layouts, typed route params, middleware, and client-side SPA navigation.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",