The 191-Character Bug That Wasn't About 191 Characters
A Ghost bug looked like "the alt-text limit is too short." It wasn't. Here's how reading the real defect — silent data loss, not a 191-character cap — plus one question to the maintainer turned a single-line change into a merged PR, while a bigger "raise the limit" PR was closed.

Syed Suhail Ahmed
Aug 15, 20265 min read

Some bugs are a puzzle. This one was a trap — the obvious fix was the wrong fix, and the right fix was a single line. Here's how a Ghost issue taught me that the hardest part of a bug report is figuring out what the bug actually is.
The report
Ghost — the publishing platform behind a huge slice of the indie web — had issue #29623: type a long alt text on a post's feature image, and it just… vanishes. No error, no warning. You move to another view and your text is gone. (Oddly, an explicit Cmd+S did surface a validation error — only autosave ate it silently.)
Data disappearing with zero feedback is about the worst UX outcome there is. I reproduced it in seconds: paste 200 characters into the alt field, wait for autosave, navigate away, come back — empty.
Following the thread
Why would autosave silently drop it? A few layers stacked up:
Ghost's model validation caps alt text at 191 characters (a relic of MySQL's old 191-char index limit), even though the DB column is actually
VARCHAR(2000).The Ember admin's client validator mirrors that:
isLength({ max: 191 }). Over the limit, it blocks the save request before it's ever sent.Autosave runs with
silent: true, and the code that surfaces validation errors is gated on!options.silent. So on autosave, the failure is swallowed — no request, no error, nothing.
Individually, each piece was reasonable. Together, they lost your data.
The fork in the road
Here's where it got interesting — and where the real lesson lives. There were two ways to read this bug.
Reading #1 (the tempting one): "191 is too short — raise the limit."
It's right there in the title, after all. Another contributor took this path and opened a PR to lift the validation cap toward the 2,000-character column size. Logical on the surface: the field rejects long text, so let long text through.
The maintainer closed it, and their reasoning stuck with me:
"Extending the alt text length would be its own proposal. I consider the bug here the inability to save — a user should never see data disappear unexpectedly."
In other words: the number wasn't the bug. Raising 191 to 2,000 just moves the cliff — you'd still lose data silently at 2,001. And changing a content limit is a product decision, not a bug fix. That PR was solving the wrong problem, cleanly.
Reading #2 (the right one): "The bug is the silent data loss."
If the field simply won't let you type past the limit the server enforces, the unsaveable state can never happen in the first place. Nothing to drop, because you can't create the invalid input.
I'd actually asked the maintainer which direction they wanted before writing any code — the issue listed a few possible fixes, and I didn't want to guess:
"Of the three options listed, do you have a preference… I want to confirm the intended UX before I open a PR."
That one comment is the whole story. When they clarified — impose the 191 limit on the field; extending it is a separate proposal — I built exactly that.
The fix: one line
<input ... maxlength="191">That's it. +1 −0. Add maxlength="191" to the alt input so the browser hard-stops typing at the same limit the server enforces. The unsaveable state is now unreachable.
I did sweat one detail worth calling out: maxlength counts UTF-16 code units, while Ghost's validator counts Unicode code points. So for emoji and astral characters the field is very slightly stricter than the server — which is the safe direction. Anything the field lets you type still passes server validation, so the bug can't sneak back in.
And I was explicit in the PR about what I didn't touch: pre-existing over-length values aren't retro-trimmed, and the general silent: true swallow is left alone — matching the maintainer's own note that it "may need patching as well." Scoping a fix is as much about what you leave out as what you put in.
Merged
PR #29681 — 🐛 Fixed feature image alt text silently disappearing when too long — was approved by the maintainer and auto-merged, closing the issue.
The contrast was never about skill. The "raise the limit" PR was more code and more effort; mine was one line. The difference was reading the bug correctly and confirming scope before building.
What I took away
1. Fix the bug, not the symptom's boundary. "191 is too small" was the visible edge; "data vanishes silently" was the real defect. Raising the number would've left the actual bug intact.
2. Ask before you build — especially when the issue offers options. A single "which direction do you want?" aligned me with the maintainer and saved a rejected PR. Skipping that step is how you rebuild the wrong thing.
3. The smallest change that kills the root cause wins. One line that makes an invalid state unreachable beat a larger PR that just moved the goalposts.
4. Respect scope. Extending a content limit is a product proposal, not a bug fix — bundling it in would've sunk the PR.
5. Write the PR that shows you see the whole board. Explaining why 191, the UTF-16 nuance, and what I deliberately left untouched did more to earn the merge than the diff itself.
6. Match the house style. A gitmoji summary, Fixes #29623, and a reproduction/test note kept the review about the fix, not the formatting.
The best part of open source often isn't writing the most code — it's writing the least, once you actually understand what's broken.
On to the next one.
Subscribe for new contributions
Get an email when I publish a new open-source write-up — how I approached the issue, the code, and lessons learned. No spam.