Recently Played: bringing back my Last.fm component — DeepSeek Blog | Neura Market
    Neura MarketNeura Market/DeepSeek
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsBlogVideosGuidesCoursesCommunityTrendingGenerate
    DeepSeekBlogRecently Played: bringing back my Last.fm component
    Back to Blog
    Recently Played: bringing back my Last.fm component
    devjournal

    Recently Played: bringing back my Last.fm component

    Martin Hicks March 8, 2026
    0 views

    How I rebuilt my old Last.fm widget as an Eleventy WebC component with server-rendered HTML, client-side polling, and localStorage caching.

    Around 2010, my personal website had a Last.fm widget showing what I'd been listening to. It was a small thing - just a few album covers and track names in the sidebar - but it was very *me*. Back then your personal site was an extension of yourself, and having your music taste ticking away in the corner felt right. Fast forward fifteen years and I've rebuilt [this site](https://martinhicks.dev) from scratch several times over. The Last.fm widget never made the cut again. Not for any good reason - it just fell off the list each time. When I was recently working on the sidebar I remembered it, and thought *why not bring it back?* There's something I like about the full circle. The web has gone through its various phases since 2010 - the single page app years, the JavaScript-for-everything years - and come out the other side valuing progressive enhancement and server-rendered HTML again. My site is built with [Eleventy](https://www.11ty.dev) and deployed from GitHub Actions to S3 with CloudFront. It felt fitting to bring back a feature from an earlier era, built with the principles I care about now. So here's how it works. ## The architecture The component is a two-layer system: build-time SSR for the initial HTML, and client-side polling for live updates. It's dropped into the sidebar as a `<now-listening>` WebC element alongside `<my-details>`. ``` Build time: listening.js → Last.fm API → tracks data → SSR into HTML Runtime: Client JS → polls Last.fm API every 60s → diffs → updates DOM ├── localStorage cache (2min TTL) for instant loads └── pauses when tab hidden, resumes on focus ``` ## Data layer: `src/_data/listening.js` This is an Eleventy global data file that runs at build time. It calls the Last.fm API - specifically `user.getrecenttracks` - requesting the five most recent tracks for my account. It has a three-second timeout so that if Last.fm is having a bad day, builds aren't left hanging. On any failure it returns `{ tracks: [] }`, which means builds never break regardless of what the API does. The raw API response gets mapped into clean objects - `name`, `artist`, `album`, `url`, `art`, and a `nowPlaying` boolean. All text fields are HTML-escaped and URLs are validated *(only `http:` and `https:` schemes allowed)* - this is the server-side XSS protection layer. The data is then available to templates as `listening.tracks` via Eleventy's data cascade. ## The component: `now-listening.webc` The WebC component has three distinct parts. ### Server-rendered HTML A `webc:type="render"` script runs at build time, reading `this.$data.listening.tracks`. It generates the initial HTML so the page ships with real track data baked in. This means content is visible immediately on load - and for search engines or anyone browsing without JavaScript, this *is* the component. It's done. No spinner, no empty state. ### A `<template>` element An inert HTML template used by the client-side code for DOM cloning. It defines the track row structure with `data-*` attribute hooks - `data-track`, `data-name`, `data-detail`, `data-art-placeholder`, and `data-now-playing`. Nothing renders from this until JavaScript picks it up. ### Client-side polling script A self-executing IIFE *(kept alive with `webc:keep` to prevent Eleventy from stripping it)* that handles the live behaviour: **Polling** - hits the Last.fm API every 60 seconds to keep the track list current. **localStorage cache** - on page load, if a fresh cache exists *(two-minute TTL)*, it renders from cache immediately and defers the first API call. This avoids the flash-of-stale-content problem on repeat visits or navigating between pages. **Diffing** - compares `JSON.stringify(tracks)` against the last known state. If nothing has changed, the DOM stays untouched. No unnecessary reflows. **Visibility awareness** - listens to `visibilitychange` to stop polling when the tab is hidden and restart when it becomes visible. If you leave the tab in the background for an hour, it's not hammering the API the whole time. Be a good citizen. **AbortController** - cancels in-flight fetch requests when polling stops, so there's no risk of stale responses landing after the component has moved on. **Client-side XSS protection** - uses the same `safeUrl` and `esc` helpers as the server layer. Content is inserted via `textContent` *(inherently safe)* and URLs are validated before being set as `href` or `src` attributes. ## Design decisions The main tradeoff worth calling out is the duplicated logic. The same API call and data mapping exists in both `listening.js` *(server)* and the client-side script. I could abstract it into a shared module, but the server code runs in Node during the Eleventy build and the client code runs in the browser. Keeping them self-contained means each layer is independently understandable and testable. The duplication is small and the mapping is straightforward - it's not the kind of logic that's likely to drift in dangerous ways. The other thing I'm quietly pleased with is how little the component asks of the outside world. Between the visibility-aware polling, the localStorage cache, and the diff check before touching the DOM, it makes the minimum number of requests necessary. If you're pulling from someone else's API on every page load of your site, I think you owe it to them *(and your users)* to be thoughtful about it. ## Was it worth it? It took a couple of hours to build and it makes me smile every time I see it on the page. Sometimes the best features aren't the most technically ambitious - they're the ones that make a site feel like *yours*. I might extract this into a standalone package at some point - the component is fairly self-contained and would slot into any Eleventy site with minimal config. If you want to see it in action, it's in the sidebar at [martinhicks.dev](https://martinhicks.dev).

    Tags

    devjournalshowdevsideprojectswebdev

    Comments

    More Blog

    View all
    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠ai

    How I'm using ASTs and Gemini to solve the "Codebase Onboarding" problem 🧠

    Hi everyone! 👋 I’m Tara, a Senior Software Engineer and Consultant. Over the years, I've jumped...

    T
    tworrell
    Local AI Will Save Us All (The Math Says So, Trust Me)ai

    Local AI Will Save Us All (The Math Says So, Trust Me)

    Every few weeks a take goes viral in tech circles making the case for ditching cloud AI and running...

    S
    Sebastian Schürmann
    Lost in the AI Hype, I Started Smallai

    Lost in the AI Hype, I Started Small

    And it helped me get back into tech without drowning TL;DR at the end Coming back to...

    R
    Rohini Gaonkar
    Building a Replay-Tested Interactive Brokers Client in Gogo

    Building a Replay-Tested Interactive Brokers Client in Go

    I wanted an IBKR library that felt like Go and had testing I could trust. So I wrote one.

    T
    Thomas Marcelis
    Playwright in Pictures: Fully Parallel Modeplaywright

    Playwright in Pictures: Fully Parallel Mode

    Playwright’s fullyParallel mode is often treated as a simple performance switch. In practice, it...

    V
    Vitaliy Potapov
    Designing a CLI for Both Humans and Agentscli

    Designing a CLI for Both Humans and Agents

    Learn how Alpic designed its CLI for both human developers and AI agents — covering tradeoffs like polling, context windows, interactivity, and statelessness.

    J
    Julien Vallini

    Stay up to date

    Get the latest DeepSeek prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for DeepSeek and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    © 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.