From 7fea629fc1d2a4c651518d5005ee680ebc73574a Mon Sep 17 00:00:00 2001
From: Rob Migchels
Date: Sat, 30 May 2026 15:46:33 +0200
Subject: [PATCH 1/2] feat: implement automated audit functionality for 56 out
of 108 items.
---
functions/api/check.ts | 733 +++++++++++++++++++++++++++++
src/components/SiteFooter.astro | 1 +
src/components/SiteHeader.astro | 2 +
src/pages/audit.astro | 793 ++++++++++++++++++++++++++++++++
src/pages/index.astro | 5 +-
5 files changed, 1533 insertions(+), 1 deletion(-)
create mode 100644 functions/api/check.ts
create mode 100644 src/pages/audit.astro
diff --git a/functions/api/check.ts b/functions/api/check.ts
new file mode 100644
index 00000000..01badc3a
--- /dev/null
+++ b/functions/api/check.ts
@@ -0,0 +1,733 @@
+/**
+ * Cloudflare Pages Function — checklist auditor API endpoint.
+ *
+ * Receives a domain parameter, performs concurrent fetches and DNS-over-HTTPS queries,
+ * and returns a JSON report outlining compliance with automatable checklist items.
+ */
+
+type CheckResult = {
+ slug: string;
+ result: 'pass' | 'fail' | 'warning' | 'manual';
+ message: string;
+ details?: string;
+};
+
+const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 SpecAuditor/1.0';
+
+export const onRequest: PagesFunction = async (context) => {
+ const corsHeaders = new Headers({
+ 'Content-Type': 'application/json; charset=utf-8',
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type',
+ });
+
+ if (context.request.method === 'OPTIONS') {
+ return new Response(null, { status: 204, headers: corsHeaders });
+ }
+
+ const requestUrl = new URL(context.request.url);
+ let domain = requestUrl.searchParams.get('domain') ?? '';
+ domain = domain.trim().toLowerCase();
+
+ if (!domain) {
+ return new Response(JSON.stringify({ error: 'Missing domain parameter' }), {
+ status: 400,
+ headers: corsHeaders,
+ });
+ }
+
+ // Clean the domain input
+ domain = domain.replace(/^(https?:\/\/)?(www\.)?/, '');
+ // Remove any trailing path/slashes
+ domain = domain.split('/')[0];
+
+ // Validate hostname structure
+ const hostRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
+ if (!hostRegex.test(domain) || !domain.includes('.')) {
+ return new Response(JSON.stringify({ error: 'Invalid domain format' }), {
+ status: 400,
+ headers: corsHeaders,
+ });
+ }
+
+ try {
+ const results: CheckResult[] = [];
+
+ // Helper for timeout-capped fetches
+ const fetchWithTimeout = async (url: string, init: RequestInit = {}, timeoutMs = 6000) => {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
+ try {
+ const response = await fetch(url, {
+ ...init,
+ signal: controller.signal,
+ headers: {
+ 'User-Agent': USER_AGENT,
+ ...(init.headers || {}),
+ },
+ });
+ clearTimeout(timeoutId);
+ return response;
+ } catch (err) {
+ clearTimeout(timeoutId);
+ throw err;
+ }
+ };
+
+ // 1. Run HTTP -> HTTPS redirect check
+ let httpRedirectPass = false;
+ let redirectMessage = 'HTTP does not redirect to HTTPS.';
+ try {
+ const httpRes = await fetchWithTimeout(`http://${domain}`, { redirect: 'manual' });
+ const status = httpRes.status;
+ const location = httpRes.headers.get('location') || '';
+ if (status >= 300 && status < 400 && location.startsWith('https://')) {
+ httpRedirectPass = true;
+ redirectMessage = `Redirects HTTP to HTTPS (${status} to ${location}).`;
+ } else {
+ redirectMessage = `HTTP returned status ${status} but did not redirect to HTTPS. Location: ${location || 'none'}`;
+ }
+ } catch (e: any) {
+ redirectMessage = `Could not connect to HTTP version: ${e.message || e}`;
+ }
+
+ results.push({
+ slug: 'https-tls',
+ result: httpRedirectPass ? 'pass' : 'fail',
+ message: redirectMessage,
+ });
+
+ // 2. Fetch primary page (HTTPS) and read headers/HTML
+ const rootUrl = `https://${domain}`;
+ let mainPageHtml = '';
+ let mainHeaders = new Headers();
+ let rootFetchSuccess = false;
+ let rootFetchErrorMsg = '';
+
+ try {
+ const res = await fetchWithTimeout(rootUrl, { redirect: 'follow' });
+ rootFetchSuccess = true;
+ mainHeaders = res.headers;
+ mainPageHtml = await res.text();
+ } catch (e: any) {
+ rootFetchErrorMsg = e.message || String(e);
+ }
+
+ if (!rootFetchSuccess) {
+ return new Response(
+ JSON.stringify({
+ domain,
+ error: `Could not connect to ${rootUrl}: ${rootFetchErrorMsg}`,
+ results: [
+ {
+ slug: 'https-tls',
+ result: 'fail',
+ message: `Could not connect to HTTPS version of site: ${rootFetchErrorMsg}`,
+ },
+ ],
+ }),
+ { headers: corsHeaders }
+ );
+ }
+
+ // --- SECURITY HEADER CHECKS ---
+ // HSTS
+ const hsts = mainHeaders.get('strict-transport-security');
+ results.push({
+ slug: 'hsts',
+ result: hsts ? 'pass' : 'fail',
+ message: hsts ? `HSTS header set: ${hsts}` : 'Strict-Transport-Security header is missing.',
+ });
+
+ // X-Content-Type-Options
+ const xcto = mainHeaders.get('x-content-type-options');
+ const xctoPass = xcto && xcto.toLowerCase().includes('nosniff');
+ results.push({
+ slug: 'x-content-type-options',
+ result: xctoPass ? 'pass' : 'fail',
+ message: xctoPass ? 'X-Content-Type-Options set to nosniff.' : `Header value is: ${xcto || 'missing'}.`,
+ });
+
+ // Content Security Policy
+ const cspHeader = mainHeaders.get('content-security-policy') || mainHeaders.get('content-security-policy-report-only');
+ const hasCspMeta = /.'
+ : 'No CSP header or meta tag found.',
+ });
+
+ // Referrer Policy
+ const refHeader = mainHeaders.get('referrer-policy');
+ const hasRefMeta = /.'
+ : 'Referrer policy not specified.',
+ });
+
+ // Permissions Policy
+ const permHeader = mainHeaders.get('permissions-policy');
+ results.push({
+ slug: 'permissions-policy',
+ result: permHeader ? 'pass' : 'warning',
+ message: permHeader ? `Permissions-Policy present: ${permHeader.substring(0, 60)}...` : 'Permissions-Policy header is missing (recommended).',
+ });
+
+ // Frame Ancestors / X-Frame-Options
+ const xfo = mainHeaders.get('x-frame-options');
+ const hasCspFrameAncestors = cspHeader && cspHeader.includes('frame-ancestors');
+ results.push({
+ slug: 'frame-ancestors',
+ result: (xfo || hasCspFrameAncestors) ? 'pass' : 'fail',
+ message: hasCspFrameAncestors
+ ? 'CSP frame-ancestors directive present.'
+ : xfo
+ ? `X-Frame-Options header present: ${xfo}`
+ : 'Frame protection header (X-Frame-Options or CSP frame-ancestors) is missing.',
+ });
+
+ // --- HTML FOUNDATIONS CHECKS ---
+ // Doctype
+ const doctypeRegex = /^\s*) is missing or malformed.',
+ });
+
+ // HTML Lang
+ const langMatch = mainPageHtml.match(/]*lang=["']([a-zA-Z-]+)["']/i);
+ results.push({
+ slug: 'html-lang',
+ result: langMatch ? 'pass' : 'fail',
+ message: langMatch ? `Language attribute found: lang="${langMatch[1]}"` : 'lang attribute is missing on the tag.',
+ });
+
+ // Character Encoding
+ const charsetMatch = mainPageHtml.match(/]*charset=["']?utf-8["']?/i) || mainPageHtml.match(/]+http-equiv=["']content-type["'][^>]*content=["'][^"']*charset=utf-8["']/i);
+ results.push({
+ slug: 'character-encoding',
+ result: charsetMatch ? 'pass' : 'fail',
+ message: charsetMatch ? 'UTF-8 character encoding declared.' : 'UTF-8 character encoding meta tag not found.',
+ });
+
+ // Viewport
+ const viewportMatch = mainPageHtml.match(/]+name=["']viewport["']/i);
+ results.push({
+ slug: 'viewport',
+ result: viewportMatch ? 'pass' : 'fail',
+ message: viewportMatch ? 'Viewport meta tag found for responsive layouts.' : 'Viewport meta tag is missing.',
+ });
+
+ // Canonical URLs
+ const canonicalMatch = mainPageHtml.match(/]+rel=["']canonical["'][^>]*href=["']([^"']+)["']/i);
+ results.push({
+ slug: 'canonical-urls',
+ result: canonicalMatch ? 'pass' : 'fail',
+ message: canonicalMatch ? `Canonical URL link tag found pointing to: ${canonicalMatch[1]}` : 'Canonical link tag is missing.',
+ });
+
+ // Favicon Link Tag Check
+ const faviconMatch = mainPageHtml.match(/]+rel=["'](?:shortcut )?icon["']/i);
+ let favIconPass = !!faviconMatch;
+ let faviconMessage = favIconPass ? 'Favicon reference found in HTML.' : '';
+
+ if (!favIconPass) {
+ try {
+ const favRes = await fetchWithTimeout(`${rootUrl}/favicon.ico`, { method: 'HEAD' });
+ if (favRes.status === 200) {
+ favIconPass = true;
+ faviconMessage = 'Favicon found at root /favicon.ico (200 OK).';
+ } else {
+ faviconMessage = 'No favicon link tag in HTML and /favicon.ico returned status ' + favRes.status;
+ }
+ } catch (e) {
+ faviconMessage = 'No favicon link tag in HTML and check on /favicon.ico failed.';
+ }
+ }
+
+ results.push({
+ slug: 'favicons',
+ result: favIconPass ? 'pass' : 'fail',
+ message: faviconMessage,
+ });
+
+ // --- SEO CHECKS ---
+ // Title Tags
+ const titleMatch = mainPageHtml.match(/([\s\S]*?)<\/title>/i);
+ const titleVal = titleMatch ? titleMatch[1].trim() : '';
+ results.push({
+ slug: 'title-tags',
+ result: titleVal ? 'pass' : 'fail',
+ message: titleVal ? `Title tag present: "${titleVal}" (length: ${titleVal.length})` : 'Title tag is missing or empty.',
+ });
+
+ // Meta Descriptions
+ const descMatch = mainPageHtml.match(/]+name=["']description["'][^>]*content=["']([^"']*)["']/i) || mainPageHtml.match(/]+content=["']([^"']*)["'][^>]*name=["']description["']/i);
+ const descVal = descMatch ? descMatch[1].trim() : '';
+ results.push({
+ slug: 'meta-descriptions',
+ result: descVal ? 'pass' : 'warning',
+ message: descVal ? `Description meta tag present: "${descVal.substring(0, 60)}..." (length: ${descVal.length})` : 'Description meta tag is missing (recommended).',
+ });
+
+ // Meta Robots
+ const metaRobMatch = mainPageHtml.match(/]+name=["']robots["'][^>]*content=["']([^"']*)["']/i);
+ results.push({
+ slug: 'meta-robots',
+ result: metaRobMatch ? 'pass' : 'warning',
+ message: metaRobMatch ? `robots meta tag present: "${metaRobMatch[1]}"` : 'robots meta tag is missing (recommended to guide crawlers).',
+ });
+
+ // Open Graph
+ const ogTitle = mainPageHtml.match(/]+(property|name)=["']og:title["']/i);
+ const ogImage = mainPageHtml.match(/]+(property|name)=["']og:image["']/i);
+ const hasOG = ogTitle && ogImage;
+ results.push({
+ slug: 'open-graph',
+ result: hasOG ? 'pass' : 'warning',
+ message: hasOG ? 'Open Graph protocol metadata found.' : 'og:title and/or og:image are missing.',
+ });
+
+ // Twitter Cards
+ const twitterCard = mainPageHtml.match(/]+(property|name)=["']twitter:card["']/i);
+ results.push({
+ slug: 'twitter-cards',
+ result: twitterCard ? 'pass' : 'warning',
+ message: twitterCard ? 'Twitter card metadata found.' : 'twitter:card tag is missing.',
+ });
+
+ // Heading Hierarchy
+ const h1Count = (mainPageHtml.match(/]/gi) || []).length;
+ results.push({
+ slug: 'heading-hierarchy',
+ result: h1Count === 1 ? 'pass' : 'warning',
+ message: h1Count === 1 ? 'Exactly one tag found.' : `Found ${h1Count} tags (exactly one is recommended).`,
+ });
+
+ // Breadcrumbs Structured Data
+ const hasBreadcrumbs = mainPageHtml.includes('BreadcrumbList') || mainPageHtml.includes('schema.org/Breadcrumb') || /
-
-
diff --git a/src/styles/global.css b/src/styles/global.css
index 3a2a87f7..cf70936e 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -229,6 +229,14 @@ body {
border-radius: 0.125rem;
}
+/* Button & interactive summary cursor reset */
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+ cursor: pointer;
+}
+
/* Print */
@media print {
header, footer, nav, .no-print { display: none; }