Using GROQ to create an excerpt for a Sanity block

Oct 22, 2022

This is a quick tip on how to truncate a Sanity block field to create an excerpt using GROQ.

When querying the field using a Sanity Client and GROQ, there is no inbuilt excerpt function, or even a truncate function, so we need to do the steps ourselves.

Truncating the field could be done using a JavaScript function after the query, but this would require the entire field to be returned, which is not ideal.

This bit of a hack and not the most efficient query, but essentially:

  1. Get the string of the body as above using the text feature of the portable text functions.
  2. Split the string into an array of every character.
  3. truncate it to 255 characters by taking a subset of characters (assuming it's actually longer than 255 characters).
  4. Join every remaining character back together.
  5. Add an ellipsis to the end.

The queries

Get the string of the portable text block field using the text function of the portable text:

1pt::text(theField)
Because pt::text is used, it will return the text of the field, but not the formatting. This means that if you have a block field with a heading, it will return the heading text, but not the heading formatting.

Split the string into an array of every character using the split function of strings:

1string::split("the string", "")

Truncate the characters to 255 characters by taking a subset of characters (assuming it's actually longer than 255 characters):

1theArray[0..255]

Join an array of characters back together using the join function of arrays:

1array::join(["t", "h", "e", " ", "s", "t", "r", "i", "n", "g"], "")

We are also concatenating the ellipsis to the end of the truncated string:

1+ "..."

Examples

Because GROQ queries can be piped together, we can do all of this in separate queries.

In the following example I will use a field called body which is a Sanity block field. Replace that as needed.

Joined together it can either be a set of queries piped together:

1*[_type == "article"] {
2   "excerpt": (pt::text(body)), // 1
3 } | {
4   "excerpt": string::split(excerpt, "") // 2
5 } | {
6   "excerpt": excerpt[0..255] // 3
7 } | {
8   "excerpt": array::join(excerpt, "") // 4
9 } | {
10   "excerpt": exceprt + "..." // 5
11 }

Or as one query:

1*[_type == "article"] {
2   "excerpt": array::join(string::split((pt::text(body)), "")[0..255], "") + "..."
3}

Issues

This is a hack, and does have some downsides for example:

  • If the last character happens to be a . then there are 4 characters in the ellipsis: .....
  • The string is split by character, so if a word is split in half, it will be split in half.
  • You could split on a space " " to do this by word, but this doesn't account for other punctuation, and there's no easy way to truncate to a specific number of character.
  • If the string is less than 255 characters, it will still return the entire string.
  • Because tags and markup is removed by pt:body() the returned value is just a string with no markup such as paragraphs or titles.
See the GROQ excerpt on the Sanity.io Exchange community