2026-03-07|3 min read|--flashbee--debugging--build-in-public--devlog

The Emoji Bug That Took Half a Day

I spent half of today chasing a broken image.

Not a crash. Not a 500 error. Just a tiny little broken image icon — that default browser placeholder that looks like a torn photograph — sitting in the corner of every flashcard in FlashBee.


The app was working fine. Kids could study, flip cards, hear the words spoken aloud. But the images? Broken. White cards with the word "red" and a broken icon where a red circle should be. "Happy" with no face. "Tree" with no tree.

It shouldn't have been that hard to fix.

It was.


The original image system used Microsoft Fluent Emoji — beautiful 3D illustrations, mapped by folder name. redRed Heart. happyGrinning Face. Clean, simple, worked great.

Until it didn't.

At some point, the CDN started returning 403 Forbidden. No announcement. No warning. The URL just... stopped working. And because 204 cards in Firestore were storing these Fluent CDN URLs directly, every single one of them broke silently.

The first fix I tried: switch to Noto Emoji. Google's emoji CDN, stable, uses Unicode codepoints instead of folder names.

red → codepoint 1f534https://fonts.gstatic.com/s/e/notoemoji/latest/1f534/128.png

Except Noto doesn't host every emoji in their latest folder. Heart emojis? Broken. Face emojis? Broken. Some color circles? Missing. Half the fixes created new breakages.

So I wrote a Node.js script to scan all 204 cards in Firestore, identify broken URLs, and replace them with Noto CDN links. It ran. It fixed 59 cards. Left 50 as empty strings because they were phrases like "confess to a crime" — no emoji for that. Fair enough.

But the colors were still broken.


Here's the thing I kept missing: the cards weren't empty. They had URLs. Fluent URLs. And my condition check in the React component was if (card.image?.url) — which is truthy for any non-empty string, including a URL that returns 403.

So even after everything, the component was happily loading the broken Fluent URL, getting a 403, and showing the broken image icon. Every time. Perfectly consistently wrong.

The fix, once I saw it, was obvious:

const _isValid = _stored &&
  !_stored.startsWith('data:image/svg') &&
  !_stored.includes('fluentui-emoji') &&
  !_stored.includes('microsoft');
const resolvedImageUrl = _isValid ? _stored : getBestImage(card.word).url;

Reject the Fluent URLs explicitly. Fall back to Twemoji — Twitter's emoji CDN, better coverage than Noto, handles basically everything.

Done. Finally.


The whole thing took maybe five hours. Debugging, wrong fixes, more debugging, reading CDN documentation, writing the Firestore script, realizing the script fixed the database but not the UI, then fixing the UI.

Five hours for a broken image.

I keep thinking about this kind of work — the invisible work. Users don't see it. They just see emoji on flashcards. But somewhere, some developer (me, today) spent an entire afternoon making sure "red" shows a red circle instead of a torn photograph.

This is what building products actually looks like most of the time. Not the exciting launch. Not the clever feature. Just: something broke, figure out why, fix it, try not to break something else.


FlashBee is a flashcard app for Vietnamese kids learning English. The whole premise is that words should have images — because that's how children learn. Show them "apple" with a picture of an apple, not just the word.

So broken images aren't just a visual bug. They're a learning gap.

That's why it was worth the five hours.

Even if I never want to debug a CDN issue again.