Serve article images locally: /images proxy route with SSRF-guarded

upstream fetch, magic-byte validation, persistent /data/media cache,
immutable cache headers, CoverImage fallback component, og:image to
absolute proxy URL. Ported from technews 6f08da3.
This commit is contained in:
2026-08-18 18:11:44 -04:00
parent 92373aa3c6
commit 86a77b4907
11 changed files with 575 additions and 12 deletions
+7 -4
View File
@@ -4,7 +4,8 @@ import { notFound } from 'next/navigation';
import { getArticleFull, getRelatedArticles } from '@/lib/queries';
import RelatedArticles from '@/components/RelatedArticles';
import { InArticleAd, SidebarAd } from '@/components/ads/placements';
import { categoryCover, resolveCoverImage } from '@/lib/cover';
import { categoryCover, ogImageUrl, resolveCoverImage } from '@/lib/cover';
import { CoverImage } from '@/components/CoverImage';
import { env } from '@/lib/env';
import { formatFull } from '@/lib/format';
import { safeParse } from '@/lib/llm/parse';
@@ -58,7 +59,7 @@ export async function generateMetadata({
section: sectionLabel,
tags: a.tags ? safeParse<string[]>(a.tags).slice(0, 5) : undefined,
images: [{
url: (a.image && a.image.trim()) || categoryCover(a.category),
url: ogImageUrl(a.image, a.category),
width: 1200,
height: 630,
alt: title,
@@ -68,7 +69,7 @@ export async function generateMetadata({
card: 'summary_large_image',
title,
description,
images: [(a.image && a.image.trim()) || categoryCover(a.category)],
images: [ogImageUrl(a.image, a.category)],
},
};
}
@@ -106,9 +107,11 @@ export default async function ArticlePage({
</h1>
</header>
<img
<CoverImage
src={resolveCoverImage(a.image, a.category)}
category={a.category}
alt={title}
loading="eager"
className="mt-6 aspect-[16/8] w-full rounded-2xl object-cover"
/>