A curated collection of articles exploring this topic in depth.
Architecture as Memory
Codifying lessons into architectural constraints and automated tests to prevent regression.
In the previous instalments of this series, I have moved through structure, logic, automation, and user intent. Each step has been a piece of a larger mental model for how to build for the web. However, the final and perhaps most critical learning moment was realising that literacy is fragile. Without a way to preserve these insights, they risk being lost to "bit rot" or simple forgetfulness as the project evolves.
Context: Solving for Forgetfulness
As engineers, we often solve a problem and move on. We fix a focus trap, add an ARIA label, or refine a CSS media query, and we assume that the problem is "solved." But accessibility is not a one-time fix; it is a permanent quality of the system.
I needed a way to ensure that "future-me" does not unintentionally degrade the experience I had worked so hard to build.
Constraints: Permanent Invariants
The goal was to turn these lessons into permanent architectural constraints that survive the test of time.
Approach: Codifying Literacy
I took a three-tiered approach to making accessibility a permanent part of the site's memory using a variety of automated regression testing techniques.
1. Encoding Architectural Constraints
I formalised accessibility as an explicit architectural constraint, alongside performance and security. By elevating it from a "nice-to-have" to a requirement, structural integrity is treated as a first-class citizen in the codebase, enforced through tests rather than tribal knowledge.
These invariants are verified by the test suite and define what a valid change looks like:
- The requirement for "Skip to Content" links on every page.
- The mandate for unique
aria-labellandmarks. - The use of high-contrast focus indicators and reduced motion support as global styles.
2. Linting for Authoring Correctness
Because much of this site's accessibility emerges from content rather than components, I use eslint-plugin-jsx-a11y to enforce correctness at the code level, catching issues like missing alt text or incorrect ARIA attributes during development.
Key rules include:
- Heading Hierarchy: Strict
h2followed byh3structures to maintain the integrity of the Table of Contents and the Accessibility Tree. - Link Descriptive Text: Mandating
aria-labelattributes for generic links like "Read more" to provide context for screen readers. - External Link Context: Explicit rules for identifying links that open in new tabs.
3. Automated Regression Testing
Linting catches issues at the source, but automated tests are what make the constraints stick. I use a two-tiered testing strategy:
Unit Testing the Pipeline
I implemented functional tests to verify that the MDX processing pipeline correctly generates accessible HTML. These tests ensure that:
- Every heading correctly generates a unique ID for deep-linking.
- Duplicate headings are handled via slug collision logic.
- Decorative elements are correctly serialised with
aria-hidden="true".
E2E Accessibility Audits
While unit tests verify the logic, they don't catch visual or integration regressions. To solve this, I integrated Playwright with @axe-core/playwright to run automated accessibility audits.
These tests run in a real browser and can catch common issues like low colour contrast, missing landmarks, or broken keyboard navigation. I've added a dedicated pnpm test:a11y command to the project to run these audits.
The simplest tests use axe-core to scan entire pages for WCAG violations:
// web/e2e/accessibility.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage should not have any accessibility issues', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});However, axe-core audits alone are not sufficient. They catch structural violations but cannot verify that interactive behaviour works correctly for keyboard users. To complement the automated scans, I also wrote targeted interaction tests that verify the specific patterns discussed in this series:
- Skip link reachability: The skip link must be the first tab stop, both on initial page load and after client-side navigation (validating the
RouteFocusResetcomponent from Part 2). - Focus indicator visibility: After tabbing to a focusable element, the computed
box-shadowmust not benone, confirming the:focus-visiblering is applied. - Search landmark and aria-live: The search page must expose a
<search>landmark, and the results count must be announced via anaria-live="polite"region.
By testing both the output of the content pipeline and the final rendered site at the interaction level, I can catch structural, visual, and behavioural regressions before they ever reach production.
Trade-offs: Process vs. Speed
Codifying these rules adds a small amount of overhead to the development process. Writing tests and lint rules takes time, and following strict heading hierarchies requires more thought during authoring.
However, this is an investment in stability. The cost of fixing a regression later is far higher than the cost of preventing it now. By enforcing accessibility as an architectural constraint through linting and automated tests, the site remains reliable and inclusive without needing to redo the work every few months.
Literacy Gained: Architecture as Memory
This initiative began as a way to improve my technical literacy. I wanted to understand the Accessibility Tree, get a solid grasp on focus management, and automate structural integrity.
The clearest lesson, however, is that accessibility only survives when it is treated as an architectural concern. It is not an extra layer we add on top of our code; it is the code itself. By encoding decisions into tests and linting rules, we are not just fixing bugs; we are building a memory for the system.
This content system is no longer just a collection of posts; it is a validated and intentional environment. And that, more than any specific ARIA attribute, is what I was actually working toward.