How to trigger code on navigation events and get current document?

5 replies
Last updated: Aug 16, 2022
I stumbled across a reference to
part:@sanity/base/router/onNavigate
in the docs, and was wondering how can we trigger some code each time a navigation event occurs, e.g. a user changes to a new page, and ideally if we can get the document that the user has navigated to (if there is one)
AI Update

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 thread
5 replies
Interesting question! What sort of code are you trying to run?
I'm not actually sure you can use the router in that way, but it may be worth it to check out the docs on it
here .
We might be going about this completely wrong, but one of the gripes out content team has is that the
document.title
for the browser tab is never in sync with the documents they're editing, so when they have 5 tabs open, they all just say "Sanity" which is ironic.
So we couldnt find anywhere in the documentation that shows how to mark a field as the title which will be represented as the title of the tab, so our plan was to listen for navigation events, then if a document was loaded, we take the title field and just do basically
document.title = props.published.title
so each time a new page/document is loaded the tab represents the name
I see! State router may work, but I'm less familiar with its implementation. A hacky sort of way to handle it would be to pass an async function to your
child()
in your structure:
.child(async documentId => {
        const title = await client.fetch(`*[_id == $documentId].title`, {
          documentId,
        });

        document.title = title;
        return S.document().schemaType('<your-document>').documentId('<your-document-id>');
      }),
This particular example is for a singleton but I bet you could tweak it to work with document lists!
Wow. Slack made that code formatting absolutely atrocious 😅
yeah these sort of hacky ways are what we've sort of gone with lol, as it happens we've got a global custom "publish" button component which asks for confirmation first, so when that component mounts/unmounts it takes the title prop which is provided and uses that, downside is it only works on the form view and not general navigating through other menus etc

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.

Was this answer helpful?