Skip to content

Adding Live Last.fm Recent Tracks to My Static Eleventy Site


Overview

I wanted to showcase my music listening habits on my static Eleventy.js website by displaying my recently played tracks from Last.fm. The challenge was making this work dynamically—automatically updating as I listen to music—without requiring a site rebuild every time. Here's how I built it with help from Claude.


The Problem

Static site generators like Eleventy are fantastic for performance and simplicity, but they have one major limitation: the content is "baked in" at build time. If I wanted to show my Last.fm listening history, I had two options:

  1. Rebuild the site constantly - Impractical and wasteful
  2. Add client-side JavaScript - But this would expose my API keys

I needed a third way: a solution that kept the site static while adding dynamic capabilities securely.


The Solution: Serverless Architecture

The answer was to use serverless functions (specifically Netlify Functions) as a secure proxy between my static site and the Last.fm API. This architecture has three main components:

  1. Netlify Function (Backend) - A secure proxy to fetch data from Last.fm
  2. Client-Side JavaScript (Frontend) - Manages dynamic updates and user interface
  3. Eleventy Template & CSS (Structure & Style) - Provides the HTML structure and visual design

The Implementation

1. netlify/functions/lastfm-recent.js - The Serverless Function

Purpose: Acts as a secure proxy between your website and the Last.fm API.

What it does:

  • Receives requests from your website's JavaScript
  • Fetches recent tracks from the Last.fm API using your API key
  • Returns the data to your website
  • Keeps your API key completely hidden from browser code
  • Implements caching (1 minute) to reduce API calls

The Key Code:

exports.handler = async function (event, context) {
  const API_KEY = process.env.LASTFM_API_KEY // Secure!
  const USERNAME = process.env.LASTFM_USERNAME

  // Fetch from Last.fm API
  const url = `https://ws.audioscrobbler.com/2.0/?method=user.getRecentTracks...`
  const response = await fetch(url)
  const data = await response.json()

  // Return to client
  return {
    statusCode: 200,
    body: JSON.stringify(data),
  }
}

Why it's needed:

  • Security: Your API key stays on the server, never exposed to browsers
  • Control: You can add rate limiting, caching, and error handling
  • Simplicity: Your client code doesn't need to know about API authentication

2. js/lastfm-recent.js - The Client-Side Controller

Purpose: Manages all the dynamic behavior on your music page.

What it does:

  • Runs when your page loads
  • Fetches data from your Netlify function (NOT directly from Last.fm)
  • Builds HTML for each track
  • Updates the page content
  • Automatically refreshes every 60 seconds

The Flow:

// 1. Initialize when page loads
document.addEventListener('DOMContentLoaded', function () {
  initRecentTracks(username, limit)
})
// 2. Fetch and display tracks
async function updateRecentTracks() {
  // Fetch from our Netlify function
  const response = await fetch('/.netlify/functions/lastfm-recent?...')
  const data = await response.json()

  // Build HTML for each track
  tracks.forEach((track) => {
    html += `<div class="track-item">...</div>`
  })

  // Update the page
  container.innerHTML = html
}
// 3. Keep updating every minute
setInterval(updateRecentTracks, 60000)

Key Features:

  • Automatic Updates: Fetches new data every minute
  • Error Handling: Shows friendly messages if something goes wrong
  • HTML Escaping: Prevents security issues from user-generated content
  • Loading States: Shows a loading message while fetching data

3. music.njk - The Eleventy Template

Purpose: Provides the HTML structure for your music page.

What it does:

  • Creates an empty container for tracks
  • Sets configuration via data attributes (username, how many tracks to show)
  • Links to the JavaScript files
  • Shows a loading message before JavaScript runs

The Structure:

<div
  id="lastfm-container"
  data-username="{{site.lastfm_username}}"
  data-limit="10"
>
  <div class="music-section">
    <h1>My Music</h1>

    <!-- This container gets filled by JavaScript -->
    <div id="recent-tracks-container">
      <div class="loading">Loading recent tracks...</div>
    </div>
  </div>
</div>
<script src="/js/lastfm-recent.js"></script>

Customization: Change data-limit="10" to show more or fewer tracks.


4. netlify.toml - Netlify Configuration

Purpose: Tells Netlify where your files are and how to build your site.

[build]
  command = "npm run build"      # How to build your site
  publish = "_site"               # Where the built site lives
  functions = "netlify/functions" # Where serverless functions live

5. .env - Environment Variables

Purpose: Stores your API keys securely for local development.

LASTFM_API_KEY=your_api_key_here
LASTFM_USERNAME=your_lastfm_username

Critical Security Notes:

  • Add .env to your .gitignore - NEVER commit API keys to git
  • For production, set these variables in your Netlify dashboard

6. _data/site.json - Site Configuration

Purpose: Stores site-wide settings accessible in templates.

{
  "title": "Your Site Title",
  "lastfm_username": "your_username"
}

This makes your username available in templates as {{site.lastfm_username}}.


How It All Works Together

Let's walk through what happens when someone visits your music page:

Step 1: Initial Page Load (Static, Instant)

User types in your URL
↓
Netlify serves pre-built HTML (super fast!)
↓
Browser loads the page HTML
↓
Browser downloads CSS and JavaScript files
↓
User sees: "My Music" heading and "Loading recent tracks..."

At this point, everything is static HTML—no API calls yet, just an instant page load.

Step 2: JavaScript Takes Over (Dynamic)

Page finishes loading
↓
DOMContentLoaded event fires
↓
JavaScript reads username from data-username attribute
↓
JavaScript makes request: GET /.netlify/functions/lastfm-recent?username=...&limit=10

Step 3: The Netlify Function Does Its Job

Netlify Function receives request
↓
Function reads API_KEY from environment variable (secure!)
↓
Function makes request to Last.fm API
↓
Last.fm returns recent tracks as JSON
↓
Function returns this data to your JavaScript

Step 4: Displaying the Results

JavaScript receives track data
↓
Loops through each track
↓
Builds HTML for each track:
  - Album artwork (or placeholder)
  - Track name
  - Artist name
  - Album name
  - When it was played
↓
Replaces loading message with track grid
↓
User sees your recent listening history!

Step 5: Automatic Updates

60 seconds pass
↓
JavaScript automatically fetches new data
↓
Repeats Steps 3 & 4
↓
Page updates with any new tracks
↓
User always sees current listening history (no refresh needed!)

The Benefits

1. Security First

  • API keys never appear in browser code
  • No way for users to see or steal your credentials
  • All sensitive operations happen server-side

2. Performance

  • Initial load is instant (just static HTML)
  • Data fetches in background (doesn't block page rendering)
  • Caching reduces load (1 minute server-side cache)
  • Only fetches what's needed (configurable limit)

3. User Experience

  • Page loads immediately (no waiting for API calls)
  • Smooth updates (new tracks appear automatically)
  • Works on all devices (responsive design)
  • Graceful errors (friendly message if Last.fm is down)

4. Easy to Maintain

  • Separation of concerns (backend, frontend, styling are separate)
  • Easy to customize (change data-limit to show more/fewer tracks)
  • Well-organized (each file has one clear purpose)
  • Documented code (comments explain what each part does)

5. Respects Last.fm

  • Reasonable polling (only checks once per minute)
  • Server-side caching (reduces load on Last.fm's API)
  • Rate limit friendly (won't hit API limits with normal usage)

Setting It Up Yourself

Want to add this to your own Eleventy site? Here's how:

Prerequisites

  1. An Eleventy.js site
  2. A Last.fm account
  3. A Netlify account (free tier works perfectly)

Step-by-Step Setup

1. Get a Last.fm API Key

  • Go to https://www.last.fm/api/account/create
  • Fill out the form (you can use your website URL)
  • Copy your API key

2. Create the Files

Create these files in your Eleventy project:

netlify/functions/lastfm-recent.js
js/lastfm-recent.js
[your-templates-dir]/music.njk
netlify.toml
.env

Copy the code from each artifact into the corresponding file.

3. Install Dependencies

npm install node-fetch
npm install dotenv --save-dev

4. Configure Environment Variables

Create .env file:

LASTFM_API_KEY=your_actual_api_key
LASTFM_USERNAME=your_lastfm_username

Add .env to your .gitignore:

.env

5. Update Your Site Configuration

In _data/site.json:

{
  "lastfm_username": "your_lastfm_username"
}

6. Test Locally

# Install Netlify CLI if you haven't
npm install -g netlify-cli
# Run your site with functions
netlify dev

Visit http://localhost:8888/music/ to test.

7. Deploy to Netlify

Push to your Git repository, then in your Netlify dashboard:

  1. Go to Site Settings → Build & Deploy → Environment
  2. Add environment variables:
    • LASTFM_API_KEY: your API key
    • LASTFM_USERNAME: your username
  1. Trigger a new deploy

8. Customize

Want to show more or fewer tracks? Edit your template:

<div
  id="lastfm-container"
  data-username="{{site.lastfm_username}}"
  data-limit="20"
>
  <!-- Changed from 10 to 20 -->
</div>

Key Learnings

Challenge: API Key Security

Problem: Can't put API keys in client-side JavaScript

Solution: Netlify Functions act as a secure proxy

Result: Keys stay on the server, safe from prying eyes

Challenge: Static Site, Dynamic Data

Problem: Eleventy builds static HTML at build time

Solution: Use JavaScript to fetch data after page load

Result: Best of both worlds—instant load + live updates

Challenge: Rate Limiting

Problem: Don't want to hammer Last.fm's API

Solution:

  • Polling interval of 60 seconds (not 1 second!)
  • Server-side caching for 1 minute
  • Configurable limit on number of tracks

Result: Respectful of Last.fm's resources


File Structure Summary

Here's how everything is organized:

my-eleventy-site/
├── netlify/
│   └── functions/
│       └── lastfm-recent.js     # Serverless function (backend)
├── _data/
│   └── site.json                # Site configuration
├── js/
│   └── lastfm-recent.js         # Dynamic updates (frontend)
├── [templates-directory]/
│   └── music.njk                # Page template
├── .env                         # API keys (local, not committed)
├── .gitignore                   # Includes .env
└── netlify.toml                 # Netlify configuration

The Result

After implementing this, I have:

  • ✅ A music page that updates automatically as I listen
  • ✅ No need to rebuild my site to show new tracks
  • ✅ Secure API key management
  • ✅ Fast page loads (static HTML)
  • ✅ Live data updates (serverless functions + JavaScript)
  • ✅ Mobile-friendly responsive design
  • ✅ Easy to customize and maintain

All without compromising the benefits of a static site!


Potential Extensions

Want to take this further? Here are some ideas:

  1. Add more track details - Display genre, length, or play count
  2. Add track links - Link to songs on Last.fm or Spotify
  3. Show listening stats - Calculate total plays, favorite artist, etc.
  4. Add filters - Let visitors filter by artist or time period
  5. Visual enhancements - Add animations when new tracks appear
  6. Embed players - Use Spotify/YouTube APIs to let visitors preview songs

Conclusion

This project demonstrates how you can add dynamic, real-time data to a static site without sacrificing performance, security, or simplicity. By using serverless functions as a secure proxy and client-side JavaScript for interactivity, we get the best of both worlds: the speed and simplicity of static sites with the dynamic capabilities of traditional web apps.

The result is a music page that automatically updates as I listen to music, respects API rate limits, keeps my API keys secure, and provides a smooth user experience—all while keeping my site blazingly fast.


Resources

Hello! I’m Brandon Templar, a product designer in Washington, D.C.

I am a designer, photographer, and tech enthusiast that has decided to write more about my thoughts and process. Thanks for following along!