Displaying now playing data with matching emoji using Netlify edge functions and Eleventy

My site is built using 11ty and is rebuilt once an hour. These frequent rebuilds accomplish a few things, notably updating webmention data and keeping my now page current. Recently, however, I decided to add the track I'm other currently listening to on my home page which, ideally, would be updated in real time. Enter client-side JavaScript and Netlify's Edge Functions.

The function I've written works by making a pair of API calls: one to Last.fm which returns an mbid (MusicBrainz ID) and another directly to MusicBrainz to retrieve genre data. It looks like this:

export default async () => {
  // access our Last.fm API key and interpolate it into a call to their recent tracks endpoint
  const MUSIC_KEY = Netlify.env.get('API_KEY_LASTFM')
  const trackUrl = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=cdrn_&api_key=${MUSIC_KEY}&limit=1&format=json`
  // fetch the track data
  const trackRes = await fetch(trackUrl, {
    type: 'json',
  const trackData = await trackRes.json()
  // extract the `mbid`
  const track = trackData['recenttracks']['track'][0]
  const mbid = track['artist']['mbid']
  let genre = ''

  // IF we get a valid mbid make the call to MusicBrainz
  if (mbid && mbid !== '') {
    const genreUrl = `https://musicbrainz.org/ws/2/artist/${mbid}?inc=aliases+genres&fmt=json`
    const genreRes = await fetch(genreUrl, {
      type: 'json',
    const genreData = await genreRes.json()
    genre = genreData.genres.sort((a, b) => b.count - a.count)[0]['name']

  // return our required data
  return Response.json({
    artist: track['artist']['#text'],
    title: track['name'],
    emoji: emojiMap(genre, track['artist']['#text']),

export const config = { path: '/api/now-playing' }

In the past I've displayed a single emoji with the current track but, in the interest of injecting some — let's say — whimsy into this whole exercise, I'm taking the genre from MusicBrainz and attempting to match it to an appropriate emoji:

const emojiMap = (genre, artist) => {
  const DEFAULT = '🎧'
  if (!genre) return DEFAULT // early return for bad input
  if (artist === 'David Bowie') return '👨đŸģ‍🎤'
  if (artist === 'Minor Threat') return '👨đŸģ‍đŸĻ˛'
  if (genre.includes('death metal')) return '💀'
  if (genre.includes('black metal')) return 'đŸĒĻ'
  if (genre.includes('metal')) return '🤘'
  if (genre.includes('emo') || genre.includes('blues')) return 'đŸ˜ĸ'
  if (genre.includes('grind') || genre.includes('powerviolence')) return 'đŸĢ¨'
  if (
    genre.includes('country') ||
    genre.includes('americana') ||
    genre.includes('bluegrass') ||
    return 'đŸĒ•'
  if (genre.includes('post-punk')) return '😔'
  if (genre.includes('dance-punk')) return 'đŸĒŠ'
  if (genre.includes('punk') || genre.includes('hardcore')) return '✊'
  if (genre.includes('hip hop')) return '🎤'
  if (genre.includes('progressive') || genre.includes('experimental')) return '🤓'
  if (genre.includes('jazz')) return 'đŸŽē'
  if (genre.includes('psychedelic')) return '💊'
  if (genre.includes('dance') || genre.includes('electronic')) return 'đŸ’ģ'
  if (
    genre.includes('alternative') ||
    genre.includes('rock') ||
    genre.includes('shoegaze') ||
    return '🎸'
  return DEFAULT

This could all be done with an object with the genre names assigned to keys but given how nebulous genres can be I've instead settled for a range of conditions checking for substring matches[1].

More precise genre names get checked earlier in the function, with less precise matches taking place after (e.g. post-punk and dance-punk are evaluated before matches to punk alone) and then defaulting to the headphones emoji if a match isn't found or an empty string is passed in for the genre.

The client side JavaScript to display the retrieve data is pretty straightforward:

;(async function () {
  // cache DOM selectors
  const nowPlaying = document.getElementById('now-playing')
  if (nowPlaying) {
    const emoji = document.getElementById('now-playing-emoji')
    const content = document.getElementById('now-playing-content')
    const loading = document.getElementById('now-playing-loading')
    const populateNowPlaying = (data) => {
      loading.style.display = 'none'
      emoji.innerText = data.emoji
      content.innerText = `${data.title} by ${data.artist}`

	// try and retrieve cached track data
    try {
      const cache = JSON.parse(localStorage.getItem('now-playing'))
      if (cache) populateNowPlaying(cache)
    } catch (e) {
      /* quiet catch */

	// fetch now playing data from our edge function
    const res = await fetch('/api/now-playing', {
      type: 'json',
    const data = await res.json()

	// cache retrieved data
    try {
      localStorage.setItem('now-playing', JSON.stringify(data))
    } catch (e) {
      /* quiet catch */

    if (!JSON.parse(localStorage.getItem('now-playing')) && !data) nowPlaying.remove()

	// update the DOM with the data we've retrieved from the edge function

The template to be populated looks like the following:

<p id="now-playing" class="client-side mb-0 flex flex-row items-start md:items-center">
  <span id="now-playing-loading" class="icon--spin">
    {% tablericon 'loader-2' 'Loading...' %}
  <span id="now-playing-display">
    <span id="now-playing-emoji" class="pt-1 md:pt-0 mr-1"></span>
    <span id="now-playing-content"></span>

Finally, if the page this all lives on is loaded by a client without JavaScript enabled, it will be hidden by the client-side class which is simply:

	.client-side {
	  display: none

All of this, yields the single line at the bottom of this image — updated on each visit.

Now playing
  1. Plus explicit conditions matching David Bowie and Minor Threat. ↩ī¸Ž

Cory Dransfeldt
Cory Dransfeldt

I'm a software developer in Camarillo, California. I enjoy hanging out with my beautiful family and 4 rescue dogs, technology, automation, music, writing, reading and tv and movies.