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
pagedocuments, which must walk up to five levels ofparent->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
pathnamefield 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:
- 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. - Propagates to dependents — documents whose pathname embeds a reference to the changed document. For example, changing a
categorySupporterservicedocument's slug must update everyqaSupporterservicethat 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
| Layer | Role |
|---|---|
| Sanity Studio | Displays and indexes the stored pathname. Editors can search for a document by its URL path. |
| sanity-sdk queries | Read pathname directly from the document — no inline GROQ construction needed. Query text shrinks significantly. |
| Frontend | Uses 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
- Add a
pathnamestring field (read-only) to the document's Sanity schema. - Add an entry to
PATHNAME_RESOLVERSinpathname-resolver.tswith a GROQ expression that resolves to the path string. - If changing this document type should re-path other documents, add a
dependentsFilter. - 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
pathnameis 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.
Related
apps/sanity-functions/sync-pathname/— the Sanity Function implementationapps/sanity-functions/sync-pathname/pathname-resolver.ts—PATHNAME_RESOLVERSandPATHNAME_PROJECTION_BY_TYPEpackages/sdks/sanity-sdk/src/fields/pathname.ts— legacy inline GROQ fragment (kept for backward compatibility with queries not yet migrated to the stored field)