Recently Played: bringing back my Last.fm component —…
    Neura MarketNeura Market/DeepSeek
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityDeepSeekDeepSeek
    CoPilotCoPilotStable DiffusionStable DiffusionMidjourneyMidjourney
    View All Directories
    OverviewRulesPromptsMCPsAgentsGamesBlogVideosGuidesCoursesCommunityTrending
    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 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 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.

    Tags

    devjournalshowdevsideprojectswebdev

    Comments

    More Blog

    View all
    Five Gemma-4 models, one accelerator: what porting E2B 31B to AWS Inferentia2 taught megemma

    Five Gemma-4 models, one accelerator: what porting E2B 31B to AWS Inferentia2 taught me

    I ported the whole Gemma-4 family — E2B, E4B, 12B, 31B, and the 26B-A4B MoE — to run on...

    X
    xbill
    Hey DEV, I'm Tobore. Let's actually connect.community

    Hey DEV, I'm Tobore. Let's actually connect.

    Hey DEV, I'm Tobore. Let's actually connect. I've been on here for a while now, mostly writing and...

    L
    Laurina Ayarah
    I burned through thousands of AI tokens. Then a friend did it for freeai

    I burned through thousands of AI tokens. Then a friend did it for free

    (yep, kinda clickbait, just for the funsies 😊) At the beginning of the year, I relaunched my...

    P
    Paulo Henrique
    Claude might be saturating your machineai

    Claude might be saturating your machine

    My laptop was sitting idle with the fan at full tilt. Nothing was running that I knew of. The culprit...

    S
    Sidhant Panda
    Automated GitHub Code Reviews Using Google Geminigithubactions

    Automated GitHub Code Reviews Using Google Gemini

    I Built a Thing! TL;DR — Google Gemini-based Pull Request reviews and Issue Triaging for...

    D
    Darren "Dazbo" Lester
    What is an "agentic harness," actually?ai

    What is an "agentic harness," actually?

    I've been hearing the word "harness" thrown around a lot lately. I assumed it just meant "the IDE" or...

    T
    Tilde A. Thurium

    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.

    Neura Market

    Custom AI Systems & Services

    Our team of experienced AI builders will help build custom AI systems, workflows, and solutions for your business.

    Request custom work

    Ready-made automations for this

    Workflows from the Neura Market marketplace related to this DeepSeek resource

    • eBay Enhances Data Access for AI Agents with MCP Server Integrationn8n · $9.99 · Related topic
    • Create a REST API for PDF Digital Signatures with Webhooksn8n · $24.99 · Related topic
    • Build AI Agents with Think-Plan-Act Architecture Using Llama-4 Reasoningn8n · $24.99 · Related topic
    • Auto-Publish Content to 9 Social Platforms with Blotato & Airtablen8n · $24.99 · Related topic
    Browse all workflows