Accounts soon

45 rules · 5 stated gaps

The standard

Every rule this site is built against, how each one is actually met, and — at the bottom — the ones that are not fully met. A checklist that claims total coverage is worth less than one that names its edges.

These rules are not aspirational. A dependency-free auditor checks every mechanically-checkable one of them and exits non-zero on a miss, so a missing item blocks a deploy rather than becoming a to-do nobody reads.

Craft

What a visitor can see and use.

custom-404
A 404 in the register's voice that offers the actual catalogue and the two most recent entries, rather than a dead end.
cta-above-fold
The primary action sits in the homepage hero, before any section boundary.
internal-links
Every page carries at least two contextual onward links in its own content. The footer existing does not count.
thank-you-page
A confirmation page that states who reads the message, how long a reply takes, and what to do if none arrives. noindex.
breadcrumbs
The visible trail and the BreadcrumbList schema are generated from one array, so they cannot drift apart.
case-studies
Four decisions, each with a problem, what changed, the result, and a number checkable against the repository it names.
five-faqs
Six questions with FAQPage structured data, generated from the same array the page renders.
response-time-promise
A stated turnaround in the footer of every page, and again on the contact page.
sticky-mobile-cta
Small screens only, appears after a sentinel at the foot of the hero leaves the viewport, dismissible, and the dismissal sticks for the visit.
maps-directions
Honestly omitted. There are no premises, and the footer says so in words rather than quietly leaving out a map.
real-reviews
Present, empty, and labelled. Nobody has said anything on the record yet, and a fabricated testimonial would break the rule it satisfies.
interactive-scroll
A fixed accession index reports which record is in view, driven by IntersectionObserver — about the subject, not a parallax bolted on.

Discoverability

What a crawler and a share card see.

unique-page-titles
One entry per route in src/lib/site.ts. The auditor reads that file and fails the build on a duplicate.
meta-descriptions
All unique and 50–160 characters. For register entries the summary IS the description, and the database constrains it to the same bound.
social-share-img
A 1200×630 card composed in code from the live register, so the counts on it cannot go stale. Real alt text.
robots-txt
Generated from the same PRIVATE_PATHS constant the sitemap uses, never hand-written.
sitemap
Derived from the route config and the register, so a new entry appears automatically. Private routes opt OUT by name.
alt-text
Required by the TypeScript type and by a CHECK constraint, so an image with no alt cannot be stored or built. Decorative SVG is aria-hidden.
local-schema
CollectionPage and Person — what this actually is. Never LocalBusiness, which would be a lie to a crawler for a rich result.

Security

Enforced in the framework and the database, not remembered per page.

hide-api-keys
Only NEXT_PUBLIC_* reaches the client. The auditor scans every client component for a non-public env read and fails the build on one.
purge-git-secrets
.gitignore covered .env, .env.* and var/ in the first commit, before any secret existed. .env.example carries names, never values.
public-db-key-only
There is no client-side database access at all. The connection string is server-only and the database has no public network path.
encrypt-sensitive-data
The rate limiter records IP addresses, which are personal data, encrypted with pgcrypto. Lookup goes through a keyed fingerprint, so nothing is decrypted to make a lockout decision.
server-side-auth
The console verifies a session token hash against a live Postgres row in a server component, and every write route checks again. The client gate is a form.
lock-record-access
Row-level security ENABLED and FORCED on all four tables, with the role created NOBYPASSRLS. Setup reads both facts back and refuses to finish if either is missing.
block-field-tampering
The slug is re-derived from the name and a submitted one is never read; the accession comes from a database trigger; the condition is checked against the union and again by a constraint.
secure-session-cookies
HttpOnly, Secure in production, SameSite=Strict, path-scoped. Ten minutes for the doorway stages, eight hours for a granted session.
hash-passwords
scrypt at N=2^15, and verification burns a real scrypt call even when the stored hash is missing — so an unconfigured gate and a wrong answer take the same time.
rate-limit-login
Checked before the credential comparison, with an escalating 15/20/25/30-minute ladder held in Postgres so it survives a restart.
parameterize-queries
Every statement uses $1 placeholders through node-postgres. There is no string-built SQL anywhere in the repository.
validate-all-output
Every API route bounds its body before parsing and clips every field after. Nothing unbounded reaches the database or a log.
escape-user-content
dangerouslySetInnerHTML appears only on JSON.stringify of structured data built in the same file. The auditor enforces that pairing.
trim-api-responses
/sites.json is a projection. localPath exists in the database for correlating an entry to a folder and stops at the export boundary.
security-headers
HSTS, nosniff, X-Frame-Options DENY, Referrer-Policy, Permissions-Policy with every device denied, and a CSP.
force-https
HSTS with a two-year max-age, includeSubDomains and preload.
scan-dependencies
npm audit at high severity runs inside npm run verify, so a CVE blocks the same command that ships.

Design

The rules that keep it one object rather than a pile of pages.

privacy-policy
Written from this codebase rather than a template. Every claim is true of the repository as it stands.
semantic-color
Every token is named for its role — ground, ink, accent, stamp, live, partial, draft, dormant. The auditor fails on a token named for a hue.
uncluttered-ui
Five navigation destinations against a cap of six. The console is deliberately not among them.
one-easing
A single curve site-wide. Mixing curves is what makes a site feel assembled rather than designed.
continuous-motion
Drifting paper grain and a blinking index caret, both running while the page is idle.
editorial-type
Bodoni Moda for display at up to 140px with hand-tuned tracking, Public Sans for body, IBM Plex Mono for data. None on the ban list.
reduced-motion
prefers-reduced-motion holds the whole site still, including smooth scrolling, and restores fill-mode animations to their resting state instead of freezing them invisible.
focus-visible
A visible ring on every interactive element, never removed.
skip-link
The first tabbable element on every page, targeting main.

What is not fully met

Five of them. Listing these is the only thing that makes the section above worth reading — anyone can publish a page of ticks.

csp-unsafe-inlineThe CSP allows inline script
Next's App Router inlines a bootstrap script, so script-src carries 'unsafe-inline'. That defeats a meaningful share of what a CSP is for. Removing it means threading a nonce through middleware, which is real work and is not done yet. It is listed here rather than quietly tolerated.
csp-report-onlyThe CSP is report-only until it has been watched
The policy ships in report-only mode and reports to an endpoint on this site. It gets widened from what actually lands there and then flipped to enforcing. Widening a policy from guesswork is how a CSP becomes a policy that permits everything.
contact-limiterThe contact form's rate limit is weaker than the console's
The lockout ladder lives in Postgres, which is on one machine with no public network path — so the deployed contact endpoint cannot reach it and falls back to per-instance memory. That is a speed bump rather than a wall. It is acceptable for a form that sends an email and would not be acceptable for the gate, which is why the gate never uses the fallback.
no-uploadsrestrict-file-upload passes because there is nothing to restrict
There is no upload endpoint. Screenshots are committed to the repository as files. The rule is satisfied in the strongest possible sense — you cannot misuse a path that does not exist — but it is worth saying plainly rather than claiming credit for a control that was never built.
analyticsAnalytics are not switched on
The analytics component renders nothing until a measurement ID is configured. This site deliberately does not reuse the ID from another project: two sites reporting into one property produce a single blended stream that cannot be separated afterwards.

What no auditor can check

  • Whether the writing is any good.
  • Whether the one strong idea is actually strong.
  • Whether a case study’s numbers are true.
  • Whether the design is beautiful.

Those stay human judgements. Being mechanical about the mechanical part is what buys the attention to spend on these.