How to trigger code on navigation events and get current document?
The part:@sanity/base/router/onNavigate you found is from Sanity Studio v2, which used the deprecated Parts System. That system was completely removed in Studio v3+, and there's no direct equivalent for tracking navigation events in the modern Studio.
The Challenge
Unfortunately, Studio v3 doesn't expose a built-in navigation tracking hook or API. The router internals aren't designed to be publicly consumed in the way v2's parts system allowed. This is a known limitation that developers have worked around in various ways.
Practical Workarounds
Based on your use case (updating browser tab titles to match the document being edited), here are some approaches that have worked for others:
1. Track Navigation in Custom Document Actions
If you have a custom document action (like a publish button), you can update the document title when that component mounts:
import { definePlugin } from 'sanity'
import { useEffect } from 'react'
export const myPlugin = definePlugin({
name: 'navigation-tracker',
document: {
actions: (prev, context) => {
return prev.map(action => {
return (props) => {
useEffect(() => {
// Access the document being edited
if (props.published?.title) {
document.title = props.published.title
}
}, [props.published])
const Action = action
return <Action {...props} />
}
})
}
}
})This only works when viewing the document form, not when navigating through lists or other Studio views.
2. Use Structure Builder's Child Function
You can intercept document loads in your Structure Builder configuration by using an async function in .child():
import { StructureBuilder as S } from 'sanity/structure'
export const structure = (S, context) =>
S.list()
.title('Content')
.items([
S.documentTypeListItem('post')
.child(async (documentId) => {
// Fetch the document title when navigating
const doc = await context.getClient({ apiVersion: '2024-01-01' })
.fetch(`*[_id == $id][0]{ title }`, { id: documentId })
if (doc?.title) {
document.title = doc.title
}
return S.document()
.schemaType('post')
.documentId(documentId)
})
])This approach works but feels "hacky" and only triggers when navigating through your custom structure.
3. Monitor URL Changes with a Layout Component
Since Studio v3 uses React Router internally, you could monitor browser URL changes using a custom layout component:
import { definePlugin } from 'sanity'
import { useEffect } from 'react'
import { useClient } from 'sanity'
export const titleTrackerPlugin = definePlugin({
name: 'title-tracker',
studio: {
components: {
layout: (props) => {
const client = useClient({ apiVersion: '2024-01-01' })
useEffect(() => {
// Extract document ID from URL pattern like /desk/post;abc123
const pathMatch = window.location.pathname.match(/;([^/]+)$/)
if (pathMatch) {
const docId = pathMatch[1]
client.fetch(
`*[_id == $id][0]{ title }`,
{ id: docId }
).then(doc => {
if (doc?.title) {
document.title = `${doc.title} - Sanity Studio`
}
}).catch(() => {
// Handle errors silently
})
}
}, [window.location.pathname])
return props.renderDefault(props)
}
}
}
})This is fragile because it relies on Studio's internal URL structure, which could change without notice.
The Reality
None of these solutions are as clean as what v2's part:@sanity/base/router/onNavigate provided. The Studio v3 architecture intentionally doesn't expose navigation events as a stable public API. Your best bet is:
- For document editing views: Use the custom document action approach
- For structured navigation: Use the Structure Builder
.child()approach - For general tracking: Use the layout component with URL monitoring (but be aware it's fragile)
For the specific problem of browser tab titles being unhelpful when multiple tabs are open, you might also consider opening a feature request with the Sanity team, as this is a legitimate UX concern that could benefit from official support. As noted in this community discussion, others have encountered the same frustration.
Show original thread5 replies
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.