--- date: '2023-03-18' title: 'Building my /now page using Eleventy' description: "As part of my commitment to writing about things I've written in other frameworks in Eleventy, this is how I re-engineered my /now page in Eleventy." draft: false tags: ['Eleventy', 'JavaScript', 'Last.fm', 'Oku', 'Trakt', 'Letterboxd', 'API'] --- As part of my commitment to writing about things I've written in other frameworks in Eleventy, this is how I re-engineered [my /now page](/now) in [Eleventy](https://www.11ty.dev/).[^1] My /now page is a series of discreet sections — the **Currently** block is [populated by data from](https://github.com/cdransf/coryd.dev/blob/e886857387661ceeba4f2b368989ec32f0c3f121/src/_includes/now.liquid#L14) [omg.lol](https://omg.lol)'s status.lol service and [several static bullet points and complimentary SVGs](https://github.com/cdransf/coryd.dev/blob/e886857387661ceeba4f2b368989ec32f0c3f121/src/_includes/now.liquid#L14-L31). The data request to retrieve my status looks like the following: ```javascript const EleventyFetch = require('@11ty/eleventy-fetch') module.exports = async function () { const url = 'https://api.omg.lol/address/cory/statuses/' const res = EleventyFetch(url, { duration: '1h', type: 'json', }) const status = await res return status.response.statuses[0] } ``` The **Listening: artists** and **Listening: albums** sections draw on data from [Last.fm's API](https://www.last.fm/api). The artist request looks like this: ```javascript const EleventyFetch = require('@11ty/eleventy-fetch') module.exports = async function () { const MUSIC_KEY = process.env.API_KEY_LASTFM const url = `https://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=cdme_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day` const res = EleventyFetch(url, { duration: '1h', type: 'json', }) const artists = await res return artists.topartists.artist } ``` The **Listening: albums** call is quite similar, swapping the `user.gettopartists` method for `user.gettopalbums`. The liquid templating for artists iterates through the retrieved and cached data to populate the section: {% raw %} ```liquid {% if artists %}

Listening: artists

{% for artist in artists %}
{{artist.name}}
{{artist.playcount}} plays
{{artist.name | escape}}
{% endfor %}
{% endif %} ``` {% endraw %} Artist images are populated by passing the `artist` object to [an `artist` filter](https://github.com/cdransf/coryd.dev/blob/e886857387661ceeba4f2b368989ec32f0c3f121/config/mediaFilters.js#L4-L5) which strips spaces, replacing them with `-` and normalizing the artist name string to lowercase: ```javascript artist: (media) => `https://cdn.coryd.dev/artists/${media.replace(/\s+/g, '-').toLowerCase()}.jpg`, ``` These images are all cropped to `350x350` and hosted over on Bunny.net[^2]. [Much like artists, we populate albums from data sourced from Last.fm](https://github.com/cdransf/coryd.dev/blob/e886857387661ceeba4f2b368989ec32f0c3f121/src/_data/albums.js) {% raw %} ```liquid {% if albums %}

Listening: albums

{% for album in albums %}
{{album.name}}
{{album.artist.name}}
{{album.name | escape}}
{% endfor %}
{% endif %} ``` {% endraw %} [Albums use a filter that, in this case, evaluates a denylist,](https://github.com/cdransf/coryd.dev/blob/e886857387661ceeba4f2b368989ec32f0c3f121/config/mediaFilters.js#L6-L10) simply a `string[]`, and replaces images contained therein. Anything not in the denylist is served directly from Last.fm: ```javascript album: (media) => { const img = !ALBUM_DENYLIST.includes(media.name.replace(/\s+/g, '-').toLowerCase()) ? media.image[media.image.length - 1]['#text'] : `https://cdn.coryd.dev/artists/${media.name.replace(/\s+/g, '-').toLowerCase()}.jpg` return img ``` Moving down the page, **Reading** data is sourced from [Oku](https://oku.club): ```javascript const { extract } = require('@extractus/feed-extractor') const { AssetCache } = require('@11ty/eleventy-fetch') module.exports = async function () { const url = 'https://oku.club/rss/collection/POaRa' const asset = new AssetCache('books_data') if (asset.isCacheValid('1h')) return await asset.getCachedValue() const res = await extract(url).catch((error) => {}) const data = res.entries await asset.save(data, 'json') return data } ``` Rather than dealing with an API that returns JSON, I'm transforming the RSS feed that Oku exposes for my currently reading collection, using [@extractis/feed-extractor](https://www.npmjs.com/package/@extractus/feed-extractor) to transform the XML into JSON and leveraging Eleventy's [@11ty/eleventy-fetch](https://www.npmjs.com/package/@11ty/eleventy-fetch) package for caching. Because I'm simply rendering a list of what I'm reading, the liquid templating is a bit simpler: {% raw %} ```liquid {% if books %}

Reading

{% endif %} ``` {% endraw %} For **Watching: movies** and **Watching: tv** we're following a nearly identical pattern (outside of object name semantics that are specific to the media type for each). Both Trakt and Letterboxd expose RSS feeds for watched media activity and both are passed through, fetched and cached using the same dependencies. [You can view the tv.js data file here](https://github.com/cdransf/coryd.dev/blob/e886857387661ceeba4f2b368989ec32f0c3f121/src/_data/tv.js) and [movies here](https://github.com/cdransf/coryd.dev/blob/e886857387661ceeba4f2b368989ec32f0c3f121/src/_data/movies.js), while [the full `now.liquid` combines all the discussed snippets](https://github.com/cdransf/coryd.dev/blob/e886857387661ceeba4f2b368989ec32f0c3f121/src/_includes/now.liquid). Currently, this page is refreshed on an hourly basis using scheduled builds on Vercel triggered by GitHub actions, [which you can read about here](/posts/2023/scheduled-eleventy-builds-cron-github-actions/). [^1]: You can learn more about /now pages [here](https://nownownow.com/about). [^2]: They're awesome, easy to use and super-affordable. Highly recommended.