Retrieving previous and next items with GROQ in Sanity.io

14 replies
Last updated: Jun 3, 2021
How can I retrieve with GROQ the previous and next item from the same type, such as a news post?
{
    "post": *[_type == "news" && slug.current == "${page.params.slug}"] [0],
    "prev": "???",
    "next": "???",
}
AI Update

You can retrieve previous and next items in GROQ by using comparison operators with ordering. Here's how to structure your query:

{
  "post": *[_type == "news" && slug.current == $slug][0],
  "prev": *[_type == "news" && _createdAt < *[_type == "news" && slug.current == $slug][0]._createdAt] | order(_createdAt desc)[0],
  "next": *[_type == "news" && _createdAt > *[_type == "news" && slug.current == $slug][0]._createdAt] | order(_createdAt asc)[0]
}

However, this approach queries the current post multiple times, which isn't efficient. A better pattern is to use a projection to avoid redundant queries:

*[_type == "news" && slug.current == $slug][0] {
  ...,
  "prev": *[_type == "news" && _createdAt < ^._createdAt] | order(_createdAt desc)[0],
  "next": *[_type == "news" && _createdAt > ^._createdAt] | order(_createdAt asc)[0]
}

The ^ operator references the parent scope (the current post), so you can compare against its _createdAt field without re-querying.

Key points:

  • Use < to get items before the current one, then order(_createdAt desc)[0] to get the most recent previous item
  • Use > to get items after the current one, then order(_createdAt asc)[0] to get the next item
  • Replace _createdAt with whatever field you want to use for ordering (like publishedAt, orderRank, etc.)
  • The ^ operator helps you reference the parent document's fields within projections

If you're ordering by a custom field like publishedAt or want to handle published vs draft documents, adjust the filter accordingly:

*[_type == "news" && slug.current == $slug && !(_id in path("drafts.**"))][0] {
  ...,
  "prev": *[_type == "news" && !(_id in path("drafts.**")) && publishedAt < ^.publishedAt] | order(publishedAt desc)[0],
  "next": *[_type == "news" && !(_id in path("drafts.**")) && publishedAt > ^.publishedAt] | order(publishedAt asc)[0]
}

This pattern works well for pagination-style navigation through content of the same type. Just make sure the field you're ordering by is indexed properly for better query performance.

It is indeed having a date field
I would start with something like this to see if it works for you. You can change
_createdAt
to whichever date field you want (ideally a dateTime, and it will need to be unique). Remove
_id
after the filters if you want to return the whole object.

*[_type == 'news' && slug.current == "${page.params.slug}"][0] {
  ...,
  'prev': *[_type == 'news' && !(_id in path('drafts.**')) && _createdAt < ^._createdAt]._id | order(_createdAt desc)[0],
  'next': *[_type == 'news' && !(_id in path('drafts.**')) && _createdAt > ^._createdAt]._id | order(_createdAt desc)[0]
}
I would start with something like this to see if it works for you. You can change
_createdAt
to whichever date field you want (ideally a dateTime, and it will need to be unique). Remove
_id
after the filters if you want to return the whole object.

*[_type == 'news' && slug.current == "${page.params.slug}"][0] {
  ...,
  'prev': *[_type == 'news' && !(_id in path('drafts.**')) && _createdAt < ^._createdAt]._id | order(_createdAt desc)[0],
  'next': *[_type == 'news' && !(_id in path('drafts.**')) && _createdAt > ^._createdAt]._id | order(_createdAt desc)[0]
}
user A
seems to work pretty well! You're a true genius haha, thanks
Glad it works for you! 🙌
Glad it works for you! 🙌
Alright
user A
, spoke too soon haha. So it does work, but let's say I'm on the post 3 (the oldest from the date field) and want to go to the previous one, it jumps to the first post in date and not the one just before the current one in date if that makes sense
*[_type == "news" && slug.current == "${page.params.slug}"] [0] {
    ...,
    "next": *[_type == 'news' && !(_id in path('drafts.**')) && date > ^.date] | order(date desc) [0],
    "prev": *[_type == 'news' && !(_id in path('drafts.**')) && date < ^.date] | order(date desc) [0]
}
I believe it's because it's taking the first with [0] on
prev
, but I have no clue how to get the one just before 🤔
Just to make sure we’re using the same terms, what do you mean by “the previous one?” If you’re on post 3, and it’s the oldest post, I would expect
prev
to be null.
By previous I mean the one just before according to the date
By previous I mean the one just before according to the date
so let's say we have 2021-04-23, 2020-09-15, 2019-07-21 and I'm on 2019-07-21, I should have 2020-09-15 as my "previous" post
but right now it gets the first of all, so 2021-04-23
K, thank you. It might be a game of playing with your orders (using
asc
instead of
desc
, for example).

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?