Skip to content

ADR-006: Document Pathname Field

Status

Accepted

Context

Every routable Sanity document has a URL on the frontend (e.g. nl/news/match-report). Before this decision, that URL was constructed at query time inside the sanity-sdk: a GROQ select(...) block traversed the document's type, language, parent chain, and slug to assemble the path inline, and that block was inlined into every query that needed a link.

This created several problems:

  • Query size growth. The pathname expression is large — especially for page documents, which must walk up to five levels of parent-> references. Every query that needed a link had this entire block embedded in it, bloating the GROQ text and the resulting request payload.
  • Duplication. The same logic existed in two places: in the sanity-sdk's pathname field definition (packages/sdks/sanity-sdk/src/fields/pathname.ts) and implicitly in every consumer that needed to resolve a URL. Any change to a URL pattern had to be found and updated in multiple locations.
  • Studio discoverability. Because a document's URL was never stored on the document itself, editors could not see or search by URL inside Sanity Studio. Finding "the document for /nl/service/partners" required knowing the slug, language, and type upfront.

Decision

We add a pathname string field to every routable document type and keep it up to date automatically using a Sanity Function (apps/sanity-functions/sync-pathname).

The pathname field

Each document that maps to a frontend URL gets a read-only pathname field containing its full path without a leading slash, e.g. nl/news/match-report. The field is computed — editors never type it manually.

The sync-pathname Sanity Function

A Sanity document event handler runs whenever a document is created or mutated. It:

  1. Resolves the document's own pathname using a type-specific GROQ expression defined in pathname-resolver.ts (PATHNAME_RESOLVERS). If the computed value differs from what is currently stored, it patches the document.
  2. Propagates to dependents — documents whose pathname embeds a reference to the changed document. For example, changing a categorySupporterservice document's slug must update every qaSupporterservice that belongs to that category. Propagation only happens from published documents because GROQ -> dereferences published versions.

Changes are committed in a single transaction per event (visibility: "async") and only when at least one patch is needed (dirty flag).

How URL resolution works across layers

LayerRole
Sanity StudioDisplays and indexes the stored pathname. Editors can search for a document by its URL path.
sanity-sdk queriesRead pathname directly from the document — no inline GROQ construction needed. Query text shrinks significantly.
FrontendUses the stored pathname to resolve links (e.g. to prefix with a locale or map to a Next.js route).

Adding a new document type

  1. Add a pathname string field (read-only) to the document's Sanity schema.
  2. Add an entry to PATHNAME_RESOLVERS in pathname-resolver.ts with a GROQ expression that resolves to the path string.
  3. If changing this document type should re-path other documents, add a dependentsFilter.
  4. The function handles the rest automatically on the next save.

Consequences

Positive

  • Smaller queries. The sanity-sdk no longer inlines the pathname construction block into every GROQ query. Queries that fetch links only need pathname — a single field projection.
  • Single source of truth for URL logic. Pathname computation lives entirely in PATHNAME_RESOLVERS. Updating a URL pattern is a one-file change.
  • Studio discoverability. Editors can search and filter documents by their URL path directly in Sanity Studio.
  • Cascading updates are handled automatically. Renaming a parent page or a category slug triggers a propagation pass that updates all affected child documents without any manual intervention.

Negative

  • Eventual consistency. The stored pathname is written asynchronously after a document is saved. There is a brief window where the stored value may be stale.
  • Function must stay in sync with schema. Adding a new routable document type requires updating both the Sanity schema and PATHNAME_RESOLVERS. Forgetting either side leaves the field empty.
  • Draft pathnames are not propagated. Dependent documents are only updated when a published document changes, so draft slug changes are not visible to dependents until the document is published.
  • apps/sanity-functions/sync-pathname/ — the Sanity Function implementation
  • apps/sanity-functions/sync-pathname/pathname-resolver.tsPATHNAME_RESOLVERS and PATHNAME_PROJECTION_BY_TYPE
  • packages/sdks/sanity-sdk/src/fields/pathname.ts — legacy inline GROQ fragment (kept for backward compatibility with queries not yet migrated to the stored field)