diff --git a/packages/configuration-web-recommended/index.json b/packages/configuration-web-recommended/index.json index ab84c1dc472..8b0022e86b1 100644 --- a/packages/configuration-web-recommended/index.json +++ b/packages/configuration-web-recommended/index.json @@ -28,6 +28,7 @@ "meta-charset-utf-8", "meta-viewport", "no-bom", + "no-broken-links", "no-disallowed-headers", "no-friendly-error-pages", "no-html-only-headers", diff --git a/packages/hint-no-broken-links/src/hint.ts b/packages/hint-no-broken-links/src/hint.ts index 6b7311789d2..3ffe5ba96ad 100644 --- a/packages/hint-no-broken-links/src/hint.ts +++ b/packages/hint-no-broken-links/src/hint.ts @@ -218,6 +218,16 @@ export default class NoBrokenLinksHint implements IHint { const requests: Promise[] = []; for (const url of urls) { + // Skip anchor links with rel="nofollow". + if (element.nodeName === 'A') { + const rel = element.getAttribute('rel'); + + if (rel && rel.toLowerCase().includes('nofollow')) { + debug(`Skipping URL ${url} due to rel="nofollow"`); + continue; + } + } + /* * If the URL is not HTTP or HTTPS (e.g. `mailto:`), * there is no need to validate. diff --git a/packages/hint-no-broken-links/tests/tests.ts b/packages/hint-no-broken-links/tests/tests.ts index b1a963a42a3..6dc1dff4530 100644 --- a/packages/hint-no-broken-links/tests/tests.ts +++ b/packages/hint-no-broken-links/tests/tests.ts @@ -91,6 +91,30 @@ const bodyWithInvalidDomainPreconnectLinkTag = `
`; +// Snippets for nofollow tests +const bodyWithNoFollowBrokenLink = `
+Broken NoFollow +
`; + +const bodyWithNoFollowValidLink = `
+Valid NoFollow +
`; + +const bodyWithNoFollowMultipleRel = `
+Broken NoFollow Multiple +
`; + +const bodyWithMixedLinks = `
+Broken NoFollow +Broken Regular +
`; + +const bodyWithNoFollowAndRegularBroken = `
+Broken Anchor NoFollow + +
`; + + const tests: HintTest[] = [ { name: `This test should pass as it has links with valid href value`, @@ -288,6 +312,58 @@ const tests: HintTest[] = [ }], serverConfig: generateHTMLPage('', bodyWithInvalidDomainPreconnectLinkTag) }, + // New tests for rel="nofollow" + { + name: `Anchor with rel="nofollow" pointing to a 404 should not report an error`, + serverConfig: { + '/': { content: generateHTMLPage('', bodyWithNoFollowBrokenLink) }, + '/404': { status: 404 } + } + // No reports expected + }, + { + name: `Anchor with rel="nofollow" pointing to a valid resource should not report an error`, + serverConfig: { + '/': { content: generateHTMLPage('', bodyWithNoFollowValidLink) }, + '/valid': { content: 'Valid page' } + } + // No reports expected + }, + { + name: `Anchor with multiple rel values including "nofollow" pointing to a 404 should not report an error`, + serverConfig: { + '/': { content: generateHTMLPage('', bodyWithNoFollowMultipleRel) }, + '/404': { status: 404 } + } + // No reports expected + }, + { + name: `Mixed content: Regular broken link should report, nofollow broken link should not`, + reports: [{ + message: `Broken link found (404 response).`, + severity: Severity.error, + location: { column: 50, line: 2 } // Location of the regular link + }], + serverConfig: { + '/': { content: generateHTMLPage('', bodyWithMixedLinks) }, + '/404-nofollow': { status: 404 }, + '/404-regular': { status: 404 } + } + }, + { + name: `Mixed content: Broken image should report, nofollow broken anchor should not`, + reports: [{ + message: `Broken link found (404 response).`, + severity: Severity.error, + location: { column: 8, line: 3 } // Location of the image tag + }], + serverConfig: { + '/': { content: generateHTMLPage('', bodyWithNoFollowAndRegularBroken) }, + '/404-anchor': { status: 404 }, + '/404.png': { status: 404 } + } + }, + // End of new tests for rel="nofollow" { name: `This test should fail as it has a loop`, reports: [