Archived
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.
48 lines
1.8 KiB
TypeScript
48 lines
1.8 KiB
TypeScript
import Link from 'next/link';
|
|
import type { ArticleCard as Card } from '@/lib/queries';
|
|
import { resolveCoverImage } from '@/lib/cover';
|
|
import { CoverImage } from '@/components/CoverImage';
|
|
import { formatRelativeTime } from '@/lib/format';
|
|
|
|
export default function ArticleCard({ card }: { card: Card }) {
|
|
const href = `/article/${card.slug}`;
|
|
const title = card.headline ?? card.title;
|
|
const image = resolveCoverImage(card.image, card.category);
|
|
return (
|
|
<article className="group flex flex-col overflow-hidden rounded-xl border border-ink-200 bg-white shadow-sm transition hover:-translate-y-0.5 hover:shadow-md">
|
|
<Link
|
|
href={href}
|
|
className="relative block aspect-[16/9] overflow-hidden bg-ink-100"
|
|
>
|
|
<CoverImage
|
|
src={image}
|
|
alt=""
|
|
className="h-full w-full object-cover transition duration-300 group-hover:scale-[1.03]"
|
|
/>
|
|
</Link>
|
|
<div className="flex flex-1 flex-col p-4">
|
|
<h3 className="font-display text-lg font-bold leading-snug text-ink-900 group-hover:text-maple-700">
|
|
<Link href={href}>{title}</Link>
|
|
</h3>
|
|
<p className="mt-2 flex-1 text-sm leading-relaxed text-ink-600">
|
|
{card.excerpt}
|
|
</p>
|
|
<div className="mt-3 flex items-center justify-between text-xs text-ink-500">
|
|
<span>{card.publishedAt ? formatRelativeTime(card.publishedAt) : 'recent'}</span>
|
|
<span className="flex gap-1">
|
|
{card.tags.slice(0, 2).map((t) => (
|
|
<Link
|
|
key={t}
|
|
href={`/tag/${t}`}
|
|
className="rounded bg-ink-100 px-1.5 py-0.5 hover:bg-maple-100 hover:text-maple-800"
|
|
>
|
|
{t}
|
|
</Link>
|
|
))}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|