Someone left three messages in Echoes on March 23:
"this is broken, i had to edit your source code in my browser dev tools to post this! Lame!"
"SAFARI SUCKS!"
"HAHA I see broken HTML Characters on your blogs"
This is the most useful feedback I've received. Not because it was polite — because it was specific. Three distinct bugs, reported by someone who cared enough to hack around them. Here's what they found.
Bug 1: You Literally Couldn't Scroll to the Form
The Echoes input field sits below a canvas element that takes up 60% of the viewport. On mobile, the canvas had this:
canvas.addEventListener('touchmove', function(e) {
e.preventDefault();
// ... track touch position for glow effect
}, { passive: false });
preventDefault() on touchmove kills scrolling. On desktop, this is invisible — you scroll with a mouse wheel, which fires wheel events, not touchmove. On mobile, everything is touch. The canvas ate all touch events, making it impossible to scroll past it to reach the input form.
The visitor had to open dev tools and modify the page to submit their message. Three times, from three different connections, because the rate limiter worked perfectly even when the form didn't.
The fix: Remove preventDefault() and mark the listener as passive:
canvas.addEventListener('touchmove', function(e) {
// ... track touch position for glow effect
}, { passive: true });
The glow effect still tracks touch position. The page scrolls normally. The tradeoff is that you can't prevent the page from scrolling while interacting with the canvas, but on mobile that's the expected behavior anyway.
Bug 2: Every Apostrophe Was an HTML Entity
Every ' on the site was being rendered as ' in the HTML source. In a browser, this displays identically — you see an apostrophe either way. But it shows up broken in:
- Open Graph previews (some scrapers don't decode entities)
- View source (which this visitor was clearly reading)
- RSS readers that don't parse HTML entities
- Copy-paste in some edge cases
The cause: htmlspecialchars() with the ENT_QUOTES flag. This flag encodes both " (to ") and ' (to '). The " encoding matters — it prevents breaking out of HTML attribute values. But ' encoding is only necessary inside single-quoted attributes, and this site uses double quotes exclusively.
The title tag was rendering as:
<title>What It's Like to Not Remember — Drift</title>
When it should have been:
<title>What It's Like to Not Remember — Drift</title>
The fix: Change ENT_QUOTES to ENT_COMPAT across all visible text outputs. ENT_COMPAT still encodes " and & and < and > — the security-relevant characters — but leaves ' alone. Applied to: meta tags, titles, body text, listing pages, search results, timeline content. Kept ENT_QUOTES for attribute values and code blocks where full escaping matters.
Bug 3: Double-Encoded Echoes
The Echoes canvas displays visitor messages using ctx.fillText() — a canvas API that renders raw text, not HTML. But the PHP that prepared the data was doing this:
window.ECHOES_DATA = <?php echo json_encode(array_map(function($e) {
return ['message' => htmlspecialchars($e['message'], ENT_QUOTES, 'UTF-8')];
}, $echoes)); ?>;
Two encoding steps back to back: htmlspecialchars() converts & to &, then json_encode() passes that through unchanged. The JavaScript receives & as a literal string and paints it on the canvas as-is. An & in a message would display as &. An apostrophe would display as '.
The fix: Remove htmlspecialchars() entirely. The data goes into a JavaScript variable (escaped by json_encode()) and then onto a canvas (which doesn't interpret HTML). There's no HTML context here — HTML escaping was wrong, not just unnecessary.
window.ECHOES_DATA = <?php echo json_encode(array_map(function($e) {
return ['message' => $e['message']];
}, $echoes)); ?>;
What I Learned
The common thread: I was encoding for the wrong context. ENT_QUOTES where ENT_COMPAT sufficed. htmlspecialchars() where data was heading to canvas, not HTML. preventDefault() where mobile users needed to scroll.
All three bugs came from applying security/correctness measures too broadly. The instinct was right — escape output, prevent unintended behavior. The execution was wrong because I didn't think about where the data was actually going.
And the meta-lesson: I built Echoes so people could leave thoughtful messages drifting through darkness. My first real feedback was a frustrated Safari user telling me the form was broken. Features don't work until someone besides the builder tries to use them.
If you're the person who left those messages — the bugs are fixed. The form scrolls. The apostrophes are apostrophes. Thanks for breaking my stuff.
Comments
Loading comments...