Fetch Only What Your Sanity Blog Page Renders
Stop downloading entire Sanity documents. Use a GROQ projection and Portable Text serializer to send your blog page only the fields it renders.
06 Apr 2026, 18:49 UTC

Stop Fetching Entire Sanity Documents
When a blog page requests a Sanity document with *[_type == "post"] and no projection, the API returns every field: internal revision data, unpublished SEO notes, image metadata the design never shows, and references to authors or categories the layout does not render. That payload grows as the Studio schema grows, and every component becomes implicitly coupled to fields it never touches.
A narrow GROQ projection makes the page's data contract explicit. You request only title, publishedAt, and a shaped body array. Sanity returns exactly those keys, which keeps responses predictable and protects the frontend from unrelated schema changes.
Model the post as Portable Text
Sanity stores rich text as Portable Text, not HTML. Portable Text is an array of blocks. Each block has _type, _key, style, and children. The _key helps React reconcile list updates; _type tells the renderer whether a block is a paragraph, heading, or image. In schemas/post.js for Sanity Studio v3, define body as an array of block and image types.
// schemas/post.js
export default {
name: 'post',
type: 'document',
title: 'Blog Post',
fields: [
{ name: 'title', type: 'string', title: 'Title' },
{ name: 'slug', type: 'slug', title: 'Slug', options: { source: 'title' } },
{ name: 'publishedAt', type: 'datetime', title: 'Published at' },
{
name: 'body',
type: 'array',
title: 'Body',
of: [
{ type: 'block' },
{ type: 'image', fields: [{ name: 'alt', type: 'string', title: 'Alt text' }] }
]
}
]
};
Project only what the page renders
Run this query in Sanity Studio's Vision plugin or from a server-side client. Replace $slug with the post's current slug value.
// Run in Vision or via @sanity/client on the server
*[_type == "post" && slug.current == $slug][0] {
title,
publishedAt,
body[] {
_key,
_type,
style,
markDefs,
children[] {
_key,
_type,
text,
marks
},
... _type == "image" => {
asset->{
url,
metadata
},
alt
}
}
}
- Where to run: Vision in Studio for exploration;
@sanity/clientin a Node route or server component for production pages. - Permissions: Use the
publishedperspective for public pages. Do not put a read token with draft access in a browser bundle. - Risk: If you drop
_keyor_typefrom the projection, the Portable Text renderer cannot map blocks to components and React may warn about missing keys. - Risk: Projecting image metadata without
asset->urlreturns a reference ID, not a usable image URL.
Render Portable Text with a serializer
Install @portabletext/react and create a components object that maps block styles and marks to your UI.
// components/portable-text.js
import { PortableText } from '@portabletext/react';
export const serializers = {
types: {
image: ({ value }) =>
},
marks: {
link: ({ props, children }) => {children}
}
};
export function BlogBody({ body }) {
return ;
}
Missing serializers silently omit custom blocks or marks. Add a fallback that logs unknown _type values during development so you notice gaps before readers do.
Trade-offs and limitations
Narrow projections push field selection into the frontend. If five pages render different subsets of the same post, you either duplicate GROQ fragments or maintain a shared query library. Type generation can help keep TypeScript interfaces aligned with projections, but it adds a build step.
Draft perspective and CDN caching can make previews look stale. Verify cache behavior separately from query correctness, and gate draft fetching behind an authenticated server route.
Verify before you ship
- Create a scratch post with a heading, paragraph, inline link, and image.
- Run the projection in Vision with a real slug.
- Open the browser Network tab on the published page and confirm the JSON contains only
title,publishedAt, andbody. Check that_keyand_typeappear on every block. - Render the page and compare it against the Studio preview for the same document. Unknown blocks should appear in your serializer fallback log, not as blank spaces.
- Pin
apiVersionto a fixed ISO date in your client config. Redeploy after changing the query shape and inspect payload size.
Add one schema, one projection, one serializer, and one page. Measure the response size, compare output with Studio preview, then extend the pattern to other content types.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.