mnfst-render 0.5.10 → 0.5.11

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.
@@ -1271,6 +1271,49 @@ function buildSubstitutionPairs(defaultLocaleData, targetLocaleData) {
1271
1271
  return pairs;
1272
1272
  }
1273
1273
 
1274
+ /**
1275
+ /**
1276
+ * Prefix internal navigation links with the target locale so that prerendered
1277
+ * MPA pages link directly to the correct locale variant (e.g. /platform →
1278
+ * /fr/platform on the French page). Without this, users must rely on runtime
1279
+ * JS interception (`installMpaStickyLocaleLinks`) which may not be ready by
1280
+ * the time they click — causing navigation to fall back to English.
1281
+ *
1282
+ * Only rewrites `<a href="...">` where the href is a root-relative path that
1283
+ * doesn't already carry a locale prefix and isn't an excluded route.
1284
+ */
1285
+ function prefixLocaleInternalLinks(html, locale, locales, localeRouteExclude) {
1286
+ if (!locale || !locales || !locales.length) return html;
1287
+ const localeSet = new Set(locales);
1288
+ const excludeSet = new Set(localeRouteExclude || []);
1289
+
1290
+ // Match <a ... href="..." ...> — capture the href value
1291
+ return html.replace(
1292
+ /(<a\b[^>]*\shref=["'])(\/?[^"'#][^"']*)(["'][^>]*>)/gi,
1293
+ (full, prefix, href, suffix) => {
1294
+ // Only process root-relative paths
1295
+ if (!href.startsWith('/')) return full;
1296
+ // Skip external protocols embedded as relative (shouldn't happen but guard)
1297
+ if (/^\/\//.test(href)) return full;
1298
+
1299
+ const withoutSlash = href.replace(/^\//, '');
1300
+ const firstSeg = withoutSlash.split('/')[0].split('#')[0].split('?')[0];
1301
+
1302
+ // Already has a locale prefix
1303
+ if (localeSet.has(firstSeg)) return full;
1304
+
1305
+ // Skip asset-like paths
1306
+ if (/\.(css|js|json|svg|png|jpg|jpeg|gif|ico|woff2?|ttf|eot|pdf|xml|txt)$/i.test(href)) return full;
1307
+
1308
+ // Respect localeRouteExclude — these routes stay locale-neutral
1309
+ if (excludeSet.has(firstSeg)) return full;
1310
+
1311
+ // Prefix with locale
1312
+ return `${prefix}/${locale}${href}${suffix}`;
1313
+ }
1314
+ );
1315
+ }
1316
+
1274
1317
  /**
1275
1318
  * Apply locale text substitution to rendered HTML.
1276
1319
  * Replaces content in text nodes (between > and <) and in key attributes:
@@ -1334,6 +1377,12 @@ function generateLocaleVariantHtml({
1334
1377
  // Apply locale text substitution
1335
1378
  html = applyLocaleSubstitution(html, substitutionPairs);
1336
1379
 
1380
+ // Prefix internal <a> links with the locale so MPA navigation stays in-locale
1381
+ // without relying on runtime JS interception.
1382
+ if (targetLocale && targetLocale !== defaultLocale) {
1383
+ html = prefixLocaleInternalLinks(html, targetLocale, locales, config.localeRouteExclude);
1384
+ }
1385
+
1337
1386
  // Standard Node.js post-processing (same sequence as processPath)
1338
1387
  html = stripDevOnlyContent(html);
1339
1388
  html = stripInjectedPluginScripts(html);
@@ -1778,7 +1827,13 @@ async function runPrerender(config) {
1778
1827
  console.error(' npm i -D puppeteer-core @sparticuz/chromium');
1779
1828
  process.exit(1);
1780
1829
  }
1781
- return await puppeteer.default.launch({ headless: true });
1830
+ return await puppeteer.default.launch({
1831
+ headless: true,
1832
+ args: [
1833
+ '--no-sandbox',
1834
+ '--disable-setuid-sandbox',
1835
+ ],
1836
+ });
1782
1837
  }
1783
1838
  }
1784
1839
  let browser = await launchBrowser();
@@ -2398,6 +2453,18 @@ async function runPrerender(config) {
2398
2453
  const src = source[name];
2399
2454
  const cur = name in currentAttrs ? currentAttrs[name] : null;
2400
2455
  if (src !== cur) {
2456
+ // If the source value is null (attribute didn't exist originally)
2457
+ // but a reactive Alpine binding controls this attribute, skip the
2458
+ // restoration. Alpine will re-evaluate the binding on init and
2459
+ // set the correct value; nulling it in the contract would flash
2460
+ // the element unstyled until Alpine + async data loads catch up.
2461
+ // The baked value IS the correct initial render.
2462
+ if (src === null) {
2463
+ const hasBinding =
2464
+ (name === 'style' && (':style' in source || 'x-bind:style' in source)) ||
2465
+ (name === 'class' && (':class' in source || 'x-bind:class' in source));
2466
+ if (hasBinding) continue;
2467
+ }
2401
2468
  attrsOut[name] = src; // may be null (means "remove this attribute")
2402
2469
  dirty = true;
2403
2470
  }
@@ -2798,6 +2865,13 @@ async function runPrerender(config) {
2798
2865
  html = stripEmptyInlineMaskStyles(html);
2799
2866
  html = stripResolvedXIconDirectives(html);
2800
2867
  html = markPrerenderedManifestComponents(html);
2868
+
2869
+ // Prefix internal <a> links with the locale for non-default locales so
2870
+ // MPA navigation stays in-locale without relying on runtime JS.
2871
+ if (currentLocale && currentLocale !== defaultLocale) {
2872
+ html = prefixLocaleInternalLinks(html, currentLocale, locales, config.localeRouteExclude);
2873
+ }
2874
+
2801
2875
  html = rewriteHtmlAssetPaths(html, fileSegments.length);
2802
2876
  const liveBase = config.liveUrl.replace(/\/$/, '');
2803
2877
  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.5.10",
3
+ "version": "0.5.11",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {