mnfst-render 0.3.8 → 0.4.0

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/manifest.render.mjs +37 -16
  2. package/package.json +1 -1
@@ -897,6 +897,10 @@ function stripDuplicatedLoopDirectives(html) {
897
897
  return html;
898
898
  }
899
899
 
900
+ function isHydrateMarkedAttrs(attrsStr) {
901
+ return /\sdata-prerender-hydrate(?:\s*=|[\s>])/i.test(attrsStr || '');
902
+ }
903
+
900
904
  // --- Strip x-text and x-html that reference $x when static/SEO (content already in snapshot).
901
905
  // Do NOT strip when expression is user-driven: $route(, $search, $query. Those stay so Alpine can update.
902
906
  // Same rule for :attr in stripPrerenderDynamicBindings: bindings with $x are kept (content stays for SEO).
@@ -906,9 +910,13 @@ function stripPrerenderedXDataDirectives(html) {
906
910
  if (expr.includes('$search') || expr.includes('$query')) return false;
907
911
  return true;
908
912
  }
909
- let out = html.replace(/\s+x-text="([^"]*\$x[^"]*)"/g, (match, expr) => (isStatic(expr) ? '' : match));
910
- out = out.replace(/\s+x-html="([^"]*\$x[^"]*)"/g, (match, expr) => (isStatic(expr) ? '' : match));
911
- return out;
913
+ return html.replace(/<(\w+)([^>]*)>/g, (full, tag, attrs) => {
914
+ if (isHydrateMarkedAttrs(attrs)) return full;
915
+ let outAttrs = attrs;
916
+ outAttrs = outAttrs.replace(/\s+x-text="([^"]*\$x[^"]*)"/g, (match, expr) => (isStatic(expr) ? '' : match));
917
+ outAttrs = outAttrs.replace(/\s+x-html="([^"]*\$x[^"]*)"/g, (match, expr) => (isStatic(expr) ? '' : match));
918
+ return `<${tag}${outAttrs}>`;
919
+ });
912
920
  }
913
921
 
914
922
  // --- Don't bake Alpine-only state into the snapshot; only $x-driven content should be prerendered.
@@ -921,6 +929,7 @@ function stripPrerenderedXDataDirectives(html) {
921
929
  function stripPrerenderDynamicBindings(html) {
922
930
  return html.replace(/<(\w+)([^>]*)>/g, (match, tagName, attrsStr) => {
923
931
  if (tagName.toLowerCase() === 'script') return match;
932
+ if (isHydrateMarkedAttrs(attrsStr)) return match;
924
933
  const isAnchor = tagName.toLowerCase() === 'a';
925
934
  const isImg = tagName.toLowerCase() === 'img';
926
935
  let workAttrs = attrsStr;
@@ -960,6 +969,7 @@ function stripPrerenderDynamicBindings(html) {
960
969
  // Drop :src / x-bind:src when img already has a baked src= (x-for / iterator expressions break hydrate).
961
970
  function stripRedundantImgSrcBindings(html) {
962
971
  return html.replace(/<img\b([^>]*)>/gi, (full, attrs) => {
972
+ if (isHydrateMarkedAttrs(attrs)) return full;
963
973
  const srcM = attrs.match(/\ssrc=(["'])([\s\S]*?)\1/i);
964
974
  if (!srcM || !String(srcM[2] || '').trim()) return full;
965
975
  if (!/\s:src\s*=|\sx-bind:src\s*=/i.test(attrs)) return full;
@@ -978,6 +988,7 @@ function stripRedundantImgSrcBindings(html) {
978
988
  // loop/item expressions while the attribute remains for CSS (e.g. inline layout that keys off [x-icon]).
979
989
  function stripResolvedXIconDirectives(html) {
980
990
  return html.replace(/<i\b([^>]*)>([\s\S]*?)<\/i>/gi, (full, attrs, inner) => {
991
+ if (isHydrateMarkedAttrs(attrs)) return full;
981
992
  if (!/\sx-icon\s*=/i.test(attrs)) return full;
982
993
  if (!/<svg\b/i.test(inner) || !/\bdata-icon\s*=/i.test(inner)) return full;
983
994
  const cleaned = attrs
@@ -989,6 +1000,10 @@ function stripResolvedXIconDirectives(html) {
989
1000
  });
990
1001
  }
991
1002
 
1003
+ function stripPrerenderHydrateMarkers(html) {
1004
+ return html.replace(/\sdata-prerender-hydrate(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?/gi, '');
1005
+ }
1006
+
992
1007
  function markPrerenderedManifestComponents(html) {
993
1008
  return html.replace(/<(x-[a-z][\w-]*)([^>]*)>/gi, (full, tag, attrs) => {
994
1009
  const a = attrs || '';
@@ -1385,7 +1400,7 @@ function startStaticServer(rootDir) {
1385
1400
  // --- Copy project into output so website is self-contained (e.g. for Appwrite). ---
1386
1401
  const COPY_EXCLUDE = new Set([
1387
1402
  'node_modules', '.git', 'package.json', 'package-lock.json',
1388
- 'index.html', 'prerender.mjs', 'prerender.js',
1403
+ 'index.html', 'prerender.mjs', 'prerender.js', '_redirects',
1389
1404
  ]);
1390
1405
 
1391
1406
  function copyProjectIntoDist(rootResolved, outputResolved) {
@@ -1795,15 +1810,27 @@ async function runPrerender(config) {
1795
1810
  });
1796
1811
  });
1797
1812
 
1813
+ // Mark data-hydrate islands so static compile transforms skip them.
1814
+ await page.evaluate(() => {
1815
+ document.querySelectorAll('[data-hydrate]').forEach((root) => {
1816
+ root.setAttribute('data-prerender-hydrate', '1');
1817
+ root.querySelectorAll('*').forEach((el) => el.setAttribute('data-prerender-hydrate', '1'));
1818
+ });
1819
+ });
1820
+
1798
1821
  // x-for lists: keep static lists in the HTML for SEO; collapse only dynamic lists so Alpine re-renders.
1799
- // Explicit: data-dynamic or data-prerender="dynamic"|"skip". Inferred: x-for uses $search/$query,
1822
+ // Explicit: data-prerender="dynamic"|"skip". Inferred: x-for uses $search/$query,
1800
1823
  // $url, $auth, or iterates over getter names (filtered*, results, searchResults). See docs prerender + local.data.
1801
1824
  await page.evaluate(() => {
1802
1825
  document.querySelectorAll('template[x-for]').forEach((tpl) => {
1826
+ if (tpl.hasAttribute('data-prerender-hydrate') || tpl.closest('[data-prerender-hydrate]')) {
1827
+ tpl.removeAttribute('data-prerender-collapsed');
1828
+ tpl.removeAttribute('data-prerender-static-generated');
1829
+ return;
1830
+ }
1803
1831
  const xFor = (tpl.getAttribute('x-for') || '').trim();
1804
1832
  const prerender = (tpl.getAttribute('data-prerender') || '').toLowerCase();
1805
- const hasDataDynamic = tpl.hasAttribute('data-dynamic');
1806
- const explicit = hasDataDynamic || prerender === 'dynamic' || prerender === 'skip';
1833
+ const explicit = prerender === 'dynamic' || prerender === 'skip';
1807
1834
  const inferred = xFor.includes('$search') || xFor.includes('$query') ||
1808
1835
  xFor.includes('$url') || xFor.includes('$auth') ||
1809
1836
  /\bin\s+(filtered\w*|results|searchResults)\b/.test(xFor);
@@ -1917,6 +1944,7 @@ async function runPrerender(config) {
1917
1944
  };
1918
1945
 
1919
1946
  document.querySelectorAll('template[x-for]').forEach((tpl) => {
1947
+ if (tpl.hasAttribute('data-prerender-hydrate') || tpl.closest('[data-prerender-hydrate]')) return;
1920
1948
  const xFor = (tpl.getAttribute('x-for') || '').trim();
1921
1949
  const m = xFor.match(loopVarRegex);
1922
1950
  const itemVar = m ? (m[1] || m[3] || '') : '';
@@ -1945,6 +1973,7 @@ async function runPrerender(config) {
1945
1973
  const runBatch = typeof A?.mutateDom === 'function' ? (fn) => A.mutateDom(fn) : (fn) => fn();
1946
1974
  runBatch(() => {
1947
1975
  document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
1976
+ if (tpl.hasAttribute('data-prerender-hydrate') || tpl.closest('[data-prerender-hydrate]')) return;
1948
1977
  const parent = tpl.parentNode;
1949
1978
  if (!parent) {
1950
1979
  tpl.remove();
@@ -2017,15 +2046,6 @@ async function runPrerender(config) {
2017
2046
  });
2018
2047
  });
2019
2048
 
2020
- // Remove elements marked data-dynamic (so they are not in static HTML; client will render them).
2021
- // Skip <template> since we only collapse those above; other elements and their subtree are removed.
2022
- await page.evaluate(() => {
2023
- const toRemove = Array.from(document.querySelectorAll('[data-dynamic]')).filter((el) => el.tagName !== 'TEMPLATE');
2024
- const depth = (el) => { let d = 0; let n = el; while (n && n !== document.body) { d++; n = n.parentElement; } return d; };
2025
- toRemove.sort((a, b) => depth(a) - depth(b));
2026
- toRemove.forEach((el) => { if (document.contains(el)) el.remove(); });
2027
- });
2028
-
2029
2049
  const visibilityNormalizedPath = logicalPathToVisibilityNormalizedPath(pathSeg, locales);
2030
2050
  await page.evaluate((np) => {
2031
2051
  try {
@@ -2081,6 +2101,7 @@ async function runPrerender(config) {
2081
2101
  html = stripRedundantImgSrcBindings(html);
2082
2102
  html = stripEmptyInlineMaskStyles(html);
2083
2103
  html = stripResolvedXIconDirectives(html);
2104
+ html = stripPrerenderHydrateMarkers(html);
2084
2105
  html = rewriteHtmlAssetPaths(html, fileSegments.length);
2085
2106
  const liveBase = config.liveUrl.replace(/\/$/, '');
2086
2107
  const canonicalHreflang = buildCanonicalAndHreflang(is404 ? '' : pathSeg, locales, defaultLocale, liveBase);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.3.8",
3
+ "version": "0.4.0",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {