Новые правки в компоенты

This commit is contained in:
Web-serfer 2026-04-26 18:09:40 +05:00
parent 107b7f461f
commit a14c18542e
15 changed files with 417 additions and 233 deletions

View file

@ -114,6 +114,29 @@
- Использовать Lighthouse и WebPageTest для аудита производительности - Использовать Lighthouse и WebPageTest для аудита производительности
- Реализовать performance budgets - Реализовать performance budgets
## SEO рекомендации
### Оптимальное количество страниц
- **50-100 страниц** — хороший объём для тематики "автоюрист"
- Минимум 30-50 страниц с уникальным контентом
### Структура страниц
- Услуги (12 шт) — уже есть
- Кейсы — минимум 20-30 реальных примеров
- Блог — минимум 30-50 статей
- FAQ — 20-30 вопросов
- Городские страницы ("юрист в Сургуте", "возврат прав Сургут" и пр.)
### Требования к контенту
- Каждая страница с уникальным текстом (не шаблон)
- Длинные статьи в блог (3000-5000 слов)
- Реальные кейсы с описанием проблемы и решения
### Внутренние ссылки
- Связывать страницы между собой (услуги → кейсы → блог)
- Использовать sitemap для всех публичных страниц
## Ccылки на документацию ## Ccылки на документацию
- URL документации Astro: https://docs.astro.build/en/getting-started/ - URL документации Astro: https://docs.astro.build/en/getting-started/
- URL документации PocketBase: https://pocketbase.io/docs/ - URL документации PocketBase: https://pocketbase.io/docs/

View file

@ -84,14 +84,29 @@ const { items } = Astro.props;
padding: 0 0 0.5rem; padding: 0 0 0.5rem;
} }
.breadcrumbs-list {
flex-wrap: nowrap;
overflow: hidden;
}
.breadcrumb-link, .breadcrumb-link,
.breadcrumb-current { .breadcrumb-current {
font-size: 0.8rem; font-size: 0.8rem;
display: inline-block;
max-width: 90px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.breadcrumb-separator { .breadcrumb-separator {
width: 0.875rem; width: 0.875rem;
height: 0.875rem; height: 0.875rem;
flex-shrink: 0;
}
.breadcrumb-item {
flex-shrink: 0;
} }
} }
</style> </style>

View file

@ -1,8 +1,5 @@
--- ---
import telegramIcon from '../../icons/telegram.svg?raw'; import { Icon } from 'astro-icon/components';
import vkIcon from '../../icons/vk.svg?raw';
import whatsappIcon from '../../icons/whatsapp.svg?raw';
import okIcon from '../../icons/ok.svg?raw';
interface Props { interface Props {
title: string; title: string;
@ -27,7 +24,7 @@ const encodedUrl = encodeURIComponent(url);
class="social-icon telegram" class="social-icon telegram"
aria-label="Поделиться в Telegram" aria-label="Поделиться в Telegram"
> >
<span class="icon-wrapper" set:html={telegramIcon} /> <span class="icon-wrapper"><Icon name="telegram" /></span>
</a> </a>
<!-- VK --> <!-- VK -->
@ -38,7 +35,7 @@ const encodedUrl = encodeURIComponent(url);
class="social-icon vk" class="social-icon vk"
aria-label="Поделиться в VK" aria-label="Поделиться в VK"
> >
<span class="icon-wrapper" set:html={vkIcon} /> <span class="icon-wrapper"><Icon name="vk" /></span>
</a> </a>
<!-- WhatsApp --> <!-- WhatsApp -->
@ -49,7 +46,7 @@ const encodedUrl = encodeURIComponent(url);
class="social-icon whatsapp" class="social-icon whatsapp"
aria-label="Поделиться в WhatsApp" aria-label="Поделиться в WhatsApp"
> >
<span class="icon-wrapper" set:html={whatsappIcon} /> <span class="icon-wrapper"><Icon name="whatsapp" /></span>
</a> </a>
<!-- Одноклассники --> <!-- Одноклассники -->
@ -60,7 +57,7 @@ const encodedUrl = encodeURIComponent(url);
class="social-icon odnoklassniki" class="social-icon odnoklassniki"
aria-label="Поделиться в Одноклассниках" aria-label="Поделиться в Одноклассниках"
> >
<span class="icon-wrapper" set:html={okIcon} /> <span class="icon-wrapper"><Icon name="ok" /></span>
</a> </a>
</div> </div>
</div> </div>

View file

@ -1,5 +1,4 @@
--- ---
import BlogCard from '@components/blog/BlogCard.astro';
import { getPostImageUrl } from '@lib/pb'; import { getPostImageUrl } from '@lib/pb';
interface Post { interface Post {
@ -22,7 +21,6 @@ interface Props {
const { posts, currentSlug } = Astro.props; const { posts, currentSlug } = Astro.props;
// Форматируем дату
const formatDate = (date: string) => { const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('ru-RU', { return new Date(date).toLocaleDateString('ru-RU', {
day: 'numeric', day: 'numeric',
@ -31,46 +29,51 @@ const formatDate = (date: string) => {
}); });
}; };
// Фильтруем текущую статью const filteredPosts = currentSlug
const filteredPosts = currentSlug ? posts.filter((post: Post) => post.slug !== currentSlug)
? posts.filter(post => post.slug !== currentSlug).slice(0, 3) : posts;
: posts.slice(0, 3);
--- ---
<section class="related-posts"> <section class="related-posts">
<h3 class="section-title">Читайте также</h3> <h3 class="section-title">Похожие статьи</h3>
<div class="related-grid"> <div class="related-grid">
{filteredPosts.map((post) => ( {filteredPosts.slice(0, 3).map((post: Post) => (
<BlogCard <Fragment>
title={post.title} <article class="blog-card-wrapper">
description={post.description} <a href={`/blog/${post.slug}`} class="card-link">
category={post.category} <div class="card-image">
categoryColor={post.categoryColor} <img src={getPostImageUrl(post)} alt={post.title} loading="lazy" width="400" height="250" />
date={formatDate(post.date)} <span class={`category-badge ${post.categoryColor || 'bg-gold'}`}>{post.category}</span>
readTime={post.readTime} </div>
readmeTime={post.readmeTime} <div class="card-content">
image={getPostImageUrl(post)} <h3 class="card-title">{post.title}</h3>
slug={`/blog/${post.slug}`} <p class="card-description">{post.description}</p>
/> <div class="card-meta">
<span class="meta-item">{formatDate(post.date)}</span>
<span class="meta-item">{post.readTime} мин</span>
</div>
</div>
</a>
</article>
</Fragment>
))} ))}
</div> </div>
</section> </section>
<style is:global> <style>
.related-posts { .related-posts {
margin-top: 3rem; margin-top: 3rem;
padding-top: 2rem; padding-top: 2rem;
border-top: 2px solid #f1f5f9; border-top: 2px solid #f1f5f9;
} }
/* Сбрасываем нумерацию заголовков от ArticleLayout */ .related-posts .blog-card-wrapper .card-title {
.related-posts .blog-card .card-title {
counter-increment: none !important; counter-increment: none !important;
display: block !important; display: block !important;
} }
.related-posts .blog-card .card-title::before { .related-posts .blog-card-wrapper .card-title::before {
display: none !important; display: none !important;
} }
@ -80,6 +83,11 @@ const filteredPosts = currentSlug
font-weight: 800; font-weight: 800;
margin: 0 0 2rem; margin: 0 0 2rem;
letter-spacing: -0.02em; letter-spacing: -0.02em;
font-family: 'Merriweather', Georgia, serif;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
} }
.related-grid { .related-grid {
@ -88,6 +96,80 @@ const filteredPosts = currentSlug
gap: 2rem; gap: 2rem;
} }
.blog-card-wrapper {
border-radius: 1rem;
overflow: hidden;
background: #fff;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
transition: all 0.3s ease;
}
.blog-card-wrapper:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0,0,0,0.1);
}
.blog-card-wrapper .card-link {
text-decoration: none;
color: inherit;
display: block;
}
.blog-card-wrapper .card-image {
position: relative;
aspect-ratio: 16/10;
overflow: hidden;
}
.blog-card-wrapper .card-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.blog-card-wrapper:hover .card-image img {
transform: scale(1.05);
}
.blog-card-wrapper .category-badge {
position: absolute;
top: 1rem;
left: 1rem;
padding: 0.25rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.75rem;
font-weight: 600;
}
.blog-card-wrapper .card-content {
padding: 1.25rem;
}
.blog-card-wrapper .card-title {
font-size: 1rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: #1e293b;
}
.blog-card-wrapper .card-description {
font-size: 0.875rem;
color: #64748b;
margin-bottom: 1rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.blog-card-wrapper .card-meta {
display: flex;
gap: 1rem;
font-size: 0.75rem;
color: #94a3b8;
}
@media (max-width: 1024px) { @media (max-width: 1024px) {
.related-grid { .related-grid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
@ -99,5 +181,15 @@ const filteredPosts = currentSlug
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 1.5rem; gap: 1.5rem;
} }
.section-title {
font-size: 1.25rem;
margin-bottom: 1.5rem;
}
.related-posts {
margin-top: 2rem;
padding-top: 1.5rem;
}
} }
</style> </style>

View file

@ -3,6 +3,8 @@ import { createSignal, onMount } from "solid-js";
export default function CommentLock() { export default function CommentLock() {
const [currentPath, setCurrentPath] = createSignal("/auth/sign-in"); const [currentPath, setCurrentPath] = createSignal("/auth/sign-in");
const commentsSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`;
onMount(() => { onMount(() => {
const path = window.location.pathname; const path = window.location.pathname;
const hash = window.location.hash; const hash = window.location.hash;
@ -11,51 +13,82 @@ export default function CommentLock() {
}); });
return ( return (
<div class="max-w-4xl mx-auto mt-12 pt-8 border-t border-gray-200"> <>
<div class="flex items-center gap-3 mb-8"> <div class="max-w-4xl mx-auto mt-12 pt-8 border-t border-gray-200">
<h3 class="text-2xl font-bold text-gray-900">Комментарии</h3> <div class="flex items-center justify-center gap-3 mb-8">
<span class="px-3 py-1 bg-blue-100 text-blue-700 text-sm font-medium rounded-full"> <span class="comment-icon" innerHTML={commentsSvg} />
0 <h3 class="text-2xl font-bold text-gray-900">Комментарии</h3>
</span> <span class="px-3 py-1 bg-blue-100 text-blue-700 text-sm font-medium rounded-full">
</div> 0
</span>
<div
class="bg-linear-to-br from-gray-50 to-gray-100 rounded-2xl p-8 md:p-12 border border-gray-200 text-center"
style="background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);"
>
<div
class="w-20 h-20 mx-auto mb-6 rounded-full flex items-center justify-center"
style="background: linear-gradient(135deg, rgba(37, 99, 235, 0.2) 0%, rgba(30, 64, 175, 0.3) 100%);"
>
<svg class="w-10 h-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div> </div>
<h4 class="text-xl font-semibold text-gray-900 mb-3"> <div
Комментарии доступны только авторизованным пользователям class="bg-linear-to-br from-gray-50 to-gray-100 rounded-2xl p-8 border border-gray-200 text-center"
</h4> style="background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);"
<a
href={currentPath()}
class="inline-flex items-center justify-center font-semibold transition-all duration-300 rounded-md cursor-pointer px-6 py-3 text-lg"
style="background: linear-gradient(to bottom, #2563eb, #1e40af); color: white; box-shadow: 0 2px 4px rgba(37, 99, 235, 0.3);"
> >
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <div
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"/> class="w-16 h-16 mx-auto mb-5 rounded-full flex items-center justify-center"
</svg> style="background: linear-gradient(135deg, rgba(37, 99, 235, 0.2) 0%, rgba(30, 64, 175, 0.3) 100%);"
Войти в аккаунт
</a>
<p class="mt-4 text-sm text-gray-600">
Нет аккаунта?{" "}
<a
href={`/auth/sign-up?redirect=${encodeURIComponent(window.location.pathname)}`}
class="font-medium hover:underline"
style="color: #2563eb;"
> >
Зарегистрироваться <svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div>
<h4 class="text-lg font-semibold text-gray-900 mb-3">
Нужна авторизация для комментариев
</h4>
<a
href={currentPath()}
class="inline-flex items-center justify-center font-semibold transition-all duration-300 rounded-md cursor-pointer px-5 py-2.5 text-base"
style="background: linear-gradient(to bottom, #2563eb, #1e40af); color: white; box-shadow: 0 2px 4px rgba(37, 99, 235, 0.3);"
>
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"/>
</svg>
Войти в аккаунт
</a> </a>
</p> <p class="mt-3 text-sm text-gray-600">
Нет аккаунта?{" "}
<a
href={`/auth/sign-up?redirect=${encodeURIComponent(window.location.pathname)}`}
class="font-medium hover:underline"
style="color: #2563eb;"
>
Зарегистрироваться
</a>
</p>
</div>
</div> </div>
</div> <style>{`
.comment-icon {
width: 1.75rem;
height: 1.75rem;
color: #2563eb;
}
.comment-icon svg {
width: 100%;
height: 100%;
}
@media (max-width: 768px) {
.max-w-4xl { max-width: 100% !important; }
.max-w-4xl .text-2xl { font-size: 1.25rem !important; }
.max-w-4xl .bg-blue-100 { padding: 0.25rem 0.5rem; font-size: 0.7rem; }
.max-w-4xl .rounded-2xl { padding: 1.5rem 1rem !important; margin-left: 0.5rem; margin-right: 0.5rem; }
.max-w-4xl .w-16 { width: 2.5rem !important; height: 2.5rem !important; }
.max-w-4xl .w-8 { width: 1.25rem !important; height: 1.25rem !important; }
.max-w-4xl .text-lg { font-size: 0.95rem !important; margin-bottom: 0.5rem !important; }
.max-w-4xl .text-base { font-size: 0.85rem !important; padding: 0.5rem 1rem !important; }
.max-w-4xl .w-4 { width: 0.875rem !important; height: 0.875rem !important; }
.max-w-4xl .text-sm { font-size: 0.75rem !important; }
.max-w-4xl .mt-3 { margin-top: 0.75rem !important; }
.max-w-4xl .mb-5 { margin-bottom: 1rem !important; }
.max-w-4xl .mb-8 { margin-bottom: 1.5rem !important; padding-top: 1.5rem !important; }
.max-w-4xl .pt-8 { padding-top: 2rem !important; }
.comment-icon { width: 1.25rem !important; height: 1.25rem !important; }
}
`}</style>
</>
); );
} }

View file

@ -1,38 +1,17 @@
import { createSignal, For, Show, onMount } from "solid-js"; import { createSignal, For, Show, onMount } from "solid-js";
import CommentLock from "./CommentLock"; import CommentLock from "./CommentLock";
import CommentForm from "./CommentForm"; import CommentForm from "./CommentForm";
import type { CommentWithReplies } from "../../../types/comments"; import type { CommentWithReplies, CommentRecord } from "../../../types/comments";
interface CommentsProps {
postSlug: string;
}
interface ApiComment {
id: string;
post_slug: string;
user: string;
content: string;
parent?: string | null;
is_verified: boolean;
status: "pending" | "published" | "spam";
created: string;
updated: string;
expand?: {
user?: {
id: string;
firstName?: string;
name?: string;
email: string;
avatar?: string;
};
};
}
interface ToastMessage { interface ToastMessage {
type: "success" | "error"; type: "success" | "error";
message: string; message: string;
} }
interface CommentsProps {
postSlug: string;
}
export default function Comments(props: CommentsProps) { export default function Comments(props: CommentsProps) {
const [isAuthenticated, setIsAuthenticated] = createSignal(false); const [isAuthenticated, setIsAuthenticated] = createSignal(false);
const [currentUser, setCurrentUser] = createSignal<{ const [currentUser, setCurrentUser] = createSignal<{
@ -95,7 +74,7 @@ export default function Comments(props: CommentsProps) {
const data = await response.json(); const data = await response.json();
const commentsWithReplies: CommentWithReplies[] = await Promise.all( const commentsWithReplies: CommentWithReplies[] = await Promise.all(
data.items.map(async (comment: ApiComment) => { data.items.map(async (comment: CommentRecord) => {
const repliesResponse = await fetch( const repliesResponse = await fetch(
`/api/comments?post_slug=${encodeURIComponent(props.postSlug)}&parent=${comment.id}`, `/api/comments?post_slug=${encodeURIComponent(props.postSlug)}&parent=${comment.id}`,
{ {
@ -289,6 +268,15 @@ export default function Comments(props: CommentsProps) {
return ( return (
<> <>
<style>{`
@media (max-width: 768px) {
.comments-wrapper { padding: 0 0.75rem; }
.comments-wrapper h3 { font-size: 1.5rem !important; }
.comment-card { padding: 1rem !important; }
.comment-avatar { width: 40px !important; height: 40px !important; }
.reply-form { margin-left: 0 !important; padding-left: 1rem !important; }
}
`}</style>
<Show when={toast()}> <Show when={toast()}>
{(t) => ( {(t) => (
<div <div

View file

@ -2,8 +2,6 @@
import Logo from "./Logo.astro"; import Logo from "./Logo.astro";
import Navbar from "./Navbar.astro"; import Navbar from "./Navbar.astro";
import MobileMenu from "./MobileMenu.astro"; import MobileMenu from "./MobileMenu.astro";
import LoginButton from "./LoginButton.astro";
import UserMenu from "./UserMenu.astro";
import { COMPANY } from "@constants"; import { COMPANY } from "@constants";
--- ---

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 20 20"><!-- Icon from OOUI by OOUI Team - https://github.com/wikimedia/oojs-ui/blob/master/LICENSE-MIT --><path fill="currentColor" d="M7 0a2 2 0 0 0-2 2h9a2 2 0 0 1 2 2v12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/><path fill="currentColor" d="M13 20a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2zM9 5h4v5H9zM4 5h4v1H4zm0 2h4v1H4zm0 2h4v1H4zm0 2h9v1H4zm0 2h9v1H4zm0 2h9v1H4z"/></svg>

After

Width:  |  Height:  |  Size: 467 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Stash Icons by Pingback LLC - https://github.com/stash-ui/icons/blob/master/LICENSE --><path fill="currentColor" fill-rule="evenodd" d="M9.5 3.75c-3.383 0-6.25 2.509-6.25 5.75a5.42 5.42 0 0 0 1.138 3.31l-.612 2.243c-.16.59.448 1.125 1.012.89l2.8-1.167a.75.75 0 0 0-.577-1.385l-1.384.577l.31-1.137a.75.75 0 0 0-.162-.695C5.127 11.405 4.75 10.49 4.75 9.5c0-2.282 2.058-4.25 4.75-4.25c2.08 0 3.791 1.183 4.454 2.772c-3.14.253-5.704 2.663-5.704 5.728c0 3.241 2.867 5.75 6.25 5.75c.766 0 1.503-.127 2.184-.36l2.528 1.052c.564.236 1.172-.3 1.012-.89l-.612-2.242a5.42 5.42 0 0 0 1.138-3.31c0-2.897-2.29-5.208-5.191-5.667c-.694-2.53-3.184-4.333-6.059-4.333m.25 10c0-2.281 2.058-4.25 4.75-4.25s4.75 1.969 4.75 4.25c0 .989-.377 1.905-1.025 2.636a.75.75 0 0 0-.162.695l.31 1.137l-1.384-.577a.75.75 0 0 0-.565-.004A5.2 5.2 0 0 1 14.5 18c-2.692 0-4.75-1.968-4.75-4.25" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 981 B

View file

@ -106,11 +106,9 @@ const {
</span> </span>
</div> </div>
<div class="article-actions"> <div class="article-actions">
<PostReactionButtons postId={postId} initialLikes={initialLikes} initialDislikes={initialDislikes} /> <PostReactionButtons postId={postId} initialLikes={initialLikes} initialDislikes={initialDislikes} />
<div class="share-wrapper"> <PostSocialShare title={postTitle} url={postUrl} />
<PostSocialShare title={postTitle} url={postUrl} />
</div>
</div> </div>
</div> </div>
</div> </div>
@ -123,9 +121,7 @@ const {
<div class="site-container"> <div class="site-container">
<div class="article-layout-grid"> <div class="article-layout-grid">
<div class="article-main"> <div class="article-main">
<article class="article-content-wrapper" id="post-content"> <slot />
<slot />
</article>
</div> </div>
<aside class="article-sidebar" id="sticky-sidebar"> <aside class="article-sidebar" id="sticky-sidebar">
@ -137,6 +133,117 @@ const {
</main> </main>
<Footer /> <Footer />
<style>
/* ЧЕРНАЯ НУМЕРАЦИЯ */
.article-content-wrapper { counter-reset: post-h2; }
.article-content-wrapper .post-content h2 { counter-reset: post-h3; counter-increment: post-h2; display: flex; align-items: baseline; gap: 0.6rem; color: #1e3050; font-weight: 800; }
.article-content-wrapper .post-content h2::before { content: counter(post-h2) "."; color: #1e3050 !important; flex-shrink: 0; }
.article-content-wrapper .post-content h3 { counter-increment: post-h3; display: flex; align-items: baseline; gap: 0.5rem; color: #1e3050; font-weight: 700; }
.article-content-wrapper .post-content h3::before { content: counter(post-h2) "." counter(post-h3) "."; color: #1e3050 !important; font-size: 0.9em; flex-shrink: 0; }
/* Исключаем заголовок "Читайте также" из нумерации */
.article-content-wrapper .related-posts .section-title {
counter-increment: none !important;
display: block !important;
}
.article-content-wrapper .related-posts .section-title::before {
content: none !important;
}
/* Исключаем секцию комментариев из нумерации */
.article-content-wrapper .comments-wrapper h2,
.article-content-wrapper .comments-wrapper h2::before {
counter-increment: none !important;
content: none !important;
}
.article-content-wrapper .comments-wrapper h3,
.article-content-wrapper .comments-wrapper h3::before {
counter-increment: none !important;
content: none !important;
}
.article-content-wrapper .comments-wrapper .section-title,
.article-content-wrapper .comments-wrapper .section-title::before {
counter-increment: none !important;
content: none !important;
}
/* СКРОЛЛ-ФИКС */
.article-content-wrapper h2, .article-content-wrapper h3 { scroll-margin-top: 125px; }
/* СТИЛЬ АКТИВНОГО ПУНКТА В TOC */
.toc-item.active .toc-link {
color: #eac26e !important;
font-weight: 700;
background: rgba(234, 194, 110, 0.1);
}
.toc-item.active .toc-link::before { color: #eac26e !important; }
html, body { overflow-x: clip !important; }
</style>
<style>
.h-full { height: 100%; }
.main-content { padding-top: 0; overflow: visible; }
.breadcrumbs-wrapper { padding-top: 5.5rem; background: #f8fafc; }
/* HERO SECTION */
.article-hero { position: relative; width: 100%; height: 550px; background: #0a1a2e; overflow: hidden; }
.article-hero-image { position: absolute; inset: 0; z-index: 1; }
.article-hero-image img { width: 100%; height: 100%; object-fit: cover; filter: brightness(0.35) grayscale(0.2); transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1); transform: scale(1.04); }
.article-hero-overlay { position: absolute; inset: 0; background: linear-gradient(180deg, transparent 0%, rgba(10, 26, 46, 0.9) 100%); z-index: 2; transition: opacity 0.7s ease; }
.article-hero:hover .article-hero-image img { filter: brightness(1) grayscale(0); transform: scale(1); }
.article-hero:hover .article-hero-overlay { opacity: 0.4; }
.article-hero-content { position: relative; z-index: 10; height: 100%; }
.article-hero-inner { display: flex; flex-direction: column; height: 100%; padding: 3rem 0; }
.article-hero-top { margin-bottom: 2rem; }
.category-strip { background: #eac26e; padding: 0.5rem 1.5rem; display: inline-block; border-radius: 4px; margin-bottom: 2rem; }
.category-text { color: #0a1a2e; font-weight: 800; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; font-family: 'Merriweather', Georgia, serif; }
.article-title { color: #fff; font-size: clamp(2rem, 5vw, 3.5rem); font-weight: 900; line-height: 1.15; max-width: 950px; text-shadow: 0 4px 15px rgba(0,0,0,0.4); font-family: 'Merriweather', Georgia, serif; }
.article-hero-bottom { margin-top: auto; display: flex; justify-content: space-between; align-items: center; padding-top: 2rem; border-top: 1px solid rgba(255, 255, 255, 0.15); }
.article-actions { display: flex; align-items: center; gap: 2rem; }
.article-meta { display: flex; gap: 2rem; justify-content: flex-start; width: auto; }
.meta-item { display: flex; align-items: center; gap: 0.6rem; color: #fff; font-size: 0.95rem; white-space: nowrap; }
.meta-icon { width: 1.1rem; height: 1.1rem; color: #eac26e; }
.meta-author { color: #eac26e; font-weight: 700; }
/* GRID & STICKY SIDEBAR */
.article-body { padding: 0 0 4rem; background: #f8fafc; overflow: visible; }
.article-layout-grid { display: flex; gap: 3.5rem; align-items: flex-start; }
.article-main { flex: 1; min-width: 0; }
.article-content-wrapper { background: #fff; padding: 1.5rem 2.5rem 2.5rem; border-radius: 2rem; box-shadow: 0 4px 30px rgba(0,0,0,0.02); }
.article-sidebar { width: 320px; flex-shrink: 0; position: sticky; top: 110px; z-index: 50; }
@media (max-width: 1200px) {
.article-sidebar { display: none; }
.article-layout-grid { flex-direction: column; }
}
@media (max-width: 1024px) {
.article-hero-bottom { flex-direction: column; align-items: flex-start; gap: 1.5rem; }
.article-actions { width: 100%; justify-content: flex-start; }
.article-meta { gap: 1.5rem; }
}
@media (max-width: 768px) {
.article-hero { height: auto; min-height: 385px; }
.article-title { font-size: 1.5rem; }
.article-hero-bottom { flex-direction: column; align-items: flex-start; gap: 2rem; }
.article-actions { width: 100%; justify-content: flex-start; align-items: flex-end; gap: 1.5rem; }
.share-wrapper { display: flex; align-items: flex-end; }
.article-content-wrapper { padding: 2.5rem 1.5rem; padding-top: 0; }
.article-hero-inner { padding: 2rem 0; }
.meta-item { font-size: 0.85rem; }
.article-meta { gap: 0.75rem; flex-wrap: wrap; }
.category-strip { margin-bottom: 1.5rem; padding: 0.4rem 1rem; }
.category-text { font-size: 0.7rem; }
}
</style>
<script> <script>
function setupArticleLogic() { function setupArticleLogic() {
const sidebar = document.getElementById('sticky-sidebar'); const sidebar = document.getElementById('sticky-sidebar');
@ -199,103 +306,4 @@ const {
document.addEventListener("DOMContentLoaded", setupArticleLogic); document.addEventListener("DOMContentLoaded", setupArticleLogic);
document.addEventListener("astro:page-load", setupArticleLogic); document.addEventListener("astro:page-load", setupArticleLogic);
</script> </script>
<style is:global>
/* ЧЕРНАЯ НУМЕРАЦИЯ */
#post-content { counter-reset: post-h2; }
#post-content h2 { counter-reset: post-h3; counter-increment: post-h2; display: flex; align-items: baseline; gap: 0.6rem; color: #1e3050; font-weight: 800; }
#post-content h2::before { content: counter(post-h2) "."; color: #1e3050 !important; flex-shrink: 0; }
#post-content h3 { counter-increment: post-h3; display: flex; align-items: baseline; gap: 0.5rem; color: #1e3050; font-weight: 700; }
#post-content h3::before { content: counter(post-h2) "." counter(post-h3) "."; color: #1e3050 !important; font-size: 0.9em; flex-shrink: 0; }
/* Исключаем заголовок "Читайте также" из нумерации */
#post-content .related-posts .section-title {
counter-increment: none !important;
display: block !important;
}
#post-content .related-posts .section-title::before {
content: none !important;
}
/* Исключаем секцию комментариев из нумерации */
#post-content .comments-wrapper h2,
#post-content .comments-wrapper h2::before {
counter-increment: none !important;
content: none !important;
}
#post-content .comments-wrapper h3,
#post-content .comments-wrapper h3::before {
counter-increment: none !important;
content: none !important;
}
#post-content .comments-wrapper .section-title,
#post-content .comments-wrapper .section-title::before {
counter-increment: none !important;
content: none !important;
}
/* СКРОЛЛ-ФИКС */
#post-content h2, #post-content h3 { scroll-margin-top: 125px; }
/* СТИЛЬ АКТИВНОГО ПУНКТА В TOC */
.toc-item.active .toc-link {
color: #eac26e !important;
font-weight: 700;
background: rgba(234, 194, 110, 0.1);
}
.toc-item.active .toc-link::before { color: #eac26e !important; }
html, body { overflow-x: clip !important; }
</style>
<style>
.h-full { height: 100%; }
.main-content { padding-top: 0; overflow: visible; }
.breadcrumbs-wrapper { padding-top: 5.5rem; background: #f8fafc; }
/* HERO SECTION */
.article-hero { position: relative; width: 100%; height: 550px; background: #0a1a2e; overflow: hidden; }
.article-hero-image { position: absolute; inset: 0; z-index: 1; }
.article-hero-image img { width: 100%; height: 100%; object-fit: cover; filter: brightness(0.35) grayscale(0.2); transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1); transform: scale(1.04); }
.article-hero-overlay { position: absolute; inset: 0; background: linear-gradient(180deg, transparent 0%, rgba(10, 26, 46, 0.9) 100%); z-index: 2; transition: opacity 0.7s ease; }
.article-hero:hover .article-hero-image img { filter: brightness(1) grayscale(0); transform: scale(1); }
.article-hero:hover .article-hero-overlay { opacity: 0.4; }
.article-hero-content { position: relative; z-index: 10; height: 100%; }
.article-hero-inner { display: flex; flex-direction: column; height: 100%; padding: 3rem 0; }
.article-hero-top { margin-bottom: 2rem; }
.category-strip { background: #eac26e; padding: 0.5rem 1.5rem; display: inline-block; border-radius: 4px; margin-bottom: 2rem; }
.category-text { color: #0a1a2e; font-weight: 800; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; }
.article-title { color: #fff; font-size: clamp(2rem, 5vw, 3.5rem); font-weight: 900; line-height: 1.15; max-width: 950px; text-shadow: 0 4px 15px rgba(0,0,0,0.4); }
.article-hero-bottom { margin-top: auto; display: flex; justify-content: space-between; align-items: center; padding-top: 2rem; border-top: 1px solid rgba(255, 255, 255, 0.15); }
.article-actions { display: flex; align-items: center; gap: 2rem; }
.article-meta { display: flex; gap: 2.5rem; }
.meta-item { display: flex; align-items: center; gap: 0.6rem; color: #fff; font-size: 0.95rem; }
.meta-icon { width: 1.1rem; height: 1.1rem; color: #eac26e; }
.meta-author { color: #eac26e; font-weight: 700; }
/* GRID & STICKY SIDEBAR */
.article-body { padding: 4rem 0; background: #f8fafc; overflow: visible; }
.article-layout-grid { display: flex; gap: 3.5rem; align-items: flex-start; }
.article-main { flex: 1; min-width: 0; }
.article-content-wrapper { background: #fff; padding: 1.5rem 2.5rem 2.5rem; border-radius: 2rem; box-shadow: 0 4px 30px rgba(0,0,0,0.02); }
.article-sidebar { width: 320px; flex-shrink: 0; position: sticky; top: 110px; z-index: 50; }
@media (max-width: 1200px) {
.article-sidebar { display: none; }
.article-layout-grid { flex-direction: column; }
}
@media (max-width: 768px) {
.article-hero { height: auto; min-height: 550px; }
.article-title { font-size: 1.75rem; }
.article-hero-bottom { flex-direction: column; align-items: flex-start; gap: 2rem; }
.article-actions { width: 100%; justify-content: space-between; }
.article-content-wrapper { padding: 2.5rem 1.5rem; }
}
</style>

View file

@ -25,6 +25,9 @@ const { title, description, canonicalLink, breadcrumbs } = Astro.props;
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicons/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicons/favicon.svg" />
<link rel="icon" href="/favicons/favicon.ico" /> <link rel="icon" href="/favicons/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400&display=swap" rel="stylesheet" />
<title>{title} {SITE_TITLE_SUFFIX}</title> <title>{title} {SITE_TITLE_SUFFIX}</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
{canonicalLink && <link rel="canonical" href={canonicalLink} />} {canonicalLink && <link rel="canonical" href={canonicalLink} />}

View file

@ -37,14 +37,13 @@ while ((match = headingRegex.exec(body)) !== null) {
const text = match[2].trim(); const text = match[2].trim();
tocItems.push({ level, id: `heading-${headingIndex++}`, text }); tocItems.push({ level, id: `heading-${headingIndex++}`, text });
} }
console.log('[DEBUG] tocItems:', JSON.stringify(tocItems));
// Форматируем дату
const formatDate = (date: string) => { const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('ru-RU', { const d = new Date(date);
day: 'numeric', const day = d.getDate().toString().padStart(2, '0');
month: 'long', const month = (d.getMonth() + 1).toString().padStart(2, '0');
year: 'numeric' const year = new Date().getFullYear().toString().slice(-2);
}); return `${day}/${month}/${year}`;
}; };
// Для related posts берем те же категории // Для related posts берем те же категории
@ -77,22 +76,24 @@ const heroImage = getPostImageUrl(post);
initialLikes={likes} initialLikes={likes}
initialDislikes={dislikes} initialDislikes={dislikes}
> >
<!-- Содержимое статьи --> <!-- Содержимое статьи в article -->
<div class="post-content" set:html={contentHtml} /> <article class="article-content-wrapper" id="post-content">
<div class="post-content" set:html={contentHtml} />
</article>
<!-- Система комментариев --> <!-- Комментарии и похожие статьи - ВНЕ article -->
<div class="comments-wrapper"> {/*
<div class="comments-wrapper">
<Comments postSlug={post.slug} client:load /> <Comments postSlug={post.slug} client:load />
</div>
<RelatedPosts posts={filteredRelated} currentSlug={slug} />
*/}
<!-- Оглавление - пробуем без slot -->
<div slot="sidebar">
<ArticleTableOfContents items={tocItems} />
</div> </div>
<!-- Похожие статьи -->
<RelatedPosts
posts={filteredRelated}
currentSlug={slug}
/>
<!-- Оглавление в сайдбаре -->
<ArticleTableOfContents items={tocItems} slot="sidebar" />
</ArticleLayout> </ArticleLayout>
<style> <style>
@ -100,7 +101,8 @@ const heroImage = getPostImageUrl(post);
.post-content { .post-content {
padding: 0; padding: 0;
color: #334155; color: #334155;
line-height: 1.8; line-height: 1.85;
font-family: 'Merriweather', Georgia, serif;
} }
.post-content :global(h2) { .post-content :global(h2) {
@ -109,6 +111,10 @@ const heroImage = getPostImageUrl(post);
font-weight: 700; font-weight: 700;
margin: 2rem 0 1rem; margin: 2rem 0 1rem;
letter-spacing: -0.02em; letter-spacing: -0.02em;
font-family: 'Merriweather', Georgia, serif;
display: flex;
align-items: baseline;
gap: 0.6rem;
} }
.post-content :global(h3) { .post-content :global(h3) {
@ -116,6 +122,10 @@ const heroImage = getPostImageUrl(post);
font-size: 1.375rem; font-size: 1.375rem;
font-weight: 700; font-weight: 700;
margin: 1.75rem 0 1rem; margin: 1.75rem 0 1rem;
font-family: 'Merriweather', Georgia, serif;
display: flex;
align-items: baseline;
gap: 0.5rem;
} }
.post-content :global(p) { .post-content :global(p) {
@ -148,11 +158,19 @@ const heroImage = getPostImageUrl(post);
} }
.post-content :global(h2) { .post-content :global(h2) {
font-size: 1.5rem; font-size: 1.15rem;
} }
.post-content :global(h3) { .post-content :global(h3) {
font-size: 1.25rem; font-size: 1.05rem;
}
.post-content :global(p) {
font-size: 0.9rem;
}
.post-content :global(li) {
font-size: 0.9rem;
} }
} }
</style> </style>

View file

@ -1,6 +1,6 @@
--- ---
import Layout from '@layouts/Layout.astro';
import { SITE_URL } from '@constants'; import { SITE_URL } from '@constants';
import Layout from '@layouts/Layout.astro';
import PageHero from "@components/base/PageHero.astro"; import PageHero from "@components/base/PageHero.astro";
import CasesList from "@components/cases/CasesList.astro"; import CasesList from "@components/cases/CasesList.astro";
--- ---

View file

@ -26,4 +26,10 @@ html, body {
.site-container { .site-container {
padding: 0 2rem; padding: 0 2rem;
} }
} }
/* НУМЕРАЦИЯ ЗАГОЛОВКОВ */
.post-content { counter-reset: post-h2; }
.post-content h2 { counter-reset: post-h3; }
.post-content h2::before { counter-increment: post-h2; content: counter(post-h2) "."; margin-right: 0.5rem; color: #1e3050; font-weight: 700; }
.post-content h3::before { counter-increment: post-h3; content: counter(post-h2) "." counter(post-h3) "."; margin-right: 0.4rem; color: #1e3050; font-weight: 600; font-size: 0.9em; }

View file

@ -15,7 +15,8 @@
"@layouts/*": ["src/layouts/*"], "@layouts/*": ["src/layouts/*"],
"@assets/*": ["src/assets/*"], "@assets/*": ["src/assets/*"],
"@pages/*": ["src/pages/*"], "@pages/*": ["src/pages/*"],
"@lib/*": ["src/lib/*"] "@icons/*": ["src/icons/*"],
"@lib/*": ["src/lib/*"],
} }
} }
} }