feat: мобильная версия + поиск + SEO-инфраструктура

Мобилка:
- Header: hide-on-scroll, мобильный burger-menu, тонкая адаптация
- Hero: текст и кнопки оптимизированы под узкие экраны (full-width buttons)
- ArticleCard featured: на мобилке в столбик, картинка сверху
- Stats: компактная сетка 2x2 с уменьшенным шрифтом
- Глобально: scroll-behavior smooth, safe-area-inset, tap targets 40px+
- prefers-reduced-motion respected

Страница статьи:
- ReadingProgress: прогресс-бар сверху при скролле
- ScrollToTop: круглая кнопка наверху после 800px скролла
- ShareButton: Web Share API на мобилках, копирование URL на десктопе
- Related articles: подбираем по пересечению тегов (max 3)
- Мобильная типографика: prose-base sm:prose-lg, leading-relaxed

SEO/инфра:
- /api/search: простой поиск по title/excerpt/tags с подсветкой и скорингом
- SearchBox: оверлей с / хоткеем, дебаунс 250ms, мобиле-friendly
- /rss.xml: полноценный RSS-фид
- sitemap.xml: динамический через next sitemap()
- robots.txt: динамический
- viewport metadata + theme-color для светлой/тёмной темы
- alternates rel=alternate type=application/rss+xml
This commit is contained in:
Alexey Pavlov
2026-05-31 09:43:11 +03:00
parent 9e77f920c1
commit 4702614896
15 changed files with 595 additions and 77 deletions
+47 -21
View File
@@ -4,15 +4,15 @@
/* === Theme tokens === */
:root {
--bg: 250 250 249; /* почти белый, тёплый */
--surface: 255 255 255; /* карточки */
--surface-2: 245 245 244; /* приглушённый фон */
--bg: 250 250 249;
--surface: 255 255 255;
--surface-2: 245 245 244;
--border: 231 229 228;
--ink: 28 25 23; /* основной текст */
--mute: 120 113 108; /* приглушённый текст */
--accent: 16 185 129; /* emerald-500 */
--accent-2: 5 150 105; /* emerald-600 */
--accent-soft: 209 250 229; /* emerald-100 */
--ink: 28 25 23;
--mute: 120 113 108;
--accent: 16 185 129;
--accent-2: 5 150 105;
--accent-soft: 209 250 229;
}
.dark {
@@ -31,28 +31,39 @@
html {
background: rgb(var(--bg));
color: rgb(var(--ink));
scroll-behavior: smooth;
/* лучше для мобильных WebKit */
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
body {
@apply font-sans antialiased;
/* safe areas на iOS */
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
body { @apply font-sans antialiased; }
::selection { background: rgb(var(--accent) / 0.25); }
/* tap target минимум 44px для кнопок и ссылок в навигации/действиях */
button, a.btn {
min-height: 40px;
}
/* плавный скролл к якорям с учётом sticky header */
:where(h1, h2, h3, h4, [id]) {
scroll-margin-top: 80px;
}
}
@layer components {
.btn {
@apply inline-flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-primary {
background: rgb(var(--accent));
color: white;
}
.btn-primary { background: rgb(var(--accent)); color: white; }
.btn-primary:hover { background: rgb(var(--accent-2)); }
.btn-ghost {
color: rgb(var(--mute));
}
.btn-ghost:hover {
background: rgb(var(--surface-2));
color: rgb(var(--ink));
}
.btn-ghost { color: rgb(var(--mute)); }
.btn-ghost:hover { background: rgb(var(--surface-2)); color: rgb(var(--ink)); }
.container-narrow { @apply max-w-3xl mx-auto px-4; }
.container-wide { @apply max-w-6xl mx-auto px-4; }
@@ -70,10 +81,15 @@
.dark .article-card:hover {
box-shadow: 0 10px 25px -10px rgb(0 0 0 / 0.5);
}
/* на мобилке hover-эффект transform не нужен (тач) */
@media (hover: none) {
.article-card:hover { transform: none; box-shadow: none; }
.article-card:active { transform: scale(0.99); }
}
.tag {
display: inline-block;
@apply text-xs px-2.5 py-1 rounded-full transition-colors;
@apply text-xs px-2.5 py-1 rounded-full transition-colors whitespace-nowrap;
background: rgb(var(--surface-2));
color: rgb(var(--mute));
}
@@ -91,3 +107,13 @@
.border-b-soft { border-bottom: 1px solid rgb(var(--border)); }
.border-t-soft { border-top: 1px solid rgb(var(--border)); }
}
/* Уважаем системную настройку "меньше анимаций" */
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}