mnfst-render 0.5.34 → 0.5.35

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.
@@ -1800,6 +1800,56 @@ 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
+ const state = () => {
1818
+ let styleLen = 0;
1819
+ for (const s of document.querySelectorAll('style')) styleLen += (s.textContent || '').length;
1820
+ let mdEmpty = 0;
1821
+ for (const e of document.querySelectorAll('[x-markdown]')) {
1822
+ if (!e.innerHTML.trim()) mdEmpty++;
1823
+ }
1824
+ const sig = `${mdEmpty}:${document.body?.innerText.length || 0}:${styleLen}:${document.fonts ? document.fonts.status : ''}`;
1825
+ return { sig, mdEmpty };
1826
+ };
1827
+ const start = Date.now();
1828
+ let last = state();
1829
+ let stableSince = Date.now();
1830
+ while (Date.now() - start < capMs) {
1831
+ await new Promise((r) => setTimeout(r, 100));
1832
+ const cur = state();
1833
+ const mdPending = cur.mdEmpty > 0 && Date.now() - start < mdWaitMs;
1834
+ if (cur.sig !== last.sig || mdPending) {
1835
+ last = cur;
1836
+ stableSince = Date.now();
1837
+ } else if (Date.now() - stableSince >= settleMs) {
1838
+ break;
1839
+ }
1840
+ }
1841
+ if (document.fonts?.ready) { try { await document.fonts.ready; } catch { /* best-effort */ } }
1842
+ // Let the compositor paint the settled state — but raced against a
1843
+ // timeout: a frame-idle headless page may never fire rAF, and an
1844
+ // unresolved evaluate() wedges the whole render worker.
1845
+ await new Promise((r) => {
1846
+ const t = setTimeout(r, 250);
1847
+ requestAnimationFrame(() => requestAnimationFrame(() => { clearTimeout(t); r(); }));
1848
+ });
1849
+ }, { settleMs, mdWaitMs, capMs });
1850
+ } catch { /* settle is best-effort — never blocks the snapshot */ }
1851
+ }
1852
+
1803
1853
  /**
1804
1854
  * Snapshot the page at 1200×630 and write to <output>/og/<slug>.png. Cache
1805
1855
  * sidecar lives in <root>/.mnfst-cache/og/ — outside the output dir, which is
@@ -1811,6 +1861,10 @@ function computeGlobalAssetSignature(rootDir) {
1811
1861
  * - html.className (theme variant: light/dark/etc.)
1812
1862
  */
1813
1863
  async function takeOgSnapshot(page, outputDir, pathSeg, globalAssetSignature, cacheDir) {
1864
+ // Settle BEFORE the cache fingerprint: the hash must describe the same
1865
+ // steady-state DOM the screenshot captures, or late-arriving content makes
1866
+ // the cache key unstable across runs.
1867
+ await waitForVisualSettle(page);
1814
1868
  // The 404 page must NOT share the homepage's 'index' slug: both pages render
1815
1869
  // concurrently and would otherwise race on the same og/index.png + cache
1816
1870
  // files. The 404 (route hidden, near-empty body) screenshots blank, and that
@@ -2836,9 +2890,12 @@ function startStaticServer(rootDir) {
2836
2890
  }
2837
2891
 
2838
2892
  // --- Copy project into output so website is self-contained (e.g. for Appwrite). ---
2893
+ // Internal/tooling files stay out of the published output: agent instructions,
2894
+ // the schema copy, and OS metadata anywhere in the tree.
2839
2895
  const COPY_EXCLUDE = new Set([
2840
2896
  'node_modules', '.git', 'package.json', 'package-lock.json',
2841
2897
  'index.html', 'prerender.mjs', 'prerender.js', '_redirects',
2898
+ 'CLAUDE.md', 'AGENTS.md', 'manifest.schema.json',
2842
2899
  ]);
2843
2900
 
2844
2901
  function copyProjectIntoDist(rootResolved, outputResolved) {
@@ -2850,7 +2907,10 @@ function copyProjectIntoDist(rootResolved, outputResolved) {
2850
2907
  if (COPY_EXCLUDE.has(name) || name.startsWith('.')) continue;
2851
2908
  const src = join(rootResolved, name);
2852
2909
  const dest = join(outputResolved, name);
2853
- cpSync(src, dest, { recursive: true });
2910
+ cpSync(src, dest, {
2911
+ recursive: true,
2912
+ filter: (p) => { const b = basename(p); return b !== '.DS_Store' && b !== 'Thumbs.db'; },
2913
+ });
2854
2914
  }
2855
2915
  COPY_EXCLUDE.delete(outputDirName);
2856
2916
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.5.34",
3
+ "version": "0.5.35",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {