Насторил работу блога через Backend

This commit is contained in:
Web-serfer 2026-04-15 02:12:25 +05:00
parent 014439d565
commit edd730b438
33 changed files with 1019 additions and 200 deletions

View file

@ -18,6 +18,7 @@
"@tailwindcss/vite": "^4.2.2",
"astro": "^6.0.8",
"astro-icon": "^1.1.5",
"marked": "^18.0.0",
"tailwindcss": "^4.2.2"
},
"devDependencies": {

View file

@ -1,26 +1,5 @@
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
// Сортируем и форматируем для поиска
const searchData = posts
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
.map(post => ({
title: post.data.title,
description: post.data.description,
slug: post.id,
category: post.data.category,
categoryColor: post.data.categoryColor,
date: post.data.date.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
})
}));
const title = 'Поиск по статьям';
const postsJson = JSON.stringify(searchData);
---
<div id="search-modal" class="modal-overlay" aria-hidden="true">
@ -61,16 +40,33 @@ const postsJson = JSON.stringify(searchData);
</div>
</div>
<script define:vars={{ postsJson }}>
<script>
(function() {
const modal = document.getElementById('search-modal');
const closeBtn = document.getElementById('search-modal-close-btn');
const searchInput = document.getElementById('search-input');
const resultsContainer = document.getElementById('search-results');
const posts = JSON.parse(postsJson);
let postsCache = null;
if (!modal || !searchInput || !resultsContainer) return;
async function fetchPosts() {
if (postsCache) return postsCache;
try {
const res = await fetch('/api/posts?per_page=100');
const data = await res.json();
postsCache = data.posts || [];
return postsCache;
} catch (e) {
return [];
}
}
function formatDate(dateStr) {
const d = new Date(dateStr);
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
}
function openModal() {
modal.classList.add('active');
modal.setAttribute('aria-hidden', 'false');
@ -86,7 +82,7 @@ const postsJson = JSON.stringify(searchData);
resultsContainer.innerHTML = '';
}
function handleSearch(query) {
async function handleSearch(query) {
const trimmed = query.trim().toLowerCase();
if (trimmed.length < 2) {
@ -94,6 +90,8 @@ const postsJson = JSON.stringify(searchData);
return;
}
const posts = await fetchPosts();
const filtered = posts.filter(post =>
post.title.toLowerCase().includes(trimmed) ||
post.description.toLowerCase().includes(trimmed) ||
@ -112,10 +110,9 @@ const postsJson = JSON.stringify(searchData);
return;
}
// вывод динамического списка статей
resultsContainer.innerHTML = filtered.map(post => `
<a href="/blog/${post.id}" class="search-result-item">
<span class="result-date">${post.date}</span>
<a href="/blog/${post.slug}" class="search-result-item">
<span class="result-date">${formatDate(post.date)}</span>
<h4 class="result-title">${post.title}</h4>
<p class="result-description">${post.description.substring(0, 120)}...</p>
<span class="result-arrow">

View file

@ -1,29 +1,28 @@
---
import BlogCard from '@components/blog/BlogCard.astro';
interface CollectionEntry {
interface Post {
id: string;
data: {
title: string;
description: string;
category: string;
categoryColor: string;
date: Date;
readTime: string;
imageUrl: string;
};
slug: string;
title: string;
description: string;
category: string;
categoryColor: string;
date: string;
readTime: string;
imageUrl: string;
}
interface Props {
posts: CollectionEntry[];
posts: Post[];
currentSlug?: string;
}
const { posts, currentSlug } = Astro.props;
// Форматируем дату
const formatDate = (date: Date) => {
return date.toLocaleDateString('ru-RU', {
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
@ -32,7 +31,7 @@ const formatDate = (date: Date) => {
// Фильтруем текущую статью
const filteredPosts = currentSlug
? posts.filter(post => post.id !== currentSlug).slice(0, 3)
? posts.filter(post => post.slug !== currentSlug).slice(0, 3)
: posts.slice(0, 3);
---
@ -42,14 +41,14 @@ const filteredPosts = currentSlug
<div class="related-grid">
{filteredPosts.map((post) => (
<BlogCard
title={post.data.title}
description={post.data.description}
category={post.data.category}
categoryColor={post.data.categoryColor}
date={formatDate(post.data.date)}
readTime={post.data.readTime}
imageUrl={post.data.imageUrl}
slug={`/blog/${post.id}`}
title={post.title}
description={post.description}
category={post.category}
categoryColor={post.categoryColor}
date={formatDate(post.date)}
readTime={post.readTime}
imageUrl={post.imageUrl}
slug={`/blog/${post.slug}`}
/>
))}
</div>

74
frontend/src/lib/pb.ts Normal file
View file

@ -0,0 +1,74 @@
import PocketBase from 'pocketbase';
const PB_URL = import.meta.env.POCKETBASE_URL || 'http://127.0.0.1:8090';
export const pb = new PocketBase(PB_URL);
export interface Post {
id: string;
slug: string;
title: string;
description: string;
author: string;
category: string;
categoryColor: string;
date: string;
readTime: string;
imageUrl: string;
content?: string;
draft: boolean;
}
export async function getPosts(options?: {
page?: number;
perPage?: number;
category?: string;
search?: string;
}): Promise<{ posts: Post[]; total: number; page: number; totalPages: number }> {
const page = options?.page || 1;
const perPage = options?.perPage || 10;
const filter: string[] = ['draft = false'];
if (options?.category && options.category !== 'Все') {
filter.push(`category = "${options.category}"`);
}
if (options?.search) {
filter.push(`(title ~ "${options.search}" || description ~ "${options.search}")`);
}
const result = await pb.collection('posts').getList(page, perPage, {
filter: filter.join(' && '),
sort: '-date',
});
return {
posts: (result.items || []) as unknown as Post[],
total: result.totalItems || 0,
page: (result.pageInfo?.page || 1),
totalPages: result.totalPages || 1,
};
}
export async function getPostBySlug(slug: string): Promise<Post | null> {
const result = await pb.collection('posts').getList(1, 1, {
filter: `slug="${slug}" && draft = false`,
});
if (!result.items || result.totalItems === 0) {
return null;
}
return result.items[0] as unknown as Post;
}
export async function getAllCategories(): Promise<string[]> {
const result = await pb.collection('posts').getFullList({
filter: 'draft = false',
fields: 'category',
});
const categories = (result || []).map((post: any) => post.category).filter(Boolean);
return ['Все', ...new Set(categories)];
}

View file

@ -0,0 +1,45 @@
import type { APIRoute } from 'astro';
import PocketBase from 'pocketbase';
export const POST: APIRoute = async ({ request, cookies }) => {
try {
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
const data = await request.json();
const { email, password } = data;
if (!email || !password) {
return new Response(JSON.stringify({
success: false,
error: 'Email и пароль обязательны'
}), { status: 400 });
}
const authRecord = await pb.collection('users').authWithPassword(email, password);
cookies.set('pb_auth', JSON.stringify(pb.authStore.exportToCookie()), {
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
});
return new Response(JSON.stringify({
success: true,
user: {
id: authRecord.id,
name: authRecord.name,
email: authRecord.email,
}
}), { status: 200 });
} catch (error: any) {
console.error('Sign in error:', error);
return new Response(JSON.stringify({
success: false,
error: error.message || 'Неверный email или пароль'
}), { status: 401 });
}
};

View file

@ -0,0 +1,10 @@
import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ cookies }) => {
cookies.delete('pb_auth', { path: '/' });
return new Response(JSON.stringify({
success: true,
message: 'Вы успешно вышли из аккаунта'
}), { status: 200 });
};

View file

@ -0,0 +1,45 @@
import type { APIRoute } from 'astro';
import PocketBase from 'pocketbase';
export const POST: APIRoute = async ({ request, redirect }) => {
try {
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
const data = await request.json();
const { name, email, phone, password } = data;
if (!email || !password) {
return new Response(JSON.stringify({
success: false,
error: 'Email и пароль обязательны'
}), { status: 400 });
}
const record = await pb.collection('users').create({
name,
email,
phone,
password,
passwordConfirm: password,
});
await pb.collection('users').authWithPassword(email, password);
return new Response(JSON.stringify({
success: true,
record: {
id: record.id,
name: record.name,
email: record.email,
}
}), { status: 201 });
} catch (error: any) {
console.error('Sign up error:', error);
return new Response(JSON.stringify({
success: false,
error: error.message || 'Ошибка при регистрации'
}), { status: 400 });
}
};

View file

@ -0,0 +1,40 @@
import type { APIRoute } from 'astro';
import PocketBase from 'pocketbase';
export const POST: APIRoute = async ({ request }) => {
try {
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
const data = await request.json();
const { name, phone, service } = data;
if (!name || !phone) {
return new Response(JSON.stringify({
success: false,
error: 'Имя и телефон обязательны'
}), { status: 400 });
}
const record = await pb.collection('consultations').create({
name,
phone,
service: service || '',
status: 'new',
created_at: new Date().toISOString(),
});
return new Response(JSON.stringify({
success: true,
message: 'Заявка отправлена! Мы свяжемся с вами в течение 15 минут.',
id: record.id
}), { status: 201 });
} catch (error: any) {
console.error('Consultation error:', error);
return new Response(JSON.stringify({
success: false,
error: error.message || 'Ошибка при отправке заявки'
}), { status: 400 });
}
};

View file

@ -0,0 +1,48 @@
import type { APIRoute } from 'astro';
import PocketBase from 'pocketbase';
export const GET: APIRoute = async ({ params }) => {
try {
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
const { slug } = params;
if (!slug) {
return new Response(JSON.stringify({
error: 'slug обязателен'
}), { status: 400 });
}
const result = await pb.collection('posts').getList(1, 1, {
filter: `slug="${slug}" && draft = false`,
});
if (result.totalItems === 0) {
return new Response(JSON.stringify({
error: 'Пост не найден'
}), { status: 404 });
}
const post = result.items[0];
return new Response(JSON.stringify({
id: post.id,
slug: post.slug,
title: post.title,
description: post.description,
author: post.author,
category: post.category,
categoryColor: post.categoryColor,
date: post.date,
readTime: post.readTime,
imageUrl: post.imageUrl,
content: post.content,
}), { status: 200 });
} catch (error: any) {
console.error('API post error:', error);
return new Response(JSON.stringify({
error: error.message || 'Ошибка при получении поста'
}), { status: 500 });
}
};

View file

@ -0,0 +1,54 @@
import type { APIRoute } from 'astro';
import PocketBase from 'pocketbase';
export const GET: APIRoute = async ({ url }) => {
try {
const pb = new PocketBase(import.meta.env.POCKETBASE_URL);
const page = parseInt(url.searchParams.get('page') || '1');
const perPage = parseInt(url.searchParams.get('per_page') || '10');
const category = url.searchParams.get('category');
const search = url.searchParams.get('search');
const filter: string[] = ['draft = false'];
if (category) {
filter.push(`category = "${category}"`);
}
if (search) {
filter.push(`(title ~ "${search}" || description ~ "${search}")`);
}
const result = await pb.collection('posts').getList(page, perPage, {
filter: filter.join(' && '),
sort: '-date',
});
return new Response(JSON.stringify({
posts: result.items.map(post => ({
id: post.id,
slug: post.slug,
title: post.title,
description: post.description,
author: post.author,
category: post.category,
categoryColor: post.categoryColor,
date: post.date,
readTime: post.readTime,
imageUrl: post.imageUrl,
})),
total: result.totalItems,
page: result.pageInfo.page,
perPage: result.pageInfo.perPage,
totalPages: result.totalPages,
}), { status: 200 });
} catch (error: any) {
console.error('API posts error:', error);
return new Response(JSON.stringify({
error: error.message || 'Ошибка при получении постов'
}), { status: 500 });
}
};

View file

@ -4,16 +4,10 @@ import { SITE_URL } from '@constants';
import PostCommentForm from '@components/blog/PostCommentForm.astro';
import RelatedPosts from '@components/blog/RelatedPosts.astro';
import ArticleTableOfContents from '@components/blog/ArticleTableOfContents.astro';
import { getCollection, getEntry, render } from 'astro:content';
import { getPostBySlug, getPosts } from '@lib/pb';
import { marked } from 'marked';
export const prerender = true;
export async function getStaticPaths() {
const posts = await getCollection('blog') as { id: string; data: Record<string, any> }[];
return posts.map((post: { id: string }) => ({
params: { slug: post.id },
}));
}
export const prerender = false;
const slug = Astro.params.slug;
@ -21,87 +15,77 @@ if (!slug) {
return Astro.redirect('/blog');
}
const post = await getEntry('blog', slug);
const post = await getPostBySlug(slug);
if (!post) {
return Astro.redirect('/blog');
}
const { Content } = await render(post);
// Конвертируем markdown в HTML
const contentHtml = marked(post.content || '');
// Извлекаем заголовки из MDX тела для оглавления
const body = post.body || '';
// Извлекаем заголовки для оглавления
const headingRegex = /^(#{2,3})\s+(.+)$/gm;
const tocItems: { id: string; text: string; level: number }[] = [];
const body = post.content || '';
let match;
let headingIndex = 0;
while ((match = headingRegex.exec(body)) !== null) {
const level = match[1].length;
const text = match[2].trim();
// Генерируем ID из текста заголовка
const id = text.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-');
tocItems.push({ level, id: `heading-${headingIndex++}`, text });
}
console.log('=== TOC ITEMS ===', tocItems);
// Форматируем дату
const formatDate = (date: Date) => {
return date.toLocaleDateString('ru-RU', {
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
};
// Логика авторизации (пока статичная переменная)
const isAuthorized = false;
// Для related posts берем те же категории
const { posts: relatedPosts } = await getPosts({ perPage: 4, category: post.category });
const filteredRelated = relatedPosts.filter(p => p.slug !== slug).slice(0, 3);
// URL текущей страницы
const currentUrl = `${SITE_URL}/blog/${post.id}`;
// Получаем все посты для блока "Читайте также"
const allPosts = await getCollection('blog');
const currentUrl = `${SITE_URL}/blog/${slug}`;
---
<ArticleLayout
title={`${post.data.title} — автоюрист в Сургуте`}
description={post.data.description}
canonicalLink={`${SITE_URL}/blog/${post.id}`}
title={`${post.title} — автоюрист в Сургуте`}
description={post.description}
canonicalLink={`${SITE_URL}/blog/${slug}`}
breadcrumbs={[
{ label: 'Главная', href: '/' },
{ label: 'Блог', href: '/blog' },
{ label: post.data.title }
{ label: post.title }
]}
heroImage={post.data.imageUrl}
heroAlt={post.data.title}
category={post.data.category}
postTitle={post.data.title}
date={formatDate(post.data.date)}
author={post.data.author}
readTime={post.data.readTime}
heroImage={post.imageUrl}
heroAlt={post.title}
category={post.category}
postTitle={post.title}
date={formatDate(post.date)}
author={post.author}
readTime={post.readTime}
postId={post.id}
postUrl={currentUrl}
initialLikes={12}
initialDislikes={2}
>
<!-- Содержимое статьи -->
<div class="post-content">
<Content />
</div>
<div class="post-content" set:html={contentHtml} />
<!-- Форма комментариев -->
<PostCommentForm
postId={post.id}
isAuthorized={isAuthorized}
isAuthorized={false}
/>
<!-- Похожие статьи -->
<RelatedPosts
posts={allPosts}
currentSlug={post.id}
posts={filteredRelated}
currentSlug={slug}
/>
<!-- Оглавление в сайдбаре -->
@ -168,4 +152,4 @@ const allPosts = await getCollection('blog');
font-size: 1.25rem;
}
}
</style>
</style>

View file

@ -7,32 +7,21 @@ import BlogCard from '@components/blog/BlogCard.astro';
import Pagination from '@components/base/Pagination.astro';
import CTA from '@components/base/CTA.astro';
import SearchModal from '@components/base/SearchModal.astro';
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
// Сортируем посты по дате (новые сверху)
const sortedPosts = posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
import { getPosts, getAllCategories } from '@lib/pb';
const POSTS_PER_PAGE = 6;
const currentPage = 1;
const totalPages = Math.ceil(sortedPosts.length / POSTS_PER_PAGE);
const startIndex = 0;
const endIndex = POSTS_PER_PAGE;
const paginatedPosts = sortedPosts.slice(startIndex, endIndex);
const { posts, total, totalPages } = await getPosts({ page: currentPage, perPage: POSTS_PER_PAGE });
const categories = await getAllCategories();
// Форматируем дату
const formatDate = (date: Date) => {
return date.toLocaleDateString('ru-RU', {
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
};
// Категории
const categories = ['Все', ...new Set(posts.map((post: any) => post.data.category))];
---
<Layout
@ -66,17 +55,17 @@ const categories = ['Все', ...new Set(posts.map((post: any) => post.data.cate
<section class="blog-grid-section">
<div class="site-container">
<div class="blog-grid" id="blog-grid">
{paginatedPosts.map((post: any) => (
<article class="blog-card-wrapper" data-category={post.data.category}>
{posts.map((post: any) => (
<article class="blog-card-wrapper" data-category={post.category}>
<BlogCard
title={post.data.title}
description={post.data.description}
category={post.data.category}
categoryColor={post.data.categoryColor}
date={formatDate(post.data.date)}
readTime={post.data.readTime}
imageUrl={post.data.imageUrl}
slug={`/blog/${post.id}`}
title={post.title}
description={post.description}
category={post.category}
categoryColor={post.categoryColor}
date={formatDate(post.date)}
readTime={post.readTime}
imageUrl={post.imageUrl}
slug={`/blog/${post.slug}`}
/>
</article>
))}
@ -206,4 +195,4 @@ const categories = ['Все', ...new Set(posts.map((post: any) => post.data.cate
setupAnimations();
setupFilter();
});
</script>
</script>

View file

@ -7,49 +7,28 @@ import BlogCard from '@components/blog/BlogCard.astro';
import Pagination from '@components/base/Pagination.astro';
import CTA from '@components/base/CTA.astro';
import SearchModal from '@components/base/SearchModal.astro';
import { getCollection } from 'astro:content';
import { getPosts, getAllCategories } from '@lib/pb';
export const prerender = true;
export async function getStaticPaths() {
const posts = await getCollection('blog');
const POSTS_PER_PAGE = 6;
const totalPages = Math.ceil(posts.length / POSTS_PER_PAGE);
return Array.from({ length: totalPages }, (_, i) => ({
params: { page: String(i + 2) }, // Начинаем со 2-й страницы (1-я это /blog/)
}));
}
const posts = await getCollection('blog');
// Сортируем посты по дате (новые сверху)
const sortedPosts = posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
export const prerender = false;
const POSTS_PER_PAGE = 6;
const currentPage = Number(Astro.params.page) || 1;
const totalPages = Math.ceil(sortedPosts.length / POSTS_PER_PAGE);
const startIndex = (currentPage - 1) * POSTS_PER_PAGE;
const endIndex = startIndex + POSTS_PER_PAGE;
const paginatedPosts = sortedPosts.slice(startIndex, endIndex);
const { posts, total, totalPages } = await getPosts({ page: currentPage, perPage: POSTS_PER_PAGE });
const categories = await getAllCategories();
// Форматируем дату
const formatDate = (date: Date) => {
return date.toLocaleDateString('ru-RU', {
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
};
// Категории
const categories = ['Все', ...new Set(posts.map(post => post.data.category))];
---
<Layout
title="Блог — страница ${currentPage} — автоюрист в Сургуте"
description="Полезные статьи и советы по автоспорам, ДТП, ОСАГО, лишении прав и защите прав водителей. Страница ${currentPage}."
title={`Блог — страница ${currentPage} — автоюрист в Сургуте`}
description={`Полезные статьи и советы по автоспорам, ДТП, ОСАГО, лишении прав и защите прав водителей. Страница ${currentPage}.`}
canonicalLink={`${SITE_URL}/blog/page/${currentPage}`}
breadcrumbs={[
{ label: 'Главная', href: '/' },
@ -78,17 +57,17 @@ const categories = ['Все', ...new Set(posts.map(post => post.data.category))]
<section class="blog-grid-section">
<div class="site-container">
<div class="blog-grid" id="blog-grid">
{paginatedPosts.map((post: any) => (
<article class="blog-card-wrapper" data-category={post.data.category}>
{posts.map((post: any) => (
<article class="blog-card-wrapper" data-category={post.category}>
<BlogCard
title={post.data.title}
description={post.data.description}
category={post.data.category}
categoryColor={post.data.categoryColor}
date={formatDate(post.data.date)}
readTime={post.data.readTime}
imageUrl={post.data.imageUrl}
slug={`/blog/${post.id}`}
title={post.title}
description={post.description}
category={post.category}
categoryColor={post.categoryColor}
date={formatDate(post.date)}
readTime={post.readTime}
imageUrl={post.imageUrl}
slug={`/blog/${post.slug}`}
/>
</article>
))}
@ -179,4 +158,4 @@ const categories = ['Все', ...new Set(posts.map(post => post.data.category))]
setupAnimations();
document.addEventListener('astro:after-swap', setupAnimations);
</script>
</script>

View file

@ -3,40 +3,23 @@ import Layout from '@layouts/Layout.astro';
import { SITE_URL } from '@constants';
import BlogCard from '@components/blog/BlogCard.astro';
import SearchModal from '@components/base/SearchModal.astro';
import { getCollection } from 'astro:content';
import { getPosts } from '@lib/pb';
const posts = await getCollection('blog');
// Получаем параметр поиска из URL
const url = new URL(Astro.request.url);
const searchQuery = url.searchParams.get('q') || '';
const { posts: searchResults } = searchQuery
? await getPosts({ perPage: 20, search: searchQuery })
: { posts: [], total: 0, page: 1, totalPages: 1 };
const breadcrumbsItems = [
{ label: 'Главная', href: '/' },
{ label: 'Блог', href: '/blog' },
{ label: searchQuery ? `Поиск: "${searchQuery}"` : 'Поиск' }
];
// Функция поиска по статьям
function searchArticles(query: string, allPosts: typeof posts) {
if (!query.trim()) return [];
const lowerQuery = query.toLowerCase();
return allPosts.filter(post => {
const titleMatch = post.data.title.toLowerCase().includes(lowerQuery);
const descriptionMatch = post.data.description.toLowerCase().includes(lowerQuery);
const categoryMatch = post.data.category.toLowerCase().includes(lowerQuery);
return titleMatch || descriptionMatch || categoryMatch;
});
}
const searchResults = searchArticles(searchQuery, posts);
// Форматируем дату
const formatDate = (date: Date) => {
return date.toLocaleDateString('ru-RU', {
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
@ -89,14 +72,14 @@ const formatDate = (date: Date) => {
<div class="results-grid">
{searchResults.map((post: any) => (
<BlogCard
title={post.data.title}
description={post.data.description}
category={post.data.category}
categoryColor={post.data.categoryColor}
date={formatDate(post.data.date)}
readTime={post.data.readTime}
imageUrl={post.data.imageUrl}
slug={`/blog/${post.id}`}
title={post.title}
description={post.description}
category={post.category}
categoryColor={post.categoryColor}
date={formatDate(post.date)}
readTime={post.readTime}
imageUrl={post.imageUrl}
slug={`/blog/${post.slug}`}
/>
))}
</div>
@ -149,7 +132,6 @@ const formatDate = (date: Date) => {
}));
});
// Автоматически открываем поиск при загрузке страницы без параметров
document.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('q');
@ -338,4 +320,4 @@ const formatDate = (date: Date) => {
font-size: 1.5rem;
}
}
</style>
</style>

View file

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