diff --git a/app/api/search/route.js b/app/api/search/route.js new file mode 100644 index 0000000..b13136b --- /dev/null +++ b/app/api/search/route.js @@ -0,0 +1,28 @@ +import { NextResponse } from 'next/server'; +import { listArticles } from '@/lib/engine'; + +// GET /api/search?q=foo — простой поиск по title/excerpt/tags +export async function GET(req) { + const q = (req.nextUrl.searchParams.get('q') || '').trim().toLowerCase(); + if (!q || q.length < 2) return NextResponse.json([]); + + try { + const all = await listArticles({ limit: 100 }); + const tokens = q.split(/\s+/).filter(Boolean); + + const matched = all + .map(a => { + const haystack = [a.title, a.excerpt, (a.tags || []).join(' ')] + .filter(Boolean).join(' ').toLowerCase(); + const score = tokens.reduce((acc, t) => acc + (haystack.includes(t) ? 1 : 0), 0); + return { ...a, score }; + }) + .filter(a => a.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, 12); + + return NextResponse.json(matched); + } catch (err) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.js index 749fac2..b4fa6eb 100644 --- a/app/blog/[slug]/page.js +++ b/app/blog/[slug]/page.js @@ -2,7 +2,11 @@ import { notFound } from 'next/navigation'; import Link from 'next/link'; import Header from '@/components/Header'; import Footer from '@/components/Footer'; -import { getArticle } from '@/lib/engine'; +import ReadingProgress from '@/components/ReadingProgress'; +import ScrollToTop from '@/components/ScrollToTop'; +import ShareButton from '@/components/ShareButton'; +import ArticleCard from '@/components/ArticleCard'; +import { getArticle, listArticles } from '@/lib/engine'; import { renderMarkdown, formatDate } from '@/lib/markdown'; import { Clock, ArrowLeft } from 'lucide-react'; @@ -21,70 +25,106 @@ export async function generateMetadata({ params }) { 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 }); + const scored = 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); + return scored; +} + 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 = renderMarkdown(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}
)}
-
+

Статья сгенерирована ИИ под редакторским присмотром.

+ + {related.length > 0 && ( +
+

+ Похожее +

+
+ {related.map(a => )} +
+
+ )} + +