If you've ever worked with a global team or managed content that needs precise publishing times, you've likely encountered the frustration of datetime handling in content management systems. Sanity CMS, while powerful in many ways, has a particularly confusing default approach to handling dates and times that can lead to unexpected publishing issues and team confusion.
Consider this real scenario that recently happened to a content editor in New York:
One evening, a content editor, based in NYC, created a blog post and set the "publishDate" field to be that current day, March 10, 2025.
After publishing, she visited the live site only to find her post displaying a publish date of March 11, 2025. Confused, she double-checked the Sanity Studio, which still showed March 10.
What went wrong?
This discrepancy occurs because Sanity displays dates in your local timezone within the Studio interface but stores them internally as UTC. For the NYC editor, 9:30 PM EST converts to 1:30 AM or 2:30 AM UTC on May 11 (depending on daylight saving time) — hence the incorrect display date on the frontend.
Fortunately, there's a solution: the rich-date-input
plugin for Sanity CMS. This powerful tool stores datetime information with complete timezone context, solving these issues once and for all.
By the end of this tutorial, you'll be able to:
Sanity's built-in datetime
type seems straightforward but contains a subtle yet significant issue: timezone confusion.
Here's what happens:
For teams spanning multiple timezones, this creates constant confusion. An editor in Los Angeles setting a display datetime of 7:30 PM PST might be surprised when their content displays a datetime of 2:30 AM UTC.
Even worse, they might unintentionally schedule content for the wrong datetime.
Storing datetimes in UTC is actually a standard best practice in software development. The problem isn't with UTC storage itself, but rather with losing timezone context in the process.
Most CMS systems follow one of these approaches:
rich-date-input
takes)The third approach is clearly superior for content management because it preserves the editor's exact intent while providing the technical flexibility needed for proper rendering.
The rich-date-input
plugin for Sanity solves these issues by storing a rich data structure that includes:
This comprehensive approach ensures that no matter how you need to display or manipulate the datetime, you have all the necessary context.
The typical data output from the plugin looks like this:
1{
2 "_type": 'richDate',
3 "local": '2023-02-21T10:15:00+01:00',
4 "utc": '2023-02-12T09:15:00Z',
5 "timezone": 'Europe/Oslo',
6 "offset": 60
7}
With this structure, you can:
First, let's install the plugin in your Sanity project:
1npm install @sanity/rich-date-input
This package provides the richDate
input component and the corresponding data type that will store our timezone-aware dates.
Next, you need to add the plugin to your Sanity configuration. Open your sanity.config.ts
(or .js
) file and update it as follows:
1import { defineConfig } from 'sanity'
2import { richDate } from '@sanity/rich-date-input'
3
4export default defineConfig({
5 // ...
6 plugins: [richDate()],
7 // ...
8})
This registers the plugin with your Sanity Studio, making the richDate
type available for use in your schemas.
Now, let's create or update a schema to use the richDate
type. Here's an example for blog schema:
1import { defineField, defineType } from 'sanity'
2
3export default defineType({
4 name: 'blogPost',
5 title: 'Blog Post',
6 type: 'document',
7 fields: [
8 // Other fields...
9 defineField({
10 name: 'publishDate',
11 title: 'Publish Date',
12 type: 'richDate',
13 }),
14 // More fields...
15 ],
16})
After implementing the richDate
field, restart your Sanity Studio and create or edit a document with your new field.
You'll notice a few key differences from the standard datetime input:
When a user selects a date, the timezone will be stored in the document. They can choose a different timezone if desired. The date displayed will be the time as it would be in that timezone, and UTC will be calculated from the timezone and local time.
When querying your Sanity dataset, the richDate fields will return the complete structure:
1// Example GROQ query
2*[_type == "event"] {
3 title,
4 publishDatetime,
5 // Other fields...
6}
This will return data with the full richDate structure:
1{
2 "title": "Annual Conference",
3 "publishDate": {
4 "_type": "richDate",
5 "local": "2025-05-10T21:30:00-04:00",
6 "utc": "2025-05-11T01:30:00Z",
7 "timezone": "America/New_York",
8 "offset": -240
9 }
10}
Now, in your frontend code, you can choose how to display this information:
1import { format } from 'date-fns';
2
3// Display in the original input timezone (as the editor intended)
4const displayLocalTime = (richDate) => {
5 if (!richDate) return '';
6 return format(new Date(richDate.local), 'MMMM d, yyyy h:mm a');
7};
8
9// Display in UTC
10const displayUtcTime = (richDate) => {
11 if (!richDate) return '';
12 return format(new Date(richDate.utc), 'MMMM d, yyyy h:mm a') + ' UTC';
13};
14
15// Display in the viewer's local timezone
16const displayViewerLocalTime = (richDate) => {
17 if (!richDate) return '';
18 // Just use the UTC time and let the browser convert to the viewer's timezone
19 return format(new Date(richDate.utc), 'MMMM d, yyyy h:mm a (zzz)');
20};
For most content purposes, you'll want to display the date in the original input timezone to match the editor's intent. For example, if an article mentions "the event happening on May 10th," you want that date to remain consistent regardless of who is viewing it.
Problem: You have existing content using Sanity's default datetime
fields.
Solution: Create a migration script that converts these fields to richDate
format:
1// Migration script example
2import client from './sanityClient';
3
4// Fetch all documents with datetime fields
5const documents = await client.fetch(`*[_type == "yourType" && defined(publishDate)]`);
6
7// Update each document
8for (const doc of documents) {
9 const utcDate = doc.publishDate;
10
11 // Create richDate structure
12 const richDateValue = {
13 _type: 'richDate',
14 utc: utcDate,
15 local: utcDate, // Note: You'll lose the original local time intention
16 timezone: 'UTC', // Assuming UTC as default
17 offset: 0
18 };
19
20 // Update the document
21 await client
22 .patch(doc._id)
23 .set({publishDate: richDateValue})
24 .commit();
25
26 console.log(`Updated document ${doc._id}`);
27}
Problem: TypeScript doesn't recognize the richDate
structure.
Solution: Define a type for richDate fields:
1interface RichDate {
2 _type: 'richDate';
3 local: string;
4 utc: string;
5 timezone: string;
6 offset: number;
7}
8
9interface YourDocumentType {
10 _id: string;
11 title: string;
12 publishedAt: RichDate;
13 // Other fields...
14}
Problem: Dates display incorrectly or inconsistently on the frontend.
Solution: Use a reliable date library like date-fns or Luxon, and always specify which part of the richDate structure you're using:
1import { parseISO, format } from 'date-fns';
2
3// Always be explicit about which datetime you're using
4const formattedDate = format(parseISO(richDate.local), 'MMMM d, yyyy');
For content teams working across multiple regions, richDate
ensures that when an editor in Paris schedules content to go live at "9:00 AM local time," team members in New York understand that means 3:00 AM their time.
For event platforms, storing the complete timezone context allows you to display event times appropriately for each viewer while maintaining the original intended time in the originating timezone.
When coordinating product launches or marketing campaigns across different regions, richDate
allows you to schedule region-specific timing while maintaining a clear global overview.
You can create custom UI components that leverage the rich date structure for specialized needs:
1import React from 'react';
2
3const GlobalTimezoneDisplay = ({richDate}) => {
4 if (!richDate) return null;
5
6 const timezones = [
7 { name: 'New York', zone: 'America/New_York' },
8 { name: 'London', zone: 'Europe/London' },
9 { name: 'Tokyo', zone: 'Asia/Tokyo' },
10 // Add more as needed
11 ];
12
13 // Calculate display times for each timezone
14 const displayTimes = timezones.map(tz => {
15 // Complex timezone calculation logic here
16 // This is simplified
17 return {
18 name: tz.name,
19 formattedTime: '...' // Use a timezone library for proper conversion
20 };
21 });
22
23 return (
24 <div className="timezone-display">
25 <h4>Event Time Across Regions</h4>
26 <ul>
27 {displayTimes.map(dt => (
28 <li key={dt.name}>
29 <strong>{dt.name}:</strong> {dt.formattedTime}
30 </li>
31 ))}
32 </ul>
33 </div>
34 );
35};
The rich-date-input
plugin for Sanity CMS solves one of the most persistent challenges in content management: correctly handling dates and times across timezones. By storing complete datetime context, it ensures that your content team's intentions are preserved regardless of where your content is viewed.
Implementing this plugin requires minimal effort but provides significant benefits:
For teams working across multiple regions or publishing time-sensitive content, the rich-date-input
plugin isn't just a nice-to-have—it's essential infrastructure that prevents mistakes and ensures smooth operations.
Learn how to add the missing "Open in New Tab" option to Sanity CMS links. This step-by-step guide shows you how to customize link annotations and properly implement frontend rendering for better content management.
In this tutorial, you will learn how to extend Sanity CMS with custom superscript and subscript decorators for block array fields.
Frustrated by Sanity's inability to wrap text around images? This tutorial shows you how to build a custom component that gives your content editors the power to float images left or right with adjustable width controls — just like Webflow.