DEV Community

Cover image for The one-line bug that broke Open Graph cards on 836 pages
Liam
Liam

Posted on

The one-line bug that broke Open Graph cards on 836 pages

For about a month, every social preview card on a site I build was silently cut off mid-word. Not truncated with an ellipsis — cut inside a word. PDF & image tools, con. The con is converters, sliced in half.

Nobody reported it. Preview cards are the one thing you never see on your own site — they only show up when someone else shares your link on Slack, Discord, or X. So it just sat there, on every page, in every language.

Here's the line that did it:

// OG card subtitle
<text ...>${esc(String(sub).slice(0, 60))}</text>
Enter fullscreen mode Exit fullscreen mode

slice(0, 60). Take the first 60 characters. Simple, obvious, and wrong — because "60 characters" has no idea where words end. If character 60 lands in the middle of converters, you ship conv.

Why it survived so long

Two reasons, and both are worth internalizing:

  1. The output is invisible to the author. OG cards render off-site. My build succeeded, the page looked fine, the card image generated without error. Everything green. The bug only exists in the one context I never look at.

  2. It was in a shared template. One ogSvg() function renders the card for every tool page. So the bug wasn't on one page — it was on 164 tools × the locales each ships. 836 cards, all the same slice, all cut mid-word.

That second point is the real lesson. A bug in a leaf component hurts one page. A bug in the thing that generates every page hurts all of them at once, and it does it quietly.

The naive fix, and why it isn't enough

The obvious fix is "cut at the last space instead of at character 60":

const cut = s.slice(0, n);
return cut.slice(0, cut.lastIndexOf(" ")) + "";
Enter fullscreen mode Exit fullscreen mode

This works great for English. It falls apart the moment you have more than one language.

Japanese, Chinese, and Korean don't put spaces between words. 無料オンラインツール is one run of characters with no space to find. lastIndexOf(" ") returns -1, slice(0, -1) chops off the last character, and now your CJK subtitle is missing a character and has a floating ellipsis. You traded a visible English bug for a subtle CJK one.

What actually shipped

function clip(s, n) {
  s = String(s == null ? "" : s).trim();
  if (s.length <= n) return s;
  const cut = s.slice(0, n);
  const sp = cut.lastIndexOf(" ");
  // Latin scripts break on the space. CJK has no spaces, so if there's no
  // space — or it's too early to be a real word boundary — just hard-cut.
  return (sp > n * 0.5 ? cut.slice(0, sp) : cut).replace(/[,·、,\s]+$/, "") + "";
}
Enter fullscreen mode Exit fullscreen mode

The sp > n * 0.5 guard is the whole trick. If the last space sits in a reasonable spot, break there (Latin). If there's no space, or it's suspiciously early — meaning it's probably not a real word boundary — hard-cut the character run (CJK). The trailing replace strips a dangling comma or CJK punctuation so you never get image tools,….

It's not linguistically perfect. It doesn't need to be. It needs to never cut converters into conv and never eat a Japanese character. Those two failures are what people actually see.

The takeaway

If you generate anything from a shared template — OG cards, emails, PDFs, filenames — the failure mode isn't "one broken thing." It's "one broken thing, replicated everywhere, in the one place you never look." Two habits catch it:

  • Look at the artifact, not the build log. The build passing tells you nothing about whether the card reads correctly.
  • slice() is not a text-truncation function. It's a character-count function. The moment human-readable text meets a non-space-delimited language, they stop being the same thing.

I hit this building Toolio, a set of small browser tools that ship in 16 languages — which is exactly why the CJK edge case wasn't optional.

Top comments (0)