Adding a Last.fm Now Playing Widget to Your Website
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:
- Astro Component: Handles the UI and client-side JavaScript
- Server-side API Endpoint: Securely accesses your API key and proxies requests
- 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:
---
// 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:
// 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:
<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:
// 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:
npm install @astrojs/cloudflare
Part 5: Integration
To use the component in your site, simply import and include it with your Last.fm username:
---
// 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:
- Register your application at Last.fm API
- Get your API key from the account dashboard
- Store it securely (never commit API keys to your repository)
Local Development
For local development, create a .env
file in your project root:
PUBLIC_LASTFM_API_KEY=your_actual_api_key_here
Production Deployment (Cloudflare Pages)
For production, use Cloudflare’s secret management:
-
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
-
Via Wrangler CLI:
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:
/* 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 containsPUBLIC_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
:
const runtime = (context.locals as any).runtime;
Missing Dependencies
If you encounter Cannot find module 'tslib'
error:
npm install tslib
Best Practices
When implementing a Now Playing widget, consider these best practices:
-
Security: Never expose your API key in client-side code - always use a server endpoint
-
Rate limiting: Last.fm allows 5 requests per second per API key. The 10-second interval ensures you stay well within limits
-
Caching: The endpoint includes
no-cache
headers to ensure fresh data -
Privacy: Only display information you’re comfortable sharing publicly
-
Fallback content: Always provide meaningful fallback text for error states
-
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:
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.