mnfst-render 0.4.1 → 0.4.3

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 +54 -16
  2. package/package.json +1 -1
@@ -956,6 +956,7 @@ function markPrerenderedManifestComponents(html) {
956
956
  return html.replace(/<(x-[a-z][\w-]*)([^>]*)>/gi, (full, tag, attrs) => {
957
957
  const a = attrs || '';
958
958
  if (/\bdata-pre-rendered\s*=/i.test(a) || /\bdata-processed\s*=/i.test(a)) return full;
959
+ if (/\bdata-prerender-hydrate\b/i.test(a)) return full; // Inside data-hydrate island — skip
959
960
  const spacer = /\S/.test(a) ? ' ' : '';
960
961
  return `<${tag}${a}${spacer}data-pre-rendered="1">`;
961
962
  });
@@ -1161,14 +1162,24 @@ function loadAllLocaleContentData(manifest, rootDir, locales) {
1161
1162
  */
1162
1163
  function buildSubstitutionPairs(defaultLocaleData, targetLocaleData) {
1163
1164
  const pairs = [];
1164
- for (const [key, rawDefault] of Object.entries(defaultLocaleData)) {
1165
- const rawTarget = targetLocaleData[key];
1166
- if (rawTarget == null || rawDefault == null) continue;
1167
- const from = String(rawDefault).trim();
1168
- const to = String(rawTarget).trim();
1169
- if (!from || from === to) continue;
1170
- pairs.push([from, to]);
1165
+ function collectPairs(defaultObj, targetObj) {
1166
+ if (!defaultObj || !targetObj) return;
1167
+ for (const key of Object.keys(defaultObj)) {
1168
+ const defaultVal = defaultObj[key];
1169
+ const targetVal = targetObj[key];
1170
+ if (defaultVal && typeof defaultVal === 'object') {
1171
+ // Recurse into nested objects (produced by setNestedKey for dotted CSV keys)
1172
+ collectPairs(defaultVal, targetVal && typeof targetVal === 'object' ? targetVal : {});
1173
+ } else {
1174
+ const from = String(defaultVal ?? '').trim();
1175
+ const to = String(targetVal ?? '').trim();
1176
+ if (!from || from === to) continue;
1177
+ pairs.push([from, to]);
1178
+ }
1179
+ }
1171
1180
  }
1181
+ collectPairs(defaultLocaleData, targetLocaleData);
1182
+ // Sort longest-first so more specific strings are replaced before shorter substrings
1172
1183
  pairs.sort((a, b) => b[0].length - a[0].length);
1173
1184
  return pairs;
1174
1185
  }
@@ -1266,6 +1277,9 @@ function generateLocaleVariantHtml({
1266
1277
  html = stripRedundantImgSrcBindings(html);
1267
1278
  html = stripEmptyInlineMaskStyles(html);
1268
1279
  html = stripResolvedXIconDirectives(html);
1280
+ // markPrerenderedManifestComponents must run BEFORE stripPrerenderHydrateMarkers so it can
1281
+ // detect data-prerender-hydrate markers and skip components inside hydrate islands.
1282
+ html = markPrerenderedManifestComponents(html);
1269
1283
  html = stripPrerenderHydrateMarkers(html);
1270
1284
 
1271
1285
  const fileSegments = pathToFileSegments(pathSeg ? '/' + pathSeg : '/');
@@ -1290,7 +1304,6 @@ function generateLocaleVariantHtml({
1290
1304
  '</head>',
1291
1305
  `${canonicalHreflang}${injectOgLocale ? ogLocale : ''}${routeMeta}${baseMeta}<meta name="manifest:prerendered" content="1">\n<meta name="manifest:router-base-depth" content="${routeDepth}">\n</head>`
1292
1306
  );
1293
- html = markPrerenderedManifestComponents(html);
1294
1307
 
1295
1308
  return { html, utilityBlocks: pageUtilityBlocks };
1296
1309
  }
@@ -1683,16 +1696,39 @@ async function runPrerender(config) {
1683
1696
  const puppeteerPaths = [];
1684
1697
  const localeVariantPaths = []; // { pathSeg, basePathSeg, targetLocale }
1685
1698
 
1699
+ // Two-pass categorisation: locale substitution only applies when the locale-neutral base path
1700
+ // (e.g. 'about' for 'fr/about') is itself in the path list and will be Puppeteer-rendered.
1701
+ //
1702
+ // Paths whose data is inherently locale-specific (e.g. 'en/articles/slug', 'fr/articles/slug'
1703
+ // discovered from per-locale data sources) have no locale-neutral counterpart and must be
1704
+ // rendered by Puppeteer directly — their content differs per locale and substitution cannot
1705
+ // produce correct output. This mirrors the framework's own data model: locale-neutral paths
1706
+ // use a shared structure with CSV text overlay; locale-prefixed paths carry per-locale content.
1707
+
1708
+ // Pass 1: collect all locale-neutral path segments (no locale prefix in the first segment).
1709
+ const localeNeutralPathSet = new Set();
1710
+ for (const seg of pathList) {
1711
+ if (!seg || seg === NOT_FOUND_PATH) continue;
1712
+ if (!localeSet.has(seg.split('/')[0])) localeNeutralPathSet.add(seg);
1713
+ }
1714
+
1715
+ // Pass 2: categorise.
1686
1716
  for (const seg of pathList) {
1687
1717
  if (!localeSubstEnabled || seg === NOT_FOUND_PATH || !seg) {
1688
1718
  puppeteerPaths.push(seg);
1689
1719
  continue;
1690
1720
  }
1691
- const firstPart = seg.split('/')[0];
1692
- if (localeSet.has(firstPart) && !localeSubstExclude.has(firstPart)) {
1693
- const basePathSeg = seg.slice(firstPart.length + 1); // strip 'fr/'
1694
- localeVariantPaths.push({ pathSeg: seg, basePathSeg: basePathSeg || '', targetLocale: firstPart });
1721
+ const fp = seg.split('/')[0];
1722
+ if (!localeSet.has(fp) || localeSubstExclude.has(fp)) {
1723
+ puppeteerPaths.push(seg);
1724
+ continue;
1725
+ }
1726
+ const basePathSeg = seg.slice(fp.length + 1) || '';
1727
+ if (localeNeutralPathSet.has(basePathSeg)) {
1728
+ // Locale-neutral base exists and will be Puppeteer-rendered → safe to substitute.
1729
+ localeVariantPaths.push({ pathSeg: seg, basePathSeg, targetLocale: fp });
1695
1730
  } else {
1731
+ // No locale-neutral base — this path has per-locale content; Puppeteer required.
1696
1732
  puppeteerPaths.push(seg);
1697
1733
  }
1698
1734
  }
@@ -2272,6 +2308,9 @@ async function runPrerender(config) {
2272
2308
  html = stripRedundantImgSrcBindings(html);
2273
2309
  html = stripEmptyInlineMaskStyles(html);
2274
2310
  html = stripResolvedXIconDirectives(html);
2311
+ // markPrerenderedManifestComponents must run BEFORE stripPrerenderHydrateMarkers so it can
2312
+ // detect data-prerender-hydrate markers and skip components inside hydrate islands.
2313
+ html = markPrerenderedManifestComponents(html);
2275
2314
  html = stripPrerenderHydrateMarkers(html);
2276
2315
  html = rewriteHtmlAssetPaths(html, fileSegments.length);
2277
2316
  const liveBase = config.liveUrl.replace(/\/$/, '');
@@ -2291,7 +2330,6 @@ async function runPrerender(config) {
2291
2330
  '</head>',
2292
2331
  `${canonicalHreflang}${injectOgLocale ? ogLocale : ''}${routeMeta}${baseMeta}${prerenderedMeta}<meta name="manifest:router-base-depth" content="${routeDepth}">\n</head>`
2293
2332
  );
2294
- html = markPrerenderedManifestComponents(html);
2295
2333
  mkdirSync(outDir, { recursive: true });
2296
2334
  writeFileSync(outFile, html, 'utf8');
2297
2335
  pushDebug({
@@ -2344,9 +2382,9 @@ async function runPrerender(config) {
2344
2382
  substIndex++;
2345
2383
  const rawHtml = baseHtmlCache.get(basePathSeg);
2346
2384
  if (!rawHtml) {
2347
- // Base path wasn't rendered log and skip (shouldn't happen in normal builds)
2348
- failedPaths.push({ path: '/' + pathSeg, message: `substitution skipped: base path "${basePathSeg || '/'}" not in cache` });
2349
- process.stderr.write(`prerender: substitution skipped /${pathSeg} (base not found)\n`);
2385
+ // Base path was expected to be Puppeteer-rendered but is absent its render likely failed.
2386
+ failedPaths.push({ path: '/' + pathSeg, message: `base path "${basePathSeg || '/'}" missing from cache (did its Puppeteer render fail?)` });
2387
+ process.stderr.write(`prerender: skipped /${pathSeg} base "${basePathSeg || '/'}" not in cache\n`);
2350
2388
  continue;
2351
2389
  }
2352
2390
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {