mnfst-render 0.2.5 → 0.2.7

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 +122 -1
  2. package/package.json +1 -1
@@ -39,6 +39,7 @@ function parseArgs() {
39
39
  if (args[i] === '--wait-after-idle' && args[i + 1]) { out.waitAfterIdle = parseInt(args[++i], 10); continue; }
40
40
  if (args[i] === '--concurrency' && args[i + 1]) { out.concurrency = parseInt(args[++i], 10); continue; }
41
41
  if (args[i] === '--dry-run') { out.dryRun = true; continue; }
42
+ if (args[i] === '--debug-prerender') { out.debugPrerender = true; continue; }
42
43
  }
43
44
  return out;
44
45
  }
@@ -86,6 +87,7 @@ function resolveConfig() {
86
87
  waitAfterIdle: Math.max(0, cli.waitAfterIdle ?? pre.waitAfterIdle ?? 0),
87
88
  concurrency: Math.max(1, cli.concurrency ?? pre.concurrency ?? 6),
88
89
  dryRun: !!cli.dryRun,
90
+ debugPrerender: !!(cli.debugPrerender ?? pre.debugPrerender),
89
91
  };
90
92
  }
91
93
 
@@ -729,6 +731,29 @@ function stripPrerenderDynamicBindings(html) {
729
731
  });
730
732
  }
731
733
 
734
+ // Remove empty inline mask-image styles emitted before data resolves
735
+ // (e.g. style="mask-image: url()"), while keeping any :style/x-bind:style bindings.
736
+ function stripEmptyInlineMaskStyles(html) {
737
+ return html.replace(/<(\w+)([^>]*)>/g, (full, tag, attrs) => {
738
+ const styleMatch = attrs.match(/\sstyle=(["'])([\s\S]*?)\1/i);
739
+ if (!styleMatch) return full;
740
+ const quote = styleMatch[1];
741
+ const rawStyle = styleMatch[2] || '';
742
+ const cleaned = rawStyle
743
+ .replace(/\bmask-image\s*:\s*url\(\s*(?:''|""|)\s*\)\s*;?/gi, '')
744
+ .replace(/\b-webkit-mask-image\s*:\s*url\(\s*(?:''|""|)\s*\)\s*;?/gi, '')
745
+ .trim()
746
+ .replace(/^\s*;\s*|\s*;\s*$/g, '');
747
+
748
+ if (!cleaned) {
749
+ const newAttrs = attrs.replace(/\sstyle=(["'])[\s\S]*?\1/i, '');
750
+ return `<${tag}${newAttrs}>`;
751
+ }
752
+ const rebuilt = attrs.replace(/\sstyle=(["'])[\s\S]*?\1/i, ` style=${quote}${cleaned}${quote}`);
753
+ return `<${tag}${rebuilt}>`;
754
+ });
755
+ }
756
+
732
757
  // --- Rewrite asset URLs: depth = segments from this HTML file up to output root (website). ----
733
758
  // All project assets are copied into output, so root-relative paths become relative within output.
734
759
  // Do NOT rewrite href on <a> tags (navigation links); only rewrite link/script/img so router gets clean paths.
@@ -1222,8 +1247,14 @@ async function runPrerender(config) {
1222
1247
  const concurrency = config.concurrency;
1223
1248
  const pathTotal = pathList.length;
1224
1249
  const failedPaths = [];
1250
+ const debugRows = [];
1225
1251
  process.stdout.write(`Prerendering ${pathTotal} path(s)...\n`);
1226
1252
 
1253
+ function pushDebug(row) {
1254
+ if (!config.debugPrerender) return;
1255
+ debugRows.push(row);
1256
+ }
1257
+
1227
1258
  async function processPath(pathSeg, pathIndex) {
1228
1259
  const is404 = pathSeg === NOT_FOUND_PATH;
1229
1260
  const pathname = is404 ? `/${NOT_FOUND_PATH}` : (pathSeg ? `/${pathSeg}` : '/');
@@ -1242,6 +1273,7 @@ async function runPrerender(config) {
1242
1273
 
1243
1274
  const page = await browser.newPage();
1244
1275
  try {
1276
+ pushDebug({ path: displayPath, stage: 'start' });
1245
1277
  await page.goto(url, {
1246
1278
  waitUntil: 'domcontentloaded',
1247
1279
  timeout: Math.min(timeout, 30000),
@@ -1281,6 +1313,36 @@ async function runPrerender(config) {
1281
1313
  });
1282
1314
  }).catch(() => { });
1283
1315
 
1316
+ if (config.debugPrerender) {
1317
+ const before = await page.evaluate(() => {
1318
+ const templates = Array.from(document.querySelectorAll('template[x-for]'));
1319
+ const entries = templates.slice(0, 60).map((tpl) => {
1320
+ const first = tpl.content?.firstElementChild;
1321
+ const tag = first ? first.tagName : null;
1322
+ const cls = first ? (first.getAttribute('class') || '') : '';
1323
+ let cloneCount = 0;
1324
+ let next = tpl.nextElementSibling;
1325
+ while (next && (!tag || next.tagName === tag)) {
1326
+ if (tag && (next.getAttribute('class') || '') !== cls) break;
1327
+ cloneCount++;
1328
+ next = next.nextElementSibling;
1329
+ }
1330
+ return {
1331
+ xFor: (tpl.getAttribute('x-for') || '').slice(0, 140),
1332
+ collapsed: tpl.getAttribute('data-prerender-collapsed') === '1',
1333
+ staticGenerated: tpl.getAttribute('data-prerender-static-generated') === '1',
1334
+ cloneCount,
1335
+ };
1336
+ });
1337
+ return {
1338
+ templateCount: templates.length,
1339
+ nonCollapsedTemplateCount: templates.filter((t) => t.getAttribute('data-prerender-collapsed') !== '1').length,
1340
+ entries,
1341
+ };
1342
+ }).catch(() => null);
1343
+ pushDebug({ path: displayPath, stage: 'post-dom-settle', metrics: before });
1344
+ }
1345
+
1284
1346
  await page.waitForNetworkIdle({ idleTime: 1500, timeout: 10000 }).catch(() => { });
1285
1347
 
1286
1348
  await page.evaluate(() => {
@@ -1390,10 +1452,13 @@ async function runPrerender(config) {
1390
1452
  const first = tpl.content?.firstElementChild;
1391
1453
  if (first) {
1392
1454
  const tag = first.tagName;
1455
+ const cls = first.getAttribute('class') || '';
1393
1456
  let next = tpl.nextElementSibling;
1394
1457
  let generatedCount = 0;
1395
1458
  while (next) {
1396
1459
  if (next.tagName !== tag) break;
1460
+ const sameClass = (next.getAttribute('class') || '') === cls;
1461
+ if (!sameClass) break;
1397
1462
  generatedCount++;
1398
1463
  next = next.nextElementSibling;
1399
1464
  }
@@ -1491,7 +1556,35 @@ async function runPrerender(config) {
1491
1556
  if (!bindingAttrRegex.test(attr.name)) continue;
1492
1557
  const expr = attr.value || '';
1493
1558
  if (hasVar(expr, itemVar) || hasVar(expr, indexVar)) {
1494
- node.removeAttribute(attr.name);
1559
+ const name = attr.name;
1560
+ // Remove text/html bindings only when static content already exists.
1561
+ if (name === 'x-text' || name === 'x-html') {
1562
+ if ((node.textContent || '').trim() || (node.innerHTML || '').trim()) {
1563
+ node.removeAttribute(name);
1564
+ }
1565
+ continue;
1566
+ }
1567
+
1568
+ // Remove x-show/x-if if they reference loop vars; cloned node is now static.
1569
+ if (name === 'x-show' || name === 'x-if') {
1570
+ node.removeAttribute(name);
1571
+ continue;
1572
+ }
1573
+
1574
+ // For :attr / x-bind:attr, only remove binding if a concrete attr is present.
1575
+ let boundAttr = '';
1576
+ if (name.startsWith(':')) boundAttr = name.slice(1);
1577
+ else if (name.startsWith('x-bind:')) boundAttr = name.slice('x-bind:'.length);
1578
+ if (boundAttr) {
1579
+ const concrete = node.getAttribute(boundAttr);
1580
+ if (concrete != null && String(concrete).trim() !== '') {
1581
+ node.removeAttribute(name);
1582
+ }
1583
+ continue;
1584
+ }
1585
+
1586
+ // Event/other loop-scoped bindings are unsafe on static clones.
1587
+ node.removeAttribute(name);
1495
1588
  }
1496
1589
  }
1497
1590
  }
@@ -1537,6 +1630,15 @@ async function runPrerender(config) {
1537
1630
  });
1538
1631
 
1539
1632
  let html = await page.evaluate(() => document.documentElement.outerHTML);
1633
+ if (config.debugPrerender) {
1634
+ const post = await page.evaluate(() => {
1635
+ const templates = document.querySelectorAll('template[x-for]').length;
1636
+ const links = document.querySelectorAll('a[href="#"]').length;
1637
+ const hidden = document.querySelectorAll('[style*="display: none"]').length;
1638
+ return { templateCountAfterCleanup: templates, hashHrefCount: links, displayNoneCount: hidden };
1639
+ }).catch(() => null);
1640
+ pushDebug({ path: displayPath, stage: 'pre-serialize', metrics: post });
1641
+ }
1540
1642
  html = stripDevOnlyContent(html);
1541
1643
  html = stripInjectedPluginScripts(html);
1542
1644
  if (tailwindBuilt) {
@@ -1556,6 +1658,7 @@ async function runPrerender(config) {
1556
1658
  const xData = { manifest, content };
1557
1659
  html = resolveHeadXBindings(html, xData);
1558
1660
  html = stripPrerenderDynamicBindings(html);
1661
+ html = stripEmptyInlineMaskStyles(html);
1559
1662
  html = rewriteHtmlAssetPaths(html, fileSegments.length);
1560
1663
  const liveBase = config.liveUrl.replace(/\/$/, '');
1561
1664
  const canonicalHreflang = buildCanonicalAndHreflang(is404 ? '' : pathSeg, locales, defaultLocale, liveBase);
@@ -1568,6 +1671,13 @@ async function runPrerender(config) {
1568
1671
  html = html.replace('</head>', `${canonicalHreflang}${injectOgLocale ? ogLocale : ''}${baseMeta}${prerenderedMeta}<meta name="manifest:router-base-depth" content="${routeDepth}">\n</head>`);
1569
1672
  mkdirSync(outDir, { recursive: true });
1570
1673
  writeFileSync(outFile, html, 'utf8');
1674
+ pushDebug({
1675
+ path: displayPath,
1676
+ stage: 'wrote',
1677
+ outFile,
1678
+ htmlBytes: Buffer.byteLength(html, 'utf8'),
1679
+ hasXForTemplate: html.includes('template x-for') || html.includes('template[x-for]'),
1680
+ });
1571
1681
  } catch (err) {
1572
1682
  failedPaths.push({
1573
1683
  path: displayPath,
@@ -1602,6 +1712,17 @@ async function runPrerender(config) {
1602
1712
  throw new Error(`prerender failed for ${failedPaths.length}/${pathTotal} paths. Sample: ${sample}`);
1603
1713
  }
1604
1714
 
1715
+ if (config.debugPrerender) {
1716
+ const reportPath = join(outputResolved, 'prerender.debug.json');
1717
+ writeFileSync(reportPath, JSON.stringify({
1718
+ generatedAt: new Date().toISOString(),
1719
+ totalPaths: pathTotal,
1720
+ failedPaths,
1721
+ rows: debugRows,
1722
+ }, null, 2), 'utf8');
1723
+ process.stdout.write(`prerender: debug report ${reportPath}\n`);
1724
+ }
1725
+
1605
1726
  if (bundleUtilities) {
1606
1727
  const utilMerged = mergeUtilityCssBlocks(utilityBlocks);
1607
1728
  if (utilMerged.trim()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {