mnfst-render 0.1.3 → 0.1.5
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.
- package/manifest.render.mjs +90 -22
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -464,6 +464,39 @@ function buildCanonicalAndHreflang(pathSeg, locales, defaultLocale, base) {
|
|
|
464
464
|
return out;
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
+
/** Same alternate URLs as buildCanonicalAndHreflang; used for sitemap xhtml:link entries. */
|
|
468
|
+
function getAlternateLinksForPath(pathSeg, locales, defaultLocale, base) {
|
|
469
|
+
const baseClean = base.replace(/\/$/, '');
|
|
470
|
+
const defaultLoc = defaultLocale || locales[0];
|
|
471
|
+
if (!locales || locales.length <= 1) return [];
|
|
472
|
+
const currentLocale = locales.find((l) => pathSeg === l || pathSeg.startsWith(l + '/')) || defaultLoc;
|
|
473
|
+
const logicalRoute =
|
|
474
|
+
currentLocale === defaultLoc
|
|
475
|
+
? pathSeg === defaultLoc
|
|
476
|
+
? ''
|
|
477
|
+
: pathSeg.startsWith(defaultLoc + '/')
|
|
478
|
+
? pathSeg.slice(defaultLoc.length + 1)
|
|
479
|
+
: pathSeg
|
|
480
|
+
: pathSeg === currentLocale
|
|
481
|
+
? ''
|
|
482
|
+
: pathSeg.slice(currentLocale.length + 1);
|
|
483
|
+
const entries = [];
|
|
484
|
+
locales.forEach((loc) => {
|
|
485
|
+
const seg = loc === defaultLoc ? logicalRoute : (logicalRoute ? `${loc}/${logicalRoute}` : loc);
|
|
486
|
+
const href = baseClean + (seg ? `/${seg}` : '');
|
|
487
|
+
const hreflang = loc === defaultLoc ? 'x-default' : loc;
|
|
488
|
+
entries.push({ hreflang, href });
|
|
489
|
+
});
|
|
490
|
+
return entries;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function escapeXmlText(s) {
|
|
494
|
+
return String(s)
|
|
495
|
+
.replace(/&/g, '&')
|
|
496
|
+
.replace(/</g, '<')
|
|
497
|
+
.replace(/>/g, '>');
|
|
498
|
+
}
|
|
499
|
+
|
|
467
500
|
function buildOgLocale(pathSeg, locales, defaultLocale) {
|
|
468
501
|
if (locales.length <= 1) return '';
|
|
469
502
|
const defaultLoc = defaultLocale || locales[0];
|
|
@@ -530,7 +563,22 @@ function setNestedKey(obj, path, value) {
|
|
|
530
563
|
let cur = obj;
|
|
531
564
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
532
565
|
const p = parts[i];
|
|
533
|
-
|
|
566
|
+
const next = parts[i + 1];
|
|
567
|
+
const nextIsIndex = /^\d+$/.test(next);
|
|
568
|
+
if (!(p in cur) || typeof cur[p] !== 'object') {
|
|
569
|
+
cur[p] = nextIsIndex ? [] : {};
|
|
570
|
+
} else if (nextIsIndex && !Array.isArray(cur[p]) && cur[p] && typeof cur[p] === 'object') {
|
|
571
|
+
const existing = cur[p];
|
|
572
|
+
const keys = Object.keys(existing);
|
|
573
|
+
const numericOnly = keys.every((k) => /^\d+$/.test(k));
|
|
574
|
+
if (numericOnly) {
|
|
575
|
+
const arr = [];
|
|
576
|
+
keys.forEach((k) => {
|
|
577
|
+
arr[parseInt(k, 10)] = existing[k];
|
|
578
|
+
});
|
|
579
|
+
cur[p] = arr;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
534
582
|
cur = cur[p];
|
|
535
583
|
}
|
|
536
584
|
cur[parts[parts.length - 1]] = value;
|
|
@@ -568,9 +616,11 @@ function resolveHeadXBindings(html, xData) {
|
|
|
568
616
|
|
|
569
617
|
// --- SEO: robots.txt and sitemap.xml (written to output, use liveUrl for crawlers) ---
|
|
570
618
|
|
|
571
|
-
function writeSeoFiles(outputDir, pathList, liveUrl) {
|
|
619
|
+
function writeSeoFiles(outputDir, pathList, liveUrl, locales, defaultLocale) {
|
|
572
620
|
const base = liveUrl.replace(/\/$/, '');
|
|
573
621
|
const today = new Date().toISOString().slice(0, 10);
|
|
622
|
+
const localeList = Array.isArray(locales) ? locales : [];
|
|
623
|
+
const multiLocale = localeList.length > 1;
|
|
574
624
|
|
|
575
625
|
writeFileSync(
|
|
576
626
|
join(outputDir, 'robots.txt'),
|
|
@@ -582,22 +632,32 @@ Sitemap: ${base}/sitemap.xml
|
|
|
582
632
|
'utf8'
|
|
583
633
|
);
|
|
584
634
|
|
|
635
|
+
const urlsetNs = multiLocale
|
|
636
|
+
? '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">'
|
|
637
|
+
: '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
|
|
638
|
+
|
|
585
639
|
const urlEntries = pathList.map((pathSeg) => {
|
|
586
640
|
const path = pathSeg === '' ? '' : '/' + pathSeg.replace(/\/$/, '');
|
|
587
641
|
const loc = path ? `${base}${path}` : base + '/';
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
642
|
+
const escapedLoc = escapeXmlText(loc);
|
|
643
|
+
let body = ` <loc>${escapedLoc}</loc>`;
|
|
644
|
+
if (multiLocale) {
|
|
645
|
+
for (const { hreflang, href } of getAlternateLinksForPath(pathSeg, localeList, defaultLocale, liveUrl)) {
|
|
646
|
+
body += `\n <xhtml:link rel="alternate" hreflang="${escapeXmlText(hreflang)}" href="${escapeXmlText(href)}" />`;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
body += `\n <lastmod>${today}</lastmod>
|
|
592
650
|
<changefreq>monthly</changefreq>
|
|
593
|
-
<priority>${path === '' ? '1.0' : '0.8'}</priority
|
|
651
|
+
<priority>${path === '' ? '1.0' : '0.8'}</priority>`;
|
|
652
|
+
return ` <url>
|
|
653
|
+
${body}
|
|
594
654
|
</url>`;
|
|
595
655
|
});
|
|
596
656
|
|
|
597
657
|
writeFileSync(
|
|
598
658
|
join(outputDir, 'sitemap.xml'),
|
|
599
659
|
`<?xml version="1.0" encoding="UTF-8"?>
|
|
600
|
-
|
|
660
|
+
${urlsetNs}
|
|
601
661
|
${urlEntries.join('\n')}
|
|
602
662
|
</urlset>
|
|
603
663
|
`,
|
|
@@ -953,6 +1013,20 @@ async function runPrerender(config) {
|
|
|
953
1013
|
await page.evaluate(() => {
|
|
954
1014
|
const loopVarRegex = /^\s*(?:\(\s*([A-Za-z_$][\w$]*)(?:\s*,\s*([A-Za-z_$][\w$]*))?\s*\)|([A-Za-z_$][\w$]*))\s+in\s+/;
|
|
955
1015
|
const bindingAttrRegex = /^(?:x-bind:|:|x-text|x-html|x-show|x-if|x-model|x-effect|x-on:|@)/;
|
|
1016
|
+
const hasVar = (expr, varName) => varName && new RegExp(`\\b${varName}\\b`).test(expr || '');
|
|
1017
|
+
const elementReferencesLoopScope = (el, itemVar, indexVar) => {
|
|
1018
|
+
if (!el) return false;
|
|
1019
|
+
const nodes = [el, ...Array.from(el.querySelectorAll('*'))];
|
|
1020
|
+
for (const node of nodes) {
|
|
1021
|
+
const attrs = node.attributes ? Array.from(node.attributes) : [];
|
|
1022
|
+
for (const attr of attrs) {
|
|
1023
|
+
if (!bindingAttrRegex.test(attr.name)) continue;
|
|
1024
|
+
const expr = attr.value || '';
|
|
1025
|
+
if (hasVar(expr, itemVar) || hasVar(expr, indexVar)) return true;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return false;
|
|
1029
|
+
};
|
|
956
1030
|
|
|
957
1031
|
document.querySelectorAll('template[x-for]').forEach((tpl) => {
|
|
958
1032
|
const xFor = (tpl.getAttribute('x-for') || '').trim();
|
|
@@ -970,19 +1044,7 @@ async function runPrerender(config) {
|
|
|
970
1044
|
const sameTag = next.tagName === tag;
|
|
971
1045
|
if (!sameTag) break;
|
|
972
1046
|
|
|
973
|
-
|
|
974
|
-
const attrNodes = next.attributes ? Array.from(next.attributes) : [];
|
|
975
|
-
for (const attr of attrNodes) {
|
|
976
|
-
if (!bindingAttrRegex.test(attr.name)) continue;
|
|
977
|
-
const expr = attr.value || '';
|
|
978
|
-
if (
|
|
979
|
-
(itemVar && new RegExp(`\\b${itemVar}\\b`).test(expr)) ||
|
|
980
|
-
(indexVar && new RegExp(`\\b${indexVar}\\b`).test(expr))
|
|
981
|
-
) {
|
|
982
|
-
referencesLoopScope = true;
|
|
983
|
-
break;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
1047
|
+
const referencesLoopScope = elementReferencesLoopScope(next, itemVar, indexVar);
|
|
986
1048
|
|
|
987
1049
|
const toRemove = next;
|
|
988
1050
|
next = next.nextElementSibling;
|
|
@@ -1061,7 +1123,13 @@ async function runPrerender(config) {
|
|
|
1061
1123
|
await browser.close();
|
|
1062
1124
|
}
|
|
1063
1125
|
|
|
1064
|
-
writeSeoFiles(
|
|
1126
|
+
writeSeoFiles(
|
|
1127
|
+
config.output,
|
|
1128
|
+
pathList.filter((p) => p !== NOT_FOUND_PATH),
|
|
1129
|
+
config.liveUrl,
|
|
1130
|
+
locales,
|
|
1131
|
+
defaultLocale
|
|
1132
|
+
);
|
|
1065
1133
|
|
|
1066
1134
|
if (config.redirects.length > 0) {
|
|
1067
1135
|
const lines = config.redirects.map((r) => {
|