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:
@@ -95,7 +95,7 @@ export default async function AuthorDetailPage({
|
|||||||
alt={s.name}
|
alt={s.name}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
unoptimized
|
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default async function BookDetailPage({
|
|||||||
alt={t("bookDetail.coverOf", { title: book.title })}
|
alt={t("bookDetail.coverOf", { title: book.title })}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
unoptimized
|
sizes="192px"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export default async function BooksPage({
|
|||||||
alt={t("books.coverOf", { name: s.name })}
|
alt={t("books.coverOf", { name: s.name })}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
unoptimized
|
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
|
|||||||
@@ -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"
|
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
|
||||||
onLoad={() => setIsLoaded(true)}
|
onLoad={() => setIsLoaded(true)}
|
||||||
onError={() => setHasError(true)}
|
onError={() => setHasError(true)}
|
||||||
unoptimized
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default async function SeriesDetailPage({
|
|||||||
alt={t("books.coverOf", { name: displayName })}
|
alt={t("books.coverOf", { name: displayName })}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
unoptimized
|
sizes="160px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export default async function LibrarySeriesPage({
|
|||||||
alt={t("books.coverOf", { name: s.name })}
|
alt={t("books.coverOf", { name: s.name })}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
unoptimized
|
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 20vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { listFolders, createLibrary, deleteLibrary, fetchLibraries, getBookCoverUrl, LibraryDto, FolderItem } from "../../lib/api";
|
import { listFolders, createLibrary, deleteLibrary, fetchLibraries, getBookCoverUrl, LibraryDto, FolderItem } from "../../lib/api";
|
||||||
import type { TranslationKey } from "../../lib/i18n/fr";
|
import type { TranslationKey } from "../../lib/i18n/fr";
|
||||||
@@ -88,10 +89,12 @@ export default async function LibrariesPage() {
|
|||||||
{/* Thumbnail fan */}
|
{/* Thumbnail fan */}
|
||||||
{thumbnails.length > 0 ? (
|
{thumbnails.length > 0 ? (
|
||||||
<Link href={`/libraries/${lib.id}/series`} className="block relative h-48 overflow-hidden bg-muted/10">
|
<Link href={`/libraries/${lib.id}/series`} className="block relative h-48 overflow-hidden bg-muted/10">
|
||||||
<img
|
<Image
|
||||||
src={thumbnails[0]}
|
src={thumbnails[0]}
|
||||||
alt=""
|
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"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 flex items-end justify-center">
|
<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 cx = Math.cos(rad) * radius;
|
||||||
const cy = Math.sin(rad) * radius;
|
const cy = Math.sin(rad) * radius;
|
||||||
return (
|
return (
|
||||||
<img
|
<Image
|
||||||
key={i}
|
key={i}
|
||||||
src={url}
|
src={url}
|
||||||
alt=""
|
alt=""
|
||||||
className="absolute w-24 h-36 object-cover shadow-lg"
|
width={96}
|
||||||
|
height={144}
|
||||||
|
className="absolute object-cover shadow-lg"
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(${cx}px, ${cy}px) rotate(${angle}deg)`,
|
transform: `translate(${cx}px, ${cy}px) rotate(${angle}deg)`,
|
||||||
transformOrigin: 'bottom center',
|
transformOrigin: 'bottom center',
|
||||||
zIndex: count - Math.abs(Math.round(i - mid)),
|
zIndex: count - Math.abs(Math.round(i - mid)),
|
||||||
bottom: '-185px',
|
bottom: '-185px',
|
||||||
}}
|
}}
|
||||||
|
sizes="96px"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export default async function SeriesPage({
|
|||||||
alt={t("books.coverOf", { name: s.name })}
|
alt={t("books.coverOf", { name: s.name })}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
unoptimized
|
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, (max-width: 1024px) 25vw, 16vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
typedRoutes: true
|
typedRoutes: true,
|
||||||
|
images: {
|
||||||
|
minimumCacheTTL: 86400,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Reference in New Issue
Block a user