# Adding a Last.fm Now Playing Widget to Your Website

> Learn how to integrate a dynamic "Now Playing" widget that displays your current Last.fm track with real-time updates using the Last.fm API

- Page: https://midgarcorp.cc/blog/adding-lastfm-now-playing-to-your-website/
- Author: guitaripod (Midgar Oy)
- Published: 2025-06-07
- Tags: last.fm, api, javascript, astro, web-development, music, real-time

Music is a fundamental part of many developers' workflows, and sharing what you're currently listening to can add a personal touch to your website. In this guide, we'll build a dynamic "Now Playing" widget that displays your current Last.fm track with real-time updates, perfect for your personal site or portfolio.

## Why Add a Now Playing Widget?

Adding a music widget to your personal site serves several purposes:

- **Personal connection**: Shows visitors what inspires you while you work
- **Dynamic content**: Keeps your site feeling alive with real-time updates
- **Low maintenance**: Once set up, it runs automatically without intervention
- **Conversation starter**: Creates common ground with fellow music enthusiasts

## Architecture Overview

Our Now Playing widget consists of three main components:

1. **Astro Component**: Handles the UI and client-side JavaScript
2. **Server-side API Endpoint**: Securely accesses your API key and proxies requests
3. **Last.fm API**: Provides real-time track data

The flow works like this:

```
Browser → Your API Endpoint → Last.fm API
   ↑                               ↓
   └─── Update UI ← Parse Response ┘
          ↑                        ↓
          └──── 10s interval ──────┘
```

This architecture keeps your API key secure on the server while providing real-time updates to the client.

## Implementation

Let's build the widget step by step. We'll create an Astro component that can be easily integrated into any page.

### Part 1: Creating the Component

First, create a new Astro component for the Now Playing widget:

```astro
---
// src/components/LastFmNowPlaying.astro
export interface Props {
  username: string;
}

const { username } = Astro.props;
---

<div id="lastfm-now-playing" class="text-xs text-gray-600 dark:text-gray-400">
  <div class="flex items-center gap-2">
    <svg class="w-4 h-4 animate-pulse text-green-500" fill="currentColor" viewBox="0 0 24 24">
      <circle cx="12" cy="12" r="3"></circle>
      <path
        d="M12 6a6 6 0 0 0-6 6c0 1.66.67 3.16 1.76 4.24l1.42-1.42A4 4 0 0 1 8 12a4 4 0 0 1 4-4V6z"
        opacity="0.6"></path>
      <path
        d="M12 2a10 10 0 0 0-10 10c0 2.76 1.12 5.26 2.93 7.07l1.42-1.42A8 8 0 0 1 4 12a8 8 0 0 1 8-8V2z"
        opacity="0.3"></path>
    </svg>
    <span id="track-info" class="italic">Loading...</span>
  </div>
</div>
```

The component includes:

- **Animated icon**: A pulsing broadcast icon indicating live status
- **Loading state**: Shows "Loading..." while fetching data
- **Flexible styling**: Uses Tailwind classes that adapt to light/dark themes

### Part 2: Creating the API Endpoint

For production deployments, especially on platforms like Cloudflare Pages, we need to keep our API key secure. Create a server-side endpoint:

```typescript
// src/pages/api/lastfm.json.ts
import type { APIRoute } from 'astro';

export const prerender = false; // This endpoint runs on the server

export const GET: APIRoute = async (context) => {
  const url = new URL(context.request.url);
  const username = url.searchParams.get('username');

  if (!username) {
    return new Response(JSON.stringify({ error: 'Username required' }), {
      status: 400,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  // Access the API key from environment variables
  const runtime = (context.locals as any).runtime;
  const apiKey = runtime?.env?.PUBLIC_LASTFM_API_KEY || import.meta.env.PUBLIC_LASTFM_API_KEY;

  if (!apiKey) {
    return new Response(JSON.stringify({ error: 'Last.fm API key not configured' }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  try {
    const lastFmResponse = await fetch(
      `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${username}&api_key=${apiKey}&format=json&limit=1`
    );

    const data = await lastFmResponse.json();

    return new Response(JSON.stringify(data), {
      status: 200,
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 'no-cache, no-store, must-revalidate',
      },
    });
  } catch (error) {
    return new Response(JSON.stringify({ error: 'Failed to fetch Last.fm data' }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' },
    });
  }
};
```

### Part 3: Client-side JavaScript

Now update the component to use your API endpoint:

```astro
<script define:vars={{ username }}>
  async function fetchNowPlaying() {
    try {
      const response = await fetch(`/api/lastfm.json?username=${username}`);
      const data = await response.json();

      const trackElement = document.getElementById('track-info');
      if (!trackElement) return;

      if (data.error) {
        trackElement.textContent = 'Unable to load track info';
        return;
      }

      if (data.recenttracks && data.recenttracks.track) {
        // Handle both array and single object responses
        const tracks = Array.isArray(data.recenttracks.track)
          ? data.recenttracks.track
          : [data.recenttracks.track];

        if (tracks.length > 0) {
          const track = tracks[0];
          const isNowPlaying = track['@attr'] && track['@attr'].nowplaying === 'true';

          if (isNowPlaying) {
            trackElement.innerHTML = `Now playing: <strong>${track.name}</strong> by ${track.artist['#text']}`;
          } else {
            // Show last played track with timestamp
            const date = track.date ? new Date(track.date.uts * 1000) : null;
            const timeAgo = date ? getTimeAgo(date) : '';
            trackElement.innerHTML = `Last played: <strong>${track.name}</strong> by ${track.artist['#text']}${timeAgo ? ' · ' + timeAgo : ''}`;
          }
        } else {
          trackElement.textContent = 'No recent tracks';
        }
      } else {
        trackElement.textContent = 'No recent tracks';
      }
    } catch (error) {
      const trackElement = document.getElementById('track-info');
      if (trackElement) {
        trackElement.textContent = 'Unable to load track info';
      }
    }
  }

  function getTimeAgo(date) {
    const seconds = Math.floor((new Date() - date) / 1000);

    const intervals = {
      year: 31536000,
      month: 2592000,
      week: 604800,
      day: 86400,
      hour: 3600,
      minute: 60,
    };

    for (const [name, secondsInInterval] of Object.entries(intervals)) {
      const interval = Math.floor(seconds / secondsInInterval);
      if (interval >= 1) {
        return interval === 1 ? `1 ${name} ago` : `${interval} ${name}s ago`;
      }
    }

    return 'just now';
  }

  // Fetch immediately
  fetchNowPlaying();

  // Update every 10 seconds for better real-time updates
  setInterval(fetchNowPlaying, 10000);
</script>
```

Key implementation details:

- **Secure API endpoint**: Your server-side endpoint keeps the API key secure
- **Real-time detection**: Checks the `nowplaying` attribute to determine if a track is currently playing
- **Enhanced display**: Shows "Last played" with time ago when not currently playing
- **Robust parsing**: Handles both array and single object API responses
- **Error handling**: Gracefully handles network errors and missing data
- **Auto-refresh**: Updates every 10 seconds for near real-time updates

### Part 4: Astro Configuration

To enable server-side endpoints, update your Astro configuration:

```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
  // ... other config
  output: 'hybrid',
  adapter: cloudflare({
    mode: 'directory',
  }),
});
```

Don't forget to install the adapter:

```bash
npm install @astrojs/cloudflare
```

### Part 5: Integration

To use the component in your site, simply import and include it with your Last.fm username:

```astro
---
// In your page or component
import LastFmNowPlaying from '@components/LastFmNowPlaying.astro';
---

<!-- Basic usage -->
<LastFmNowPlaying username="yourusername" />

<!-- Integration with a link card -->
<div class="link-card">
  <h3>Last.fm</h3>
  <p>Music tracking and discovery</p>
  <div class="mt-auto pt-2 border-t border-gray-200 dark:border-gray-700">
    <LastFmNowPlaying username="yourusername" />
  </div>
</div>
```

## API Configuration

To use the Last.fm API, you'll need your own API key:

1. **Register your application** at [Last.fm API](https://www.last.fm/api/account/create)
2. **Get your API key** from the account dashboard
3. **Store it securely** (never commit API keys to your repository)

### Local Development

For local development, create a `.env` file in your project root:

```bash
PUBLIC_LASTFM_API_KEY=your_actual_api_key_here
```

### Production Deployment (Cloudflare Pages)

For production, use Cloudflare's secret management:

1. **Via Dashboard**:

   - Go to your Pages project → Settings → Environment variables
   - Add `PUBLIC_LASTFM_API_KEY` as a **Secret** (not plaintext)
   - Secrets are encrypted and only accessible at runtime

2. **Via Wrangler CLI**:
   ```bash
   wrangler pages secret put PUBLIC_LASTFM_API_KEY --project-name=your-project-name
   ```

**Important**: When using Cloudflare Pages with secrets, you must:

- Use the `hybrid` output mode in Astro
- Install and configure the Cloudflare adapter
- Create a server-side endpoint to access the secret

For other platforms, consult their documentation on setting environment variables.

## Styling Variations

The widget can be customized to match your site's design. Here are some variations:

```css
/* Minimal style */
.now-playing-minimal {
  font-size: 0.875rem;
  color: var(--text-secondary);
}

/* Card style */
.now-playing-card {
  padding: 1rem;
  background: var(--bg-secondary);
  border-radius: 0.5rem;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

/* Inline style */
.now-playing-inline {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}
```

## Troubleshooting Common Issues

### "API key missing" Error

If you see this in the console:

- **Local**: Ensure your `.env` file exists and contains `PUBLIC_LASTFM_API_KEY`
- **Production**: Check that the secret is properly set in Cloudflare Pages
- **Build vs Runtime**: Remember that secrets are only available at runtime, not during build

### "No recent tracks" When Playing Music

- Ensure your Last.fm account is set to public
- Check that your music player is scrobbling correctly
- The API sometimes returns tracks as a single object instead of an array

### TypeScript Errors

If you get TypeScript errors about `context.locals.runtime`:

```typescript
const runtime = (context.locals as any).runtime;
```

### Missing Dependencies

If you encounter `Cannot find module 'tslib'` error:

```bash
npm install tslib
```

## Best Practices

When implementing a Now Playing widget, consider these best practices:

1. **Security**: Never expose your API key in client-side code - always use a server endpoint

2. **Rate limiting**: Last.fm allows 5 requests per second per API key. The 10-second interval ensures you stay well within limits

3. **Caching**: The endpoint includes `no-cache` headers to ensure fresh data

4. **Privacy**: Only display information you're comfortable sharing publicly

5. **Fallback content**: Always provide meaningful fallback text for error states

6. **Performance**: The widget loads asynchronously and doesn't block page rendering

## Advanced Features

You can extend the widget with additional features:

- **Album artwork**: Display cover art using the `image` field from the API response
- **Track progress**: Show how long the track has been playing
- **Recently played**: Display a list of recent tracks when nothing is currently playing
- **Click to play**: Link to the track on Last.fm or Spotify

Example with album art:

```javascript
if (track.image && track.image[0]) {
  const albumArt = track.image[0]['#text'];
  const display = isNowPlaying
    ? `Now playing: <strong>${track.name}</strong>`
    : `Last played: <strong>${track.name}</strong>`;

  trackElement.innerHTML = `
    <div class="flex items-center gap-2">
      <img src="${albumArt}" alt="Album art" class="w-8 h-8 rounded">
      <div>
        ${display} by ${track.artist['#text']}
      </div>
    </div>
  `;
}
```

## Conclusion

Adding a Last.fm Now Playing widget is a simple way to bring dynamic, personal content to your website. With just a few lines of code, you can share your musical tastes with visitors and create a more engaging experience. The real-time updates keep your site feeling fresh, while the minimal implementation ensures it doesn't impact performance.

Whether you're building a personal portfolio, blog, or any site where you want to add a human touch, this widget provides an easy way to connect with visitors through the universal language of music.
