← Blog

Task 3 - Web Quality Auditing

TLDR

  • Audited cainappleby.net across security, performance, accessibility, SEO, and HTML validity using SSL Labs, PageSpeed Insights, WAVE, and the W3C Validator.
  • Found contrast failures, a missing meta description, no HTTP security headers, and no DNS CAA record.
  • Fixed everything with Claude Code. The CSP header broke the site on the first attempt because of an inline style block — had to externalise the CSS first.
  • Final scores: PageSpeed 100/100/100/100, WAVE 10/10, W3C clean, SSL Labs A.

Task Requirements

Run the site through a set of standard web quality tools covering TLS/SSL, page performance, accessibility, SEO, and HTML validity. Document what each tool found, fix whatever needed fixing, and re-test to confirm everything passes.

Goal

Find out where cainappleby.net actually stands against real auditing tools, fix the gaps, and end up with a site that scores well across the board rather than just looking fine in a browser.

Implementation Steps

  1. I picked four tools that each test a different layer. SSL Labs checks TLS configuration — protocol versions, cipher suites, certificate validity, known vulnerabilities. PageSpeed Insights runs the page in a headless browser and scores performance, accessibility, best practices, and SEO. WAVE checks WCAG accessibility by looking at contrast, heading structure, labels, and screen reader navigation. The W3C Validator checks that the HTML actually conforms to spec. I also tried Security Headers but it timed out and couldn't reach the server.

  2. Starting results were mostly fine but not clean. SSL Labs came back Grade A — TLS 1.3 and 1.2 only, all older versions disabled, all vulnerability checks (POODLE, BEAST, Heartbleed, ROBOT) clean, Forward Secrecy marked Robust. PageSpeed Performance was already 100. The W3C Validator was already clean. The problems were in the other scores: PageSpeed Accessibility was 90 (contrast ratio too low on text), PageSpeed SEO was 91 (no meta description), PageSpeed Best Practices flagged five missing HTTP security headers as advisory, and WAVE gave 6.4/10 with five contrast errors.

  3. First fix was the HTTP security headers. Each one tells the browser how to handle the page — Content-Security-Policy stops scripts loading from unauthorised sources, X-Frame-Options: DENY prevents clickjacking via iframes, X-Content-Type-Options: nosniff stops the browser guessing file types, Referrer-Policy controls what URL info leaks on outbound clicks, Permissions-Policy blocks silent camera/mic/location grabs, and Cross-Origin-Opener-Policy stops other tabs accessing the page's window object. I added all of them as a header block in the Caddyfile.

  4. This is where it went wrong. The style-src 'self' directive in the CSP broke the site immediately. The problem was that base.html had an inline <style> block, and 'self' only covers files served from the same origin — not inline styles. The browser blocked the entire stylesheet. Claude Code added 'unsafe-inline' as a hotfix, which fixed the rendering but weakened the CSP. The proper fix was to do it in the right order: move all the CSS out of the inline <style> block into static/css/style.css, replace the block with a <link> tag, then remove 'unsafe-inline' from the Caddyfile. After that the CSP was fully strict with no exceptions.

  5. The contrast fix turned out to be simple. All five WAVE errors pointed at the same muted text colour — .site-nav, .card-meta, .list-date, .back-link, and .post-meta all used the --muted CSS variable. The old value #6b7280 was too close in brightness to the dark backgrounds, giving ratios around 3:1 to 3.4:1. WCAG minimum is 4.5:1. Changed --muted to #9ca3af and all five elements jumped above the threshold in one go.

  6. The meta description was a one-liner. Added <meta name="description" content="Cain Appleby - platform engineer. Personal site and server."> to templates/base.html inside <head>. Without it, search engines just pick whatever text they find on the page.

  7. DNS CAA was a registrar-side change. A CAA record declares which certificate authorities are allowed to issue certs for your domain. Without one, any CA could legitimately issue a cert for cainappleby.net. Added a single record: CAA 0 issue "letsencrypt.org". Verified it with dig CAA cainappleby.net. HSTS I left off intentionally — it permanently locks browsers to HTTPS and a Raspberry Pi server can realistically break HTTPS during an update.

  8. OCSP Stapling showed as off in the SSL Labs report, but this was a cached result. Caddy handles OCSP stapling automatically with Let's Encrypt managed certs, so it was already active.

  9. Re-ran everything. PageSpeed: 100/100/100/100. WAVE: 10/10, zero contrast errors. W3C Validator: still clean. SSL Labs: still Grade A with the CAA record now present.

Notes & Decisions

  • The unsafe-inline situation was the most useful thing that came out of this. The instinct is to apply the strictest config straight away and fix what breaks. The correct order is to externalise everything first, then apply the strict header. That way it works on the first try instead of breaking the live site and patching reactively.
  • DNS CAA surprised me by being this simple. One DNS record closes a real attack surface with no server config needed. Should be one of the first things set up after getting a cert.
  • The contrast fix being a single CSS variable was lucky. The site uses a design token for muted text so every instance updated at once. If those colours had been hardcoded per-element it would have been five separate changes to track down.

Next Ideas / Follow-ups

None captured yet.