# How specification.website Improved Ondernemen in de Kempen

> A reusable write-up of the audit we ran on **ondernemenindekempen.nl** against
> [The Website Specification](https://specification.website/), what we found,
> what we shipped, and a playbook to repeat it on any other project.
>
> Written for Joost (and future-us). Last run: **2026-05-29** · shipped in **v1.58.0**.

---

## 1. What is specification.website?

[The Website Specification](https://specification.website/) is a free,
open-source (MIT), platform-agnostic reference of the technical features "a good
website should have." It is **not a legal standard** — it's a practical,
opinionated checklist maintained in the open. Every topic page is also available
as raw Markdown, and there's a machine-readable checklist plus an MCP server for
automated querying.

It organises everything into **10 themes**:

| # | Theme | What it covers |
|---|-------|----------------|
| 1 | **Foundations** | doctype, `lang`, charset, viewport, title/description, canonical, favicons, Open Graph, feed discovery |
| 2 | **SEO** | robots.txt, sitemaps, URL structure, redirects, headings, internal links, JSON-LD, breadcrumbs, IndexNow |
| 3 | **Accessibility** | contrast, alt text, labels, keyboard nav, focus indicators, skip links, semantic HTML, ARIA, reduced motion, touch targets |
| 4 | **Security** | HTTPS/TLS, HSTS, CSP, security.txt, nosniff, frame-ancestors, Referrer-Policy, Permissions-Policy, SRI, cookie flags, CAA/DNSSEC |
| 5 | **Well-Known URIs** | standardised `/.well-known/` paths |
| 6 | **Agent Readiness** | llms.txt, Markdown endpoints, AI-crawler rules, stable URLs, machine-readable feeds, HTTP Link headers, MCP discovery |
| 7 | **Performance** | Core Web Vitals, image optimisation, lazy loading, resource hints, caching, compression, fonts, HTTP/2-3 |
| 8 | **Privacy** | privacy policy, cookie consent, Global Privacy Control, third-party audit, cookieless analytics, data minimisation |
| 9 | **Resilience** | custom error pages, 503 maintenance, offline/service workers, web app manifest, monitoring |
| 10 | **Internationalisation** | hreflang, inline `lang`, RTL, locale-aware content, IDN |

---

## 2. How we ran the audit (the method)

The whole thing was done with **Claude Code** in one session. The approach is the
reusable part:

1. **Fetch the spec + the full checklist** (`/checklist`) so we had concrete,
   testable items instead of vibes.
2. **Fan out three parallel read-only audit agents**, each owning a cluster of
   themes, each required to report **PASS / PARTIAL / FAIL / N.A.** per checklist
   item *with file:line evidence*. Clustering by theme keeps each agent focused
   and runs them concurrently:
   - Agent A → Foundations + SEO + i18n
   - Agent B → Security + Well-Known + Privacy
   - Agent C → Accessibility + Performance + Resilience + Agent Readiness
3. **Verify the consequential claims by hand** before acting. This mattered:
   agents are confident but not always right (see §5).
4. **Fix, test, ship** with a version bump + changelog, then deploy and verify
   the changes *live* (not just "the build passed").

---

## 3. What the audit found (scorecard)

OIDK scored **well above the average production site** — the hard things were
already done right (privacy engineering, JSON-LD graph, CSP, redirects).

| Theme | Score | Verdict |
|-------|:-----:|---------|
| Foundations | 11/11 | Complete |
| SEO | 11/12 | Strong |
| Accessibility | 9/9 measurable | Mature |
| Security | 9/11 | Strong (2 quick wins) |
| Well-Known URIs | minimal | Intentional |
| Agent Readiness | 8/12 | Good — strong `llms.txt` |
| Performance | 8/9 | Strong |
| **Privacy** | **9/9** | **Exemplary** |
| Resilience | 4/5 | Good |
| Internationalisation | 3/5 | The one real gap |

Already-excellent and left untouched: cookieless + IP-less hashed analytics
(SHA-256 of IP+UA+date, rotated daily, 90-day retention), no Google
Analytics / Meta Pixel / LinkedIn tag, full Organization/WebSite/LocalBusiness/
BreadcrumbList/Article JSON-LD, HSTS with preload, tight CSP, clean 301/308
redirects, `noindex,follow` logic against thin combo pages, a welcoming
`llms.txt`, web app manifest, custom 404/500 pages, and a genuine skip link.

---

## 4. What we shipped (v1.58.0)

| Fix | Why it matters | Where |
|-----|----------------|-------|
| **hreflang on dynamic pages** | OIDK serves the Dutch *and* Belgian Kempen. Pages only had `canonical`, so Google couldn't read the NL/BE regional targeting. | New helper `hreflangAlternates()` in `src/lib/constants.ts`, applied to `/regio/[gemeente]`, `/categorie/[slug]`, `/categorie/[slug]/[gemeente]`, `/bedrijvengids/[slug]`, `/kennisbank/[slug]`, `/nieuws/[slug]` |
| **Permissions-Policy header** | Locks down browser features we never use (geolocation, camera, mic, payment, usb, browsing-topics). Defence-in-depth + reinforces the no-adtech stance. | `next.config.ts` |
| **HTTP `Link` header for RSS** | Advertises the 3 feeds so readers and AI agents auto-discover them without parsing the HTML `<head>`. | `next.config.ts` |
| **`/.well-known/security.txt`** | RFC 9116 disclosure channel for security researchers. On-brand for a transparency project. | `public/.well-known/security.txt` |
| **Global Privacy Control (GPC)** | `trackPageView()` now skips visitors sending `Sec-GPC: 1`, even though analytics are already cookieless. Documented in `/privacy`. | `src/lib/analytics.ts` + privacy page |

The hreflang helper is **self-referential**: every page declares `nl-NL`,
`nl-BE`, `nl` and `x-default` all pointing to its *own* canonical URL. That is
the correct pattern for one-language-serving-multiple-regions, and it mirrors the
sitewide default already in `app/layout.tsx`.

---

## 5. Lessons worth keeping

- **Verify before you fix.** The audit flagged "Belgian businesses get
  `addressCountry: NL` in their schema" as a P0 data-integrity bug. We checked:
  `getCountryForCity()` already returns `"BE"` correctly. It was a false positive
  — fixing it would have *introduced* a regression. Two other "missing" items
  (the skip link, the favicons) also already existed. **An audit produces leads,
  not facts.**
- **HTTP header values are ASCII-only.** Our first deploy 500'd on *every*
  request because the RSS `Link` header title contained an em-dash (`—`,
  U+2014). Node throws `ERR_INVALID_CHAR`. Use a plain hyphen in header values.
  This is exactly why step 4 — verify *live*, not just "build passed" — is
  non-negotiable. A green `tsc`/lint/unit-test run would never have caught it.
- **Score the spec against your context.** Several "gaps" are correctly N.A. for
  a non-PWA, single-language, non-API, non-profit guide (service worker, 503
  page, most `.well-known/` URIs, RTL, IDN). Don't chase a 10/10 — chase the
  items that change real outcomes.

---

## 6. Playbook — repeat this on any project

```
1. WebFetch https://specification.website/ and /checklist
   → get the 10 themes + concrete checkbox items.

2. Fan out 3 read-only audit agents (theme clusters above).
   Require PASS/PARTIAL/FAIL/N.A. + file:line evidence per item.
   Tell each agent the deploy target + stack so it scores in context.

3. Hand-verify every consequential claim before touching code.
   Especially anything labelled "data integrity" or "P0".

4. Fix the real gaps. Bump VERSION + CHANGELOG. Run lint + typecheck + tests.

5. Deploy, then verify LIVE:
   - curl -sI https://<host>/ | grep -iE 'permissions-policy|link|strict-transport'
   - curl -s https://<host>/.well-known/security.txt
   - view-source a dynamic page → check <link rel="alternate" hreflang=...>
   - run the homepage through securityheaders.com
```

### Smart tips we'd give next time

- **There's an MCP server + Agent Skills** on specification.website. Wiring the
  MCP server into Claude Code lets you query the spec programmatically instead of
  fetching pages — worth it if you audit multiple sites.
- **Add the live checks to CI.** A tiny test that boots the app and asserts the
  security headers are present (and ASCII-valid!) would have caught our em-dash
  500 before deploy. Cheap insurance.
- **`llms.txt` is a genuine SEO/visibility lever now**, not a curiosity. OIDK
  already tracks LLM citations — a `/llms-full.txt` with the knowledge-base
  content is a low-effort next step.
- **Re-audit after big content or routing changes.** hreflang and canonical
  correctness drift the moment you add new dynamic route shapes. Make this
  audit a recurring checkpoint, not a one-off.

---

*Generated from a live audit of ondernemenindekempen.nl. The spec itself is at
<https://specification.website/> — go read it; it's good.*
