diff --git a/web/scripts/__tests__/check-visibility.test.ts b/web/scripts/__tests__/check-visibility.test.ts index dc4ef394..95cc0ae6 100644 --- a/web/scripts/__tests__/check-visibility.test.ts +++ b/web/scripts/__tests__/check-visibility.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { buildRepositoryApiUrl, + hasAtomAutodiscoveryLink, hasTwitterImageAltText, isValidOpenGraphImageType, normalizeHttpsUrl, @@ -183,6 +184,44 @@ describe('hasTwitterImageAltText', () => { }); }); +describe('hasAtomAutodiscoveryLink', () => { + it('detects a well-formed Atom autodiscovery link tag', () => { + const html = `
+ + `; + expect(hasAtomAutodiscoveryLink(html)).toBe(true); + }); + + it('returns false when the autodiscovery link is absent', () => { + const html = ` + + `; + expect(hasAtomAutodiscoveryLink(html)).toBe(false); + }); + + it('returns false for an empty string', () => { + expect(hasAtomAutodiscoveryLink('')).toBe(false); + }); + + it('handles attribute order variations', () => { + const html = ``; + expect(hasAtomAutodiscoveryLink(html)).toBe(true); + }); + + it('is case-insensitive for the type attribute', () => { + const html = ``; + expect(hasAtomAutodiscoveryLink(html)).toBe(true); + }); + + it('detects Atom link when another alternate link comes first', () => { + const html = ` + + + `; + expect(hasAtomAutodiscoveryLink(html)).toBe(true); + }); +}); + describe('VisibilityReport', () => { it('has the expected shape with summary and checks fields', () => { const checks: CheckResult[] = [ diff --git a/web/scripts/check-visibility.ts b/web/scripts/check-visibility.ts index 7adf8650..373899c0 100644 --- a/web/scripts/check-visibility.ts +++ b/web/scripts/check-visibility.ts @@ -243,6 +243,42 @@ function extractFileBackedFaviconHref(html: string): string { return ''; } +export function hasAtomAutodiscoveryLink(html: string): boolean { + const tagPattern = /]*>/gi; + const attrPattern = (attribute: string): RegExp => + new RegExp( + `\\b${attribute}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'=<>]+))`, + 'i' + ); + + for (const match of html.matchAll(tagPattern)) { + const tag = match[0]; + const relMatch = tag.match(attrPattern('rel')); + const relValue = ( + relMatch?.[1] ?? + relMatch?.[2] ?? + relMatch?.[3] ?? + '' + ).trim(); + if (!relValue.toLowerCase().split(/\s+/).includes('alternate')) { + continue; + } + + const typeMatch = tag.match(attrPattern('type')); + const typeValue = ( + typeMatch?.[1] ?? + typeMatch?.[2] ?? + typeMatch?.[3] ?? + '' + ).trim(); + if (typeValue.toLowerCase() === 'application/atom+xml') { + return true; + } + } + + return false; +} + async function runChecks(): Promise