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[];
}
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;
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 {

View file

@ -14,9 +14,10 @@ export interface Props {
description: string;
canonicalLink?: 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>
@ -32,6 +33,17 @@ const { title, description, canonicalLink, breadcrumbs } = Astro.props;
<title>{title} {SITE_TITLE_SUFFIX}</title>
<meta name="description" content={description} />
{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" />
<!-- Yandex верификация -->
<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 }> {
// Получаем данные из коллекции голосов
const votes = await pb.collection('post_votes').getList(1, 1000, {
filter: `post="${postId}"`,
});
const likes = votes.items.filter((v: any) => v.vote_type === 'like').length;
const dislikes = votes.items.filter((v: any) => v.vote_type === 'dislike').length;
const likes = votes.items.filter((v) => v.vote_type === 'like').length;
const dislikes = votes.items.filter((v) => v.vote_type === 'dislike').length;
return { likes, dislikes };
}

View file

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

View file

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

View file

@ -182,7 +182,7 @@ export const POST: APIRoute = async ({ request }) => {
});
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) {
// Добавляем поля в коллекцию

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -167,8 +167,8 @@ export const POST: APIRoute = async ({ request, cookies }) => {
if (votesRes.ok) {
const votesData = await votesRes.json();
likes = votesData.items.filter((v: any) => v.vote_type === 'like').length;
dislikes = votesData.items.filter((v: any) => v.vote_type === 'dislike').length;
likes = votesData.items.filter((v: { vote_type: string }) => v.vote_type === 'like').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) {
const votesData = await votesCountRes.json();
likes = votesData.items.filter((v: any) => v.vote_type === 'like').length;
dislikes = votesData.items.filter((v: any) => v.vote_type === 'dislike').length;
likes = votesData.items.filter((v: { vote_type: string }) => v.vote_type === 'like').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 SearchModal from '@components/base/SearchModal.astro';
import { getPosts, getAllCategories, getPostImageUrl } from '@lib/pb';
import type { Post } from '@globalInterfaces';
export const prerender = false;
@ -65,7 +66,7 @@ const formatDate = (date: string) => {
<div class="site-container">
<div class="blog-grid" id="blog-grid">
{posts.length > 0 ? (
posts.map((post: any) => (
posts.map((post: Post) => (
<article class="blog-card-wrapper" data-category={post.category}>
<BlogCard
title={post.title}

View file

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

View file

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

View file

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

View file

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

View file

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