diff --git a/agents/console/server.mjs b/agents/console/server.mjs
index 8557758..d2792f9 100644
--- a/agents/console/server.mjs
+++ b/agents/console/server.mjs
@@ -638,15 +638,22 @@ const qaNorm = (p) => { p = String(p).split("#")[0].split("?")[0]; if (p.length
const qaAbs = (href, pagePath) => { try { return new URL(href, QA_BASE + pagePath).href; } catch { return null; } };
const qaGrabAll = (re, html) => [...String(html).matchAll(re)].map((m) => m[1]);
const qaFirst = (re, html) => { const m = String(html).match(re); return m ? m[1].trim() : ""; };
-function qaImgsNoAlt(html) {
- // An image "has alt" if it carries an alt attribute at all — including an
- // empty one (alt="" or a bare `alt`), which is the valid, deliberate signal
- // for a decorative image. Only a total absence of alt is a finding.
- const hasAlt = (tag) => /\salt(\s*=|[\s>\/])/i.test(tag);
- return [...String(html).matchAll(/
]*>/gi)].map((m) => m[0])
- .filter((tag) => !hasAlt(tag))
- .map((tag) => (tag.match(/\bsrc=["']([^"']+)["']/i) || [])[1])
- .filter(Boolean);
+// Classify each
: alt missing entirely, or present-but-empty (alt="" or a
+// bare `alt`). Both are surfaced — an empty alt is only correct for a purely
+// decorative image, so a content image (photo, cover, screenshot) with empty
+// alt is a real accessibility gap, not a pass.
+function qaAltIssues(html) {
+ const out = [];
+ for (const m of String(html).matchAll(/
]*>/gi)) {
+ const tag = m[0];
+ const src = (tag.match(/\bsrc=["']([^"']+)["']/i) || [])[1];
+ if (!src) continue;
+ const withVal = tag.match(/\salt\s*=\s*["']([^"']*)["']/i);
+ if (withVal) { if (withVal[1].trim() === "") out.push({ src, kind: "empty" }); }
+ else if (/\salt(\s|>|\/)/i.test(tag)) out.push({ src, kind: "empty" }); // bare `alt`
+ else out.push({ src, kind: "missing" });
+ }
+ return out;
}
async function runQa() {
@@ -693,7 +700,12 @@ async function runQa() {
else if (/^https?:\/\//i.test(noHash) && !external.has(noHash)) external.set(noHash, path);
}
for (const src of qaGrabAll(/
]*\bsrc=["']([^"']+)["']/gi, html)) { const u = qaAbs(src, path); if (u && /^https?:/i.test(u)) images.add(u.split("#")[0]); }
- for (const src of qaImgsNoAlt(html)) add("a11y", "warning", path, `Image without alt: ${src}`, `On ${path}, the image "${src}" has no alt text. Add descriptive alt text.`);
+ for (const a of qaAltIssues(html)) {
+ if (a.kind === "empty") add("a11y", "warning", path, `Empty alt: ${a.src}`,
+ `On ${path}, the image "${a.src}" has an empty alt attribute. If it conveys meaning (a photo, cover, or screenshot), add descriptive alt text that explains what it shows; leave it empty only if it is purely decorative.`);
+ else add("a11y", "warning", path, `Missing alt: ${a.src}`,
+ `On ${path}, the image "${a.src}" has no alt attribute. Add descriptive alt text that explains what it shows.`);
+ }
}
// duplicate titles across pages