From 50fbf2e63c5d0ff865fdd7b53cad3e1e5d244d95 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:17:43 +0800 Subject: [PATCH] fix(frontstage): classify exported page links as pages or shipped assets The editorial blog pages link their architecture diagrams with relative targets. The share-bundle smoke treated every relative as a page directory and asserted /index.html, so main has failed the Frontstage Pages build since the diagrams shipped: it demanded position-en.svg/index.html. Classify relative references instead: asset references must resolve to the asset file itself, and page references must own an index.html. Local diagram sources are now checked the same way, so a dropped diagram fails the smoke instead of passing silently. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- examples/frontstage-share-bundle-smoke.mjs | 27 ++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/examples/frontstage-share-bundle-smoke.mjs b/examples/frontstage-share-bundle-smoke.mjs index d281870ded..45c2b4a8e2 100644 --- a/examples/frontstage-share-bundle-smoke.mjs +++ b/examples/frontstage-share-bundle-smoke.mjs @@ -35,6 +35,21 @@ function assertExists(path) { } } +// Relative references in the exported editorial pages point at either another +// exported page directory or a shipped static asset. Page references must own +// an index.html; asset references must resolve to the asset file itself. +const relativeAssetReferencePattern = + /\.(?:avif|css|csv|gif|ico|jpe?g|js|json|mjs|mp4|pdf|png|svg|txt|webm|webp|woff2?|xml|zip)$/i; +const nonBundleReferencePattern = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i; + +function assertRelativeReferenceExists(pagePath, target) { + if (relativeAssetReferencePattern.test(target)) { + assertExists(resolve(dirname(pagePath), target)); + return; + } + assertExists(resolve(dirname(pagePath), target, "index.html")); +} + function assertNoLeak(text, label) { const forbidden = [ /\/Users\//, @@ -123,12 +138,20 @@ for (const locale of ["", "zh/"]) { assertExists(resolve(dirname(pagePath), stylesheet[1])); for (const match of html.matchAll(/]+href="([^"]+)"/g)) { const href = match[1]; - if (/^(https?:|#)/.test(href)) continue; + if (nonBundleReferencePattern.test(href)) continue; if (href.startsWith("/")) throw new Error("Blog navigation must preserve the hosting base"); const target = href.split(/[?#]/)[0]; // MkDocs pages are built later by the publication workflow. if (target.includes("docs/")) continue; - assertExists(resolve(dirname(pagePath), target, "index.html")); + assertRelativeReferenceExists(pagePath, target); + } + // Embedded diagrams are the primary content of these pages, so their local + // sources must ship in the bundle too. + for (const match of html.matchAll(/]+src="([^"]+)"/g)) { + const src = match[1]; + if (nonBundleReferencePattern.test(src)) continue; + if (src.startsWith("/")) throw new Error("Blog images must preserve the hosting base"); + assertRelativeReferenceExists(pagePath, src.split(/[?#]/)[0]); } } }