# Displaying Relative Time for a Static Site

Recently I was looking to make the article dates a little simpler to see how recently they had been written. Rather than looking at timestamp or date, I wanted to see now many days, months, or years ago an article was. This looked like relatively simple problem to tackle and I could have imported an `npm` package and been done with it, but where is the fun in that?

Instead I began writing a function which calculated the difference between two javascript `Date()` objects, which would then by transformed into some discrete time periods with the power of math, then display the largest useful time period in the format of `{X} {periods} ago`.

## Calculating the Relative Time

Calculating the time difference between the two instances of the `Date()` class by subtraction gives a difference in milliseconds (`ms` below). This is converted to `days` by dividing by the number of milliseconds per day, then transformed to values for `months` and `years` by dividing `days` by `30` and `365` respectively.

``````var now = new Date();
const ms = now - dateObj;
const days = Math.ceil(ms / 86400000);
const months = Math.floor(days / 30);
const years = Math.floor(months / 12);``````

Once we have calculated each of the time periods we are interested in, we can `switch` between which time period to display and return a formatted string showing a human-readable time difference between the two dates given.

``````if (years > 1) return `\${years} years ago`;
if (months > 1) return `\${months} months ago`;
return `\${days} days ago`;``````

With this in place, we can now calculate how old a date is in days, months, and years. In our `switch` statement, we are checking that `years` and `months` are greater than `1` which means we do not need to consider pluralisation for these labels. For `days`, we could apply a pluralisation if the `days` has a value greater than `1`.

This was all put assembled into a function as a filter in the `.eleventy.js` configuration file.

``````cfg.addFilter("timeAgo", (dateObj) => {
var now = new Date();
const days = Math.ceil((now - dateObj) / 86400000);
const months = Math.floor(days / 30);
const years = Math.floor(months / 12);
if (years > 1) return `\${years} years ago`;
if (months > 1) return `\${months} months ago`;
return `\${days} days ago`;
});``````

Implementing this function within the `.eleventy.js` file as a filter meant that within an template, we could format the article's date by applying this filter.

``  Published {{ page.date | timeAgo }}``

And during the eleventy build step, the page will output

``  Published 1 days ago``

This is where I ran into my first roadblock.

Eleventy has been serving my needs perfectly well for several months, since rebuilding my site with Eleventy last year, but my code was only run during the build step, which means any changes are only updated to the site when it is deployed.

With a statically generated site, a post that shows a publication time of `1 days ago` would continue to display this until the next article was written causing the site to be rebuilt and republished.

This did not seem to solve my problem as it is often months between writing new articles. There are workarounds, like using a cron task to trigger a rebuild every day, but that would be wasteful. Instead of using this function within the template, I needed to move the script into the browser to be run on page load.

## Making Relative Dates Dynamic

We will need to refactor our function and move it to a new file to publish at `/js/timeAgo.js` and add a reference to this file from the `_header` template to include it on each page of the site once it's published.

``<script src="/js/timeAgo.js" defer></script>``

This also needed to be published in the `.eleventy.js` config, here I'm publishing the whole `/js/` directory, even though there's only one file required at the moment.

``cfg.addPassthroughCopy("src/js/*.*");``

Within the `timeAgo.js` file we have three things to accomplish.

1. Running our script when the page loads
2. Find all of the dates on the page
3. Replacing the date with the relative time

Covering each of these items in reverse order.

### 3. Replacing the date with the relative time

We can lift most of our logic from the previous `timeAgo` filter function into our new file, while here, we can also take an optional second date to make testing easier.

``````function timeAgo(time, now) {
if (!now) now = new Date();

const ms = now - time;
const days = Math.ceil(ms / (86400 * 1000));
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);

if (years > 1) return `\${years} years ago`;
if (months > 1) return `\${months} months ago`;
return `\${days} days ago`;
}``````

This function will take a `Date()` object as the first argument (and an optional second argument to override the `new Date()` for testing) and return a formatted relative time string.

### 2. Find all of the dates on the page

There is a piece of setup required here. In our template file, we want to generate some HTML code that gives a clean way to find where in the page to render the relative times, but we also need the dates available as input for function.

We can use `<time>` HTML tags as our placeholder and we can use the `datetime` attribute to hold the source dates. Inside the tag, we can display the static article date, which will be overwritten with the string returned from the `timeAgo` function.

``<time datetime="{{ article.data.date | postDate }}">{{ article.data.date | postDate }}</time>``

With these dates now well structured, the code to find and replace each on the page is covered in this `relativeDates` function. We first query for all `time` elements which have the `datetime` attribute, then for each of these we parse the date and replace the contents of the element with the formatted `timeAgo` string.

``````function relativeDates() {
const els = document.querySelectorAll("time[datetime]");
els.forEach((el) => {
var date = new Date(el.getAttribute("datetime"));
el.innerHTML = timeAgo(date);
});
}``````

### 1. Running our script when the page loads

With these other changes in place we need to trigger our `relativeDates()` function to run when the pages is loaded, which can be achieved with:

``document.onload = relativeDates();``

## Bringing it all together

With our change to the template including our script

``<script src="/js/timeAgo.js" defer></script>``

And marking up our templates with a structured `time` tag.

``<time datetime="{{ article.data.date | postDate }}">{{ article.data.date | postDate }}</time>``

And the full `timeAgo.js` file now contains the three requirements listed above

``````document.onload = relativeDates();

function relativeDates() {
const els = document.querySelectorAll("time[datetime]");
els.forEach((el) => {
var date = new Date(el.getAttribute("datetime"));
el.innerHTML = timeAgo(date);
});
}

function timeAgo(time, now) {
if (!time) return "";
if (!now) now = new Date();

const ms = now - time;
const days = Math.ceil(ms / (86400 * 1000));
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);

if (years > 1) return `\${years} years ago`;
if (months > 1) return `\${months} months ago`;
return `\${days} days ago`;
}``````

## Bonus "Today"

As an added bonus, we can add a quick check to our `timeAgo` function to check if the article date is "Today" and return this

``````if (
now.getDate() == time.getDate() &&
now.getMonth() == time.getMonth() &&
now.getFullYear() == time.getFullYear()
) {
return "Today";
}``````

## Updated "Today" check

I was refactoring this `timeAgo.js` and inlining the script to reduce the number of network requests and found a neater "Today" check.

The `.toDateString()` returns just enough of the datetime object to determine if two datetime objects are within the same day.

``if (now.toDateString() == today.toDateString()) return "Today";``

After inlining the script, the `timeAgo` date formatter can be executed inline rather than waiting for a callback.

``````document.querySelectorAll("time[datetime]").forEach((el) => {
var date = new Date(el.getAttribute("datetime"));
el.innerHTML = timeAgo(date);
});``````