How to auto-populate excerpt field from blockContent in Sanity?

15 replies
Last updated: Jan 28, 2026
Hello there,I'm using Sanity for a blog, and I wonder if there's some way to create a field that gets populated with the text from another field (a blockContent).
Initial thought:

{

name: "excerpt",

title: "Excerpt",

type: "string",

options: {

source: "GET THE BLOCK CONTENT",

maxLength: 96,

},
Or can an excerpt get generated for the blockContent in an easier way?
(Sometimes the blogposts starts with a picture and sometimes with text, so I don't want to get the data by doing something static like
post.body[0]
AI Update

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.

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:

  1. pt::text(body) extracts plain text from your Portable Text
  2. string::split(pt::text(body), " ") splits it into an array of words
  3. [0..20] takes the first 20 words
  4. array::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.

Show original thread
15 replies

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.

Was this answer helpful?