Date and time handling in NextJs with Sitecore


When using traditional traditional (monolith) approach for Sitecore, the complexity in date handling is often solved more or less automatically.

With headless, things are not really different but it seems like it is relevant to remind us of date handling, hereby this blog post. Sitecore also have documentation with Date/time best practices

In the database, all dates should be persisted as UTC dates and that is also the case for Sitecore. That is important as it will allow moving the database between data centers, switch on-prem vs. cloud hosting etc. When reading and showing dates, we must handle the relevant timezone.

In the Sitecore client the timezone used for the UI is controlled by the setting ServerTimeZone so you would create a patch file like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?xml version="1.0"?>

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"
  xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <settings>
      <setting name="ServerTimeZone" set:value="Romance Standard Time"/>
    </settings>
  </sitecore>
</configuration>

Here by, in Sitecore UI you will see the UTC time in raw values, while when using the Date field, the time is for the specified (local) time zone.

Sitecore UI showing date in normal and raw values mode

With traditional monolith/ASP.Net MVC approach

When developing with ASP.Net MVC in the same process it is simple enough that. Sitecore provides the dedicated Sitecore.DateUtil methods Sitecore.Kernel and hereby the UTC dates are transformed.

These methods are also called when using the renderField pipeline or when using eg. GlassMapper.

When using Next.JS

As we the database contains the date values in UTC it is not really a surprise that we also receive those from the Layout service.

The equivalent statement for initializing the date shown above is

1
const d = new Date('2024-01-19T14:00:00Z')

For rendering in javascript, often the moment.js is used to transform from UTC to local timezones.

However, there is actually a more native approach with the Intl functions. Hereby you can render date, time and handle timezone by instantiating an Intl.DateTimeFormat which can be configured in a lot of different ways.

Formatting times

1
2
3
4
5
6
7
8
const timeFormatter = new Intl.DateTimeFormat(locale, {
    timeStyle: 'short',
    timeZone: 'Europe/Copenhagen',
  });
  <DateField
    field={rendering.fields.StartDate}
    render={(d) => timeFormatter.format(d)}
  />

The two different components, language and timezone, are specified and handled individually.

The locale or language code handles how the time should be formatted.

locale timeStyle short timeStyle long
en-US 3:00 PM 3:00:00 PM GMT+1
en-GB 15:00 15:00:00 CET
da-DK 15.00 15.00.00 CET
fr-FR 15:00 15:00:00 UTC+1

on the other hand the timezone handles transforming the actual time, so you might want to combine those

locale timezone timeStyle short timeStyle long
en-US America/New_York 9:00 AM 9:00:00 AM EST
en-GB Europe/London 14:00 14:00:00 GMT
da-DK Europe/Copenhagen 15.00 15.00.00 CET
fr-FR Europe/Paris 15:00 15:00:00 UTC+1

Formatting dates

Similar, for formatting dates you can specify a dateStyle

1
2
3
4
5
6
7
8
  const dateFormatter = new Intl.DateTimeFormat(locale, {
    dateStyle: 'medium',
    timeZone: 'Europe/Copenhagen',
  });
  <DateField
    field={rendering.fields.StartDate}
    render={(d) => dateFormatter.format(d)}
  />

Hereby both formatting and translating of month names are handled.

Eg. when running this for the date new Date(2024, 1, 2)

locale dateStyle medium dateStyle short dateStyle long
en-US Feb 1, 2024 2/1/24 February 1, 2024
en-GB 1 Feb 2024 01/02/2024 1 February 2024
da-DK 1. feb. 2024 01.02.2024 1. februar 2024
fr-FR 1 févr. 2024 01/02/2024 1 février 2024

More advanced scenarios

Ok, so the DateTimeFormat actually handles the date and time part very well and remove a lot of complexity from the custom application.

If/when both date and time should be rendered, you just specify both - and this also handles how to separate date and time

1
2
3
4
5
new Intl.DateTimeFormat(locale, {
    dateStyle: 'long',
    timeStyle: 'long',
    timeZone: 'Europe/Copenhagen',
  }).format(new Date('2024-01-02T14:00:00Z'))
locale dateStyle long and timeStyle long
en-US January 2, 2024 at 3:00:00 PM GMT+1
en-GB 2 January 2024 at 15:00:00 CET
da-DK 2. januar 2024 kl. 15.00.00 CET
fr-FR 2 janvier 2024 à 15:00:00 UTC+1

Even more advanced, when you want to format a start and end date, the Intl.DateTimeFormat also have a method for this and handles how complex the range should be represented:

1
2
3
4
5
new Intl.DateTimeFormat('da-DK', {
    dateStyle: 'medium',
    timeStyle: 'short',
    timeZone: 'Europe/Copenhagen',
  }).formatRange(startDate, endDate)
startDate endDate result
2024-01-02T10:00:00Z 2024-01-02T14:00:00Z 2. jan. 2024 11.00–15.00
2024-01-02T10:00:00Z 2024-01-03T14:00:00Z 2. jan. 2024 11.00-3. jan. 2024 15.00

Ok, but I have some very specific requirements for how the date should be formatted, the day of the week should be included, no seconds, timezone must be there… No worries, DateTimeFormat still have you covered:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
new Intl.DateTimeFormat('en-GB', {
    weekday: 'long',
    year: 'numeric',
    month: 'short',
    day: 'numeric',
    hour: 'numeric',
    minute: 'numeric',
    timeZoneName: "short",
    timeZone: 'Europe/Copenhagen',
  }).formatRange(new Date('2024-01-02T10:00:00Z'), new Date('2024-01-03T14:00:00Z'))
> 
'Tuesday, 2 Jan 2024, 11:00 CET – Wednesday, 3 Jan 2024, 15:00 CET'

Well, that is fine, but I also need some different elements and styling for various parts of the dates. Ok, there is a method for you so you can handle that. With formatToParts or formatRangeToParts you get the individual parts and can create the elements with classNames that you want:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[ 
{type: 'weekday',      value: 'Tuesday', source: 'startRange'},
{type: 'literal',      value: ', ',      source: 'startRange'},
{type: 'day',          value: '2',       source: 'startRange'},
{type: 'literal',      value: ' ',       source: 'startRange'},
{type: 'month',        value: 'Jan',     source: 'startRange'},
{type: 'literal',      value: ' ',       source: 'startRange'},
{type: 'year',         value: '2024',    source: 'startRange'},
{type: 'literal',      value: ', ',      source: 'startRange'},
{type: 'hour',         value: '11',      source: 'startRange'},
{type: 'literal',      value: ':',       source: 'startRange'},
{type: 'minute',       value: '00',      source: 'startRange'},
{type: 'literal',      value: ' ',       source: 'startRange'},
{type: 'timeZoneName', value: 'CET',     source: 'startRange'},
{type: 'literal',      value: ' – ',     source: 'shared'},
{type: 'weekday',      value: 'Wednesday', source: 'endRange'},
{type: 'literal',      value: ', ',      source: 'endRange'},
{type: 'day',          value: '3',       source: 'endRange'},
{type: 'literal',      value: ' ',       source: 'endRange'},
{type: 'month',        value: 'Jan',     source: 'endRange'},
{type: 'literal',      value: ' ',       source: 'endRange'},
{type: 'year',         value: '2024',    source: 'endRange'},
{type: 'literal',      value: ', ',      source: 'endRange'},
{type: 'hour',         value: '15',      source: 'endRange'},
{type: 'literal',      value: ':',       source: 'endRange'},
{type: 'minute',       value: '00',      source: 'endRange'},
{type: 'literal',      value: ' ',       source: 'endRange'},
{type: 'timeZoneName', value: 'CET',     source: 'endRange'}
]

Ok this Intl.DateTimeFormat is pretty cool. There is even more configuration options that I haven’t covered here (different calendars, numbering systems, )1

A few word on hydrating

We are using the pages router in NextJS as the app router is currently not supported with Sitecore JSS. Hereby the components are rendered server side/statically and served to the client, on the client the components are rendered again in the hydration phase with the same properties to activate any behaviors.

If the result of the server provided and the client side rendered are not equal you will get a client side hydration error.

It is tempting to just use the timezone of the browser, and you can do that by leaving out the timezone from the options. but that will give this hydration error. So, you must make sure that all of those options are configured consistently, or render those to properties within a Sitecore supported getServerSideComponentProps so the client is just using the rendered value - or alternatively only render the values on the client.

Browser support

When using javascript APIs it is always good to consider how well this is supported and if it matches your requirements. From my point of view the support is pretty good, all browsers support this is the newest version2.

However, as always do your testing. If you use some very specific features (number systems, to parts) you might end up in a scenario that is not that well supported. Then consider using the api server side only and just use that serialized output from the component props.

For formatting of numbers

It is not really related to dates, but there is also a similar Intl.NumberFormat to render numbers. It can also handle currency formatting and even translations of a lot of common units. Go explore the documentation3