A simple API to fetch the most recent diary entry from a Letterboxd user's diary page.
This API scrapes the Letterboxd diary page for user "pinjasaur" and returns the most recent film entry in a clean JSON format. Perfect for displaying your latest watched movie on a personal website or blog.
GET /
Returns the most recent diary entry as JSON.
GET /poster?url=<encoded_image_url>
Proxies Letterboxd poster images to avoid CORS restrictions. Returns a placeholder SVG if the original image is unavailable. The main API automatically returns proxied URLs, but you can use this endpoint directly if needed.
{ "title": "Long Story Short", "year": 2021, "rating": 3, "watchDate": "2026-01-15", "posterUrl": null, "filmUrl": "https://letterboxd.com/pinjasaur/film/long-story-short-2021/", "scrapedAt": "2026-01-16T15:31:10.959Z" }
Note: posterUrl will be null when no poster image is available, or a proxied URL when an image exists.
- title: Film title (string | null)
- year: Release year (number | null)
- rating: Star rating from 0.5 to 5 stars (number | null)
- watchDate: Date watched in YYYY-MM-DD format (string | null)
- posterUrl: Proxied poster image URL that works around CORS restrictions (string | null)
- filmUrl: Link to the film's Letterboxd page (string | null)
- scrapedAt: ISO timestamp of when the data was scraped (string)
fetch('https://your-val-town-url.web.val.run/')
.then(response => response.json())
.then(data => {
console.log(`Latest watched: ${data.title} (${data.year})`);
console.log(`Rating: ${data.rating}/5 stars`);
});
<div id="latest-movie"></div> <script> async function loadLatestMovie() { try { const response = await fetch('https://your-val-town-url.web.val.run/'); const movie = await response.json(); document.getElementById('latest-movie').innerHTML = ` <div class="movie-card"> ${movie.posterUrl ? `<img src="${movie.posterUrl}" alt="${movie.title}" width="150">` : '<div style="width:150px;height:225px;background:#2c3440;display:flex;align-items:center;justify-content:center;color:#9ab;font-size:24px;">π¬</div>'} <h3>${movie.title} (${movie.year})</h3> <p>Rating: ${'β '.repeat(movie.rating)}${'β'.repeat(5-movie.rating)}</p> <p>Watched: ${movie.watchDate}</p> <a href="${movie.filmUrl}" target="_blank">View on Letterboxd</a> </div> `; } catch (error) { console.error('Failed to load latest movie:', error); } } loadLatestMovie(); </script>
The API includes caching headers:
- Cache-Control:
public, max-age=300(5 minutes) - CORS: Enabled for all origins
The API returns appropriate HTTP status codes:
- 200: Success
- 404: No diary entries found
- 500: Server error (network issues, parsing errors, etc.)
Error responses include a descriptive message:
{ "error": "No diary entries found", "message": "The diary appears to be empty or the page structure has changed." }
- Built with TypeScript on Val Town
- Scrapes HTML from
https://letterboxd.com/pinjasaur/diary/ - Uses regex patterns to extract structured data
- Handles various edge cases and fallbacks
- Image Proxy: Automatically proxies poster images to avoid CORS issues
- Fallback Images: Returns placeholder SVG when original posters are unavailable
- Returns high-resolution poster images (300px width) when available
- Only works for public Letterboxd profiles
- Currently hardcoded to user "pinjasaur"
- Dependent on Letterboxd's HTML structure (may break if they change their layout)
- Rate limited by Letterboxd's servers
- No authentication or private diary support
Potential improvements could include:
- Support for multiple users via query parameters
- Pagination to get multiple recent entries
- Additional metadata extraction (genres, director, etc.)
- Webhook support for real-time updates
- Better error handling and retry logic