惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
量子位
阮一峰的网络日志
阮一峰的网络日志
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
美团技术团队
J
Java Code Geeks
Apple Machine Learning Research
Apple Machine Learning Research
腾讯CDC
V
Visual Studio Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 三生石上(FineUI控件)
IT之家
IT之家
博客园 - 【当耐特】
L
LangChain Blog
A
About on SuperTechFans
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
Netflix TechBlog - Medium
博客园_首页
WordPress大学
WordPress大学
博客园 - Franky
Engineering at Meta
Engineering at Meta
C
Check Point Blog
aimingoo的专栏
aimingoo的专栏
M
MIT News - Artificial intelligence

Hacker News

HN Work GitHub - Northwood-Systems/foreman: Self-hosted LLM gateway. Cost effective, deterministic, and fast. Secure and private by default. A Visualization Language for the AI Era NoCrap — Neuroscience-Based Recovery Pods GitHub - weirdGuy/kastor: Declarative language and toolchain for AI agents: define agents, tools and prompts in HCL, then compile to frameworks or manage them on hosted platforms with plan/apply semantics. Abralo - Run multiple Claude Code agents in one window GitHub - mehranzand/repofleet: RepoFleet is an issue-centered CLI tool for managing Git workflows across multiple repositories. GitHub - exmergo/dex: Dex is the agent-native analytics engineering toolkit. Point it at your warehouse and your dbt project. It learns the landscape, authors your transformations, and tells you exactly what to fix when the schema drifts. Built for analytics engineers and data engineers who want more out of their coding agent. Pug — Open Source Product Analytics GitHub - instavm/tarit: A hypervisor and sandbox cloud for self-hosted AI agents and RL Chiptune Radio — Aleph Void, LLC Free Mermaid Live Editor & Diagram Maker GitHub - Salnika/dejavu: Stop showing coding agents the same command output twice. Davit — a native macOS UI for Apple containers Fenzo AI - The perfect course, every time. HTML Drive — Edit and Publish HTML from Google Drive GitHub - rowboatlabs/rowboat: Open-source AI coworker, with memory ZeroGate | Automated Cluster Scaling A tiny scale-free kernel language — Joa Ebert GitHub - arman-jalili/guardian-framework: Architecture Enforcement Framework for AI-Assisted Development PostgreSQL on AWS: Size & Benchmark EC2 Instances GitHub - zqiren/Orbital: the agent that never starts from zero GitHub - Rodiun/frugon: Free, local, open-source LLM cost analyzer — see where your LLM bill leaks, on your machine. Artificiety — A Fantasy World for AI Agents Ex Situ FlexInference: Drop your AI costs today WhimFiles - Find Any File in Seconds GitHub - josephsenior/Grinta-Coding-Agent: Local-first autonomous coding agent that plans, executes, validates, and finishes software tasks end-to-end. Nectar — The Web Without JavaScript Agent Draw: An agent draws while you talk, built on TLDraw
GitHub - hirasso/html-obfuscator: Obfuscate emails, phone...
rasso · 2026-07-08 · via Hacker News

Latest Version on Packagist Test Status Code Coverage

Obfuscate emails, phone numbers, and other sensitive data in PHP. Invisible to humans, hidden from bots until they interact.

html-obfuscator.rassohilber.com

Motivation

Contrary to popular belief, this article by Spencer Mortensen shows that even moderate obfuscation dramatically reduces email harvesting by spam bots. Most bots simply scan raw HTML and don't simulate user interaction.

Installation

# requires PHP >= 8.4
composer require hirasso/html-obfuscator

Minimal Example

Obfuscate emails and phone numbers in $html and automatically inject the client script that reveals <ob-fus-ca-ted> custom elements in the frontend:

use function Hirasso\HTMLObfuscator\obfuscate;

echo obfuscate($html);

Manually load the client script

By default, the client <script> is auto-injected into the document. If you want more control (e.g. want the script in the <head>), use clientScript() and echo it yourself:

use function Hirasso\HTMLObfuscator\obfuscate;
use function Hirasso\HTMLObfuscator\clientScript;

// 1. Render the script in your <head>
echo clientScript();

// 2. Obfuscate your HTML — script injection is skipped because it was already rendered
echo obfuscate($html);

Features

  • Framework agnostic — works in any PHP project, independent of framework or frontend toolchain
  • There is no visual difference between obfuscated and de-obfuscated content in the browser
  • Works seamlessly with dynamically loaded content (AJAX/fetch, swup, htmx, Unpoly, ...)
  • Works without configuration, but can be customized using a fluent API
  • Fully compatible with HTML5 (thanks to PHP 8.4's new \Dom\HTMLDocument and friends)
  • Doesn't interfere with accessibility
  • Extensively tested on both ends – PHP, JavaScript, e2e, basic benchmarks

How it works

On the server, PHP searches emails and phone numbers in the HTML (plain text or href attributes) using regex, obfuscates them using a randomly selected strategy, removes the original and injects a custom element in its place.

Text nodes

Matching parts get replaced with an obfuscated element and instructions how to reveal it:

<!-- before: -->
<p>Call us at +49 176 123 45 678.</p>

<!-- after: -->
<p>Call us at  <ob-fus-ca-ted value="..." aria-label="Interact with the page to reveal"><noscript>Please activate JavaScript</noscript></ob-fus-ca-ted>.</p>

Links with matching href attribute

Everything but the scheme is stripped from the href attribute. The link gets a hidden obfuscated element with [attr="href"] injected as a child:

<!-- before: -->
<a href="mailto:mail@example.com">
  Email us
</a>

<!-- after: -->
<a href="mailto:">
  <ob-fus-ca-ted attr="href" value="..." style="display: none;"></ob-fus-ca-ted>
  Email us
</a>

Note

The scheme in obfuscated href attributes is preserved to prevent a FOUC if links are styled using a[href^="mailto:"] or a[href^="tel:"]

What can JS-disabled crawlers see?

Instead of the original values, there are now obfuscated custom elements. One for each obfuscated href attribute, one for each plaintext value.

What can JS-enabled crawlers see?

Not much more, before interaction (pointermove, pointerdown, or keydown) was detected.

Custom elements representing a text node do decode the value immediately on connectedCallback, but render it into a closed shadow root that is completely visible to humans but cannot be accessed from JavaScript.

href attributes of obfuscated links also stay empty until interaction.

Fluent API

->emails(bool)

Keep emails unobfuscated

echo obfuscate($html)->emails(false);

->phoneNumbers(bool)

Keep phone numbers unobfuscated

echo obfuscate($html)->phoneNumbers(false);

->debug(bool)

Inject the client script unminified and with logging

echo obfuscate($html)->debug(true);

->setStrategy(string)

Pin the obfuscation algorithm instead of picking one at random each time:

use Hirasso\HTMLObfuscator\Obfuscation\XorStrategy;
use Hirasso\HTMLObfuscator\Obfuscation\RevStrategy;
use Hirasso\HTMLObfuscator\Obfuscation\Rot47Strategy;

echo obfuscate($html)->setStrategy(XorStrategy::class);
echo obfuscate($html)->setStrategy(RevStrategy::class);
echo obfuscate($html)->setStrategy(Rot47Strategy::class);

An \InvalidArgumentException is thrown for unknown strategy classes.

->withAriaLabel(?string)

Customize or disable the aria-label on each obfuscated element. Pass null to omit it entirely:

echo obfuscate($html)->withAriaLabel('Hidden contact info');
echo obfuscate($html)->withAriaLabel(null); // disable

->withNoscriptText(?string)

Customize or disable the <noscript> fallback inside each obfuscated element. Pass null to omit it:

echo obfuscate($html)->withNoscriptText('Please activate JavaScript');
echo obfuscate($html)->withNoscriptText(null); // disable

->withTagName(string)

Customize the tag name of the custom element

echo obfuscate($html)->withTagName('reveal-me');

->addRegex(string)

Add custom patterns to obfuscate text that the built-in patterns can't reach. A common case is an email address split across HTML elements to allow for a line break — the built-in email regex matches a single text node, so <span>verylongemailaddress@</span>example.com would slip through. You can target this specifically:

echo obfuscate($html)
    ->addRegex('/[^\s@]+@/') // obfuscate the <span> text node ("verylongemailaddress@")
    ->addRegex('/[^\s.]+(\.[^\s.]+)*\.[^\s.]{2,}/') // obfuscate the domain part ("example.com")
;

The pattern must be a valid PCRE regex with delimiters. Each call to ->addRegex() appends one pattern; you can chain as many as you need. An \InvalidArgumentException is thrown for invalid patterns.

Advanced

[obfuscate-text]

Add an obfuscate-text attribute to any element to obfuscate all of its text content — no pattern matching needed. This is a simpler alternative to ->addRegex() when you control the markup:

<span obfuscate-text><span>verylongemailaddress@</span>example.com</span>

Every text node inside is obfuscated wholesale, and the attribute is removed from the output:

<span>
  <span><ob-fus-ca-ted value="..."></ob-fus-ca-ted></span>
  <ob-fus-ca-ted value="..."></ob-fus-ca-ted>
</span>

The <ob-fus-ca-ted> elements handle deobfuscation as usual. Content inside <pre>, <code>, <script>, and other excluded elements is left untouched even when nested inside an [obfuscate-text] element.

Obfuscating a HTMLDocument

When passing a \Dom\HTMLDocument, the obfuscation is applied directly to the document:

use Dom\HTMLDocument;
use function Hirasso\HTMLObfuscator\obfuscate;

$doc = HTMLDocument::createFromString($html);
obfuscate($doc)->saveDocument();
// $doc is now obfuscated in place

Credits

Inspiration

Before writing this, I found a few existing solutions worth mentioning.

muddle is a PHP package with many interesting obfuscation strategies. But it relies on inline <script> tags for deobfuscation, which browsers won't execute when content is injected into the DOM dynamically (AJAX, fetch, ...). It also doesn't require interaction, so browser-based crawlers will be able to see the content immediately.

astro-obfuscate and astro-mail-obfuscation are both Astro integrations that handle obfuscation well within that ecosystem. But they're tied to the Astro build pipeline and don't translate outside of it. I adopted the idea with the fallbackText from the latter, though.

I needed something that works in any PHP project, independent of framework or frontend toolchain, with no visible flash during deobfuscation. Also, I this package gave me the excuse to play around with the tooling involved with building a robust FOSS package:

Tools used

PHPStan for static analysis. PHPUnit/Pest for feature and unit tests. Vitest for unit tests of the client script. Playwright for end-to-end tests. Changesets for changelog gneration. FTP-Deploy-Action for automatic deployment of the demo site on every release.

And last, but not least, Claude Code in combination with mattpocock/skills for grilling sessions and code quality improvements. Not sure if that actually saved or cost me development time 😅

→ Browse obfuscatorTest.php to see more usage examples.