Skip to content

fix(audit): match rel and script type values case-insensitively - #309

Open
kevin9327 wants to merge 1 commit into
every-app:mainfrom
kevin9327:fix/audit-attribute-value-case
Open

kevin9327 wants to merge 1 commit into
every-app:mainfrom
kevin9327:fix/audit-attribute-value-case

Conversation

@kevin9327

Copy link
Copy Markdown

Problem

analyzeHtml compares two HTML attribute values with ===:

if (attribs["rel"] === "canonical") { ... }
else if (attribs["rel"] === "alternate" && attribs["hreflang"]) { ... }
...
if (attribs["type"] === "application/ld+json") { hasStructuredData = true; }

rel is an ASCII case-insensitive link-type token in HTML, and MIME types are
case-insensitive as well. htmlparser2 lowercases attribute names, not values,
so a page written as

<link rel="Canonical" href="/canonical">
<link rel="ALTERNATE" hreflang="de" href="/de">
<script type="application/LD+JSON">{"@type":"Article"}</script>

loses its canonical, its de hreflang, and its structured-data flag.

The same file already knows this — closeAnchor reads the very same attribute
as attribs["rel"]?.toLowerCase() ?? "" before splitting it, and the parity
fixture uses rel="NoFollow sponsored" to pin that.

Downstream, a missed canonical is not just a blank column:
isDuplicateCandidate treats a page with no canonical as eligible for
duplicate grouping, so a page that is canonicalized elsewhere gets reported
under duplicate-title / duplicate-content — the case its own comment calls
out as "telling the user to fix something they already fixed". It also
suppresses canonicalized-page and canonical-conflict.

Fix

Lowercase both values before comparing. Nothing else changes: rel="canonical"
and type="application/ld+json" behave exactly as before, and a multi-token
rel is still not matched (that is the reference behaviour and out of scope
here).

Tests

The cheerio reference the parity suite compares against already matches these
case-insensitively — css-select treats rel and type as case-insensitive
HTML attributes — so this is a straight parity gap and one expectParity case
covers it, in the existing style:

it("matches when rel and type differ only in case", () => {
  expectParity(`<html><head>
    <link rel="Canonical" href="/canonical">
    <link rel="ALTERNATE" hreflang="de" href="/de">
    <link rel="alternate" hreflang="fr" href="/fr">
    <script type="application/LD+JSON">{"@type":"Article"}</script>
    </head><body><p>text</p></body></html>`);
});

The lowercase rel="alternate" line is in there on purpose: it must keep
working, and it passes both before and after.

Against main:

Tests  1 failed | 10 passed (11)

AssertionError: expected { …(19) } to deeply equal { …(19) }
-   "canonical": "/canonical",
+   "canonical": null,
-   "hasStructuredData": true,
+   "hasStructuredData": false,
    "hreflangTags": [
-     "de",
      "fr",
    ],

With the fix:

Tests  11 passed (11)

How I tested

Windows 11, Node 22.

npx vitest run src/server/lib/audit/   → 6 files, 70 tests passed
npx tsc --noEmit                       → clean
npx prettier --check <the two files>   → clean
npx oxlint <the two files>             → 0 warnings, 0 errors

I did not run the whole suite or e2e.

The page analyzer compared `rel` and `script[type]` attribute values with
`===`, so <link rel="Canonical">, <link rel="ALTERNATE" hreflang> and
<script type="application/LD+JSON"> were all missed. Both attributes are
ASCII case-insensitive in HTML, and the cheerio reference the parity suite
compares against already matches them that way.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant