From 1ed5664a90fd83dcea88970105ca459e014990a9 Mon Sep 17 00:00:00 2001 From: CharlieHelps Date: Mon, 11 May 2026 03:22:59 +0000 Subject: [PATCH 1/4] test: add issue 368 href query entity regression --- packages/jsx-email/test/render/issues.test.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/jsx-email/test/render/issues.test.tsx b/packages/jsx-email/test/render/issues.test.tsx index c52714fb6..ed42de820 100644 --- a/packages/jsx-email/test/render/issues.test.tsx +++ b/packages/jsx-email/test/render/issues.test.tsx @@ -21,4 +21,12 @@ describe('render', () => { ); expect(await render(fragment, { minify: false })).toMatchSnapshot(); }); + + it('issue 368, href query delimiters', async () => { + const fragment = Test Link; + const result = await render(fragment); + + expect(result).toContain('href="https://example.com?foo=bar&baz=qux"'); + expect(result).not.toContain('href="https://example.com?foo=bar&baz=qux"'); + }); }); From 9ee3b3e7ad59754e04dfda5eae4a77faf3e00862 Mon Sep 17 00:00:00 2001 From: CharlieHelps Date: Mon, 11 May 2026 12:08:04 +0000 Subject: [PATCH 2/4] fix(jsx-email): prefer named entity refs in render output --- packages/jsx-email/src/renderer/render.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/jsx-email/src/renderer/render.ts b/packages/jsx-email/src/renderer/render.ts index 4e7e13cbb..b84ca6936 100644 --- a/packages/jsx-email/src/renderer/render.ts +++ b/packages/jsx-email/src/renderer/render.ts @@ -94,6 +94,9 @@ const processHtml = async (config: JsxEmailConfig, html: string) => { .use(stringify, { allowDangerousCharacters: true, allowDangerousHtml: true, + characterReferences: { + useNamedReferences: true + }, closeEmptyElements: true, collapseEmptyAttributes: true }) From 10a85e1b9a8c58af5ca9f02e60f118da26d20ca2 Mon Sep 17 00:00:00 2001 From: CharlieHelps Date: Mon, 11 May 2026 12:24:36 +0000 Subject: [PATCH 3/4] fix(jsx-email): normalize href ampersands without global entity changes --- packages/jsx-email/src/renderer/render.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/jsx-email/src/renderer/render.ts b/packages/jsx-email/src/renderer/render.ts index b84ca6936..2caf2b4e9 100644 --- a/packages/jsx-email/src/renderer/render.ts +++ b/packages/jsx-email/src/renderer/render.ts @@ -80,6 +80,7 @@ const processHtml = async (config: JsxEmailConfig, html: string) => { const settings = { emitParseErrors: true }; // Remove any stray jsx-email markers (with or without attributes) const reJsxTags = new RegExp(`<[/]?(${jsxEmailTags.join('|')})(?:\\s[^>]*)?>`, 'g'); + const reHrefAttributes = /href="([^"]*)"/g; // @ts-ignore: This is perfectly valid, see here: https://www.npmjs.com/package/rehype#examples const processor = rehype().data('settings', settings); @@ -94,9 +95,6 @@ const processHtml = async (config: JsxEmailConfig, html: string) => { .use(stringify, { allowDangerousCharacters: true, allowDangerousHtml: true, - characterReferences: { - useNamedReferences: true - }, closeEmptyElements: true, collapseEmptyAttributes: true }) @@ -104,6 +102,14 @@ const processHtml = async (config: JsxEmailConfig, html: string) => { let result = docType + String(doc).replace('', '').replace('', ''); + result = result.replace(reHrefAttributes, (match, href) => { + if (!href.includes('&')) { + return match; + } + + return `href="${href.replaceAll('&', '&')}"`; + }); + result = result.replace(reJsxTags, ''); return result; From e77dd5e4fd292697cea842842b65f2185ba826ce Mon Sep 17 00:00:00 2001 From: CharlieHelps Date: Mon, 11 May 2026 17:04:15 +0000 Subject: [PATCH 4/4] fix(jsx-email): restore useNamedReferences rendering --- packages/jsx-email/src/renderer/render.ts | 12 +++--------- .../test/render/.snapshots/render.test.tsx.snap | 8 ++++---- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/jsx-email/src/renderer/render.ts b/packages/jsx-email/src/renderer/render.ts index 2caf2b4e9..b84ca6936 100644 --- a/packages/jsx-email/src/renderer/render.ts +++ b/packages/jsx-email/src/renderer/render.ts @@ -80,7 +80,6 @@ const processHtml = async (config: JsxEmailConfig, html: string) => { const settings = { emitParseErrors: true }; // Remove any stray jsx-email markers (with or without attributes) const reJsxTags = new RegExp(`<[/]?(${jsxEmailTags.join('|')})(?:\\s[^>]*)?>`, 'g'); - const reHrefAttributes = /href="([^"]*)"/g; // @ts-ignore: This is perfectly valid, see here: https://www.npmjs.com/package/rehype#examples const processor = rehype().data('settings', settings); @@ -95,6 +94,9 @@ const processHtml = async (config: JsxEmailConfig, html: string) => { .use(stringify, { allowDangerousCharacters: true, allowDangerousHtml: true, + characterReferences: { + useNamedReferences: true + }, closeEmptyElements: true, collapseEmptyAttributes: true }) @@ -102,14 +104,6 @@ const processHtml = async (config: JsxEmailConfig, html: string) => { let result = docType + String(doc).replace('', '').replace('', ''); - result = result.replace(reHrefAttributes, (match, href) => { - if (!href.includes('&')) { - return match; - } - - return `href="${href.replaceAll('&', '&')}"`; - }); - result = result.replace(reJsxTags, ''); return result; diff --git a/packages/jsx-email/test/render/.snapshots/render.test.tsx.snap b/packages/jsx-email/test/render/.snapshots/render.test.tsx.snap index 88bd0f8c7..f0953893e 100644 --- a/packages/jsx-email/test/render/.snapshots/render.test.tsx.snap +++ b/packages/jsx-email/test/render/.snapshots/render.test.tsx.snap @@ -31,7 +31,7 @@ exports[`render > inlining 1`] = ` - +
@@ -338,7 +338,7 @@ exports[`render > inlining 2`] = ` `; exports[`render > renders the airbnb demo template 1`] = ` -"
Read undefined's review
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
Airbnb

Here's what wrote

Now that the review period is over, we’ve posted ’s review to your Airbnb profile.

While it’s too late to write a review of your own, you can send your feedback to using your Airbnb message thread.

Read undefined's review
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
Airbnb

Here's what wrote

Now that the review period is over, we’ve posted ’s review to your Airbnb profile.

While it’s too late to write a review of your own, you can send your feedback to using your Airbnb message thread.

- +
Read undefined's review
 ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏ ‌​‍‎‏
- +