Add TMDB image caching pipeline and resolve image URLs server-side

Introduce a local disk cache for TMDB images served through a proxy API
route (/api/images/[...path]), eliminating direct client-side CDN
dependencies. Images are cached by category (posters, backdrops, stills,
logos) and served with immutable cache headers.

Move all tmdbImageUrl() calls from client components to API routes and
server components so clients receive ready-to-use URLs. This removes the
need to expose TMDB_IMAGE_BASE_URL and IMAGE_CACHE_ENABLED via
next.config.ts env block. The landing page is split into a server
wrapper (app/page.tsx) and client component (components/landing-page.tsx).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 13:10:17 -05:00
co-authored by Claude Opus 4.6
parent cb0f366d44
commit ee723e41d6
27 changed files with 665 additions and 242 deletions
+2 -4
View File
@@ -5,7 +5,6 @@ import { motion } from "motion/react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { tmdbImageUrl } from "@/lib/tmdb/image";
interface HeroBannerProps {
tmdbId: number;
@@ -26,7 +25,6 @@ export function HeroBanner({
}: HeroBannerProps) {
const router = useRouter();
const [importing, setImporting] = useState(false);
const backdropUrl = tmdbImageUrl(backdropPath, "w1280");
async function handleImport() {
setImporting(true);
@@ -53,9 +51,9 @@ export function HeroBanner({
transition={{ duration: 0.6 }}
>
<div className="relative aspect-[21/9] min-h-[280px] max-h-[420px]">
{backdropUrl ? (
{backdropPath ? (
<Image
src={backdropUrl}
src={backdropPath}
alt={title}
fill
priority
+3 -2
View File
@@ -1,5 +1,6 @@
import { IconDeviceTv, IconFlame, IconMovie } from "@tabler/icons-react";
import { getGenres, getPopular, getTrending } from "@/lib/tmdb/client";
import { tmdbImageUrl } from "@/lib/tmdb/image";
import { GenreBrowser } from "./genre-browser";
import { HeroBanner } from "./hero-banner";
import { TitleRow } from "./title-row";
@@ -25,7 +26,7 @@ function mapResults(
? r.media_type
: fallbackType) as "movie" | "tv",
title: r.title ?? r.name ?? "",
posterPath: r.poster_path,
posterPath: tmdbImageUrl(r.poster_path, "w500"),
releaseDate: r.release_date ?? r.first_air_date ?? null,
voteAverage: r.vote_average,
}));
@@ -58,7 +59,7 @@ export default async function ExplorePage() {
type={heroTitle.media_type as "movie" | "tv"}
title={heroTitle.title ?? heroTitle.name ?? ""}
overview={heroTitle.overview}
backdropPath={heroTitle.backdrop_path}
backdropPath={tmdbImageUrl(heroTitle.backdrop_path, "w1280")}
voteAverage={heroTitle.vote_average}
/>
)}