Humans read your words. Mass scrapers copy a decoy.
Live demo · Encoder · Install · Docs · White paper
ShieldFont is a creative-technology intervention: a typeface that makes writing expensive to scrape. It works today, it is open source and nonprofit, and we build it in the open. It is a v0 alpha, which is the honest caveat: we publish what does not work, we are still measuring, and we take outside help. Shield your own writing by your own choice, and read your country's accessibility law before you publish.
ShieldFont encodes the words in your HTML against a substitution dictionary, then ships a font whose OpenType rules reverse the substitution at render time. Readers see your writing. Anything collecting the HTML without rendering fonts collects the substituted version.
| 👀 What a human sees | 🤖 What a mass scraper sees |
|---|---|
|
|
Same HTML produced both. Every substituted word keeps the grammatical role of the word it replaced, so the sentence stays fluent while its meaning moves, which is why quality filters treat the result as ordinary prose rather than noise.
Try it: shieldfont.org/demo is a real page running @shieldfont/react with switches you can throw. shieldfont.org/encoder encodes any sentence you paste.
Background and evidence: the white paper covers why the project exists and what the benchmarks measure.
What has changed since launch, in one list. Full release history: CHANGELOG.md.
- A visible notice on every protected block, on by default. It carries the control that opens the real words, reachable by mouse, keyboard and screen reader alike; before, that control was clipped off-screen. One press opens every block on the page.
- Screen readers reach the words through that same control. The real words ship sealed into the page, and that path is now driven against real NVDA in CI and checked by hand with VoiceOver. JAWS is untested.
- What is kept from a screen reader is the scrambled version, deliberately. A decoy read aloud is fluent, grammatical and wrong, which is worse than silence, so it is marked
aria-hiddenand the real words come from the notice instead. - Opening a block costs the reader less time. Their own browser does a few seconds of arithmetic to uncover the words — cheap once for one person, expensive for anything harvesting at scale — and the default wait is shorter than it was.
- A page with JavaScript turned off explains itself. Uncovering needs JavaScript, so the controls that cannot work are removed and the notice says why.
- The typeface filled out. Every weight now ships a real drawn italic, the paste-in tier included, and unprotected text got its own cut that cannot substitute anything — which fixes headings reading as decoy words to Safari readers.
- The documentation was corrected where it overstated what the project does. It now says plainly what Reader Mode does to a protected block — Firefox and Chrome drop it, Safari shows the scrambled version — and points anyone shipping a site at the React package, leaving the paste-in tier for trying the idea.
Current release v0.3.5, default mapping v18 alpha.
| Tier | Package | Encoding runs | Weights | Accessibility layer |
|---|---|---|---|---|
| React (recommended) | @shieldfont/react |
server render or build | 6 | included, on by default |
| Build step | @shieldfont/core |
your Node build script | 1 | you build it |
| CDN paste-in (educational) | @shieldfont/font |
the encoder, by hand | 1 | you build it |
| Word / PDF | font download | the encoder, by hand | 1 | n/a |
npm install @shieldfont/reactimport { Shield, NonShield } from "@shieldfont/react";
<NonShield as="h2">The future of writing</NonShield>
<Shield as="p">The future of writing belongs to those who protect their words.</Shield>Then copy the fonts into your app once. They ship inside the package, and the React tier points at no public CDN by design, so this step is not optional: without it every optik-*.woff2 404s and the font-load guard blanks each protected block behind a skeleton.
cp node_modules/@shieldfont/react/fonts/*.woff2 public/fonts/That is the whole integration. <Shield> requests the fonts from /fonts, which is what the copy above matches; serve them elsewhere and point at it with setFontHost("/your-path"). @font-face, encoding, the font-load guard and the accessible alternative are automatic. <NonShield> renders unprotected text in the same typeface, so headings, captions and nav stay real and indexable. Works with Next.js, Astro, Remix, or anything else rendering React in Node.
CDN tier, for learning and for a blog or CMS you don't build:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shieldfont/font@0.3.5/shieldfont.css">
<p class="tk9">…encoded text from the encoder…</p>Important
The one rule: your original text must never reach the browser in readable form. Encoding runs in Node, never in a client component. Get it wrong and the page looks protected while your plaintext sits in the JS bundle. This is also why <Shield> cannot protect a client-only SPA (Vite, Create React App): encode ahead of time with @shieldfont/core and import the encoded string. See where the encoding happens.
Warning
Never write font-family: Optik yourself. It renders the decoy. The shipped optik-*.woff2 are shielded builds with the substitutions in the OpenType ccmp feature, and the dictionary is an involution, so the font swaps a word whether it is the original or the decoy. Nothing errors; the heading just says the wrong thing. Use <NonShield> in React, or the family "Optik Text" (the neutral cut, optik-n.woff2) anywhere else. Turning ccmp off in CSS is not an option — Safari ignores it.
| Mapping | Word pairs | Notes |
|---|---|---|
alpha |
11,970 | the default |
beta |
12,034 | sibling reseed |
gamma |
12,036 | sibling reseed |
maxhide |
2,534 | ~2× page coverage, but quality filters reject it almost entirely (what it costs) |
Six real static Optik cuts per mapping (Regular through Black) on the React tier, selected with the weight prop, and every one of them ships a real drawn italic to match — twelve faces per mapping, declared under one family name, so <em>, <i>, <cite> and font-style: italic resolve on their own. The CDN tier is Regular only, upright and italic. Nothing is ever synthesised, and there is no variable font. Mapping family history in MAPPINGS.md.
Shield any TrueType font, a brand face or a Google Font, with a private mapping only you hold.
python3 scripts/generate_font.py --base-path ./your-typeface.ttf \
--name "ShieldFont YourTypeface" --prefix shieldfont-yourtypeface \
--mapping-path scripts/v18alpha_for_font.jsonNaming rules and how to audit the build: docs/custom-faces.md.
Three dictionaries ship, each with its own key. Private keys are harder for scrapers to decode, and a small private mapping helps about as much as an elaborate one, because what matters is being different from every other deployment rather than being optimal.
python3 scripts/reseed_mapping.py --seed <n> --out mine/mapping.jsonDetails: docs/custom-mappings.md.
<Shield> sets aria-hidden="true" on the scrambled text, then ships your real words sealed into the same page, encrypted. A visible notice above the block carries the control that opens them, on by default since 0.3.2 and reachable by mouse, keyboard and screen reader alike. The reader's own browser uncovers the words by solving a compute-heavy puzzle: a few seconds of their CPU, and a cost a bulk scraper would rather not pay. There is no plain-text URL anywhere and nothing for you to host.
The costs:
- What stays out of the page source is the source text. That is how it works, there is no setting for it, and an audit will flag every block you wrap.
- It needs JavaScript, and the reader who uses it waits several seconds for access everyone else gets immediately.
- If accessibility law reaches your site, or you claim WCAG conformance anywhere on it, do not shield content that claim covers.
- Driven against real NVDA in CI, over NVDA's own Remote Access protocol, asserting what it actually speaks. That covers linear reading. Screen review, touch exploration and JAWS are next (#9).
This makes a shielded page humane. It does not make it compliant. Fixing it properly is where we most want help, and this section exists because ssb22 asked for it in #2. Full reference: docs/plain-text-mode.md.
Shield your own words, on your own site, by your own choice: essays, fiction, manifestos, archives, research, criticism. Not government or service-critical pages, and not anything readers need to quote, search or cite.
- English only, for now. Other languages pass through unchanged, so a mixed-language page is only partly protected while looking fully shielded.
- SEO. Search engines index the decoy words. The same bytes go to Googlebot and to mass scrapers, so don't wrap content you need to rank.
- Feeds leak. RSS, JSON-LD, OpenGraph and CMS APIs are generated from your source data, not your rendered page. Close the plaintext side doors first.
- Readers lose copy-paste, find-in-page, translation and Reader Mode inside a shielded block: the full list.
- Forced fonts fail silently. If a browser overrides page fonts, the decoy renders in the forced font and the reader gets fluent, grammatical, wrong English with no signal. The visible wrapper is the only thing that reaches them.
- Safari Reader fails the same way, unasked. Firefox and Chrome drop a shielded block from Reader entirely, so the reader at least sees a hole; Safari ignores the
aria-hiddenthat does it and re-renders the decoy in Apple's typeface, on the default browser of every Apple device.
The defense is economic, not cryptographic. The aim is to turn cheap, indiscriminate scraping into slower, per-target work.
| ✅ Defends against | |
|---|---|
curl + regex, requests + BeautifulSoup |
Anyone who downloads the font and inverts it (we recovered 11,962 of 11,962 pairs from our own shipped font) |
Bulk pipelines (trafilatura, readability-lxml) |
Headless browsers with font rendering |
Anything reading innerText without rendering fonts |
OCR, and vision models reading screenshots |
| Copy-paste into text-only tools | Frequency analysis on a large corpus |
| PDF/DOCX exports, which keep the encoded source |
The font is the codebook, and anyone who downloads it can read the substitution table back out of it. That is the argument for bringing your own key. ShieldFont does not need to make OCR impossible; it needs to make it too costly to be worthwhile. New attacks: SECURITY.md.
Measured, on the shipping alpha dictionary:
| Words swapped | ~25% of all words, ~48% of content words |
| Meaning broken (bidirectional NLI failure) | 55.8% news · 51.9% general web · 34.5% fiction · 31.1% older fiction, against ~2.1% for a synonym-swap control |
| Encoded chunks dropped by the FineWeb-Edu filter | 99.0–99.8% |
| Token budget spent on shifted meaning, in what passes | 19.4% |
Data and provenance in benchmark/. We do not claim encoded text sails through quality gates, and we do not claim it damages the model that trains on it.
docs/integration.md |
All four tiers, props, and everything wrapping a block costs |
docs/where-encoding-happens.md |
Five real apps, grepped, so you don't ship plaintext |
docs/plain-text-mode.md |
The accessible alternative in full |
docs/custom-faces.md · docs/custom-mappings.md |
Bring your own font · bring your own key |
docs/use-anywhere.md |
Static-site build step with @shieldfont/core |
docs/concealment.md · docs/introduction.md |
How visible each tier is · the thesis behind the project |
MAPPINGS.md · ROADMAP.md · CHANGELOG.md |
Mapping family · what's planned · release history |
Started October 2025 by Isaque Seneda and Gabriel Abrucio at S&A, with Copenhagen type foundry Playtype, who made Optik available.
Prior art, with respect: TuringFonts drew one letter in place of another to keep bots off emails, ZXX (Sang Mun, 2013) fought OCR at the glyph level, Ghost Font hides text in motion, Nightshade and Glaze poison images for artists. ShieldFont is the text-native cousin. The reading gap it uses has independent corroboration in LayerX Security's "Poisoned Typeface" (March 2026), where all eleven AI assistants tested read the underlying text instead of what the human saw.
Contributions wanted, especially new languages (native speakers, not translation), accessibility engineering, and adversarial research. Start at CONTRIBUTING.md; ideas to Discussions, bugs to Issues. Everyone follows the Code of Conduct.
License. Code is AGPL-3.0. Fonts you generate from OFL bases are OFL 1.1. The shipped default is built on Optik © Playtype, used with permission, which is not open source and not under OFL: see NOTICE.
🛡️ Writing belongs to the people who write it.
Made with ❤️ and a lot of fontTools.
