20b67f11e0
TOC:
- renderMarkdownWithToc: парсит h2/h3, генерит транслит-якоря для кириллицы, возвращает {html, toc}
- TableOfContents компонент: sticky на десктопе, раскрывающийся блок на мобиле
- IntersectionObserver-free подсветка активной секции через scroll listener
- Двухколоночный layout статьи на lg+: 240px TOC + контент
SVG-обложки:
- ArticleCoverSVG: процедурно сгенерированная композиция (curve/circle/arc/rect) по seed = id статьи
- 6 палитр на выбор (emerald/teal/yellow/blue/purple/orange), seed детерминированный
- Используется в ArticleCard как fallback когда cover_url пусто
- На странице статьи тоже SVG если обложки нет
- Тег статьи отображается лейблом в углу
Архив:
- /archive: все статьи сгруппированы по месяцам, компактный список
- В Header добавлен пункт Архив (desktop+mobile)
- В Footer ссылки на Архив, Заметки, О проекте
- В sitemap.xml включён /archive
91 lines
3.7 KiB
JavaScript
91 lines
3.7 KiB
JavaScript
import Link from 'next/link';
|
||
import Header from '@/components/Header';
|
||
import Footer from '@/components/Footer';
|
||
import { listArticles } from '@/lib/engine';
|
||
import { formatDate } from '@/lib/markdown';
|
||
import { Archive, Clock } from 'lucide-react';
|
||
|
||
export const dynamic = 'force-dynamic';
|
||
export const metadata = { title: 'Архив статей' };
|
||
|
||
const MONTHS = [
|
||
'Январь','Февраль','Март','Апрель','Май','Июнь',
|
||
'Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь',
|
||
];
|
||
|
||
function groupByMonth(articles) {
|
||
const groups = new Map();
|
||
for (const a of articles) {
|
||
const d = new Date(a.published_at);
|
||
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
||
if (!groups.has(key)) groups.set(key, { year: d.getFullYear(), month: d.getMonth(), items: [] });
|
||
groups.get(key).items.push(a);
|
||
}
|
||
return Array.from(groups.entries())
|
||
.sort(([a], [b]) => b.localeCompare(a))
|
||
.map(([, v]) => v);
|
||
}
|
||
|
||
export default async function ArchivePage() {
|
||
const articles = await listArticles({ limit: 500 });
|
||
const groups = groupByMonth(articles);
|
||
|
||
return (
|
||
<>
|
||
<Header />
|
||
<main className="container-wide pt-10 pb-16">
|
||
<div
|
||
className="inline-flex items-center gap-2 text-xs accent px-3 py-1.5 rounded-full mb-4"
|
||
style={{ background: 'rgb(var(--accent) / 0.1)', border: '1px solid rgb(var(--accent) / 0.2)' }}
|
||
>
|
||
<Archive className="w-3.5 h-3.5" /> Архив
|
||
</div>
|
||
<h1 className="text-3xl sm:text-5xl font-bold ink mb-3 leading-tight">
|
||
Все статьи
|
||
</h1>
|
||
<p className="mute text-base sm:text-lg mb-10 max-w-2xl">
|
||
Полный архив. {articles.length} {articles.length === 1 ? 'материал' : 'материалов'} с момента запуска.
|
||
</p>
|
||
|
||
{articles.length === 0 && (
|
||
<p className="mute">Архив пока пуст.</p>
|
||
)}
|
||
|
||
<div className="space-y-10 sm:space-y-12">
|
||
{groups.map(g => (
|
||
<section key={`${g.year}-${g.month}`}>
|
||
<h2 className="text-xs sm:text-sm font-medium uppercase tracking-widest mute mb-4 sm:mb-5 pb-2 border-b-soft">
|
||
{MONTHS[g.month]} {g.year}
|
||
<span className="mute ml-2 normal-case tracking-normal">· {g.items.length}</span>
|
||
</h2>
|
||
<ul className="divide-y divide-[rgb(var(--border))]">
|
||
{g.items.map(a => (
|
||
<li key={a.id}>
|
||
<Link
|
||
href={`/blog/${a.slug}`}
|
||
className="group flex items-baseline gap-3 sm:gap-4 py-3 sm:py-4 -mx-2 px-2 hover:surface-2 rounded-lg transition-colors"
|
||
>
|
||
<span className="text-xs mute tabular-nums w-12 flex-shrink-0 mt-0.5">
|
||
{new Date(a.published_at).getDate().toString().padStart(2, '0')}.{(new Date(a.published_at).getMonth() + 1).toString().padStart(2, '0')}
|
||
</span>
|
||
<span className="flex-1 ink group-hover:accent transition-colors text-sm sm:text-base font-medium leading-snug">
|
||
{a.title}
|
||
</span>
|
||
{a.reading_time && (
|
||
<span className="hidden sm:inline-flex items-center gap-1 text-xs mute flex-shrink-0">
|
||
<Clock className="w-3 h-3" /> {a.reading_time} мин
|
||
</span>
|
||
)}
|
||
</Link>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</section>
|
||
))}
|
||
</div>
|
||
</main>
|
||
<Footer />
|
||
</>
|
||
);
|
||
}
|