diff --git a/src/server/lib/audit/page-analyzer.test.ts b/src/server/lib/audit/page-analyzer.test.ts
index 557a5360d..31f632e0c 100644
--- a/src/server/lib/audit/page-analyzer.test.ts
+++ b/src/server/lib/audit/page-analyzer.test.ts
@@ -207,6 +207,26 @@ describe("analyzeHtml parity with the DOM reference", () => {
});
});
+describe("analyzeHtml link resolution", () => {
+ const linkTargets = (html: string) =>
+ analyzeHtml(html, PAGE_URL, 200, 0).links.map((link) => link.targetUrl);
+
+ it("resolves relative links against ", () => {
+ expect(
+ linkTargets(
+ `
+ Guide`,
+ ),
+ ).toEqual(["https://example.com/en/guide"]);
+ });
+
+ it("resolves relative links against the page URL without a ", () => {
+ expect(
+ linkTargets(`Guide`),
+ ).toEqual(["https://example.com/blog/guide"]);
+ });
+});
+
describe("analyzeHtml extraction caps", () => {
it("caps links and images per page", () => {
const links = Array.from(
diff --git a/src/server/lib/audit/page-analyzer.ts b/src/server/lib/audit/page-analyzer.ts
index 47db8ce55..ca9897cd3 100644
--- a/src/server/lib/audit/page-analyzer.ts
+++ b/src/server/lib/audit/page-analyzer.ts
@@ -66,6 +66,10 @@ export function analyzeHtml(
let ogImage: string | null = null;
let hasStructuredData = false;
const hreflangTags: string[] = [];
+ // Resolution base for relative link targets. HTML lets a document override
+ // it with , which sits in and so is always parsed before
+ // the links it governs; the first with an href wins.
+ let linkBase: string | null = null;
const h1s: string[] = [];
const headingOrder: number[] = [];
@@ -113,7 +117,7 @@ export function analyzeHtml(
const { href, rel, text } = openAnchor;
openAnchor = null;
if (linksByTarget.size >= MAX_EXTRACTED_LINKS) return;
- const resolved = normalizeUrl(href, pageUrl);
+ const resolved = normalizeUrl(href, linkBase ?? pageUrl);
if (!resolved || linksByTarget.has(resolved)) return;
const anchor = text
.join("")
@@ -157,6 +161,11 @@ export function analyzeHtml(
case "link":
handleLinkTag(attribs);
break;
+ case "base":
+ if (linkBase === null && attribs["href"]) {
+ linkBase = normalizeUrl(attribs["href"], pageUrl);
+ }
+ break;
case "img":
if (images.length < MAX_EXTRACTED_IMAGES) {
images.push({