Zainuldin's profile picture

Zain Ul Din

Obfuscate Your Links

Recently, I added my email address to my website. A few days later, I noticed an increase in promotional emails. I can't be sure that publishing my address caused the increase, but scrapers often collect contact information from websites to find leads. Basic scrapers may use regular expressions to extract email addresses, phone numbers, and other contact details.

To make this harder for basic scrapers, I wrote a simple function that obfuscates text using XOR and hexadecimal encoding. This can help hide recognizable contact details from simple text-matching tools, but it does not guarantee less spam. More capable scrapers, including tools that execute JavaScript or use LLMs to analyze code, can still recover the original values.

To keep the original values out of the JavaScript bundle and initial HTML, encode them ahead of time and ship only the encoded strings OR send encoded string from server side if using framework like NextJS and the decoding function to the browser. Calling encode() on a plain-text address in client-side code still exposes that address in the bundle. If you decode a value into the page's text or a link's href, scrapers that inspect the rendered page can read it.

const KEY = 0x5a;
export function encode(value: string) {
const bytes = new TextEncoder().encode(value);
return Array.from(bytes, (b) => (b ^ KEY).toString(16).padStart(2, "0")).join(
"",
);
}
export function decode(hex: string): string {
const bytes = Uint8Array.from(
hex.match(/.{2}/g)!,
(b) => parseInt(b, 16) ^ KEY,
);
return new TextDecoder().decode(bytes);
}

The decode() function assumes a nonempty, valid hexadecimal string produced by encode(). If you need to decode empty strings or accept input from other sources, add empty-string handling and validate that the input contains an even number of hexadecimal digits.

Note: This is obfuscation, not secure encryption. Never use this technique to send secrets to the client: the browser receives both the encoded value and the key needed to decode it. Treat any information sent to the browser as public.

Usage in React:

"use client";
import { decode } from "@/lib/obfuscate";
import { cn } from "@/lib/utils";
import { ComponentProps, useEffect, useRef } from "react";
type LazyLinksProps = ComponentProps<"a"> & {
href: string;
};
export function LazyLink({
children,
className,
href,
...rest
}: LazyLinksProps) {
const ref = useRef<HTMLAnchorElement>(null);
useEffect(() => {
if (ref.current) ref.current.href = decode(href);
}, [href]);
return (
<a {...rest} ref={ref} className={cn(className)}>
{children}
</a>
);
}