mnfst-render 0.3.6 → 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 +64 -13
  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
  }
@@ -956,14 +974,15 @@ function stripRedundantImgSrcBindings(html) {
956
974
  * prerender-baked markup (stripped :style, expanded lists, etc.). Tag opens with data-pre-rendered
957
975
  * are skipped by manifest.components.processor — required for static prerender output to hydrate correctly.
958
976
  */
959
- // Prerender inlined Iconify SVG under <i x-icon="iterator.icon">; drop x-icon so Alpine does not evaluate module/item.
977
+ // Prerender inlined Iconify SVG under <i x-icon="iterator.icon">; clear x-icon value so Alpine does not evaluate
978
+ // loop/item expressions while the attribute remains for CSS (e.g. inline layout that keys off [x-icon]).
960
979
  function stripResolvedXIconDirectives(html) {
961
980
  return html.replace(/<i\b([^>]*)>([\s\S]*?)<\/i>/gi, (full, attrs, inner) => {
962
981
  if (!/\sx-icon\s*=/i.test(attrs)) return full;
963
982
  if (!/<svg\b/i.test(inner) || !/\bdata-icon\s*=/i.test(inner)) return full;
964
983
  const cleaned = attrs
965
- .replace(/\s+x-icon\s*=\s*"[^"]*"/gi, '')
966
- .replace(/\s+x-icon\s*=\s*'[^']*'/gi, '')
984
+ .replace(/\s+x-icon\s*=\s*"[^"]*"/gi, ' x-icon=""')
985
+ .replace(/\s+x-icon\s*=\s*'[^']*'/gi, ' x-icon=""')
967
986
  .trim();
968
987
  const sp = cleaned ? ' ' : '';
969
988
  return `<i${sp}${cleaned}>${inner}</i>`;
@@ -1006,6 +1025,10 @@ function stripEmptyInlineMaskStyles(html) {
1006
1025
  // All project assets are copied into output, so root-relative paths become relative within output.
1007
1026
  // Do NOT rewrite href on <a> tags (navigation links); only rewrite link/script/img so router gets clean paths.
1008
1027
 
1028
+ function isPrerenderBundleAssetPath(pathAfterSlash) {
1029
+ return /(^|\/)prerender\.(tailwind|utilities)\.css$/.test(pathAfterSlash);
1030
+ }
1031
+
1009
1032
  function rewriteHtmlAssetPaths(html, depthWithinOutput) {
1010
1033
  const prefix = depthWithinOutput > 0 ? '../'.repeat(depthWithinOutput) : '';
1011
1034
  if (!prefix) return html;
@@ -1015,9 +1038,10 @@ function rewriteHtmlAssetPaths(html, depthWithinOutput) {
1015
1038
  const tag = htmlBeforeMatch.slice(lastOpen + 1).match(/^(\w+)/);
1016
1039
  return tag && tag[1].toLowerCase() === 'a';
1017
1040
  }
1018
- 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) => {
1019
1042
  if (isAnchorTag(fullString.slice(0, offset))) return match;
1020
- return lead + prefix;
1043
+ if (isPrerenderBundleAssetPath(rest)) return match;
1044
+ return lead + prefix + rest;
1021
1045
  });
1022
1046
  out = out.replace(/(\s(href|src)=["'])(\.\.\/)+/g, (match, lead, attr, dots, offset, fullString) => {
1023
1047
  if (isAnchorTag(fullString.slice(0, offset))) return match;
@@ -1026,6 +1050,17 @@ function rewriteHtmlAssetPaths(html, depthWithinOutput) {
1026
1050
  return out;
1027
1051
  }
1028
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
+
1029
1064
  // --- Canonical and hreflang (per-page injection) ---
1030
1065
 
1031
1066
  function buildCanonicalAndHreflang(pathSeg, locales, defaultLocale, base) {
@@ -1861,6 +1896,10 @@ async function runPrerender(config) {
1861
1896
  node.removeAttribute(name);
1862
1897
  continue;
1863
1898
  }
1899
+ if (name === 'x-icon') {
1900
+ node.setAttribute('x-icon', '');
1901
+ continue;
1902
+ }
1864
1903
  let boundAttr = '';
1865
1904
  if (name.startsWith(':')) boundAttr = name.slice(1);
1866
1905
  else if (name.startsWith('x-bind:')) boundAttr = name.slice('x-bind:'.length);
@@ -2027,7 +2066,10 @@ async function runPrerender(config) {
2027
2066
  for (const b of extracted.blocks) utilityBlocks.push(b);
2028
2067
  }
2029
2068
  if (tailwindBuilt) {
2030
- 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
+ );
2031
2073
  }
2032
2074
  html = stripDuplicatedLoopDirectives(html);
2033
2075
  html = stripPrerenderedXDataDirectives(html);
@@ -2035,6 +2077,7 @@ async function runPrerender(config) {
2035
2077
  const xData = { manifest, content };
2036
2078
  html = resolveHeadXBindings(html, xData);
2037
2079
  html = stripPrerenderDynamicBindings(html);
2080
+ html = stripPrerenderBakedRadioCheckedForXModel(html);
2038
2081
  html = stripRedundantImgSrcBindings(html);
2039
2082
  html = stripEmptyInlineMaskStyles(html);
2040
2083
  html = stripResolvedXIconDirectives(html);
@@ -2045,9 +2088,17 @@ async function runPrerender(config) {
2045
2088
  const injectOgLocale = ogLocale && hasOtherOgMeta(html);
2046
2089
  if (injectOgLocale) html = stripOgLocaleFromHead(html);
2047
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
+ : '';
2048
2096
  const routeDepth = fileSegments.length;
2049
2097
  const prerenderedMeta = `<meta name="manifest:prerendered" content="1">\n`;
2050
- 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
+ );
2051
2102
  html = markPrerenderedManifestComponents(html);
2052
2103
  mkdirSync(outDir, { recursive: true });
2053
2104
  writeFileSync(outFile, html, 'utf8');
@@ -2108,7 +2159,7 @@ async function runPrerender(config) {
2108
2159
  if (utilMerged.trim()) {
2109
2160
  writeFileSync(join(outputResolved, 'prerender.utilities.css'), `${utilMerged}\n`, 'utf8');
2110
2161
  process.stdout.write('prerender: wrote prerender.utilities.css (Manifest custom utilities)\n');
2111
- postProcessInjectStylesheetLink(outputResolved, 'prerender.utilities.css');
2162
+ postProcessInjectStylesheetLink(outputResolved, 'prerender.utilities.css', routerBasePath || '');
2112
2163
  }
2113
2164
  }
2114
2165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {