perf: enable Next.js image optimization across backoffice

Remove `unoptimized` flag from all thumbnail/cover Image components
and add proper responsive `sizes` props. Convert raw `<img>` tags on
the libraries page to next/image. Add 24h minimumCacheTTL for
optimized images. BookPreview keeps `unoptimized` since the API
already returns optimized WebP.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 12:57:10 +01:00
parent b9e54cbfd8
commit b0185abefe
9 changed files with 20 additions and 12 deletions

View File

@@ -95,7 +95,7 @@ export default async function AuthorDetailPage({
alt={s.name}
fill
className="object-cover"
unoptimized
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
/>
</div>
<div className="p-3">

View File

@@ -95,7 +95,7 @@ export default async function BookDetailPage({
alt={t("bookDetail.coverOf", { title: book.title })}
fill
className="object-cover"
unoptimized
sizes="192px"
loading="lazy"
/>
</div>

View File

@@ -170,7 +170,7 @@ export default async function BooksPage({
alt={t("books.coverOf", { name: s.name })}
fill
className="object-cover"
unoptimized
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
/>
</div>
<div className="p-2">

View File

@@ -51,7 +51,6 @@ function BookImage({ src, alt }: { src: string; alt: string }) {
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
onLoad={() => setIsLoaded(true)}
onError={() => setHasError(true)}
unoptimized
/>
</div>
);

View File

@@ -94,7 +94,7 @@ export default async function SeriesDetailPage({
alt={t("books.coverOf", { name: displayName })}
fill
className="object-cover"
unoptimized
sizes="160px"
/>
</div>
</div>

View File

@@ -86,7 +86,7 @@ export default async function LibrarySeriesPage({
alt={t("books.coverOf", { name: s.name })}
fill
className="object-cover"
unoptimized
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 20vw"
/>
</div>
<div className="p-3">

View File

@@ -1,4 +1,5 @@
import { revalidatePath } from "next/cache";
import Image from "next/image";
import Link from "next/link";
import { listFolders, createLibrary, deleteLibrary, fetchLibraries, getBookCoverUrl, LibraryDto, FolderItem } from "../../lib/api";
import type { TranslationKey } from "../../lib/i18n/fr";
@@ -88,10 +89,12 @@ export default async function LibrariesPage() {
{/* Thumbnail fan */}
{thumbnails.length > 0 ? (
<Link href={`/libraries/${lib.id}/series`} className="block relative h-48 overflow-hidden bg-muted/10">
<img
<Image
src={thumbnails[0]}
alt=""
className="absolute inset-0 w-full h-full object-cover blur-xl scale-110 opacity-40"
fill
className="object-cover blur-xl scale-110 opacity-40"
sizes="(max-width: 768px) 100vw, 33vw"
loading="lazy"
/>
<div className="absolute inset-0 flex items-end justify-center">
@@ -104,17 +107,20 @@ export default async function LibrariesPage() {
const cx = Math.cos(rad) * radius;
const cy = Math.sin(rad) * radius;
return (
<img
<Image
key={i}
src={url}
alt=""
className="absolute w-24 h-36 object-cover shadow-lg"
width={96}
height={144}
className="absolute object-cover shadow-lg"
style={{
transform: `translate(${cx}px, ${cy}px) rotate(${angle}deg)`,
transformOrigin: 'bottom center',
zIndex: count - Math.abs(Math.round(i - mid)),
bottom: '-185px',
}}
sizes="96px"
loading="lazy"
/>
);

View File

@@ -138,7 +138,7 @@ export default async function SeriesPage({
alt={t("books.coverOf", { name: s.name })}
fill
className="object-cover"
unoptimized
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
/>
</div>
<div className="p-3">

View File

@@ -1,7 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
typedRoutes: true
typedRoutes: true,
images: {
minimumCacheTTL: 86400,
},
};
export default nextConfig;