How to Handle Relative Dates with SSG
Perform DOM changes as the document loads to avoid layout jumps
How is it possible to dynamically adapt some content based on viewers’ specific attributes? This is a common question that you might run into while jumping into SSG when usually dealing with SSR.
A simple and pratical example could be to transform publication dates into relative time. Such tweak can help readers to assess in a glance how recent is the content.
Let’s take the following example of a
<time>
HTML
element that includes both a
datetime
attribute
and a human readable date.
<time datetime="2023-02-04T00:00:00.000Z">Feb 4, 2023</time>
While different methods can be used to hydrate content client-side, some layout jumps would be observed since the browser already renderered the content.
However, the value of the <time>
DOM element can be altered before any
painting happens by using a blocking <script>
tag.
<html>
<head>
<script>
const elements = document.querySelectorAll('time[datetime]');
const now = new Date();
elements.forEach((element) => {
const elementDate = new Date(element.dateTime);
const differenceInSeconds = Math.floor(
(now.getTime() - elementDate.getTime()) / 1000,
);
element.textContent = `${-differenceInSeconds} seconds ago`;
});
</script>
</head>
<body>
<time datetime="2023-02-04T00:00:00.000Z">Feb 4, 2023</time>
</body>
</html>
The previous example only includes a simple transformation of the time difference
into a string. To improve this, the Intl.RelativeTimeFormat
API enables the generation of the relative time and is
supported in most modern browsers.
export function formatRelativeDate(
reference: Date,
value: Date,
formatter: Intl.RelativeTimeFormat,
): string {
const seconds = Math.floor((reference.getTime() - value.getTime()) / 1000);
let interval = Math.floor(seconds / (60 * 60 * 24));
if (interval >= 1) {
return formatter.format(-interval, 'day');
}
interval = Math.floor(seconds / (60 * 60));
if (interval >= 1) {
return formatter.format(-interval, 'hour');
}
interval = Math.floor(seconds / 60);
return formatter.format(-interval, 'minute');
}
console.log(
formatRelativeDate(
new Date(),
new Date('2023-02-04T00:00:00.000Z'),
new Intl.RelativeTimeFormat('en', { numeric: 'auto' }),
),
);
While such scripts can be added directly into HTML pages, it can also be bundled and inlined. This can very easily be achieved with Astro as the bundling will be handled automatically! The following snippet provides an example of an Astro page that includes the previous examples.
---
---
<!DOCTYPE html>
<html lang="en">
<head>
<script>
import { updateRelativeDates } from './update-relative-dates';
updateRelativeDates();
</script>
</head>
<body>
<time datetime="2023-02-04T00:00:00.000Z">Feb 4, 2023</time>
</body>
</html>
import { formatRelativeDate } from './format-relative-date';
export function updateRelativeDates() {
const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
const dateElements =
document.querySelectorAll<HTMLTimeElement>('time[datetime]');
const now = new Date();
dateElements.forEach((element) => {
const date = new Date(element.dateTime);
element.textContent = formatRelativeDate(now, date, formatter);
});
}
At Smallpdf, @manuelstofer used this concept to perform conditional rendering based on user attributes and power some of our experiments without the need of SSR. To reference this system, I named it Planck (from the physics terminology) as such scripts allow to perform changes to the document before it is rendered. Nowadays, Smallpdf’s engineers reference it as Planck time in contrast with Bundle time which only executes once the main bundle is downloaded. This however deserves its own short!
This trick enables other client-side dynamic logic such as authenticated user state or theme handling.
This idea of this article and especially the Astro implementation has been inspired from erika.florist. Therefore, thank you @Princesseuh!