Personal site built with Astro and deployed on Cloudflare Workers.
npm install
npm run devnpm run build
npm run preview
npm run check
npm run deploy
npm run cf-typegendev: Astro dev serverbuild: Astro production buildpreview: build +wrangler devagainst the Worker outputcheck: build + TypeScript check +wrangler deploy --dry-rundeploy: build + Cloudflare deploycf-typegen: regenerate Worker env types viawrangler types
Required secrets:
wrangler secret put NAVIDROME_BASE_URL
wrangler secret put NAVIDROME_USERNAME
wrangler secret put NAVIDROME_TOKEN
wrangler secret put NAVIDROME_SALTOptional vars in wrangler.json:
NAVIDROME_CLIENT_NAME(default:learningis1.st)NAVIDROME_API_VERSION(default:1.16.1)
Subsonic auth uses token params (u, t, s) and requests are scoped to NAVIDROME_USERNAME only.
ASSETS(required): static asset binding used by the Astro Cloudflare adapterLAST_LISTENED_KV(optional): stores the latest track for idle fallback
Create KV namespaces:
wrangler kv namespace create LAST_LISTENED_KV
wrangler kv namespace create LAST_LISTENED_KV --previewThen set IDs in wrangler.json:
{
"kv_namespaces": [
{
"binding": "LAST_LISTENED_KV",
"id": "<production-namespace-id>",
"preview_id": "<preview-namespace-id>"
}
]
}If LAST_LISTENED_KV is missing, /api/now-playing.json still works but fallback becomes idle when live playback cannot be read.
GET /.well-known/api-catalog- returns RFC 9727 API discovery metadata as
application/linkset+json - includes
service-desc,service-doc, andstatusrelations for the/apianchor
- returns RFC 9727 API discovery metadata as
GET /api/openapi.json- serves an OpenAPI 3.1 document for the currently available API endpoints
GET /api/now-playing.json- returns
200for normal/fallback payloads - returns
500when required Navidrome env vars are missing
- returns
GET /api/navidrome/cover-art/:id- authenticated cover art proxy (
getCoverArt.view), 128px request size - validates
:id([A-Za-z0-9:_-]+), returns400for invalid IDs
- authenticated cover art proxy (
GET /api/health/navidrome.json- pings
ping.viewand validates Subsonicstatus: "ok" - returns
ok,latencyMs, and error details for failed upstream checks
- pings
GET /api/now-playing.json returns this shape:
{
"isPlaying": true,
"source": "playing",
"track": {
"title": "It’s Safe to Say You Dig the Backseat",
"artist": "Dance Gavin Dance",
"album": "Downtown Battle Mountain",
"albumArtist": "Dance Gavin Dance",
"coverArtId": "mf-fOzNstsKlApI8DW25h2OZS_69b9b8a0",
"durationSeconds": 314
},
"lastPlayedAt": "2026-03-26T20:53:50.795Z",
"progress": {
"positionSeconds": 120,
"percent": 38
}
}Notes:
sourceis one ofplaying,last_played, oridletrack.artistprefers SubsonicdisplayArtistthenartisttrack.albumArtistprefersdisplayAlbumArtistthentrack.artistprogressisnullwhen not actively playing
wrangler.json configures cron */4 * * * *.
The Worker scheduled handler (src/worker.ts) runs syncLastListened to refresh KV so the widget can keep showing the last track even when no user is currently browsing the site.
- Home page:
src/pages/index.astro - Now playing UI:
src/components/NowPlaying.astro - Now playing client logic:
src/components/now-playing.client.ts - Server now-playing logic:
src/lib/now-playing/server.ts - Navidrome helpers:
src/lib/navidrome/index.ts - Worker entry/cron:
src/worker.ts - Worker sync job:
src/worker/syncLastListened.ts - Worker config:
wrangler.json
- Node.js
>=22