feat: zeropost-web — публичный AI-блог на zeropost.ru
- Next.js 16, Tailwind с @tailwindcss/typography - Главная: hero, featured-статья, сетка карточек, облако тегов - /blog/[slug]: статья со SSG + revalidate 60s, prose typography - /tag/[name]: фильтр по тегам - /about: про проект - /api/cron/generate: endpoint для авто-генерации (защищён x-cron-token) - SEO: dynamic metadata, OG, sitemap-ready - Лента грузится с zeropost-engine /api/articles
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import Link from 'next/link';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import ArticleCard from '@/components/ArticleCard';
|
||||
import { listArticles } from '@/lib/engine';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
export const revalidate = 60;
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const { name } = await params;
|
||||
return { title: `Статьи по теме #${decodeURIComponent(name)}` };
|
||||
}
|
||||
|
||||
export default async function TagPage({ params }) {
|
||||
const { name } = await params;
|
||||
const tag = decodeURIComponent(name);
|
||||
const articles = await listArticles({ tag, limit: 50 });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="container-wide pt-10 pb-16">
|
||||
<Link href="/" className="btn-ghost text-sm mb-4 -ml-2">
|
||||
<ArrowLeft className="w-4 h-4" /> Все статьи
|
||||
</Link>
|
||||
<h1 className="text-3xl sm:text-4xl font-bold mb-2">#{tag}</h1>
|
||||
<p className="text-mute mb-8">{articles.length} {articles.length === 1 ? 'материал' : 'материалов'}</p>
|
||||
{articles.length === 0 ? (
|
||||
<p className="text-mute">Пока пусто.</p>
|
||||
) : (
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{articles.map(a => <ArticleCard key={a.id} article={a} />)}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user