The problem
Most of my best work sits in private company repositories under NDA. I wanted to explain that work in detail, with diagrams and numbers, without leaking a single table name, client name or internal module.
I also wanted to build the site the way I build production software: AI agents do much of the writing, and tests, gates and my own decisions keep the output correct.
The result is a static Astro site in English and Vietnamese with 14 case studies at launch, an isometric canvas simulation in the hero, diagrams drawn as SVG from small JSON specs, and two small interactive widgets. Astro is the only dependency.
What I did
Split every intake into a public note and a private denylist
AI agent sessions read each employer repository read-only. For each area they wrote two files: a public summary at the level of patterns, and a private denylist of every company-specific identifier they saw, from tables and modules to domains, clients and people. The repositories were never modified. The trade-off is extra work per area, but it turns “be careful” into a list a machine can check.
Parallel writer agents under one written brief
Five content-writer agents ran in parallel from one brief that fixed the voice, section structure, frontmatter schema and number rules. Each produced an English page, a Vietnamese page and a diagram spec. Agents could not ask me questions, so every open question went into the file with a proposed default, and I answered them in batches. The agents also corrected my CV where repo history disagreed: one claim turned out to belong to a different repo and was hidden, and a conflicting count was surfaced and resolved. I accepted slower review in exchange for agents that never stalled.
A leak gate that fails closed, with tests first
The build runs the Astro build and then scans the whole output against about 550 private denylist terms. A missing denylist folder or zero loaded terms exits with an error, so the gate refuses to pass by accident. Any match fails the build and the deploy never runs. Matching works on whole tokens: text is split into words, including at camelCase and punctuation boundaries, diacritics are folded, and short runs of adjacent words are compared to each normalised term. Spaced, snake_case and camelCase spellings of a term all match, while a term hidden inside a longer, unrelated word does not.
The first full build reported 60 matches. I triaged every one. Two were real phrases in content, and I reworded them. The rest were false positives, and each got a new rule written as a failing test first: an allowlist for my own public identity, ASCII code identifiers versus ordinary Vietnamese words with diacritics, framework markup such as class lists, and minified script names colliding with three-letter codes. The site has 17 automated tests, 14 for the leak scan and 3 for a small plugin that lays out case-study sections, all seen failing before the code existed.
Local deploys only
Denylists never leave my machine, so a CI build from Git could not run the gate. Every deploy is a local build, scan and direct upload. I gave up push-to-deploy convenience to keep private terms private.
Measure, then fix what the numbers show
On a case-study page on mobile, Lighthouse Performance went from 79 to 87–98 depending on the run, and First Contentful Paint from 3.9 s to about 1.5–2 s, by inlining CSS and loading web fonts without blocking paint. Desktop layout shift went from 0.111 to 0.003 with local fallback fonts sized to match the web fonts. SEO went from 92 to 100 after adding robots.txt, a generated sitemap and a real 404 page.
Result
The site shipped with zero leak-gate matches, Accessibility 100 and Best Practices 100 on every audited page, and an automated check over all 14 diagrams showing no overlapping labels after I added collision avoidance.
Two things went wrong. On its first run, a deploy CLI converted the project to a different hosting mode, adding an adapter, dependencies and config files. Nothing deployed. I reverted everything, checked the lockfile for stray packages, and recreated the project in plain static mode with the CLI pinned to an older release. Separately, an early hero line could be misread at a glance as its opposite, so I replaced it with a positive one.
What I’d do differently
I would write the leak scan and its first tests before any content existed, so problems would surface one page at a time instead of 60 at once. I would also run unknown deploy tools in a throwaway folder first, since a first run can change a project as much as a deploy can.