fix: исправить типизацию, добавить SEO метатеги, исправить API Rules PB

- Добавлены типы PostVotes, VoteStats, Comment, Consultation, PostResponse
- Заменены все any на конкретные типы
- Исправлены catch (error: any) -> catch (error: unknown)
- Добавлены og: и twitter: метатеги в Layout
- Исправлены API Rules в PocketBase (posts, reviews, post_votes)
This commit is contained in:
Web-serfer 2026-05-06 22:33:44 +05:00
parent 2f57bf91ef
commit b5d2174fdf
20 changed files with 110 additions and 41 deletions

View file

@ -64,9 +64,52 @@ export interface DocumentItem {
tags?: string[]; tags?: string[];
} }
export interface NavLink { export interface PostVotes {
id: string;
post_id: string;
user_id: string;
vote_type: 'like' | 'dislike';
created: string;
updated: string;
}
export interface VoteStats {
likes: number;
dislikes: number;
userVote: 'like' | 'dislike' | null;
}
export interface Comment {
id: string;
post_id: string;
user_id: string;
author_name: string;
content: string;
status: 'pending' | 'published';
created: string;
updated: string;
}
export interface Consultation {
id: string;
name: string; name: string;
url: string; phone: string;
question: string;
status: 'new' | 'in_progress' | 'completed';
created: string;
}
export interface PostResponse {
id: string;
slug: string;
title: string;
description: string;
author: string;
category: string;
categoryColor: string;
date: string;
readTime: string;
image: string | null;
} }
export interface CompanyInfo { export interface CompanyInfo {

View file

@ -14,9 +14,10 @@ export interface Props {
description: string; description: string;
canonicalLink?: string; canonicalLink?: string;
breadcrumbs?: Array<{ label: string; href?: string }>; breadcrumbs?: Array<{ label: string; href?: string }>;
ogImage?: string;
} }
const { title, description, canonicalLink, breadcrumbs } = Astro.props; const { title, description, canonicalLink, breadcrumbs, ogImage } = Astro.props;
--- ---
<!doctype html> <!doctype html>
@ -32,6 +33,17 @@ const { title, description, canonicalLink, breadcrumbs } = Astro.props;
<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} />}
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
{canonicalLink && <meta property="og:url" content={canonicalLink} />}
{ogImage && <meta property="og:image" content={ogImage} />}
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{ogImage && <meta name="twitter:image" content={ogImage} />}
<link rel="sitemap" href="/sitemap-index.xml" /> <link rel="sitemap" href="/sitemap-index.xml" />
<!-- Yandex верификация --> <!-- Yandex верификация -->
<meta name="yandex-verification" content="be3edfd138348e43" /> <meta name="yandex-verification" content="be3edfd138348e43" />

View file

@ -60,13 +60,12 @@ export async function getPostVotes(postId: string): Promise<VoteStats> {
} }
export async function getPostVotesStats(postId: string): Promise<{ likes: number; dislikes: number }> { export async function getPostVotesStats(postId: string): Promise<{ likes: number; dislikes: number }> {
// Получаем данные из коллекции голосов
const votes = await pb.collection('post_votes').getList(1, 1000, { const votes = await pb.collection('post_votes').getList(1, 1000, {
filter: `post="${postId}"`, filter: `post="${postId}"`,
}); });
const likes = votes.items.filter((v: any) => v.vote_type === 'like').length; const likes = votes.items.filter((v) => v.vote_type === 'like').length;
const dislikes = votes.items.filter((v: any) => v.vote_type === 'dislike').length; const dislikes = votes.items.filter((v) => v.vote_type === 'dislike').length;
return { likes, dislikes }; return { likes, dislikes };
} }

View file

@ -61,7 +61,7 @@ export const POST: APIRoute = async ({ request }) => {
message: 'Email подтверждён' message: 'Email подтверждён'
}), { status: 200 }); }), { status: 200 });
} catch (error: any) { } catch (error: unknown) {
console.error('Confirm error:', error); console.error('Confirm error:', error);
return new Response(JSON.stringify({ return new Response(JSON.stringify({

View file

@ -118,7 +118,7 @@ export const POST: APIRoute = async ({ request }) => {
message: 'Ссылка для сброса пароля отправлена' message: 'Ссылка для сброса пароля отправлена'
}), { status: 200 }); }), { status: 200 });
} catch (error: any) { } catch (error: unknown) {
console.error('Forgot password error:', error); console.error('Forgot password error:', error);
return new Response(JSON.stringify({ return new Response(JSON.stringify({

View file

@ -182,7 +182,7 @@ export const POST: APIRoute = async ({ request }) => {
}); });
const collectionData = await collectionResponse.json(); const collectionData = await collectionResponse.json();
const fieldExists = collectionData.fields?.some((f: any) => f.name === 'reset_token'); const fieldExists = collectionData.fields?.some((f: { name: string }) => f.name === 'reset_token');
if (!fieldExists) { if (!fieldExists) {
// Добавляем поля в коллекцию // Добавляем поля в коллекцию

View file

@ -59,7 +59,7 @@ export const POST: APIRoute = async ({ request }) => {
message: 'Пароль успешно изменён' message: 'Пароль успешно изменён'
}), { status: 200 }); }), { status: 200 });
} catch (error: any) { } catch (error: unknown) {
console.error('Reset password error:', error); console.error('Reset password error:', error);
return new Response(JSON.stringify({ return new Response(JSON.stringify({

View file

@ -53,12 +53,12 @@ export const POST: APIRoute = async ({ request, cookies, url }) => {
redirect: redirectUrl redirect: redirectUrl
}), { status: 200 }); }), { status: 200 });
} catch (error: any) { } catch (error: unknown) {
console.error('Sign in error:', error); console.error('Sign in error:', error);
return new Response(JSON.stringify({ return new Response(JSON.stringify({
success: false, success: false,
error: error.message || 'Неверный email или пароль' error: error instanceof Error ? error.message : 'Неверный email или пароль'
}), { status: 401 }); }), { status: 401 });
} }
}; };

View file

@ -80,19 +80,20 @@ export const POST: APIRoute = async ({ request, redirect }) => {
email email
}), { status: 201 }); }), { status: 201 });
} catch (error: any) { } catch (error: unknown) {
console.error('Sign up error:', error); console.error('Sign up error:', error);
let errorMessage = 'Ошибка при регистрации'; let errorMessage = 'Ошибка при регистрации';
if (error.response?.data) { if (error && typeof error === 'object' && 'response' in error) {
const data = error.response.data; const errWithResponse = error as { response?: { data?: Record<string, unknown> } };
if (data.email) { const data = errWithResponse.response?.data as Record<string, unknown> | undefined;
errorMessage = `Email: ${data.email.message || 'уже используется'}`; if (data?.email) {
} else if (data.password) { errorMessage = `Email: ${(data.email as { message?: string }).message || 'уже используется'}`;
errorMessage = `Пароль: ${data.password.message || 'некорректный'}`; } else if (data?.password) {
errorMessage = `Пароль: ${(data.password as { message?: string }).message || 'некорректный'}`;
} }
} else if (error.message) { } else if (error instanceof Error) {
errorMessage = error.message; errorMessage = error.message;
} }

View file

@ -146,12 +146,13 @@ export const POST: APIRoute = async ({ request }) => {
id: result.id id: result.id
}), { status: 201, headers: { 'Content-Type': 'application/json' } }); }), { status: 201, headers: { 'Content-Type': 'application/json' } });
} catch (error: any) { } catch (error: unknown) {
console.error('Consultation error:', error); console.error('Consultation error:', error);
const message = error instanceof Error ? error.message : 'Ошибка при отправке заявки';
return new Response(JSON.stringify({ return new Response(JSON.stringify({
success: false, success: false,
error: error.message || 'Ошибка при отправке заявки' error: message
}), { status: 400, headers: { 'Content-Type': 'application/json' } }); }), { status: 400, headers: { 'Content-Type': 'application/json' } });
} }
}; };

View file

@ -38,11 +38,13 @@ export const GET: APIRoute = async ({ params }) => {
content: post.content, content: post.content,
}), { status: 200 }); }), { status: 200 });
} catch (error: any) { } catch (error: unknown) {
console.error('API post error:', error); console.error('API post error:', error);
const message = error instanceof Error ? error.message : 'Ошибка при получении поста';
return new Response(JSON.stringify({ return new Response(JSON.stringify({
error: error.message || 'Ошибка при получении поста' error: message
}), { status: 500 }); }), { status: 500 });
} }
}; };

View file

@ -1,5 +1,6 @@
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import PocketBase from 'pocketbase'; import PocketBase from 'pocketbase';
import type { PostResponse } from '../../globalInterfaces';
const PB_URL = import.meta.env.PB_POCKETBASE_URL || 'http://127.0.0.1:8090'; const PB_URL = import.meta.env.PB_POCKETBASE_URL || 'http://127.0.0.1:8090';
@ -27,7 +28,7 @@ export const GET: APIRoute = async ({ url }) => {
sort: '-date', sort: '-date',
}); });
const getImageUrl = (post: any) => { const getImageUrl = (post: PostResponse) => {
if (!post.image) return null; if (!post.image) return null;
const fileUrl = pb.files.getUrl(post, post.image); const fileUrl = pb.files.getUrl(post, post.image);
if (fileUrl.startsWith('/')) { if (fileUrl.startsWith('/')) {
@ -37,7 +38,7 @@ export const GET: APIRoute = async ({ url }) => {
}; };
return new Response(JSON.stringify({ return new Response(JSON.stringify({
posts: result.items.map(post => ({ posts: result.items.map((post) => ({
id: post.id, id: post.id,
slug: post.slug, slug: post.slug,
title: post.title, title: post.title,
@ -55,11 +56,13 @@ export const GET: APIRoute = async ({ url }) => {
totalPages: result.totalPages, totalPages: result.totalPages,
}), { status: 200 }); }), { status: 200 });
} catch (error: any) { } catch (error: unknown) {
console.error('API posts error:', error); console.error('API posts error:', error);
const message = error instanceof Error ? error.message : 'Ошибка при получении постов';
return new Response(JSON.stringify({ return new Response(JSON.stringify({
error: error.message || 'Ошибка при получении постов' error: message
}), { status: 500 }); }), { status: 500 });
} }
}; };

View file

@ -125,8 +125,8 @@ export const POST: APIRoute = async ({ request, cookies }) => {
if (votesRes.ok) { if (votesRes.ok) {
const votesData = await votesRes.json(); const votesData = await votesRes.json();
console.log('[ReviewVote API] Votes data:', JSON.stringify(votesData)); console.log('[ReviewVote API] Votes data:', JSON.stringify(votesData));
likes = votesData.items?.filter((v: any) => v.vote_type === 'likes').length || 0; likes = votesData.items?.filter((v: { vote_type: string }) => v.vote_type === 'likes').length || 0;
dislikes = votesData.items?.filter((v: any) => v.vote_type === 'dislikes').length || 0; dislikes = votesData.items?.filter((v: { vote_type: string }) => v.vote_type === 'dislikes').length || 0;
} else { } else {
const errorText = await votesRes.text(); const errorText = await votesRes.text();
console.error('[ReviewVote API] Votes error:', errorText); console.error('[ReviewVote API] Votes error:', errorText);
@ -179,8 +179,8 @@ export const GET: APIRoute = async ({ url, cookies }) => {
if (votesRes.ok) { if (votesRes.ok) {
const votesData = await votesRes.json(); const votesData = await votesRes.json();
console.log('[ReviewVote API GET] Votes data:', JSON.stringify(votesData)); console.log('[ReviewVote API GET] Votes data:', JSON.stringify(votesData));
likes = votesData.items?.filter((v: any) => v.vote_type === 'likes').length || 0; likes = votesData.items?.filter((v: { vote_type: string }) => v.vote_type === 'likes').length || 0;
dislikes = votesData.items?.filter((v: any) => v.vote_type === 'dislikes').length || 0; dislikes = votesData.items?.filter((v: { vote_type: string }) => v.vote_type === 'dislikes').length || 0;
} }
let userVote: 'likes' | 'dislikes' | null = null; let userVote: 'likes' | 'dislikes' | null = null;

View file

@ -167,8 +167,8 @@ export const POST: APIRoute = async ({ request, cookies }) => {
if (votesRes.ok) { if (votesRes.ok) {
const votesData = await votesRes.json(); const votesData = await votesRes.json();
likes = votesData.items.filter((v: any) => v.vote_type === 'like').length; likes = votesData.items.filter((v: { vote_type: string }) => v.vote_type === 'like').length;
dislikes = votesData.items.filter((v: any) => v.vote_type === 'dislike').length; dislikes = votesData.items.filter((v: { vote_type: string }) => v.vote_type === 'dislike').length;
} }
// Обновляем счетчики в посте только если есть токен // Обновляем счетчики в посте только если есть токен
@ -243,8 +243,8 @@ export const GET: APIRoute = async ({ url, cookies }) => {
if (votesCountRes.ok) { if (votesCountRes.ok) {
const votesData = await votesCountRes.json(); const votesData = await votesCountRes.json();
likes = votesData.items.filter((v: any) => v.vote_type === 'like').length; likes = votesData.items.filter((v: { vote_type: string }) => v.vote_type === 'like').length;
dislikes = votesData.items.filter((v: any) => v.vote_type === 'dislike').length; dislikes = votesData.items.filter((v: { vote_type: string }) => v.vote_type === 'dislike').length;
} }
// Определяем голос текущего пользователя // Определяем голос текущего пользователя

View file

@ -7,6 +7,7 @@ import BlogCard from '@components/blog/BlogCard.astro';
import Pagination from '@components/base/Pagination.astro'; import Pagination from '@components/base/Pagination.astro';
import SearchModal from '@components/base/SearchModal.astro'; import SearchModal from '@components/base/SearchModal.astro';
import { getPosts, getAllCategories, getPostImageUrl } from '@lib/pb'; import { getPosts, getAllCategories, getPostImageUrl } from '@lib/pb';
import type { Post } from '@globalInterfaces';
export const prerender = false; export const prerender = false;
@ -65,7 +66,7 @@ const formatDate = (date: string) => {
<div class="site-container"> <div class="site-container">
<div class="blog-grid" id="blog-grid"> <div class="blog-grid" id="blog-grid">
{posts.length > 0 ? ( {posts.length > 0 ? (
posts.map((post: any) => ( posts.map((post: Post) => (
<article class="blog-card-wrapper" data-category={post.category}> <article class="blog-card-wrapper" data-category={post.category}>
<BlogCard <BlogCard
title={post.title} title={post.title}

View file

@ -7,6 +7,7 @@ import BlogCard from '@components/blog/BlogCard.astro';
import Pagination from '@components/base/Pagination.astro'; import Pagination from '@components/base/Pagination.astro';
import SearchModal from '@components/base/SearchModal.astro'; import SearchModal from '@components/base/SearchModal.astro';
import { getPosts, getAllCategories, getPostImageUrl } from '@lib/pb'; import { getPosts, getAllCategories, getPostImageUrl } from '@lib/pb';
import type { Post } from '@globalInterfaces';
export const prerender = false; export const prerender = false;
@ -66,7 +67,7 @@ const formatDate = (date: string) => {
<div class="site-container"> <div class="site-container">
<div class="blog-grid" id="blog-grid"> <div class="blog-grid" id="blog-grid">
{posts.length > 0 ? ( {posts.length > 0 ? (
posts.map((post: any) => ( posts.map((post: Post) => (
<article class="blog-card-wrapper" data-category={post.category}> <article class="blog-card-wrapper" data-category={post.category}>
<BlogCard <BlogCard
title={post.title} title={post.title}

View file

@ -7,6 +7,7 @@ import BlogCard from '@components/blog/BlogCard.astro';
import Pagination from '@components/base/Pagination.astro'; import Pagination from '@components/base/Pagination.astro';
import SearchModal from '@components/base/SearchModal.astro'; import SearchModal from '@components/base/SearchModal.astro';
import { getPosts, getAllCategories, getPostImageUrl } from '@lib/pb'; import { getPosts, getAllCategories, getPostImageUrl } from '@lib/pb';
import type { Post } from '@globalInterfaces';
const POSTS_PER_PAGE = 6; const POSTS_PER_PAGE = 6;
const currentPage = 1; const currentPage = 1;
@ -56,7 +57,7 @@ const postsCountText = String(total);
<section class="blog-grid-section"> <section class="blog-grid-section">
<div class="site-container"> <div class="site-container">
<div class="blog-grid" id="blog-grid"> <div class="blog-grid" id="blog-grid">
{posts.map((post: any) => ( {posts.map((post: Post) => (
<article class="blog-card-wrapper" data-category={post.category}> <article class="blog-card-wrapper" data-category={post.category}>
<BlogCard <BlogCard
title={post.title} title={post.title}

View file

@ -7,6 +7,7 @@ import BlogCard from '@components/blog/BlogCard.astro';
import Pagination from '@components/base/Pagination.astro'; import Pagination from '@components/base/Pagination.astro';
import SearchModal from '@components/base/SearchModal.astro'; import SearchModal from '@components/base/SearchModal.astro';
import { getPosts, getAllCategories, getPostImageUrl } from '@lib/pb'; import { getPosts, getAllCategories, getPostImageUrl } from '@lib/pb';
import type { Post } from '@globalInterfaces';
export const prerender = false; export const prerender = false;
@ -59,7 +60,7 @@ const formatDate = (date: string) => {
<section class="blog-grid-section"> <section class="blog-grid-section">
<div class="site-container"> <div class="site-container">
<div class="blog-grid" id="blog-grid"> <div class="blog-grid" id="blog-grid">
{posts.map((post: any) => ( {posts.map((post: Post) => (
<article class="blog-card-wrapper" data-category={post.category}> <article class="blog-card-wrapper" data-category={post.category}>
<BlogCard <BlogCard
title={post.title} title={post.title}

View file

@ -4,6 +4,7 @@ import { SITE_URL } from '@constants';
import BlogCard from '@components/blog/BlogCard.astro'; import BlogCard from '@components/blog/BlogCard.astro';
import SearchModal from '@components/base/SearchModal.astro'; import SearchModal from '@components/base/SearchModal.astro';
import { getPosts, getPostImageUrl } from '@lib/pb'; import { getPosts, getPostImageUrl } from '@lib/pb';
import type { Post } from '@globalInterfaces';
const url = new URL(Astro.request.url); const url = new URL(Astro.request.url);
const searchQuery = url.searchParams.get('q') || ''; const searchQuery = url.searchParams.get('q') || '';
@ -70,7 +71,7 @@ const formatDate = (date: string) => {
<section class="results-section"> <section class="results-section">
<div class="site-container"> <div class="site-container">
<div class="results-grid"> <div class="results-grid">
{searchResults.map((post: any) => ( {searchResults.map((post: Post) => (
<BlogCard <BlogCard
title={post.title} title={post.title}
description={post.description} description={post.description}

View file

@ -7,12 +7,15 @@ import WhyUs from "@components/home/WhyUs.astro";
import Reviews from "@components/home/Reviews.astro"; import Reviews from "@components/home/Reviews.astro";
import Faq from "@components/home/Faq.astro"; import Faq from "@components/home/Faq.astro";
import { SITE_URL, EXPERIENCE_YEARS } from '@constants'; import { SITE_URL, EXPERIENCE_YEARS } from '@constants';
const ogImage = `${SITE_URL}/images/home/avtourist-surgut.avif`;
--- ---
<Layout <Layout
title="Автоюрист в Сургуте — юридическая помощь автовладельцам" title="Автоюрист в Сургуте — юридическая помощь автовладельцам"
description="Профессиональная юридическая помощь автовладельцам в Сургуте. Споры со страховыми, возврат прав, ДТП, споры с автосалонами." description="Профессиональная юридическая помощь автовладельцам в Сургуте. Споры со страховыми, возврат прав, ДТП, споры с автосалонами."
canonicalLink={SITE_URL} canonicalLink={SITE_URL}
ogImage={ogImage}
> >
<Hero <Hero
badgeText="ЗАЩИТА ВОДИТЕЛЕЙ В СУРГУТЕ" badgeText="ЗАЩИТА ВОДИТЕЛЕЙ В СУРГУТЕ"