Skip to content

feat(images): load TMDB images through a custom loader - #48

Open
kris1027 wants to merge 10 commits into
mainfrom
feat/tmdb-image-loader
Open

kris1027 wants to merge 10 commits into
mainfrom
feat/tmdb-image-loader

Conversation

@kris1027

@kris1027 kris1027 commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

What

Every next/image now loads straight from TMDB's CDN through a custom loader, so Vercel Image Optimization does no transformations. The privacy policy changes in the same PR, because visitors' browsers now talk to TMDB directly.

Why

The Vercel Hobby quota is 5,000 image transformations a month, and we were at 5.1K. Each unique TMDB image counts once per srcset width and format, so a catalog this size passes the quota at any traffic level. TMDB already serves every image in fixed sizes (w92 up to w1280) and returns WebP when the browser accepts it, so Vercel's resize added nothing.

Plan: prompts/tmdb-image-loader.md.

Changes

The loader (lib/tmdb/image-loader.ts, new)

  • Maps the width next/image asks for onto the smallest TMDB size that fits, from w92, w154, w185, w300, w342, w500, w780, w1280.
  • The size lib/tmdb/images.ts pinned for the field is the ceiling. A w500 poster is never upgraded on a dense screen, so no image gets heavier than it is today.
  • original is capped at w1280 and never requested as is, since it can be several MB.
  • Any URL that is not a TMDB size path (another host, h632, w45, the local logo) comes back unchanged, so the loader never builds a URL for a host it was not given.
  • When the fitting size is the source's own size, it appends a #tmdb fragment. Next warns in dev that a loader "does not implement width" whenever it returns src unchanged. The fragment never reaches TMDB, so the request and its cache entry stay the same.

Config (next.config.ts)

  • images.loader: "custom" and loaderFile: "./lib/tmdb/image-loader.ts". remotePatterns stays as the record of the one image host.

Privacy policy (lib/legal/operator.ts, app/privacy/page.tsx)

  • TMDB joins PROCESSORS: it delivers catalog images straight to your browser, which shares your IP address with it.
  • Vercel's purpose drops "delivers catalog images".
  • The "Who processes your data" paragraph now says the server makes the catalog requests (so TMDB never gets account or tracking data), while the browser loads images from TMDB, which sees the IP address and browser details.
  • "Transfers outside the EEA" notes TMDB is a United States company whose CDN may serve images from outside the EEA.
  • LEGAL_LAST_UPDATED moves to 2026-10-16.

Docs and tests

  • lib/tmdb/AGENTS.md records the loader and its ceiling rule.
  • image-loader.test.ts covers the width ladder, the ceiling, original, the #tmdb mark and pass through URLs. next.config.test.ts pins the loader setting. page.test.tsx checks the new privacy wording.

How to test / verify

You may want to run these yourself:

  1. pnpm typecheck && pnpm lint && pnpm test && pnpm build.
  2. pnpm dev, open /shows, and in DevTools, Network, Img: every poster comes from image.tmdb.org/t/p/w…/…, and nothing hits /_next/image. The console shows no "does not implement width" warning.
  3. Inspect a poster <img>: its srcset lists TMDB sizes, none above w500.
  4. Open a show page: the backdrop loads at w780 or w1280, cast photos at w92 to w185, episode stills at up to w300.
  5. Switch to a mobile viewport and reload: smaller sizes are fetched.
  6. Open a title with no poster: the placeholder still shows.
  7. Open /privacy: TMDB is listed and "Last updated" reads 16 October 2026.
  8. After deploy, Vercel, Usage, Image Optimization: the transformation count stops growing.

Results recorded when this branch was built: typecheck, lint, test (168 files, 2722 tests) and build passed. Under pnpm start, /shows and a show page had 0 /_next/image URLs and every srcset pointed at image.tmdb.org; a sample of 40 URLs all returned 200. Under pnpm dev, the width warning count went from 52 to 0 after the fragment fix.

Risk & rollout

Low code risk, no migrations, no flags. Layout, sizes, placeholders and missing image states are untouched. The real change is that visitors' IP addresses now reach TMDB's image CDN, which the policy text covers. Rollback is reverting the PR: images go back through Vercel's optimizer and the old privacy text returns.

Notes for reviewers

  • Please read the privacy wording closely. It is the operator's legal text, and it now names a new processor.
  • The ceiling rule means wide srcset entries can collapse to the same URL. That is on purpose; the browser dedupes them.
  • quality is ignored, since TMDB has no quality parameter.

🤖 Generated with Claude Code

kris1027 and others added 5 commits October 16, 2026 20:27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e optimizer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dress

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idth

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027 kris1027 added docs Documentation additions and updates feature New user-facing functionality performance Load time, responsiveness, and resource usage improvements labels Oct 16, 2026
@kris1027 kris1027 self-assigned this Oct 16, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in BeStats Oct 16, 2026
@vercel

vercel Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
bestats Ready Ready Preview Oct 16, 2026 7:09pm UTC

@kris1027 kris1027 moved this from Backlog to In review in BeStats Oct 16, 2026
@kris1027 kris1027 changed the title feat(images): serve TMDB images through a custom loader, off Vercel's optimizer feat(images): load TMDB images through a custom loader Oct 16, 2026
kris1027 and others added 5 commits October 16, 2026 21:02
…loader

The loader's ladder had w342 the size union lacked, and both files split a
TMDB URL by hand. images.ts now owns the widths and splitImageUrl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#tmdb said nothing about why it is there; #same-size does. The loader's
JSDoc now says Next's width warning flags a pass-through source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The folder header now names the one browser file, pass-through sources are
said to warn in dev, and AC-1/AC-2 expect the #same-size fragment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four files spelled https://www.themoviedb.org; TMDB_HOME_URL now does, and
the boundary test fails if a copy reappears.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/privacy wrote Supabase, Vercel and TMDB's country in page text; each
processor now carries its transfer sentence and the page renders them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 31ba78d7 Deployed Oct 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation additions and updates feature New user-facing functionality performance Load time, responsiveness, and resource usage improvements

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

1 participant