First of all, thank you for building Hono — it's been a joy to work with, especially the JSX renderer's automatic head hoisting. I ran into a small edge case while implementing hreflang + canonical links, and I'd love to help get it fixed.
Bug Report
Summary
When using jsxRenderer, a <link rel="canonical"> tag is silently dropped from the rendered HTML if a <link rel="alternate" hreflang="..."> tag shares the same href.
Reproduction
On a localized EN page (e.g. /en/about), the canonical URL and the hreflang="en" alternate URL are identical by design. When both are declared in the renderer:
<link rel="canonical" href="https://example.com/en/about" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/about" />
The rendered HTML becomes:
<!-- canonical is missing -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/about" />
Root Cause
In src/jsx/intrinsic-element/common.ts, the deduplication key for link elements is:
const deDupeKeyMap = {
link: ["href"],
}
The deduplication logic in src/jsx/intrinsic-element/components.ts uses OR semantics: if any key in deDupeKeys matches an existing tag's props, the new tag is considered a duplicate.
Because rel="canonical" and rel="alternate" hreflang="en" share the same href, the canonical tag is treated as a duplicate of the already-registered alternate tag and removed from the buffer.
Expected Behavior
Two <link> elements should only be considered duplicates when all identifying attributes match. Specifically, rel (and hreflang for alternate links) should be part of the deduplication key.
Proposed Fix
src/jsx/intrinsic-element/common.ts — add rel and hreflang to the link dedup keys:
const deDupeKeyMap = {
link: ["href", "rel", "hreflang"],
}
src/jsx/intrinsic-element/components.ts — change OR semantics to AND semantics for multi-key deduplication:
// Before (OR: any key match → duplicate)
for (const key of deDupeKeys) {
if ((tagProps?.[key] ?? null) === props?.[key]) {
duped = true
break LOOP
}
}
// After (AND: all keys must match → duplicate)
const allMatch = deDupeKeys.every(
(key) => (tagProps?.[key] ?? null) === (props?.[key] ?? null)
)
if (allMatch) {
duped = true
break LOOP
}
With this fix:
| Scenario |
Before |
After |
rel="canonical" href="X" vs rel="alternate" href="X" |
❌ deduped (canonical removed) |
✅ kept (different rel) |
rel="alternate" hreflang="en" href="X" vs rel="alternate" hreflang="ja" href="X" |
❌ deduped |
✅ kept (different hreflang) |
Two identical rel="canonical" href="X" |
✅ deduped |
✅ deduped |
Workaround
Use raw() from hono/html to bypass Hono JSX's <link> processing:
import { raw } from 'hono/html'
// In jsxRenderer:
{raw(`<link rel="canonical" href="${canonicalUrl}">`)}
Environment
- Hono: 4.11.9
- Reproduction: placing
<link rel="canonical"> and <link rel="alternate" hreflang> with the same href in jsxRenderer
I'm happy to send a PR with the fix if this looks reasonable. Thanks again for all your work on Hono!
First of all, thank you for building Hono — it's been a joy to work with, especially the JSX renderer's automatic head hoisting. I ran into a small edge case while implementing hreflang + canonical links, and I'd love to help get it fixed.
Bug Report
Summary
When using
jsxRenderer, a<link rel="canonical">tag is silently dropped from the rendered HTML if a<link rel="alternate" hreflang="...">tag shares the samehref.Reproduction
On a localized EN page (e.g.
/en/about), the canonical URL and thehreflang="en"alternate URL are identical by design. When both are declared in the renderer:The rendered HTML becomes:
Root Cause
In
src/jsx/intrinsic-element/common.ts, the deduplication key forlinkelements is:The deduplication logic in
src/jsx/intrinsic-element/components.tsuses OR semantics: if any key indeDupeKeysmatches an existing tag's props, the new tag is considered a duplicate.Because
rel="canonical"andrel="alternate" hreflang="en"share the samehref, the canonical tag is treated as a duplicate of the already-registered alternate tag and removed from the buffer.Expected Behavior
Two
<link>elements should only be considered duplicates when all identifying attributes match. Specifically,rel(andhreflangfor alternate links) should be part of the deduplication key.Proposed Fix
src/jsx/intrinsic-element/common.ts— addrelandhreflangto thelinkdedup keys:src/jsx/intrinsic-element/components.ts— change OR semantics to AND semantics for multi-key deduplication:With this fix:
rel="canonical" href="X"vsrel="alternate" href="X"rel)rel="alternate" hreflang="en" href="X"vsrel="alternate" hreflang="ja" href="X"hreflang)rel="canonical" href="X"Workaround
Use
raw()fromhono/htmlto bypass Hono JSX's<link>processing:Environment
<link rel="canonical">and<link rel="alternate" hreflang>with the samehrefinjsxRendererI'm happy to send a PR with the fix if this looks reasonable. Thanks again for all your work on Hono!