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 ( - - {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,