mnfst-render 0.5.26 → 0.5.27
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 +204 -36
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -254,6 +254,7 @@ function resolveConfig() {
|
|
|
254
254
|
serve,
|
|
255
255
|
output: resolve(root, cli.output ?? pre.output ?? 'website'),
|
|
256
256
|
root,
|
|
257
|
+
manifest,
|
|
257
258
|
routerBase: pre.routerBase ?? null,
|
|
258
259
|
/** Logical path prefixes (after locale) that skip sticky locale prefix; see manifest:locale-route-exclude */
|
|
259
260
|
localeRouteExclude: normalizeLocaleRouteExclude(
|
|
@@ -724,6 +725,18 @@ function stripDataTailwindAttr(html) {
|
|
|
724
725
|
return html.replace(/\sdata-tailwind(?:=(["']).*?\1)?/gi, '');
|
|
725
726
|
}
|
|
726
727
|
|
|
728
|
+
/** Prepend `<!DOCTYPE html>` unless one is already present.
|
|
729
|
+
*
|
|
730
|
+
* The snapshot is captured via `document.documentElement.outerHTML`, which
|
|
731
|
+
* serializes only the <html> subtree and drops the document's doctype.
|
|
732
|
+
* Shipping that doctype-less HTML triggers quirks mode in browsers and is
|
|
733
|
+
* flagged by Lighthouse/PageSpeed. Re-add it at write time so every emitted
|
|
734
|
+
* page (Puppeteer-rendered base pages and substituted locale variants) is in
|
|
735
|
+
* standards mode. */
|
|
736
|
+
function ensureDoctype(html) {
|
|
737
|
+
return /^\s*<!doctype\b/i.test(html) ? html : `<!DOCTYPE html>\n${html}`;
|
|
738
|
+
}
|
|
739
|
+
|
|
727
740
|
/** Theme class de-bake + synchronous bootstrap.
|
|
728
741
|
*
|
|
729
742
|
* Puppeteer applies `<html class="light">` or `<html class="dark">` based on
|
|
@@ -1210,7 +1223,8 @@ function stripPrerenderBakedRadioCheckedForXModel(html) {
|
|
|
1210
1223
|
|
|
1211
1224
|
// --- Canonical and hreflang (per-page injection) ---
|
|
1212
1225
|
|
|
1213
|
-
function buildCanonicalAndHreflang(pathSeg, locales, defaultLocale, base) {
|
|
1226
|
+
function buildCanonicalAndHreflang(pathSeg, locales, defaultLocale, base, opts = {}) {
|
|
1227
|
+
const { skipCanonical = false } = opts;
|
|
1214
1228
|
const baseClean = base.replace(/\/$/, '');
|
|
1215
1229
|
const defaultLoc = defaultLocale || locales[0];
|
|
1216
1230
|
const isDefaultLocalePrefixed =
|
|
@@ -1223,7 +1237,10 @@ function buildCanonicalAndHreflang(pathSeg, locales, defaultLocale, base) {
|
|
|
1223
1237
|
: pathSeg;
|
|
1224
1238
|
const canonicalHref = canonicalPath === '' ? `${baseClean}/` : `${baseClean}/${canonicalPath}`;
|
|
1225
1239
|
const esc = (s) => String(s).replace(/&/g, '&').replace(/"/g, '"');
|
|
1226
|
-
|
|
1240
|
+
// Skip the canonical <link> when the source head already declares one
|
|
1241
|
+
// (first-wins, per seo-aeo.md); hreflang alternates are still emitted since
|
|
1242
|
+
// those are framework-derived and rarely authored by hand.
|
|
1243
|
+
let out = skipCanonical ? '' : `<link rel="canonical" href="${esc(canonicalHref)}">\n`;
|
|
1227
1244
|
if (locales.length > 1) {
|
|
1228
1245
|
const currentLocale = locales.find((l) => pathSeg === l || pathSeg.startsWith(l + '/')) || defaultLoc;
|
|
1229
1246
|
const logicalRoute =
|
|
@@ -1581,7 +1598,8 @@ function generateLocaleVariantHtml({
|
|
|
1581
1598
|
html = rewriteHtmlAssetPaths(html, fileSegments.length);
|
|
1582
1599
|
|
|
1583
1600
|
const liveBase = config.liveUrl.replace(/\/$/, '');
|
|
1584
|
-
const
|
|
1601
|
+
const hasSourceCanonical = /<link\b[^>]*\brel=(["'])\s*canonical\s*\1/i.test(html);
|
|
1602
|
+
const canonicalHreflang = buildCanonicalAndHreflang(pathSeg, locales, defaultLocale, liveBase, { skipCanonical: hasSourceCanonical });
|
|
1585
1603
|
const ogLocale = buildOgLocale(pathSeg, locales, defaultLocale);
|
|
1586
1604
|
const injectOgLocale = ogLocale && hasOtherOgMeta(html);
|
|
1587
1605
|
if (injectOgLocale) html = stripOgLocaleFromHead(html);
|
|
@@ -2383,29 +2401,123 @@ function routeHtmlPath(outputDir, pathSeg) {
|
|
|
2383
2401
|
}
|
|
2384
2402
|
|
|
2385
2403
|
/**
|
|
2386
|
-
*
|
|
2387
|
-
*
|
|
2388
|
-
*
|
|
2389
|
-
*
|
|
2404
|
+
* Collect filesystem paths for all local-file data sources declared in
|
|
2405
|
+
* `manifest.json` that are relevant to the given locale. Caller stats them.
|
|
2406
|
+
*
|
|
2407
|
+
* Skips remote sources (URLs, Appwrite databases / storage) since they have
|
|
2408
|
+
* no local mtime. Locale-keyed JSON/YAML sources include only the matching
|
|
2409
|
+
* locale's file. Multilingual CSVs (`locales` key) include every listed file
|
|
2410
|
+
* because any column edit can affect the routed page.
|
|
2390
2411
|
*/
|
|
2391
|
-
function
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
const
|
|
2412
|
+
function collectDataSourceFiles(manifest, rootDir, effectiveLocale) {
|
|
2413
|
+
const files = [];
|
|
2414
|
+
const data = manifest?.data;
|
|
2415
|
+
if (!data || typeof data !== 'object') return files;
|
|
2416
|
+
|
|
2417
|
+
const isLocaleKey = (key) =>
|
|
2418
|
+
/^[a-z]{2,3}(?:-[A-Z][a-zA-Z]{1,7})?$/.test(key);
|
|
2419
|
+
|
|
2420
|
+
const localeMatches = (key) => {
|
|
2421
|
+
if (!isLocaleKey(key)) return false;
|
|
2422
|
+
if (effectiveLocale) return key === effectiveLocale;
|
|
2423
|
+
// No locale context: include any locale-shaped key.
|
|
2424
|
+
return true;
|
|
2425
|
+
};
|
|
2426
|
+
|
|
2427
|
+
const isLocalPath = (s) => typeof s === 'string' && !/^https?:\/\//i.test(s);
|
|
2428
|
+
const toAbs = (p) => join(rootDir, p.replace(/^\//, ''));
|
|
2429
|
+
|
|
2430
|
+
for (const value of Object.values(data)) {
|
|
2431
|
+
// Plain string path → single locale-agnostic file.
|
|
2432
|
+
if (isLocalPath(value)) {
|
|
2433
|
+
files.push(toAbs(value));
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
if (!value || typeof value !== 'object') continue;
|
|
2437
|
+
// Skip cloud / remote sources — they have no local file to stat.
|
|
2438
|
+
if (value.url || value.appwriteDatabaseId || value.appwriteTableId || value.appwriteBucketId) continue;
|
|
2439
|
+
|
|
2440
|
+
for (const [key, v] of Object.entries(value)) {
|
|
2441
|
+
// Multilingual CSV: { locales: "/p.csv" } or { locales: ["/a.csv", "/b.csv"] }
|
|
2442
|
+
if (key === 'locales') {
|
|
2443
|
+
if (isLocalPath(v)) files.push(toAbs(v));
|
|
2444
|
+
else if (Array.isArray(v)) {
|
|
2445
|
+
for (const p of v) if (isLocalPath(p)) files.push(toAbs(p));
|
|
2446
|
+
}
|
|
2447
|
+
continue;
|
|
2448
|
+
}
|
|
2449
|
+
// Colorpicker palette: { colorpicker: "/p.yaml" } or { colorpicker: { en: ..., fr: ... } }
|
|
2450
|
+
if (key === 'colorpicker') {
|
|
2451
|
+
if (isLocalPath(v)) files.push(toAbs(v));
|
|
2452
|
+
else if (v && typeof v === 'object') {
|
|
2453
|
+
for (const [k, p] of Object.entries(v)) {
|
|
2454
|
+
if (localeMatches(k) && isLocalPath(p)) files.push(toAbs(p));
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
continue;
|
|
2458
|
+
}
|
|
2459
|
+
// Locale-keyed JSON/YAML: { en: "/p.en.json", fr: "/p.fr.json" }
|
|
2460
|
+
if (localeMatches(key) && isLocalPath(v)) {
|
|
2461
|
+
files.push(toAbs(v));
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
return files;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
/**
|
|
2470
|
+
* Best-effort per-route lastmod date. Takes the most recent mtime across:
|
|
2471
|
+
* 1. Backing source-file conventions (markdown under articles/, pages/<path>.html)
|
|
2472
|
+
* so direct content edits are reflected.
|
|
2473
|
+
* 2. Data-source files registered in `manifest.json` that are relevant to the
|
|
2474
|
+
* route's locale, so changes to JSON/YAML/CSV content driving the page
|
|
2475
|
+
* bump the date too (important for translated sites).
|
|
2476
|
+
*
|
|
2477
|
+
* Falls back to the prerendered HTML's own mtime only when nothing else is
|
|
2478
|
+
* statable — the HTML mtime reflects rebuild time rather than content change
|
|
2479
|
+
* time, so we prefer source/data mtimes when any exist.
|
|
2480
|
+
*/
|
|
2481
|
+
function routeLastModDate(rootDir, outputDir, pathSeg, manifest, localeList, defaultLocale) {
|
|
2482
|
+
// Detect a locale prefix on the path (e.g. "fr/about" → locale "fr",
|
|
2483
|
+
// unlocalized "about"). For unprefixed paths in a multi-locale site we
|
|
2484
|
+
// fall back to the default locale when matching data-source locale keys.
|
|
2485
|
+
let locale = null;
|
|
2486
|
+
let unlocalizedPath = pathSeg;
|
|
2487
|
+
if (Array.isArray(localeList) && localeList.length) {
|
|
2488
|
+
const first = pathSeg.split('/')[0];
|
|
2489
|
+
if (localeList.includes(first)) {
|
|
2490
|
+
locale = first;
|
|
2491
|
+
unlocalizedPath = pathSeg.slice(first.length + 1);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
const effectiveLocale = locale || defaultLocale || null;
|
|
2495
|
+
|
|
2496
|
+
// Source-file candidates from common conventions, keyed on the unlocalized
|
|
2497
|
+
// path (markdown files typically aren't per-locale duplicates).
|
|
2498
|
+
const stripPrefix = unlocalizedPath.replace(/^(?:docs|blog|articles|posts|guides)\//, '');
|
|
2397
2499
|
const candidates = [
|
|
2398
2500
|
join(rootDir, 'articles', `${stripPrefix}.md`),
|
|
2399
|
-
join(rootDir, 'articles', `${
|
|
2400
|
-
join(rootDir, 'pages', `${
|
|
2401
|
-
join(rootDir, `${
|
|
2501
|
+
join(rootDir, 'articles', `${unlocalizedPath}.md`),
|
|
2502
|
+
join(rootDir, 'pages', `${unlocalizedPath}.html`),
|
|
2503
|
+
join(rootDir, `${unlocalizedPath}.md`),
|
|
2402
2504
|
];
|
|
2505
|
+
|
|
2506
|
+
// Add data-source files relevant to this locale.
|
|
2507
|
+
if (manifest) {
|
|
2508
|
+
candidates.push(...collectDataSourceFiles(manifest, rootDir, effectiveLocale));
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// Take the max mtime across all source / data candidates.
|
|
2512
|
+
let latest = null;
|
|
2403
2513
|
for (const c of candidates) {
|
|
2404
2514
|
try {
|
|
2405
2515
|
const s = statSync(c);
|
|
2406
|
-
if (s.isFile()
|
|
2516
|
+
if (s.isFile() && (!latest || s.mtime > latest)) latest = s.mtime;
|
|
2407
2517
|
} catch { /* not found */ }
|
|
2408
2518
|
}
|
|
2519
|
+
if (latest) return latest.toISOString().slice(0, 10);
|
|
2520
|
+
|
|
2409
2521
|
// Fallback to the prerendered output mtime (always present).
|
|
2410
2522
|
try {
|
|
2411
2523
|
const out = routeHtmlPath(outputDir, pathSeg || '');
|
|
@@ -2420,6 +2532,7 @@ function writeSeoFiles(outputDir, pathList, liveUrl, locales, defaultLocale, ctx
|
|
|
2420
2532
|
const localeList = Array.isArray(locales) ? locales : [];
|
|
2421
2533
|
const multiLocale = localeList.length > 1;
|
|
2422
2534
|
const rootDir = ctx.rootDir || '';
|
|
2535
|
+
const manifest = ctx.manifest || null;
|
|
2423
2536
|
|
|
2424
2537
|
writeFileSync(
|
|
2425
2538
|
join(outputDir, 'robots.txt'),
|
|
@@ -2445,7 +2558,7 @@ Sitemap: ${base}/sitemap.xml
|
|
|
2445
2558
|
body += `\n <xhtml:link rel="alternate" hreflang="${escapeXmlText(hreflang)}" href="${escapeXmlText(href)}" />`;
|
|
2446
2559
|
}
|
|
2447
2560
|
}
|
|
2448
|
-
const lastmod = routeLastModDate(rootDir, outputDir, pathSeg);
|
|
2561
|
+
const lastmod = routeLastModDate(rootDir, outputDir, pathSeg, manifest, localeList, defaultLocale);
|
|
2449
2562
|
body += `\n <lastmod>${lastmod}</lastmod>
|
|
2450
2563
|
<changefreq>monthly</changefreq>
|
|
2451
2564
|
<priority>${path === '' ? '1.0' : '0.8'}</priority>`;
|
|
@@ -2716,6 +2829,42 @@ function copyProjectIntoDist(rootResolved, outputResolved) {
|
|
|
2716
2829
|
COPY_EXCLUDE.delete(outputDirName);
|
|
2717
2830
|
}
|
|
2718
2831
|
|
|
2832
|
+
/** Remove bare `@import "tailwindcss"` (and `tailwindcss/*` sub-imports) from
|
|
2833
|
+
* CSS files copied into the output.
|
|
2834
|
+
*
|
|
2835
|
+
* Tailwind v4 conventions put `@import "tailwindcss";` at the top of a
|
|
2836
|
+
* project's main CSS so the build tool pulls in the framework. When that same
|
|
2837
|
+
* file is also linked directly in the browser (as Manifest's
|
|
2838
|
+
* `manifest.theme.css` convention does), the browser's native CSS loader
|
|
2839
|
+
* resolves the bare specifier against the page origin and fetches
|
|
2840
|
+
* `/tailwindcss` — which 404s to the SPA shell (text/html), tripping
|
|
2841
|
+
* "Refused to apply style … is not a supported stylesheet MIME type" (flagged
|
|
2842
|
+
* by PageSpeed Best Practices). Manifest supplies Tailwind its own way
|
|
2843
|
+
* (compiled `prerender.tailwind.css` for MPA, the Play-CDN style engine for
|
|
2844
|
+
* SPA), so a raw import in a browser-served stylesheet is always redundant and
|
|
2845
|
+
* harmful. Strip it from the emitted copies only; source files are untouched. */
|
|
2846
|
+
function stripTailwindCssImportsFromOutput(outputDir) {
|
|
2847
|
+
const importRx = /@import\s+(?:url\(\s*)?["']tailwindcss(?:\/[^"']*)?["']\s*\)?[^;\n]*;?[ \t]*\r?\n?/gi;
|
|
2848
|
+
const walk = (dir) => {
|
|
2849
|
+
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
|
2850
|
+
if (ent.name.startsWith('.')) continue;
|
|
2851
|
+
const p = join(dir, ent.name);
|
|
2852
|
+
if (ent.isDirectory()) {
|
|
2853
|
+
if (ent.name === 'node_modules') continue;
|
|
2854
|
+
walk(p);
|
|
2855
|
+
} else if (ent.name.endsWith('.css') && ent.name !== 'prerender.tailwind.css') {
|
|
2856
|
+
try {
|
|
2857
|
+
const css = readFileSync(p, 'utf8');
|
|
2858
|
+
if (!/tailwindcss/i.test(css)) continue;
|
|
2859
|
+
const next = css.replace(importRx, '');
|
|
2860
|
+
if (next !== css) writeFileSync(p, next, 'utf8');
|
|
2861
|
+
} catch { /* unreadable file — skip */ }
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
};
|
|
2865
|
+
walk(outputDir);
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2719
2868
|
// --- Main --------------------------------------------------------------------
|
|
2720
2869
|
|
|
2721
2870
|
async function main() {
|
|
@@ -2819,6 +2968,14 @@ async function runPrerender(config) {
|
|
|
2819
2968
|
}
|
|
2820
2969
|
mkdirSync(outputResolved, { recursive: true });
|
|
2821
2970
|
copyProjectIntoDist(rootResolved, outputResolved);
|
|
2971
|
+
// Projects that use data-tailwind get their Tailwind from Manifest (compiled
|
|
2972
|
+
// prerender.tailwind.css below, or the runtime style engine). A leftover
|
|
2973
|
+
// `@import "tailwindcss"` in a browser-linked stylesheet (e.g. the
|
|
2974
|
+
// manifest.theme.css convention) would make the browser fetch /tailwindcss
|
|
2975
|
+
// and fail; strip those imports from the copied CSS.
|
|
2976
|
+
if (indexHtmlUsesTailwind(rootResolved)) {
|
|
2977
|
+
stripTailwindCssImportsFromOutput(outputResolved);
|
|
2978
|
+
}
|
|
2822
2979
|
|
|
2823
2980
|
const pre = manifest.prerender ?? {};
|
|
2824
2981
|
const bundleUtilities = pre.utilitiesBundle !== false;
|
|
@@ -3753,11 +3910,20 @@ async function runPrerender(config) {
|
|
|
3753
3910
|
await page.evaluate(() => {
|
|
3754
3911
|
const A = window.Alpine;
|
|
3755
3912
|
const runBatch = typeof A?.mutateDom === 'function' ? (fn) => A.mutateDom(fn) : (fn) => fn();
|
|
3756
|
-
|
|
3913
|
+
// Collect every loop-scope identifier from the x-for LHS, including
|
|
3914
|
+
// destructuring forms — `item`, `(item, index)`, `[key, val]`,
|
|
3915
|
+
// `{ a, b }`. The old single/paren-only regex skipped destructured
|
|
3916
|
+
// loops entirely, leaving bindings like x-text="file?.label" on baked
|
|
3917
|
+
// clones; at runtime Alpine evaluated them outside the iteration scope
|
|
3918
|
+
// and threw "file is not defined".
|
|
3919
|
+
const extractLoopVars = (xForExpr) => {
|
|
3920
|
+
const m = String(xForExpr || '').match(/^([\s\S]*?)\s+(?:in|of)\s+/);
|
|
3921
|
+
return m ? (m[1].match(/[A-Za-z_$][\w$]*/g) || []) : [];
|
|
3922
|
+
};
|
|
3757
3923
|
// Include x-init: expanded clones still had x-init="getDescription(article)" etc.; Alpine then throws (article undefined).
|
|
3758
3924
|
const bindingAttrRegex = /^(?:x-bind:|:|x-text|x-html|x-show|x-if|x-model|x-effect|x-init|x-icon|x-on:|@)/;
|
|
3759
3925
|
const hasVar = (expr, varName) => varName && new RegExp(`\\b${varName}\\b`).test(expr || '');
|
|
3760
|
-
const stripLoopBindings = (el,
|
|
3926
|
+
const stripLoopBindings = (el, loopVars) => {
|
|
3761
3927
|
const nodes = [el, ...Array.from(el.querySelectorAll('*'))];
|
|
3762
3928
|
for (const node of nodes) {
|
|
3763
3929
|
// Skip elements inside data-hydrate islands — their bindings must remain live
|
|
@@ -3766,7 +3932,7 @@ async function runPrerender(config) {
|
|
|
3766
3932
|
for (const attr of attrs) {
|
|
3767
3933
|
if (!bindingAttrRegex.test(attr.name)) continue;
|
|
3768
3934
|
const expr = attr.value || '';
|
|
3769
|
-
if (
|
|
3935
|
+
if (loopVars.some((v) => hasVar(expr, v))) {
|
|
3770
3936
|
const name = attr.name;
|
|
3771
3937
|
if (name === 'x-text' || name === 'x-html') {
|
|
3772
3938
|
if ((node.textContent || '').trim() || (node.innerHTML || '').trim()) {
|
|
@@ -3808,10 +3974,8 @@ async function runPrerender(config) {
|
|
|
3808
3974
|
document.querySelectorAll('template[x-for]').forEach((tpl) => {
|
|
3809
3975
|
if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) return;
|
|
3810
3976
|
const xFor = (tpl.getAttribute('x-for') || '').trim();
|
|
3811
|
-
const
|
|
3812
|
-
|
|
3813
|
-
const indexVar = m ? (m[2] || '') : '';
|
|
3814
|
-
if (!itemVar && !indexVar) return;
|
|
3977
|
+
const loopVars = extractLoopVars(xFor);
|
|
3978
|
+
if (!loopVars.length) return;
|
|
3815
3979
|
|
|
3816
3980
|
const first = tpl.content?.firstElementChild;
|
|
3817
3981
|
if (!first) return;
|
|
@@ -3820,7 +3984,7 @@ async function runPrerender(config) {
|
|
|
3820
3984
|
let next = tpl.nextElementSibling;
|
|
3821
3985
|
while (next) {
|
|
3822
3986
|
if (next.tagName !== tag) break;
|
|
3823
|
-
stripLoopBindings(next,
|
|
3987
|
+
stripLoopBindings(next, loopVars);
|
|
3824
3988
|
next = next.nextElementSibling;
|
|
3825
3989
|
}
|
|
3826
3990
|
});
|
|
@@ -3914,10 +4078,13 @@ async function runPrerender(config) {
|
|
|
3914
4078
|
// Remove orphan x-for clones that still reference loop-scope vars (e.g. image/index)
|
|
3915
4079
|
// outside their template scope. These throw Alpine errors in live static hosting.
|
|
3916
4080
|
await page.evaluate(() => {
|
|
3917
|
-
const
|
|
4081
|
+
const extractLoopVars = (xForExpr) => {
|
|
4082
|
+
const m = String(xForExpr || '').match(/^([\s\S]*?)\s+(?:in|of)\s+/);
|
|
4083
|
+
return m ? (m[1].match(/[A-Za-z_$][\w$]*/g) || []) : [];
|
|
4084
|
+
};
|
|
3918
4085
|
const bindingAttrRegex = /^(?:x-bind:|:|x-text|x-html|x-show|x-if|x-model|x-effect|x-init|x-icon|x-on:|@)/;
|
|
3919
4086
|
const hasVar = (expr, varName) => varName && new RegExp(`\\b${varName}\\b`).test(expr || '');
|
|
3920
|
-
const elementReferencesLoopScope = (el,
|
|
4087
|
+
const elementReferencesLoopScope = (el, loopVars) => {
|
|
3921
4088
|
if (!el) return false;
|
|
3922
4089
|
const nodes = [el, ...Array.from(el.querySelectorAll('*'))];
|
|
3923
4090
|
for (const node of nodes) {
|
|
@@ -3925,7 +4092,7 @@ async function runPrerender(config) {
|
|
|
3925
4092
|
for (const attr of attrs) {
|
|
3926
4093
|
if (!bindingAttrRegex.test(attr.name)) continue;
|
|
3927
4094
|
const expr = attr.value || '';
|
|
3928
|
-
if (
|
|
4095
|
+
if (loopVars.some((v) => hasVar(expr, v))) return true;
|
|
3929
4096
|
}
|
|
3930
4097
|
}
|
|
3931
4098
|
return false;
|
|
@@ -3935,10 +4102,8 @@ async function runPrerender(config) {
|
|
|
3935
4102
|
// Running this on all x-for templates can remove valid prerendered list items.
|
|
3936
4103
|
document.querySelectorAll('template[x-for][data-prerender-collapsed="1"]').forEach((tpl) => {
|
|
3937
4104
|
const xFor = (tpl.getAttribute('x-for') || '').trim();
|
|
3938
|
-
const
|
|
3939
|
-
|
|
3940
|
-
const indexVar = m ? (m[2] || '') : '';
|
|
3941
|
-
if (!itemVar && !indexVar) return;
|
|
4105
|
+
const loopVars = extractLoopVars(xFor);
|
|
4106
|
+
if (!loopVars.length) return;
|
|
3942
4107
|
|
|
3943
4108
|
const first = tpl.content?.firstElementChild;
|
|
3944
4109
|
if (!first) return;
|
|
@@ -3949,7 +4114,7 @@ async function runPrerender(config) {
|
|
|
3949
4114
|
const sameTag = next.tagName === tag;
|
|
3950
4115
|
if (!sameTag) break;
|
|
3951
4116
|
|
|
3952
|
-
const referencesLoopScope = elementReferencesLoopScope(next,
|
|
4117
|
+
const referencesLoopScope = elementReferencesLoopScope(next, loopVars);
|
|
3953
4118
|
|
|
3954
4119
|
const toRemove = next;
|
|
3955
4120
|
next = next.nextElementSibling;
|
|
@@ -4072,7 +4237,8 @@ async function runPrerender(config) {
|
|
|
4072
4237
|
|
|
4073
4238
|
html = rewriteHtmlAssetPaths(html, fileSegments.length);
|
|
4074
4239
|
const liveBase = config.liveUrl.replace(/\/$/, '');
|
|
4075
|
-
const
|
|
4240
|
+
const hasSourceCanonical = /<link\b[^>]*\brel=(["'])\s*canonical\s*\1/i.test(html);
|
|
4241
|
+
const canonicalHreflang = buildCanonicalAndHreflang(is404 ? '' : pathSeg, locales, defaultLocale, liveBase, { skipCanonical: hasSourceCanonical });
|
|
4076
4242
|
const ogLocale = buildOgLocale(is404 ? '' : pathSeg, locales, defaultLocale);
|
|
4077
4243
|
const injectOgLocale = ogLocale && hasOtherOgMeta(html);
|
|
4078
4244
|
if (injectOgLocale) html = stripOgLocaleFromHead(html);
|
|
@@ -4094,6 +4260,7 @@ async function runPrerender(config) {
|
|
|
4094
4260
|
);
|
|
4095
4261
|
// (Hydration contract was already injected into the raw HTML before
|
|
4096
4262
|
// the Node.js post-processing pipeline ran, so it's already present.)
|
|
4263
|
+
html = ensureDoctype(html);
|
|
4097
4264
|
mkdirSync(outDir, { recursive: true });
|
|
4098
4265
|
writeFileSync(outFile, html, 'utf8');
|
|
4099
4266
|
pushDebug({
|
|
@@ -4276,7 +4443,7 @@ async function runPrerender(config) {
|
|
|
4276
4443
|
const fileSegments = pathToFileSegments(pathSeg ? '/' + pathSeg : '/');
|
|
4277
4444
|
const outDir = join(config.output, ...fileSegments);
|
|
4278
4445
|
mkdirSync(outDir, { recursive: true });
|
|
4279
|
-
writeFileSync(join(outDir, 'index.html'), html, 'utf8');
|
|
4446
|
+
writeFileSync(join(outDir, 'index.html'), ensureDoctype(html), 'utf8');
|
|
4280
4447
|
} catch (err) {
|
|
4281
4448
|
failedPaths.push({ path: displayPath, message: err?.message ?? String(err) });
|
|
4282
4449
|
process.stderr.write(`prerender: substitution failed ${displayPath}: ${failedPaths[failedPaths.length - 1].message}\n`);
|
|
@@ -4317,6 +4484,7 @@ async function runPrerender(config) {
|
|
|
4317
4484
|
defaultLocale,
|
|
4318
4485
|
{
|
|
4319
4486
|
rootDir: config.root,
|
|
4487
|
+
manifest: config.manifest,
|
|
4320
4488
|
siteName: config.seo?.siteName,
|
|
4321
4489
|
siteDescription: config.seo?.siteDescription,
|
|
4322
4490
|
}
|