mnfst-render 0.5.34 → 0.5.36

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 +131 -1
  2. package/package.json +1 -1
@@ -1800,6 +1800,70 @@ function computeGlobalAssetSignature(rootDir) {
1800
1800
  return sha(parts.join('|'));
1801
1801
  }
1802
1802
 
1803
+ /**
1804
+ * Wait for the page to visually settle before OG fingerprint/screenshot.
1805
+ * manifest:render-ready covers tracked data sources, but async work can land
1806
+ * after it — x-markdown article fetches, runtime-compiled utility CSS, and
1807
+ * webfonts — and screenshotting early captures a blank article region. Two
1808
+ * conditions gate the snapshot: every [x-markdown] region has rendered
1809
+ * content (or the cap expires — some regions are legitimately empty in
1810
+ * prerender, e.g. demos), and a cheap DOM signature (text length + injected
1811
+ * style bytes + font status + empty-markdown count) holds steady. Then let
1812
+ * fonts finish and the compositor paint.
1813
+ */
1814
+ async function waitForVisualSettle(page, { settleMs = 300, mdWaitMs = 5000, capMs = 8000 } = {}) {
1815
+ try {
1816
+ await page.evaluate(async ({ settleMs, mdWaitMs, capMs }) => {
1817
+ // Preferred: the framework's own settled signal (loader ≥0.5.190).
1818
+ if (window.__manifestReadyCoordinator) {
1819
+ const start = Date.now();
1820
+ while (!window.__manifestReady && Date.now() - start < capMs + 8000) {
1821
+ await new Promise((r) => setTimeout(r, 100));
1822
+ }
1823
+ if (document.fonts?.ready) { try { await document.fonts.ready; } catch { /* best-effort */ } }
1824
+ await new Promise((r) => {
1825
+ const t = setTimeout(r, 250);
1826
+ requestAnimationFrame(() => requestAnimationFrame(() => { clearTimeout(t); r(); }));
1827
+ });
1828
+ return;
1829
+ }
1830
+ // Fallback heuristic for older framework builds:
1831
+ const state = () => {
1832
+ let styleLen = 0;
1833
+ for (const s of document.querySelectorAll('style')) styleLen += (s.textContent || '').length;
1834
+ let mdEmpty = 0;
1835
+ for (const e of document.querySelectorAll('[x-markdown]')) {
1836
+ if (!e.innerHTML.trim()) mdEmpty++;
1837
+ }
1838
+ const sig = `${mdEmpty}:${document.body?.innerText.length || 0}:${styleLen}:${document.fonts ? document.fonts.status : ''}`;
1839
+ return { sig, mdEmpty };
1840
+ };
1841
+ const start = Date.now();
1842
+ let last = state();
1843
+ let stableSince = Date.now();
1844
+ while (Date.now() - start < capMs) {
1845
+ await new Promise((r) => setTimeout(r, 100));
1846
+ const cur = state();
1847
+ const mdPending = cur.mdEmpty > 0 && Date.now() - start < mdWaitMs;
1848
+ if (cur.sig !== last.sig || mdPending) {
1849
+ last = cur;
1850
+ stableSince = Date.now();
1851
+ } else if (Date.now() - stableSince >= settleMs) {
1852
+ break;
1853
+ }
1854
+ }
1855
+ if (document.fonts?.ready) { try { await document.fonts.ready; } catch { /* best-effort */ } }
1856
+ // Let the compositor paint the settled state — but raced against a
1857
+ // timeout: a frame-idle headless page may never fire rAF, and an
1858
+ // unresolved evaluate() wedges the whole render worker.
1859
+ await new Promise((r) => {
1860
+ const t = setTimeout(r, 250);
1861
+ requestAnimationFrame(() => requestAnimationFrame(() => { clearTimeout(t); r(); }));
1862
+ });
1863
+ }, { settleMs, mdWaitMs, capMs });
1864
+ } catch { /* settle is best-effort — never blocks the snapshot */ }
1865
+ }
1866
+
1803
1867
  /**
1804
1868
  * Snapshot the page at 1200×630 and write to <output>/og/<slug>.png. Cache
1805
1869
  * sidecar lives in <root>/.mnfst-cache/og/ — outside the output dir, which is
@@ -1811,6 +1875,10 @@ function computeGlobalAssetSignature(rootDir) {
1811
1875
  * - html.className (theme variant: light/dark/etc.)
1812
1876
  */
1813
1877
  async function takeOgSnapshot(page, outputDir, pathSeg, globalAssetSignature, cacheDir) {
1878
+ // Settle BEFORE the cache fingerprint: the hash must describe the same
1879
+ // steady-state DOM the screenshot captures, or late-arriving content makes
1880
+ // the cache key unstable across runs.
1881
+ await waitForVisualSettle(page);
1814
1882
  // The 404 page must NOT share the homepage's 'index' slug: both pages render
1815
1883
  // concurrently and would otherwise race on the same og/index.png + cache
1816
1884
  // files. The 404 (route hidden, near-empty body) screenshots blank, and that
@@ -2836,9 +2904,12 @@ function startStaticServer(rootDir) {
2836
2904
  }
2837
2905
 
2838
2906
  // --- Copy project into output so website is self-contained (e.g. for Appwrite). ---
2907
+ // Internal/tooling files stay out of the published output: agent instructions,
2908
+ // the schema copy, and OS metadata anywhere in the tree.
2839
2909
  const COPY_EXCLUDE = new Set([
2840
2910
  'node_modules', '.git', 'package.json', 'package-lock.json',
2841
2911
  'index.html', 'prerender.mjs', 'prerender.js', '_redirects',
2912
+ 'CLAUDE.md', 'AGENTS.md', 'manifest.schema.json',
2842
2913
  ]);
2843
2914
 
2844
2915
  function copyProjectIntoDist(rootResolved, outputResolved) {
@@ -2850,7 +2921,10 @@ function copyProjectIntoDist(rootResolved, outputResolved) {
2850
2921
  if (COPY_EXCLUDE.has(name) || name.startsWith('.')) continue;
2851
2922
  const src = join(rootResolved, name);
2852
2923
  const dest = join(outputResolved, name);
2853
- cpSync(src, dest, { recursive: true });
2924
+ cpSync(src, dest, {
2925
+ recursive: true,
2926
+ filter: (p) => { const b = basename(p); return b !== '.DS_Store' && b !== 'Thumbs.db'; },
2927
+ });
2854
2928
  }
2855
2929
  COPY_EXCLUDE.delete(outputDirName);
2856
2930
  }
@@ -4292,6 +4366,62 @@ async function runPrerender(config) {
4292
4366
  if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) return;
4293
4367
  tag(tpl._x_currentIfEl);
4294
4368
  });
4369
+
4370
+ // Clones inside CLOSED popovers carry no crawler value and are wiped
4371
+ // at boot anyway (Alpine re-renders from the template) — drop them from
4372
+ // the output entirely.
4373
+ document.querySelectorAll('[data-mnfst-prerender-clone]').forEach((el) => {
4374
+ if (!document.contains(el)) return;
4375
+ const pop = el.closest('[popover]');
4376
+ if (pop && !pop.matches(':popover-open')) el.remove();
4377
+ });
4378
+
4379
+ // Containers stamped data-mnfst-generated are fully rebuilt by their
4380
+ // plugin at boot (e.g. a colorpicker's palette library — thousands of
4381
+ // swatch nodes with full palette JSON in x-data). Serialize them empty.
4382
+ document.querySelectorAll('[data-mnfst-generated]').forEach((el) => {
4383
+ if (document.contains(el)) el.innerHTML = '';
4384
+ });
4385
+
4386
+ // Legacy bridge for colorpickers older than the stamp (<0.5.190): the
4387
+ // palette library wrapper is rebuilt from scratch at boot regardless,
4388
+ // so drop it from the output. Remove once sites pin a stamped build.
4389
+ document.querySelectorAll('.library-wrapper').forEach((el) => {
4390
+ if (el.querySelector('.library-palette, [data-cp-library-group]')) el.remove();
4391
+ });
4392
+ });
4393
+
4394
+ // Deterministic output: randomized CSS anchor-positioning names
4395
+ // (--dropdown-x7f2, --popover-…, --anchor-…) are regenerated per run and
4396
+ // made every render a phantom diff. Renumber them sequentially in DOM
4397
+ // order so identical content serializes identically.
4398
+ await page.evaluate(() => {
4399
+ const RE = /--(dropdown|popover|anchor)-[a-z0-9]+/g;
4400
+ const map = new Map();
4401
+ const counters = { dropdown: 0, popover: 0, anchor: 0 };
4402
+ const rename = (m, kind) => {
4403
+ if (!map.has(m)) map.set(m, `--${kind}-p${++counters[kind]}`);
4404
+ return map.get(m);
4405
+ };
4406
+ for (const el of document.querySelectorAll('*')) {
4407
+ for (const attr of el.attributes) {
4408
+ if (!attr.value || attr.value.indexOf('--') === -1) continue;
4409
+ const next = attr.value.replace(RE, (m, kind) => rename(m, kind));
4410
+ if (next !== attr.value) el.setAttribute(attr.name, next);
4411
+ }
4412
+ }
4413
+
4414
+ // Component bookkeeping attrs land in whichever order the swap raced —
4415
+ // re-set the pair canonically so it can't flip between runs. (They are
4416
+ // not Alpine directives; position carries no behavior.)
4417
+ for (const el of document.querySelectorAll('[data-component][data-order]')) {
4418
+ const c = el.getAttribute('data-component');
4419
+ const o = el.getAttribute('data-order');
4420
+ el.removeAttribute('data-component');
4421
+ el.removeAttribute('data-order');
4422
+ el.setAttribute('data-component', c);
4423
+ el.setAttribute('data-order', o);
4424
+ }
4295
4425
  });
4296
4426
 
4297
4427
  // SEO / AEO meta injection — see resolveConfig().seo for precedence layers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.5.34",
3
+ "version": "0.5.36",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {