Creating a field in Sanity for a blog and generating an excerpt, with discussion on previewing references and using a renderer component.
You definitely can auto-generate an excerpt from your blockContent field! The best approach is using the pt::text() function in your GROQ queries to extract plain text from Portable Text, which handles images, text blocks, and any other content automatically.
Query-time Extraction (Recommended)
Instead of storing the excerpt as a separate field, generate it when you query your posts:
*[_type == "post"] {
title,
body,
"excerpt": pt::text(body)[0..96]
}The pt::text() function extracts all plain text from your Portable Text blocks, automatically handling whether your post starts with an image or text—exactly what you need! Then you can use array slicing ([0..96]) to limit it to the first 96 characters.
For a word-based excerpt instead of character-based, you can combine pt::text() with the string::split() function:
*[_type == "post"] {
title,
body,
"excerpt": array::join(string::split(pt::text(body), " ")[0..20], " ")
}This takes the first 20 words instead of cutting mid-word. Here's what's happening:
pt::text(body)extracts plain text from your Portable Textstring::split(pt::text(body), " ")splits it into an array of words[0..20]takes the first 20 wordsarray::join(..., " ")joins them back into a string with spaces
Schema-level initialValue (Alternative)
If you really want a stored excerpt field in your schema, you can use initialValue:
{
name: "excerpt",
title: "Excerpt",
type: "string",
initialValue: async ({document, getClient}) => {
if (!document?.body) return '';
const client = getClient({apiVersion: '2023-01-01'});
const result = await client.fetch(
`*[_id == $id][0]{"text": pt::text(body)}`,
{id: document._id}
);
return result?.text?.substring(0, 96) || '';
}
}However, this approach is more complex and has downsides—the excerpt won't update automatically when the body changes, and you'd need to manually re-save documents to refresh it.
Why Query-time is Better
The query-time approach using pt::text() is simpler and more maintainable because:
- It always reflects the current content (no sync issues)
- Handles images, embeds, and text blocks automatically—no need to worry about
post.body[0]being an image - No need to update the excerpt when body changes
- Works with GROQ's string and array manipulation functions for flexible text processing
The pt::text() function is specifically designed for this use case—extracting searchable, readable text from rich Portable Text content for things like excerpts, search indexing, and SEO meta descriptions.
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.