fix: prevent duplicate title tags in document head
PR Description:
π Problem & User Need
The Original Need
I want to display meaningful page titles immediately during route loading, before head() executes. Currently, there's a title gap because:
- Route matches β empty/stale title period begins
- beforeLoad executes β still no title update
- loader executes β still no title update
- head() executes β title finally updates
This creates poor UX with blank or stale titles during navigation.
Current Workaround & Issue
Based on existing APIs, the only way to set immediate titles is in beforeLoad:
export const Route = createFileRoute('/posts/$postId')({
beforeLoad: ({ params }) => {
// Only way to set immediate title with current API
document.title = `Loading Post ${params.postId}...`
},
head: ({ loaderData }) => ({
meta: [{ title: loaderData.title }], // Final title after loading
}),
})
This causes duplicate title tags:
<!-- After beforeLoad sets document.title -->
<head>
<title>Loading Post 123...</title> <!-- Set by beforeLoad -->
</head>
<!-- After head() executes and Asset renders -->
<head>
<title>Loading Post 123...</title> <!-- Original from document.title -->
<title>My Amazing Post</title> <!-- New one from head() -->
</head>
The result is invalid HTML with multiple <title> elements, causing SEO and browser issues.
β Solution
This PR enables the workaround mentioned above by fixing the duplicate title issue. Now developers can safely use beforeLoad to set immediate titles without worrying about invalid HTML.
The fix modifies title handling in Asset.tsx to:
-
Clean up conflicts: On first router-managed title update, automatically remove any existing
<title>tags -
Use document.title API on client: Directly set
document.titleinstead of rendering additional<title>elements -
Maintain SSR compatibility: Continue rendering
<title>tags normally on the server for SEO
Result: Clean HTML
<!-- After this fix, only one title exists -->
<head>
<title>My Amazing Post</title> <!-- Only the final title -->
</head>
Technical Implementation
Client-side behavior:
- First title update removes all existing
<title>DOM elements - Subsequent updates only use
document.titleAPI - No additional
<title>tags are rendered
Server-side behavior:
- Normal
<title>tag rendering for SSR/SEO - No changes to existing SSR behavior
π§ Implementation Details
File Changed: packages/react-router/src/Asset.tsx
Before:
case 'title':
return (
<title {...attrs} suppressHydrationWarning>
{children}
</title>
)
After:
case 'title':
return <Title attrs={attrs}>{children}</Title>
// New Title component that handles deduplication
function Title({ attrs, children }) {
const router = useRouter()
React.useEffect(() => {
if (typeof children === 'string') {
// Clean up existing titles on first update
if (!titleControlled) {
document.querySelectorAll('title').forEach(el => el.remove())
titleControlled = true
}
// Use document.title API directly
document.title = children
}
}, [children])
// Client: no DOM rendering, Server: normal SSR
return !router.isServer ? null : <title {...attrs}>{children}</title>
}
π Usage & Developer Experience
Before This Fix
Developers faced a dilemma:
- Use only
head()β good HTML, but title gaps during loading - Use
beforeLoad+head()β immediate titles, but invalid HTML with duplicates
After This Fix
Developers can now safely use the beforeLoad pattern without HTML validity concerns:
export const Route = createFileRoute('/posts/$postId')({
beforeLoad: ({ params }) => {
// β
Now safe to use - no more duplicate title tags!
if (typeof window !== 'undefined') {
document.title = `Loading Post ${params.postId}...`
}
},
head: ({ loaderData }) => ({
meta: [{ title: loaderData.title }], // Will cleanly replace the loading title
}),
})
Summary: This PR fixes duplicate title tags by implementing clean title management that automatically handles conflicts while maintaining full backward compatibility and SSR support.
Summary by CodeRabbit
-
Refactor
- Optimized how page titles are managed, improving client-side performance by updating the document title more efficiently while maintaining proper server-side behavior.
βοΈ Tip: You can customize this high-level summary in your review settings.
Pre-merge checks and finishing touches
β Passed checks (3 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | β Passed | Check skipped - CodeRabbitβs high-level summary is enabled. |
| Title check | β Passed | The title 'fix: prevent duplicate title tags in document head' directly and clearly describes the main problem being solved by this changeset: preventing duplicate title tags from appearing in the document head when titles are set both via document.title (in beforeLoad) and via head(). This is the core issue the PR addresses. |
| Docstring Coverage | β Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
β¨ Finishing touches
- [ ] π Generate docstrings
π§ͺ Generate unit tests (beta)
- [ ] Create PR with unit tests
- [ ] Post copyable unit tests in a comment
[!TIP]
π Customizable high-level summaries are now available in beta!
You can now customize how CodeRabbit generates the high-level summary in your pull requests β including its content, structure, tone, and formatting.
- Provide your own instructions using the
high_level_summary_instructionssetting.- Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
- Use
high_level_summary_in_walkthroughto move the summary from the description to the walkthrough section.Example instruction:
"Divide the high-level summary into five sections:
- π Description β Summarize the main change in 50β60 words, explaining what was done.
- π References β List relevant issues, discussions, documentation, or related PRs.
- π¦ Dependencies & Requirements β Mention any new/updated dependencies, environment variable changes, or configuration updates.
- π Contributor Summary β Include a Markdown table showing contributions:
| Contributor | Lines Added | Lines Removed | Files Changed |- βοΈ Additional Notes β Add any extra reviewer context. Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.