From 1ffc1a90e22e0f9d4193edacb54ade2ae0dd7bf5 Mon Sep 17 00:00:00 2001 From: ioslh Date: Thu, 20 Nov 2025 14:35:53 +0800 Subject: [PATCH] fix: prevent duplicate title tags in document head MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fix addresses invalid HTML caused by multiple elements when users manually set titles before router head management kicks in. Changes: - Modify Asset.tsx to use document.title API on client side - Automatically remove conflicting title tags on first update - Maintain normal SSR behavior for SEO compatibility - No API changes - fully backward compatible Fixes duplicate title tags while keeping the developer experience simple. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --- packages/react-router/src/Asset.tsx | 48 ++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/react-router/src/Asset.tsx b/packages/react-router/src/Asset.tsx index 79ceb8ed77..258623bcf0 100644 --- a/packages/react-router/src/Asset.tsx +++ b/packages/react-router/src/Asset.tsx @@ -16,11 +16,7 @@ export function Asset({ }: RouterManagedTag & { nonce?: string }): React.ReactElement | null { switch (tag) { case 'title': - return ( - <title {...attrs} suppressHydrationWarning> - {children} - - ) + return {children} case 'meta': return case 'link': @@ -40,6 +36,48 @@ export function Asset({ } } +// Track if we've taken control of the title +let titleControlled = false + +function Title({ + attrs, + children, +}: { + attrs?: Record + children?: string +}) { + const router = useRouter() + + React.useEffect(() => { + if (typeof children === 'string') { + // On the first title update, clean up any existing titles + if (!titleControlled) { + // Remove all existing title tags - router will now manage the title + const existingTitles = Array.from(document.querySelectorAll('title')) + existingTitles.forEach(titleEl => { + titleEl.remove() + }) + titleControlled = true + } + + // Set document.title directly - no DOM title tags needed on client + document.title = children + } + }, [children]) + + if (!router.isServer) { + // On client, don't render title tag - we manage document.title directly + return null + } + + // On server, render title tag normally for SSR + return ( + + {children} + + ) +} + function Script({ attrs, children,