
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeThe 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.
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.
Based on your use case (updating browser tab titles to match the document being edited), here are some approaches that have worked for others:
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.
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.
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.
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:
.child() approachFor 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.
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.
Content backend


The only platform powering content operations


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store