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: 'Статья не найдена' };
return {
title: article.seo_title || article.title,
description: article.seo_descr || article.excerpt,
openGraph: {
title: article.title,
description: article.excerpt,
type: 'article',
publishedTime: article.published_at,
tags: article.tags || [],
images: article.cover_url ? [{ url: article.cover_url }] : undefined,
},
};
}
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 (
<>