Cómo añadir un widget de Spotify "Now Playing" a tu web personal (paso a paso)
Añadir un indicador de lo que estás escuchando en Spotify en el pie de página o en el perfil de tu web es un detalle personal muy agradecido. En sitios estáticos (creados con Astro, Next.js, Hugo o HTML plano), el reto consiste en consultar la API de Spotify de forma dinámica sin exponer tus credenciales secretas en el código público.
En este tutorial explico la arquitectura completa paso a paso: cómo obtener el token de acceso permanente, crear un endpoint serverless y montar un componente en frontend con actualización en vivo.
Cómo funciona la arquitectura
- Spotify API: Servicio que informa de la canción en reproducción (
user-read-currently-playing) o la última escuchada (user-read-recently-played). - Serverless Function (
/api/spotify): Endpoint intermedio en tu hosting (Vercel, Netlify o Cloudflare Workers) que almacena tus claves en secreto, pide un token nuevo a Spotify y devuelve un JSON limpio. - Componente Frontend: Elemento en tu web que consulta ese endpoint y actualiza el título, artista y animación del ecualizador.
[ Navegador del usuario ]
↓ (fetch cada 30s)
[ /api/spotify (Serverless Edge) ]
↓ (con Refresh Token)
[ Spotify Accounts API ] → Devuelve Access Token
↓
[ Spotify Web API ] → Consulta canción actual
Paso 1: Crear una App en Spotify Developer Dashboard
- Accede a Spotify Developer Dashboard e inicia sesión con tu cuenta de Spotify.
- Haz clic en Create App:
- App name:
Mi Web Personal(o el nombre que prefieras). - App description:
Widget Now Playing. - Redirect URIs: Añade
https://localhost:3000/callback(ohttps://tu-dominio.com/callback). - Marca la casilla de Developer Terms of Service y pulsa Save.
- App name:
- En la pestaña de configuración (Settings) de tu app, copia:
- Client ID
- Client Secret (pulsa en View client secret).
Paso 2: Obtener tu Refresh Token permanente
Para que el servidor pueda consultar tu actividad musical sin pedirte iniciar sesión cada vez, necesitas un Refresh Token de un solo uso con los permisos (scopes) adecuados:
user-read-currently-playing: Saber qué suena en este momento.user-read-recently-played: Saber cuál fue la última canción si Spotify está en pausa.
1. Autorizar la app en tu navegador
Pega la siguiente URL en tu navegador sustituyendo TU_CLIENT_ID y TU_REDIRECT_URI:
https://accounts.spotify.com/authorize?client_id=TU_CLIENT_ID&response_type=code&redirect_uri=https%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=user-read-currently-playing%20user-read-recently-played
Inicia sesión y pulsa en Aceptar. El navegador te redirigirá a una URL como esta:
https://localhost:3000/callback?code=AQD...TU_CODIGO_TEMPORAL...
Copia todo el valor que aparece tras code=.
2. Canjear el código por el Refresh Token
Crea un archivo temporal get-token.mjs en tu ordenador con el siguiente código:
const CLIENT_ID = 'TU_CLIENT_ID';
const CLIENT_SECRET = 'TU_CLIENT_SECRET';
const REDIRECT_URI = 'https://localhost:3000/callback';
const CODE = 'EL_CODIGO_QUE_COPIASTE';
async function getRefreshToken() {
const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
const response = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
Authorization: `Basic ${basic}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: CODE,
redirect_uri: REDIRECT_URI,
}),
});
const data = await response.json();
console.log('Tu Refresh Token es:', data.refresh_token);
}
getRefreshToken();
Ejecuta en terminal node get-token.mjs. Guarda el refresh_token obtenido y elimina el archivo temporal.
Paso 3: Crear el Endpoint Serverless (/api/spotify.ts)
Si usas plataformas como Vercel o Next.js, crea el archivo en api/spotify.ts:
export const config = {
runtime: 'edge',
};
const CLIENT_ID = process.env.SPOTIFY_CLIENT_ID;
const CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET;
const REFRESH_TOKEN = process.env.SPOTIFY_REFRESH_TOKEN;
const TOKEN_ENDPOINT = 'https://accounts.spotify.com/api/token';
const NOW_PLAYING_ENDPOINT = 'https://api.spotify.com/v1/me/player/currently-playing';
const RECENTLY_PLAYED_ENDPOINT = 'https://api.spotify.com/v1/me/player/recently-played?limit=1';
async function getAccessToken(): Promise<string> {
const basic = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
const response = await fetch(TOKEN_ENDPOINT, {
method: 'POST',
headers: {
Authorization: `Basic ${basic}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: REFRESH_TOKEN || '',
}),
});
const data = await response.json();
return data.access_token;
}
export default async function handler() {
try {
const accessToken = await getAccessToken();
// 1. Intentar consultar la canción actual
const nowPlayingRes = await fetch(NOW_PLAYING_ENDPOINT, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (nowPlayingRes.status === 200) {
const song = await nowPlayingRes.json();
if (song.item && song.is_playing) {
return new Response(
JSON.stringify({
isPlaying: true,
title: song.item.name,
artist: song.item.artists.map((a: any) => a.name).join(', '),
songUrl: song.item.external_urls.spotify,
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, s-maxage=15, stale-while-revalidate=30',
},
}
);
}
}
// 2. Fallback: última canción reproducida
const recentlyPlayedRes = await fetch(RECENTLY_PLAYED_ENDPOINT, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (recentlyPlayedRes.status === 200) {
const data = await recentlyPlayedRes.json();
const lastTrack = data.items?.[0]?.track;
if (lastTrack) {
return new Response(
JSON.stringify({
isPlaying: false,
title: lastTrack.name,
artist: lastTrack.artists.map((a: any) => a.name).join(', '),
songUrl: lastTrack.external_urls.spotify,
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120',
},
}
);
}
}
return new Response(JSON.stringify({ isPlaying: false }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ isPlaying: false, error: 'Error fetching track' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
La cabecera Cache-Control: s-maxage=15 cachea las respuestas en la CDN de Vercel/Cloudflare durante 15 segundos, evitando saturar los límites de la API de Spotify si recibes muchas visitas simultáneas.
Paso 4: Configurar Variables de Entorno
En local, añade a tu archivo .env:
SPOTIFY_CLIENT_ID=tu_client_id_aqui
SPOTIFY_CLIENT_SECRET=tu_client_secret_aqui
SPOTIFY_REFRESH_TOKEN=tu_refresh_token_aqui
En tu panel de despliegue (por ejemplo, en Vercel → Project Settings → Environment Variables), añade esas mismas tres variables para los entornos de producción y preview.
Paso 5: Montar el Componente en Frontend con Auto-Refresco
Este componente (ejemplo en Astro / HTML / Tailwind) incluye ecualizador animado y actualiza la canción automáticamente cuando la pestaña está en primer plano:
<div id="spotify-widget" class="hidden items-center gap-2 text-xs text-slate-500">
<span id="spotify-icon">
<!-- SVG de Spotify o barras de ecualizador -->
</span>
<span id="spotify-status">Escuchando:</span>
<a
id="spotify-link"
href="#"
target="_blank"
rel="noopener noreferrer"
class="font-medium text-slate-700 hover:underline truncate max-w-[220px]"
>
<span id="spotify-title"></span> — <span id="spotify-artist"></span>
</a>
</div>
<script>
let intervalId: number | null = null;
async function updateSpotify() {
try {
const res = await fetch('/api/spotify');
if (!res.ok) return;
const data = await res.json();
const widget = document.getElementById('spotify-widget');
const title = document.getElementById('spotify-title');
const artist = document.getElementById('spotify-artist');
const link = document.getElementById('spotify-link') as HTMLAnchorElement;
const status = document.getElementById('spotify-status');
if (!data.title) {
widget?.classList.add('hidden');
return;
}
widget?.classList.remove('hidden');
widget?.classList.add('inline-flex');
if (title) title.textContent = data.title;
if (artist) artist.textContent = data.artist;
if (link) link.href = data.songUrl || '#';
if (status) status.textContent = data.isPlaying ? 'Escuchando:' : 'Última escuchada:';
} catch (e) {
console.error('Error cargando Spotify:', e);
}
}
function startPolling() {
updateSpotify();
if (!intervalId) {
intervalId = window.setInterval(updateSpotify, 30000); // 30 segundos
}
}
function stopPolling() {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
// Pausar peticiones si la pestaña no está activa para ahorrar recursos
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
stopPolling();
} else {
startPolling();
}
});
startPolling();
</script>
Conclusión
Con esta configuración consigues:
- Seguridad total: Tus claves secretas y tokens nunca llegan al navegador del cliente.
- Rendimiento óptimo: Respuestas cacheadas en CDN con Edge runtime para tiempos de respuesta menores a 50ms.
- Experiencia viva: La información se actualiza automáticamente cada 30 segundos si el usuario tiene la web abierta mientras escucha música.