Linting our marketing: 26 phrases fail the build
On 2026-06-27 we ran an adversarial audit over every page of twindevs.ai: 13 pages, 143 findings, 33 of them high-severity. Sentence by sentence the copy read fine. The structure gave it away. The same five slogans were stamped onto all five product pages, the hero carried a template tagline, and two adjacent blog cards ended on the identical teaser closer. Each finding recorded a verbatim quote, the named tell, and a rewrite direction. Then we did to the copy what CI already does to the code — put a linter in the build and let it fail the whole thing.
Four specimens from the ban list
Quoting banned phrases inside a post that gets linted takes some care; the mechanics are disclosed at the end. The exhibits, verbatim from the old site:
- "Inbox at the speed of intent." The old hero line for TwinMail (Technical Preview), and the stock "speed of X" template. The audit's verdict: it means nothing and could front any email product unchanged. The audit quotes it seven times, including every cross-sell card.
- "Privacy is the product, not the tax." A balanced antithesis stamped as a repeated heading; the audit calls that heading shape the single loudest AI tell. The uncomfortable part is that the contrast is genuinely a tic in Dylan's own writing, where it lives inside sentences, at most once per page. Templating it burned it. The style guide reserves exactly one permitted home for his original comma form, as body prose on the About page; the ban list is stricter than the guide, and the shipped page does not carry the line.
- "Connect, organize, operate." A generic imperative-verb triplet, mail-merged as the how-it-works heading onto all five product pages. The replacements name each product's real steps: Import · Dedupe · Merge · Maintain · Export for contacts, clone → mesh → panel for TwinSystem.
- "seamless", "effortless", "delve". The old site never used these; the audit shows zero hits. They are banned anyway, because they were verified at zero occurrences in the founder's writing corpus. Any future appearance flags machine-written copy before a reader ever sees it.
The guide came first
The ban list is the smaller artifact. docs/voice.md is a 318-line style guide distilled from Dylan's verified writing corpus, with 14 hard rules and block-by-block rewrite directives for every page. Most of the rules are positive, and a substring check can't enforce them. "Headline the mechanism, never the virtue." Prove a quality claim with one micro-example; the guide's model case is a shared family phone with different first names that never auto-merges. Software quality takes physical vocabulary: things hold up, are sturdy or brittle, have drag. Every guarantee ships with its reason attached.
One rule keeps the system honest with itself: never invent vendors, benchmarks, dates, or policies to satisfy a specificity rule. A claim missing its fact gets written as a gap and marked Known follow-up: for whoever owns the answer. An anti-slop pipeline that padded its own copy would be the worst slop on the site.
The enforcement layer
check-voice.mjs carries 26 lowercase substrings, each chosen so no legitimate use survives a match. The build wires it in as a second stage: node build.mjs && node check-voice.mjs. The linter walks every .html file in dist/, strips scripts and tags, lowercases what remains, and runs plain substring checks. A hit prints SLOP with the file, the pattern, and 40 characters of context on each side, then the process exits 1 and the build fails. It reads the rendered output, not the source, so a phrase that sneaks in through a shared card template gets caught at the same gate as one typed by hand.
Two carve-outs. The legal pages under /messaging, /privacy, and /terms are exempt because their content is compliance-locked and byte-preserved. And em-dash overload draws a warning above nine per page instead of a failure; the editorial cap in voice.md is two, but punctuation texture is a judgment call a build script shouldn't make.
It caught its own blind spot in two minutes
The voice pass landed as commit 9197345 at 21:21: 28 high-severity tells removed, the guide and the linter added in the same change. At 21:23, commit 19f9cdf followed. The retired closer "Your data stays yours" was still alive in the About page's meta description, because the linter only read <main>. Titles and meta descriptions are copy too, and they were the blind spot. The fix killed the residual slogan and taught the linter to scan the <title> tag and every <meta> content attribute. Two minutes after shipping, the linter got stricter.
This page went through the same gate
This post sits in the same posts file as every other entry and renders into dist/ through the same generator, so the linter read it before you did. That forced the quoting mechanics above: each banned specimen carries an invisible zero-width space at one joint. The phrase reads verbatim on your screen; the raw HTML never contains the banned substring. That is the whole trick, disclosed here because a slop linter you can quietly route around is a slop linter you will eventually route around.
If any of those lines comes back as live copy, the build prints SLOP and fails. This page is published, which means the run that built it printed check-voice: clean.