mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
Tags were decorative: a plain span on every card and post header, with no page, filter or search behind them, and a vocabulary of 32 slugs across 16 posts where 22 slugs appeared once. They now form a closed vocabulary of ten topics (src/lib/blog-tags.ts) that the content collection schema enforces, so an unknown slug fails the build instead of minting a new tag. Every used tag gets a hub at /blog/tag/<tag>/ with CollectionPage and BreadcrumbList structured data; the blog index and the hubs show a "Topics" rail with post counts; and every chip links to its hub. The cards become <article> elements with the title link stretched over the card, because chip links cannot nest inside a card that is one big <a>. Posts are retagged to the new vocabulary. A structural test covers the rail, each hub, the chip targets, the sitemap and the absence of nested anchors. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
92 lines
2.9 KiB
Plaintext
92 lines
2.9 KiB
Plaintext
---
|
|
import BlogTagChip from './BlogTagChip.astro';
|
|
import type { BlogTag } from '../lib/blog-tags';
|
|
|
|
interface Props {
|
|
title: string;
|
|
description: string;
|
|
pubDate: Date;
|
|
slug: string;
|
|
tags?: BlogTag[];
|
|
heroImage?: string;
|
|
}
|
|
|
|
const { title, description, pubDate, slug, tags = [], heroImage } = Astro.props;
|
|
|
|
const formattedDate = pubDate.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
});
|
|
---
|
|
|
|
{/* Same stretched-link pattern as BlogPostCard: the tag chips are links of their own. */}
|
|
<article class="card-panel group relative block overflow-hidden">
|
|
<div class="grid items-center gap-8 md:grid-cols-2">
|
|
<!-- Thumbnail -->
|
|
<div class="overflow-hidden rounded-lg -mx-6 -mt-6 md:-my-6 md:-ml-6 md:mr-0">
|
|
{heroImage ? (
|
|
<img
|
|
src={heroImage}
|
|
alt=""
|
|
class="aspect-video w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
|
|
loading="lazy"
|
|
/>
|
|
) : (
|
|
<div class="flex aspect-video w-full items-center justify-center bg-gradient-to-br from-surface-800 to-surface-900">
|
|
<img
|
|
src="/iptvnator/favicon-256x256.png"
|
|
alt=""
|
|
class="h-12 w-12 opacity-20"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="py-2">
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-mono text-[10px] uppercase tracking-[0.15em] text-accent-500">Featured</span>
|
|
<span class="text-surface-700">·</span>
|
|
<time datetime={pubDate.toISOString()} class="font-mono text-xs text-surface-500">
|
|
{formattedDate}
|
|
</time>
|
|
</div>
|
|
|
|
{tags.length > 0 && (
|
|
<div class="mt-2 flex flex-wrap gap-1.5">
|
|
{tags.map((tag) => (
|
|
<BlogTagChip tag={tag} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<h3 class="mt-4 font-display text-2xl font-bold tracking-tight text-surface-50 transition-colors duration-200 group-hover:text-accent-400 sm:text-3xl">
|
|
<a
|
|
href={`/iptvnator/blog/${slug}/`}
|
|
class="card-link after:absolute after:inset-0 after:rounded-xl after:content-[''] focus-visible:outline-none focus-visible:after:ring-2 focus-visible:after:ring-accent-500"
|
|
>
|
|
{title}
|
|
</a>
|
|
</h3>
|
|
|
|
<p class="mt-3 text-sm leading-relaxed text-surface-400">
|
|
{description}
|
|
</p>
|
|
|
|
<span class="mt-5 inline-flex items-center gap-1.5 font-mono text-xs uppercase tracking-[0.1em] text-accent-500 transition-colors group-hover:text-accent-400">
|
|
Read more
|
|
<svg
|
|
class="h-3.5 w-3.5 transition-transform duration-200 group-hover:translate-x-1"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
|
</svg>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|