Stop Fetching Whole Sanity Documents: Use GROQ Projections as a Query Contract
Blog listings that fetch whole Sanity documents waste bandwidth and couple UI to unused fields. Use precise GROQ projections and parameterized named queries so pages fetch only what they render.
11 Jun 2026, 00:06 UTC

The problem is over-fetching, not the CDN
Blog listings often fetch entire post documents and then filter and sort in the browser. That means Portable Text bodies, SEO objects, internal notes and draft fields travel to the client even when a card only needs title, slug and author name. The result is larger payloads, harder caching and a UI that is coupled to fields it never renders.
The useful takeaway: treat GROQ as the query contract. Push selection, reference expansion and ordering into the query and let components request data shapes, not documents.
Make GROQ the contract with named, parameterized queries
Keep a small module of named queries that mirrors page needs. Components call a query name with parameters instead of building GROQ strings. Parameters like $slug, $start, $end avoid concatenation and reduce injection risk.
A practical shape for a post detail page:
*[_type == "post" && slug.current == $slug][0]{
title,
publishedAt,
authorName: author->name,
categories: categories[]->title,
body
}And a cards query for a listing:
*[_type == "post"] | order(coalesce(publishedAt, _updatedAt) desc, _id asc) [$start...$end]{
title,
slug: slug.current,
publishedAt,
authorName: author->name
}The projection lists only rendered fields. The -> operator expands references inside the same dataset. Renaming a field in the schema will break the projection at runtime, which is why the projection is an implicit schema‑UI contract.
Ordering and pagination stability
GROQ array slicing [$start...$end] is simple offset pagination. It is easy to use but deep pages become slower and unstable when new items insert between requests.
Add a deterministic tiebreaker to the order. coalesce(publishedAt, _updatedAt) desc, _id asc gives a total order even when publishedAt is missing or duplicated. Keep page sizes modest.
Keyset cursor pagination is more robust for large lists but requires more query complexity. For most content sites, stable ordering with a tiebreaker and modest page size is a reasonable trade‑off.
Caching, preview and safety boundaries
With useCdn:true reads are fast and cached. Published content may lag briefly behind writes. For preview or read‑your‑writes, disable the CDN or use a preview perspective and do not cache privileged responses.
Never bundle a write token or a private dataset token into client JavaScript. Use a server route or server component for privileged reads. Pin apiVersion in the client and confirm client options match the installed SDK version.
Portable Text is structured JSON. Render it with a Portable Text renderer rather than converting to HTML yourself.
Limitations and how to check the result
Projections create a runtime contract. There is no compile‑time guarantee unless you generate types from the schema and test queries.
Practical verification:
- Run the exact query in Sanity Studio Vision/GROQ tool with the same parameters and confirm the returned shape matches the projection.
- Run
client.fetchwithuseCdn:trueanduseCdn:falseand compare payload size and freshness after a publish. - Add a lightweight smoke test that executes each named query against seeded data and asserts required fields exist.
- Check the client configuration for
projectId,datasetandapiVersionand confirm no token is bundled into client JavaScript. - Request page 1 and a deep page with fixed ordering and tiebreaker to verify no duplicates or gaps when a new item inserts.
These checks surface mismatches before users do.
Actionable closing
Adopt a small set of named, parameterized queries with explicit projections. Keep ordering deterministic, respect CDN staleness for published reads and isolate privileged reads server‑side. Generate types from schema and run a smoke test on each named query so renames fail fast.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.