mnfst-render 0.3.0 → 0.3.2

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.
@@ -895,25 +895,37 @@ function stripPrerenderedXDataDirectives(html) {
895
895
 
896
896
  // --- Don't bake Alpine-only state into the snapshot; only $x-driven content should be prerendered.
897
897
  // For any :attr or x-bind:attr whose expression does NOT contain $x, remove the literal attr from the tag
898
- // so Alpine re-evaluates on load. Bindings that use $x are left as-is (content stays for SEO).
898
+ // so Alpine re-evaluates on load. Bindings that use $x are left as-is (content stays for SEO), except
899
+ // :style / x-bind:style with $x: those must be removed when a baked inline style exists, or Alpine will
900
+ // overwrite prerendered values (e.g. mask-image) on hydrate when $x is briefly empty in production.
899
901
  // Use (?<!:) so we only strip literal attr=, not :attr= (e.g. class= not :class=).
900
902
  // Never touch <script> tags (loader + injected plugins must be preserved; static HTML still runs them).
901
903
  function stripPrerenderDynamicBindings(html) {
902
904
  return html.replace(/<(\w+)([^>]*)>/g, (match, tagName, attrsStr) => {
903
905
  if (tagName.toLowerCase() === 'script') return match;
904
906
  const isAnchor = tagName.toLowerCase() === 'a';
907
+ let workAttrs = attrsStr;
908
+ workAttrs = workAttrs.replace(/\s+:style=(?:"([^"]*)"|'([^']*)')/gi, (sub, d, s) => {
909
+ const val = (d !== undefined ? d : s) || '';
910
+ return val.indexOf('$x') !== -1 ? '' : sub;
911
+ });
912
+ workAttrs = workAttrs.replace(/\s+x-bind:style=(?:"([^"]*)"|'([^']*)')/gi, (sub, d, s) => {
913
+ const val = (d !== undefined ? d : s) || '';
914
+ return val.indexOf('$x') !== -1 ? '' : sub;
915
+ });
916
+
905
917
  const toStrip = new Set();
906
918
  const bindingRegex = /(?:^|\s)(?::|x-bind:)(\w+)=(?:"([^"]*)"|'([^']*)')/g;
907
919
  let m;
908
- while ((m = bindingRegex.exec(attrsStr)) !== null) {
920
+ while ((m = bindingRegex.exec(workAttrs)) !== null) {
909
921
  const attrName = (m[1] || '').toLowerCase();
910
922
  // Keep href on anchors so prerendered static navigation stays valid.
911
923
  if (attrName === 'class' || attrName === 'style' || (isAnchor && attrName === 'href')) continue;
912
924
  const val = (m[2] !== undefined ? m[2] : m[3]) || '';
913
925
  if (val.indexOf('$x') === -1) toStrip.add(attrName);
914
926
  }
915
- if (toStrip.size === 0) return match;
916
- let newAttrs = attrsStr;
927
+ if (toStrip.size === 0 && workAttrs === attrsStr) return match;
928
+ let newAttrs = workAttrs;
917
929
  for (const attr of toStrip) {
918
930
  const esc = attr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
919
931
  newAttrs = newAttrs.replace(new RegExp(`\\s*(?<!:)${esc}="[^"]*"`, 'gi'), '');
@@ -1664,6 +1676,8 @@ async function runPrerender(config) {
1664
1676
  return {
1665
1677
  templateCount: templates.length,
1666
1678
  nonCollapsedTemplateCount: templates.filter((t) => t.getAttribute('data-prerender-collapsed') !== '1').length,
1679
+ hint:
1680
+ 'entries.staticGenerated is read before the x-for mark pass and is always false; use stage post-xfor-mark for data-prerender-static-generated.',
1667
1681
  entries,
1668
1682
  listDiagnostics,
1669
1683
  };
@@ -1758,6 +1772,26 @@ async function runPrerender(config) {
1758
1772
  });
1759
1773
  });
1760
1774
 
1775
+ if (config.debugPrerender) {
1776
+ const afterMark = await page.evaluate(() => {
1777
+ const rows = [];
1778
+ for (const tpl of document.querySelectorAll('template[x-for]')) {
1779
+ rows.push({
1780
+ xFor: (tpl.getAttribute('x-for') || '').slice(0, 140),
1781
+ collapsed: tpl.getAttribute('data-prerender-collapsed') === '1',
1782
+ staticGenerated: tpl.getAttribute('data-prerender-static-generated') === '1',
1783
+ });
1784
+ }
1785
+ return {
1786
+ templateCount: rows.length,
1787
+ staticMarkedCount: rows.filter((r) => r.staticGenerated).length,
1788
+ collapsedCount: rows.filter((r) => r.collapsed).length,
1789
+ entries: rows.slice(0, 60),
1790
+ };
1791
+ }).catch(() => null);
1792
+ pushDebug({ path: displayPath, stage: 'post-xfor-mark', metrics: afterMark });
1793
+ }
1794
+
1761
1795
  // Strip loop-scope bindings from x-for clones while <template> nodes still exist.
1762
1796
  // (If we remove static templates first, querySelectorAll('template[x-for]') misses them and clones
1763
1797
  // keep x-text/x-bind referencing card/item — Alpine then mutates or errors on the static HTML.)
@@ -1821,10 +1855,35 @@ async function runPrerender(config) {
1821
1855
  });
1822
1856
 
1823
1857
  // Remove static x-for templates once static clones are generated.
1824
- // This prevents Alpine from rendering duplicate lists at runtime.
1858
+ // Alpine registers a cleanup on <template x-for> that removes every node in _x_lookup when the
1859
+ // template is detached — so tpl.remove() alone deletes all sibling clones (empty grids in output).
1860
+ // Replace each clone with a deep cloneNode first so teardown targets detached nodes; copies stay in DOM.
1825
1861
  await page.evaluate(() => {
1826
- document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
1827
- tpl.remove();
1862
+ const A = window.Alpine;
1863
+ const runBatch = typeof A?.mutateDom === 'function' ? (fn) => A.mutateDom(fn) : (fn) => fn();
1864
+ runBatch(() => {
1865
+ document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
1866
+ const parent = tpl.parentNode;
1867
+ if (!parent) {
1868
+ tpl.remove();
1869
+ return;
1870
+ }
1871
+ const first = tpl.content?.firstElementChild;
1872
+ if (!first) {
1873
+ tpl.remove();
1874
+ return;
1875
+ }
1876
+ const tag = first.tagName;
1877
+ const cls = first.getAttribute('class') || '';
1878
+ let n = tpl.nextElementSibling;
1879
+ while (n && n.tagName === tag) {
1880
+ if ((n.getAttribute('class') || '') !== cls) break;
1881
+ const next = n.nextElementSibling;
1882
+ n.replaceWith(n.cloneNode(true));
1883
+ n = next;
1884
+ }
1885
+ tpl.remove();
1886
+ });
1828
1887
  });
1829
1888
  });
1830
1889
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {