import { notFound } from 'next/navigation'; import Link from 'next/link'; import Header from '@/components/Header'; import Footer from '@/components/Footer'; import ReadingProgress from '@/components/ReadingProgress'; import ScrollToTop from '@/components/ScrollToTop'; import ShareButton from '@/components/ShareButton'; import ArticleCard from '@/components/ArticleCard'; import ArticleMeta from '@/components/ArticleMeta'; import ArticleCoverSVG from '@/components/ArticleCoverSVG'; import TableOfContents from '@/components/TableOfContents'; import { getArticle, listArticles } from '@/lib/engine'; import { renderMarkdownWithToc, formatDate } from '@/lib/markdown'; import { Clock, ArrowLeft } from 'lucide-react'; export const dynamic = 'force-dynamic'; export async function generateMetadata({ params }) { const { slug } = await params; const article = await getArticle(slug); if (!article) return { title: 'Статья не найдена' }; const ogImages = article.cover_url ? [{ url: article.cover_url.startsWith('http') ? article.cover_url : `https://zeropost.ru${article.cover_url}`, width: 1600, height: 900, alt: article.title }] : [{ url: 'https://zeropost.ru/og-default.png', width: 1200, height: 630 }]; return { title: article.seo_title || article.title, description: article.seo_descr || article.excerpt, alternates: { canonical: `https://zeropost.ru/blog/${slug}` }, openGraph: { title: article.title, description: article.excerpt, type: 'article', url: `https://zeropost.ru/blog/${slug}`, publishedTime: article.published_at, tags: article.tags || [], images: ogImages, }, twitter: { card: 'summary_large_image', title: article.seo_title || article.title, description: article.seo_descr || article.excerpt, images: ogImages.map(i => i.url), }, }; } async function loadRelated(article) { if (!article.tags?.length) return []; const all = await listArticles({ limit: 20 }); return all .filter(a => a.id !== article.id) .map(a => ({ ...a, score: (a.tags || []).filter(t => article.tags.includes(t)).length })) .filter(a => a.score > 0) .sort((a, b) => b.score - a.score) .slice(0, 3); } export default async function ArticlePage({ params }) { const { slug } = await params; const article = await getArticle(slug); if (!article) notFound(); const related = await loadRelated(article); const contentWithoutH1 = article.content.replace(/^#\s+.+$/m, '').trim(); const { html, toc } = renderMarkdownWithToc(contentWithoutH1); return ( <>
Все статьи
{(article.tags || []).map(t => ( #{t} ))}

{article.title}

{article.author} · {formatDate(article.published_at)} {article.reading_time && ( <> · {article.reading_time} мин чтения )}
{article.cover_url ? ( {article.title} ) : ( )}
{/* Двухколоночный layout на больших экранах: TOC + контент */}
{related.length > 0 && (

Похожее

{related.map(a => )}
)} {/* TG-банер после контента */}
Зеро
Понравилась заметка?
Зеро публикует новые материалы каждый день в Telegram. Подпишитесь — следующая уже завтра.
✈️ В канал