Skip to content

fix(react-email): avoid splitting surrogate pairs when truncating Preview text - #3761

Closed
RaphaelFakhri wants to merge 2 commits into
resend:canaryfrom
RaphaelFakhri:fix/preview-truncate-surrogate
Closed

RaphaelFakhri wants to merge 2 commits into
resend:canaryfrom
RaphaelFakhri:fix/preview-truncate-surrogate

Conversation

@RaphaelFakhri

@RaphaelFakhri RaphaelFakhri commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

<Preview> truncates its text with substring(0, 200). When an emoji straddles the limit, the cut leaves a lone high surrogate, which shows up as a replacement character in the <title> and the preview text. The fix drops the dangling high surrogate after truncation.

Changes

  • Add a truncate helper in preview.tsx that removes a trailing high surrogate when the text was cut.
  • Add a regression test to preview.spec.tsx.
  • Add a patch changeset for react-email.

Testing

  • The new test fails without the fix and passes with it.
  • vitest run src/components/preview: 6 passed.
  • biome check is clean for the changed files.

Checklist

  • Tests added
  • Changeset added

Summary by cubic

Fixes react-email's <Preview> truncation so emoji aren't cut in half at the 200-character limit. Truncation now cuts on whole graphemes with Intl.Segmenter, so families, flags, and skin-tone emoji are kept or dropped whole instead of leaving a lone surrogate that renders as a replacement character.

  • Adds a truncate helper in preview.tsx and a regression test covering multiple emoji types at the cut point.
  • Adds a patch changeset.

Written for commit 9b5a540. Summary will update on new commits.

Review in cubic

@changeset-bot

changeset-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9b5a540

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
react-email Patch
@react-email/editor Patch
@react-email/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

@RaphaelFakhri is attempting to deploy a commit to the resend Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the linear-synced PR has been synced to Linear label Sep 29, 2026

@dielduarte dielduarte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, @RaphaelFakhri thanks for the contributions.

Similar requests as your CLI PR

1 - Cut on whole visible characters (graphemes), not just surrogate pairs. The current check handles simple emoji but still splits emoji made of several characters. At the 200-character cut:

  • 👨‍👩‍👧 becomes 👨‍ (a stray invisible joiner is left)
  • 🇺🇸 becomes 🇺 (a lone regional indicator, shown as a boxed "U")
  • 👍🏽 becomes 👍 (the skin tone is silently dropped)

Intl.Segmenter is built in and handles all of these.

  1. Test the multi-part emoji at the cut point. Please add cases for a family (👨‍👩‍👧), a flag (🇺🇸) and a skin-tone emoji (👍🏽) placed so they straddle character 200. Each should be either kept

  2. Check for any unpaired surrogate. not.toContain('\une high surrogate. A regex catches all of them.

  3. Return early when the text fits. Checking text.length <= PREVIEW_MAX_LENGTH first reads more clearly than slicing and checking the length last, and it skips the work for short previews, which are most of them.

Cut on whole visible characters with Intl.Segmenter so families, flags and
skin-tone emoji are kept or dropped whole, return early when the text fits,
and test the cut point with an unpaired-surrogate check.
@RaphaelFakhri
RaphaelFakhri force-pushed the fix/preview-truncate-surrogate branch from 25bfd68 to 9b5a540 Compare October 3, 2026 16:21
@RaphaelFakhri

Copy link
Copy Markdown
Contributor Author

Thanks, done. Preview now returns early when the text fits and otherwise cuts at 200 on graphemes. The tests slide a family, a flag and a skin-tone emoji across character 200 and check for unpaired surrogates.

@vercel

vercel Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
react-email Ready Ready Preview Oct 7, 2026 8:27pm UTC
react-email-demo Ready Ready Preview Oct 7, 2026 8:27pm UTC

Request Review

@pkg-pr-new

pkg-pr-new Bot commented Oct 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/react-email@3761

commit: 9b5a540

@dielduarte

Copy link
Copy Markdown
Contributor

Hey @RaphaelFakhri this is pretty much the same comment as the CLI one: resend/resend-cli#395 (comment)

After digging deeper, we haven't found an approach that solves this fully without trade-offs we're not comfortable shipping yet:

Code points (for...of): no more broken �, but multi-part emoji (families, flags, skin tones) can still be cut to part of themselves.
Intl.Segmenter: handles emoji correctly, but crashes in runtimes with reduced ICU (like our pkg-built CLI binary) and fails to load where it's not available.
Unicode regex: covers official emoji, but it's an approximation that still splits some scripts.
A grapheme library: correct, but adds a dependency we'd want to avoid that for now.

Because of that, I'm going to close this PR for now. This isn't a "no" to the problem: it's real, and we want to fix it properly. If you have ideas or want to keep exploring, we'd love to keep the discussion going on feel free to open a dicussion or even new prs based on this one, and any future PRs are very welcome.

Thanks again for caring about this!

@dielduarte dielduarte closed this Oct 8, 2026

This branch was successfully deployed

2 active deployments
Preview – react-email-demo — 9b5a540a Deployed Oct 7, 2026 by vercel[bot]
Preview – react-email — 9b5a540a Deployed Oct 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

linear-synced PR has been synced to Linear

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants