qa fixes: full sitemap coverage + a11y check recognises empty alt

- sitemap.xml.js now includes services, projects (+ DB-driven project pages),
  resume, changelog, faq (was blog-only) — clears 12 QA sitemap warnings
- runQa a11y check treats a bare/empty alt as present (valid decorative image),
  fixing a false positive on the blog cover
This commit is contained in:
Carlos Arias 2026-07-24 10:20:14 +00:00
parent d548d8a8a8
commit 87fceea726
2 changed files with 19 additions and 2 deletions

View file

@ -639,8 +639,12 @@ const qaAbs = (href, pagePath) => { try { return new URL(href, QA_BASE + pagePat
const qaGrabAll = (re, html) => [...String(html).matchAll(re)].map((m) => m[1]); 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() : ""; }; const qaFirst = (re, html) => { const m = String(html).match(re); return m ? m[1].trim() : ""; };
function qaImgsNoAlt(html) { 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(/<img\b[^>]*>/gi)].map((m) => m[0]) return [...String(html).matchAll(/<img\b[^>]*>/gi)].map((m) => m[0])
.filter((tag) => !/\balt\s*=/i.test(tag)) .filter((tag) => !hasAlt(tag))
.map((tag) => (tag.match(/\bsrc=["']([^"']+)["']/i) || [])[1]) .map((tag) => (tag.match(/\bsrc=["']([^"']+)["']/i) || [])[1])
.filter(Boolean); .filter(Boolean);
} }

View file

@ -1,14 +1,27 @@
import { SITE, authors, categories, sortedPosts, tags } from "../lib/blog-data.js"; import { SITE, authors, categories, sortedPosts, tags } from "../lib/blog-data.js";
import { getProjects } from "../lib/projects.js";
const BASE_URL = SITE.url || ""; const BASE_URL = SITE.url || "";
export async function GET() { export async function GET() {
const posts = await sortedPosts(); const posts = await sortedPosts();
const projects = await getProjects();
const entries = [ const entries = [
{ path: "/", changefreq: "weekly", priority: "1.0" }, { path: "/", changefreq: "weekly", priority: "1.0" },
{ path: "/blog", changefreq: "daily", priority: "0.9" },
{ path: "/about", changefreq: "monthly", priority: "0.6" }, { path: "/about", changefreq: "monthly", priority: "0.6" },
{ path: "/services", changefreq: "monthly", priority: "0.7" },
{ path: "/services/website-design", changefreq: "monthly", priority: "0.6" },
{ path: "/projects", changefreq: "weekly", priority: "0.8" },
{ path: "/resume", changefreq: "monthly", priority: "0.6" },
{ path: "/contact", changefreq: "monthly", priority: "0.5" }, { path: "/contact", changefreq: "monthly", priority: "0.5" },
{ path: "/faq", changefreq: "monthly", priority: "0.4" },
{ path: "/changelog", changefreq: "weekly", priority: "0.3" },
{ path: "/blog", changefreq: "daily", priority: "0.9" },
...projects.map((project) => ({
path: `/projects/${project.slug}`,
changefreq: "monthly",
priority: "0.7",
})),
...posts.map((post) => ({ ...posts.map((post) => ({
path: `/blog/${post.slug}`, path: `/blog/${post.slug}`,
lastmod: post.updated || post.date, lastmod: post.updated || post.date,