• 3 min read

Relative timestamps in Astro JS

I recently migrated this website to the Astro web framework. This led to substantial increases in performance across the site by utilizing island architecture, as most content is static and doesn't have interactive interfaces that would require Javascript to function. The couple of pages that do need JS can do so in a compartmentalized manner, without impacting the rest of the website.

This change required a few manual tweaks, including how I show relative timestamps on the homepage for my "Recent photos" feed ("Posted about 1 week ago").

Depending on how recently the photos were posted, this timestamp might change frequently (from "a few minutes ago" to "3 hours ago", etc). The homepage is static, and generally isn't regenerated more than a couple times a month, so this timestamp would be highly inaccurate for most of this website's visitors.

Instead, I chose to render the exact timestamp into my HTML and then let client-side Javascript take care of running the time conversion.

How to

First, I'll be using parseISO and formatDistanceToNow from date-fns on the client-side processing, so make sure that date-fns is installed in your project.

yarn install date-fns

Next, let's set up the HTML tag where I'll put the computed string.

...
  {
    !!mostRecentTime && parseISO(mostRecentTime) && (
      <p class="recent-timestamp">
        ---
      </p>
    )
  }
...

Here I'm only rendering this section if the most recent time is defined and parseable by parseISO. Then inside the element, I put --- as a placeholder that will be swapped out by our Javascript.

Finally, here's the Javascript. I've combined it all into one statement, which will cause an error that I'll address immediately afterward.

First, I have mostRecentTime defined in the Astro template's front matter.

---
import { parseISO } from "date-fns";

const mostRecentTime = myDataSource.createdAt;
---

I'll transfer over the variable to the client side using define:vars.

<script define:vars={{ mostRecentTime }}>
  //
</script>

Then I'll add my function inside those tags to transform the timestamp and paste it into the HTML.

<script define:vars={{ mostRecentTime }}>
  import { parseISO, formatDistanceToNow } from "date-fns";
  if (typeof mostRecentTime !== "undefined") {
    const relativeDate = formatDistanceToNow(parseISO(mostRecentTime));
    document.getElementsByClassName(
      "recent-timestamp"
    )[0].innerText = `Posted ${relativeDate} ago.`;
  }
</script>

Theoretically, this should work but we just hit a limitation of Astro. If you try to run the code above, the client-side Javascript will return an error about an unexpected import call or syntax error.

Normally, if you use import inside of a <script> tag in an Astro template, Astro will take care of importing and bundling the scripts you want to use on the page. Using define:vars prevents that automation from happening.

So, we need to divide out our define:vars from our import, into two separate scripts, like so.

<script define:vars={{ mostRecentTime }}>
  window.MyNamespace.data.mostRecentTime = mostRecentTime;
</script>
<script>
  import { parseISO, formatDistanceToNow } from "date-fns";
  const { mostRecentTime } = window.MyNamespace.data;
  if (typeof mostRecentTime !== "undefined") {
    const relativeDate = formatDistanceToNow(parseISO(mostRecentTime));
    document.getElementsByClassName(
      "recent-timestamp"
    )[0].innerText = `Posted ${relativeDate} ago.`;
  }
</script>

Now the code should work as expected on the client. We're setting the value to a key on the global window object (MyNamespace can be anything you'd like, but be careful to avoid pre-existing namespaces). Then Astro automatically bundles the helpers we need from date-fns and includes them on the page. Finally, we read the date value from our custom namespace data store, process it, and post the result in the HTML.


Anson Lichtfuss

Written by Anson Lichtfuss, a frontend engineer building useful, beautiful interfaces in Seattle.