How to sort by date without time in Sanity dateTime fields?

10 replies
Last updated: May 6, 2022
Hey Sanity Land đź‘‹
I'm having a hard time wrapping my head around ordering content based on dateTimes. Is there a way to remove time from
dateTime
fields in Sanity?
I need to be able to sort by multiple fields while having posts maintain chronological order.

The only way I can think about having this work is by adding another field to our schema that is
date
and run a script to add data to these to the existing 3000+ document dataset.
Thoughts?
AI Update

You don't need to add a new field or migrate your 3000+ documents! You can handle date-only sorting directly in your GROQ queries, though the approach is a bit different than you might expect.

The Core Challenge

dateTime fields in Sanity store ISO 8601 strings like 2024-01-15T14:30:00Z. When you sort by these fields directly, posts published on the same day will be ordered by their exact time, which can cause inconsistent ordering when combined with other sort criteria.

Solution: Sort by the Full DateTime Field

The most straightforward approach is to use the dateTime() function to ensure proper chronological sorting:

*[_type == "post"] | order(dateTime(publishedAt) desc)

For multiple field sorting while maintaining date order:

*[_type == "post"] 
  | order(category asc, dateTime(publishedAt) desc)

This sorts by category first, then by full datetime within each category. Posts published on the same day will maintain a consistent order based on their publication time.

If You Truly Need Date-Only Sorting

GROQ doesn't have built-in functions to extract just the date portion from a datetime field. However, since ISO 8601 datetime strings start with the date in YYYY-MM-DD format, you have a couple options:

Option 1: Use the datetime as-is - In most cases, sorting by the full datetime is actually desirable because it provides consistent, deterministic ordering. Posts from the same day will always appear in the same order.

Option 2: Add a date-only field - If you genuinely need date-only sorting (where posts from the same day should be treated as equal for sorting purposes), adding a separate date field is the cleanest approach. You could use a Sanity Function to automatically populate this field whenever a document is created or updated, avoiding manual migration.

Multi-Field Sorting

The order() function supports multiple sort expressions separated by commas. The order matters - later expressions only affect ordering when earlier ones are equal:

*[_type == "post"] 
  | order(
      featured desc,
      category asc, 
      dateTime(publishedAt) desc
    )

This would show featured posts first, then sort by category, then by publication date within each category.

Why Not String Manipulation?

While you might think to extract just the date portion using string operations, GROQ has limited string manipulation functions available. The datetime approach is the officially supported method for working with temporal data in queries.

Hope this helps clarify your options! The dateTime() function approach should work well for most use cases without requiring any schema changes.

Show original thread
10 replies
Hi User. I think you’re right. Without a substring or slice function I don’t believe you can currently do this solely in GROQ.
user A
Is there a substring or slice function that's undocumented? đź‘€
No, afraid not.
Any other creative solutions that you are aware of? I can't imagine that this is an uncommon use case or need.
All I can think of are either a document action to store a date on your document based on the dateTime field, like you suggested, or doing the sort outside of GROQ after you’ve fetched.
I’ll add this to the internal ticket as a +1.
Thanks User. Document action is a good call, unfortunately won't work in our case but a great suggestion.
Thank you for the internal ticket. Much appreciated.
user A
While we're on the topic, what's the most simple way to backfill the data needed?
I might start with something like this, though I would urge testing on a non-production dataset first to make sure it works as expected.
You’d put the following somewhere in your studio folder and run it with
sanity exec path/to/script.js --with-user-token
, first changing out
NEW_DATE_FIELD
and `DATETIME_FIELD`:

import sanityClient from 'part:@sanity/base/client'

const client = sanityClient.withConfig({ apiVersion: '2022-05-06' })

const fetchDocuments = () =>
  client.fetch(`*[DATETIME_FIELD != null && NEW_DATE_FIELD == null][0...100] {_id, _rev, DATETIME_FIELD, NEW_DATE_FIELD}`)

const buildPatches = docs =>
  docs.map(doc => ({
      id: doc._id,
      patch: {
        set: {NEW_DATE_FIELD: doc.DATETIME_FIELD.slice(0, 10)},
        // this will cause the migration to fail if any of the documents has been
        // modified since it was fetched.
        ifRevisionID: doc._rev
      }
    }))

const createTransaction = patches =>
  patches.reduce((tx, patch) => tx.patch(patch.id, patch.patch), client.transaction())

const commitTransaction = tx => tx.commit()

const migrateNextBatch = async () => {
  const documents = await fetchDocuments()
  const patches = buildPatches(documents)
  if (patches.length === 0) {
    console.log('No more documents to migrate!')
    return null
  }
  console.log(
    `Migrating batch:\n %s`,
    patches.map(patch => `${patch.id} => ${JSON.stringify(patch.patch)}`).join('\n')
  )
  const transaction = createTransaction(patches)
  await commitTransaction(transaction)
  return migrateNextBatch()
}

migrateNextBatch().catch(err => {
  console.error(err)
  process.exit(1)
})
This will find all the documents where your
DATETIME_FIELD
exists and
NEW_DATE_FIELD
does not (if you exclude the second one, this would keep running forever). It then sets
NEW_DATE_FIELD
to be the first 10 characters of your
DATETIME_FIELD
, which is the date as
YYYY-MM-DD
. Finally, it bundles those up as packages of 100 so you don’t rate limit yourself.

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?