mnfst-render 0.3.7 → 0.3.8

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 +56 -10
  2. package/package.json +1 -1
@@ -252,6 +252,13 @@ function loadConfig(rootDir) {
252
252
  return manifest;
253
253
  }
254
254
 
255
+ function normalizeLocaleRouteExclude(val) {
256
+ if (val == null) return [];
257
+ if (Array.isArray(val)) return val.map((s) => String(s).trim()).filter(Boolean);
258
+ if (typeof val === 'string') return val.split(',').map((s) => s.trim()).filter(Boolean);
259
+ return [];
260
+ }
261
+
255
262
  function resolveConfig() {
256
263
  const cli = parseArgs();
257
264
  const cwd = process.cwd();
@@ -274,6 +281,10 @@ function resolveConfig() {
274
281
  output: resolve(root, cli.output ?? pre.output ?? 'website'),
275
282
  root,
276
283
  routerBase: pre.routerBase ?? null,
284
+ /** Logical path prefixes (after locale) that skip sticky locale prefix; see manifest:locale-route-exclude */
285
+ localeRouteExclude: normalizeLocaleRouteExclude(
286
+ pre.localeRouteExclude ?? pre.localeStickyExclude
287
+ ),
277
288
  locales: pre.locales,
278
289
  redirects: Array.isArray(pre.redirects) ? pre.redirects : [],
279
290
  wait: cli.wait ?? pre.wait ?? null,
@@ -853,19 +864,26 @@ function depthFromOutputRoot(outputDir, filePath) {
853
864
  return rel.split(sep).filter(Boolean).length;
854
865
  }
855
866
 
856
- /** Inject stylesheet link with correct relative href for static hosting (after prerender wrote files). */
857
- function postProcessInjectStylesheetLink(outputDir, filename) {
867
+ /** Root-absolute path for prerender bundles (same URL from every page depth; supports manifest:router-base). */
868
+ function buildRootAssetPath(routerBasePath, filename) {
869
+ const base = String(routerBasePath || '').replace(/^\/+|\/+$/g, '');
870
+ const name = String(filename || '').replace(/^\/+/, '');
871
+ const path = base ? `${base}/${name}` : name;
872
+ return '/' + path.replace(/\/{2,}/g, '/');
873
+ }
874
+
875
+ /** Inject stylesheet link with root-absolute href (avoids ../ resolving under locale segments like /en/page/). */
876
+ function postProcessInjectStylesheetLink(outputDir, filename, routerBasePath) {
858
877
  const cssPath = join(outputDir, filename);
859
878
  if (!existsSync(cssPath)) return;
860
879
  const stat = statSync(cssPath);
861
880
  if (stat.size === 0) return;
862
881
 
882
+ const href = buildRootAssetPath(routerBasePath, filename);
883
+ const tag = `<link rel="stylesheet" href="${href}">`;
863
884
  const files = walkHtmlFiles(outputDir);
864
885
  for (const file of files) {
865
886
  let html = readFileSync(file, 'utf8');
866
- const depth = depthFromOutputRoot(outputDir, file);
867
- const prefix = depth ? '../'.repeat(depth) : '';
868
- const tag = `<link rel="stylesheet" href="${prefix}${filename}">`;
869
887
  html = injectBeforeHeadClose(html, tag);
870
888
  writeFileSync(file, html, 'utf8');
871
889
  }
@@ -1007,6 +1025,10 @@ function stripEmptyInlineMaskStyles(html) {
1007
1025
  // All project assets are copied into output, so root-relative paths become relative within output.
1008
1026
  // Do NOT rewrite href on <a> tags (navigation links); only rewrite link/script/img so router gets clean paths.
1009
1027
 
1028
+ function isPrerenderBundleAssetPath(pathAfterSlash) {
1029
+ return /(^|\/)prerender\.(tailwind|utilities)\.css$/.test(pathAfterSlash);
1030
+ }
1031
+
1010
1032
  function rewriteHtmlAssetPaths(html, depthWithinOutput) {
1011
1033
  const prefix = depthWithinOutput > 0 ? '../'.repeat(depthWithinOutput) : '';
1012
1034
  if (!prefix) return html;
@@ -1016,9 +1038,10 @@ function rewriteHtmlAssetPaths(html, depthWithinOutput) {
1016
1038
  const tag = htmlBeforeMatch.slice(lastOpen + 1).match(/^(\w+)/);
1017
1039
  return tag && tag[1].toLowerCase() === 'a';
1018
1040
  }
1019
- let out = html.replace(/(\s(href|src)=["'])\/(?!\/)/g, (match, lead, attr, offset, fullString) => {
1041
+ let out = html.replace(/(\s(href|src)=["'])\/(?!\/)([^'"]*)/g, (match, lead, _attr, rest, offset, fullString) => {
1020
1042
  if (isAnchorTag(fullString.slice(0, offset))) return match;
1021
- return lead + prefix;
1043
+ if (isPrerenderBundleAssetPath(rest)) return match;
1044
+ return lead + prefix + rest;
1022
1045
  });
1023
1046
  out = out.replace(/(\s(href|src)=["'])(\.\.\/)+/g, (match, lead, attr, dots, offset, fullString) => {
1024
1047
  if (isAnchorTag(fullString.slice(0, offset))) return match;
@@ -1027,6 +1050,17 @@ function rewriteHtmlAssetPaths(html, depthWithinOutput) {
1027
1050
  return out;
1028
1051
  }
1029
1052
 
1053
+ // Alpine x-data drives radio state; baked checked="" from the live DOM (e.g. yearly) fights monthly defaults.
1054
+ function stripPrerenderBakedRadioCheckedForXModel(html) {
1055
+ return html.replace(/<input\b([^>]*)>/gi, (full, attrs) => {
1056
+ if (!/\btype\s*=\s*["']radio["']/i.test(attrs)) return full;
1057
+ if (!/\bx-model\s*=/i.test(attrs)) return full;
1058
+ const next = attrs.replace(/\s+checked(?:\s*=\s*["'][^"']*["']|\s*=\s*[^\s>]+)?/gi, '');
1059
+ if (next === attrs) return full;
1060
+ return `<input${next}>`;
1061
+ });
1062
+ }
1063
+
1030
1064
  // --- Canonical and hreflang (per-page injection) ---
1031
1065
 
1032
1066
  function buildCanonicalAndHreflang(pathSeg, locales, defaultLocale, base) {
@@ -2032,7 +2066,10 @@ async function runPrerender(config) {
2032
2066
  for (const b of extracted.blocks) utilityBlocks.push(b);
2033
2067
  }
2034
2068
  if (tailwindBuilt) {
2035
- html = injectBeforeHeadClose(html, '<link rel="stylesheet" href="/prerender.tailwind.css">');
2069
+ html = injectBeforeHeadClose(
2070
+ html,
2071
+ `<link rel="stylesheet" href="${buildRootAssetPath(routerBasePath, 'prerender.tailwind.css')}">`
2072
+ );
2036
2073
  }
2037
2074
  html = stripDuplicatedLoopDirectives(html);
2038
2075
  html = stripPrerenderedXDataDirectives(html);
@@ -2040,6 +2077,7 @@ async function runPrerender(config) {
2040
2077
  const xData = { manifest, content };
2041
2078
  html = resolveHeadXBindings(html, xData);
2042
2079
  html = stripPrerenderDynamicBindings(html);
2080
+ html = stripPrerenderBakedRadioCheckedForXModel(html);
2043
2081
  html = stripRedundantImgSrcBindings(html);
2044
2082
  html = stripEmptyInlineMaskStyles(html);
2045
2083
  html = stripResolvedXIconDirectives(html);
@@ -2050,9 +2088,17 @@ async function runPrerender(config) {
2050
2088
  const injectOgLocale = ogLocale && hasOtherOgMeta(html);
2051
2089
  if (injectOgLocale) html = stripOgLocaleFromHead(html);
2052
2090
  const baseMeta = routerBasePath !== null ? `<meta name="manifest:router-base" content="${String(routerBasePath).replace(/"/g, '&quot;')}">\n` : '';
2091
+ const routeEx = config.localeRouteExclude || [];
2092
+ const routeMeta =
2093
+ routeEx.length > 0
2094
+ ? `<meta name="manifest:locale-route-exclude" content="${JSON.stringify(routeEx).replace(/"/g, '&quot;')}">\n`
2095
+ : '';
2053
2096
  const routeDepth = fileSegments.length;
2054
2097
  const prerenderedMeta = `<meta name="manifest:prerendered" content="1">\n`;
2055
- html = html.replace('</head>', `${canonicalHreflang}${injectOgLocale ? ogLocale : ''}${baseMeta}${prerenderedMeta}<meta name="manifest:router-base-depth" content="${routeDepth}">\n</head>`);
2098
+ html = html.replace(
2099
+ '</head>',
2100
+ `${canonicalHreflang}${injectOgLocale ? ogLocale : ''}${routeMeta}${baseMeta}${prerenderedMeta}<meta name="manifest:router-base-depth" content="${routeDepth}">\n</head>`
2101
+ );
2056
2102
  html = markPrerenderedManifestComponents(html);
2057
2103
  mkdirSync(outDir, { recursive: true });
2058
2104
  writeFileSync(outFile, html, 'utf8');
@@ -2113,7 +2159,7 @@ async function runPrerender(config) {
2113
2159
  if (utilMerged.trim()) {
2114
2160
  writeFileSync(join(outputResolved, 'prerender.utilities.css'), `${utilMerged}\n`, 'utf8');
2115
2161
  process.stdout.write('prerender: wrote prerender.utilities.css (Manifest custom utilities)\n');
2116
- postProcessInjectStylesheetLink(outputResolved, 'prerender.utilities.css');
2162
+ postProcessInjectStylesheetLink(outputResolved, 'prerender.utilities.css', routerBasePath || '');
2117
2163
  }
2118
2164
  }
2119
2165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {