Fix SEO/UX audit gaps: canonical tags, org schema, nav, header claims
Add canonical URLs and a sitewide Organization JSON-LD (linked via @id to page-specific LocalBusiness/Service/Article schemas), move FAQ and Reviews into the top nav, attach the 24/7 support claim to a tooltip on the header badge instead of leaving it unexplained, drop the unverifiable incidents_resolved stat from the hero terminal animation, and clean up the empty-reviews state instead of showing a thin placeholder. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
This commit is contained in:
1 parent
130c5382cd
commit
30e1aea20b
16 files changed
+72
-11
No files matched your search
@@ -7,6 +7,7 @@ export const metadata = {
|
|||||||
title: 'Акции на IT-аутсорсинг, техподдержку и почтовые серверы',
|
title: 'Акции на IT-аутсорсинг, техподдержку и почтовые серверы',
|
||||||
description:
|
description:
|
||||||
'Актуальные акции и специальные предложения TopSysOps: скидки на IT-аутсорсинг при годовом договоре, почтовый сервер под ключ, бот с ИИ-ассистентом.',
|
'Актуальные акции и специальные предложения TopSysOps: скидки на IT-аутсорсинг при годовом договоре, почтовый сервер под ключ, бот с ИИ-ассистентом.',
|
||||||
|
alternates: { canonical: '/aktsii' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function PromotionsPage() {
|
export default async function PromotionsPage() {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export async function generateMetadata({ params }) {
|
|||||||
return {
|
return {
|
||||||
title: post.title,
|
title: post.title,
|
||||||
description: post.excerpt || undefined,
|
description: post.excerpt || undefined,
|
||||||
|
alternates: { canonical: `/blog/${params.slug}` },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export default async function BlogPostPage({ params }) {
|
|||||||
headline: post.title,
|
headline: post.title,
|
||||||
...(post.excerpt ? { description: post.excerpt } : {}),
|
...(post.excerpt ? { description: post.excerpt } : {}),
|
||||||
...(post.published_at ? { datePublished: post.published_at } : {}),
|
...(post.published_at ? { datePublished: post.published_at } : {}),
|
||||||
author: { '@type': 'Organization', name: 'TopSysOps' },
|
author: { '@id': `${process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru'}/#organization` },
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { serverApi } from '../../lib/serverApi.js';
|
|||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Блог',
|
title: 'Блог',
|
||||||
description: 'Заметки об IT-инфраструктуре, кибербезопасности и автоматизации от TopSysOps.',
|
description: 'Заметки об IT-инфраструктуре, кибербезопасности и автоматизации от TopSysOps.',
|
||||||
|
alternates: { canonical: '/blog' },
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatDate(iso) {
|
function formatDate(iso) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { serverApi } from '../../lib/serverApi.js';
|
|||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Частые вопросы об IT-аутсорсинге и услугах',
|
title: 'Частые вопросы об IT-аутсорсинге и услугах',
|
||||||
description: 'Ответы на частые вопросы об IT-аутсорсинге, техподдержке, оплате услуг и конфиденциальности данных TopSysOps.',
|
description: 'Ответы на частые вопросы об IT-аутсорсинге, техподдержке, оплате услуг и конфиденциальности данных TopSysOps.',
|
||||||
|
alternates: { canonical: '/faq' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function FaqPage() {
|
export default async function FaqPage() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const metadata = {
|
|||||||
title: 'Кейсы и портфолио — сетевая инфраструктура, ИИ, кибербезопасность',
|
title: 'Кейсы и портфолио — сетевая инфраструктура, ИИ, кибербезопасность',
|
||||||
description:
|
description:
|
||||||
'Реализованные проекты TopSysOps: миграция сетевой инфраструктуры, внедрение ИИ-ассистента, аудит кибербезопасности финтех-проекта.',
|
'Реализованные проекты TopSysOps: миграция сетевой инфраструктуры, внедрение ИИ-ассистента, аудит кибербезопасности финтех-проекта.',
|
||||||
|
alternates: { canonical: '/keysy' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function CasesPage() {
|
export default async function CasesPage() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { serverApi } from '../../lib/serverApi.js';
|
|||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Контакты',
|
title: 'Контакты',
|
||||||
description: 'Контакты TopSysOps: адрес офиса в Санкт-Петербурге, телефон, email и режим работы. Работаем с клиентами по всей России.',
|
description: 'Контакты TopSysOps: адрес офиса в Санкт-Петербурге, телефон, email и режим работы. Работаем с клиентами по всей России.',
|
||||||
|
alternates: { canonical: '/kontakty' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function ContactPage() {
|
export default async function ContactPage() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import './globals.css';
|
|||||||
import { ThemeProvider } from '../components/ThemeProvider.jsx';
|
import { ThemeProvider } from '../components/ThemeProvider.jsx';
|
||||||
import SupportChat from '../components/SupportChat.jsx';
|
import SupportChat from '../components/SupportChat.jsx';
|
||||||
import VisitTracker from '../components/VisitTracker.jsx';
|
import VisitTracker from '../components/VisitTracker.jsx';
|
||||||
|
import { getOrganizationSchema } from '../lib/schema.js';
|
||||||
|
|
||||||
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru';
|
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru';
|
||||||
|
|
||||||
@@ -33,6 +34,8 @@ export default function RootLayout({ children }) {
|
|||||||
<head>
|
<head>
|
||||||
{/* eslint-disable-next-line react/no-danger */}
|
{/* eslint-disable-next-line react/no-danger */}
|
||||||
<script dangerouslySetInnerHTML={{ __html: NO_FLASH_SCRIPT }} />
|
<script dangerouslySetInnerHTML={{ __html: NO_FLASH_SCRIPT }} />
|
||||||
|
{/* eslint-disable-next-line react/no-danger */}
|
||||||
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(getOrganizationSchema()) }} />
|
||||||
</head>
|
</head>
|
||||||
<body className="bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100">
|
<body className="bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100">
|
||||||
<ThemeProvider>{children}</ThemeProvider>
|
<ThemeProvider>{children}</ThemeProvider>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const metadata = {
|
|||||||
title: 'О компании',
|
title: 'О компании',
|
||||||
description:
|
description:
|
||||||
'TopSysOps — IT-аутсорсинг и техподдержка с 2023 года. Команда 20+ специалистов, штаб-квартира в Санкт-Петербурге, работаем с клиентами по всей России.',
|
'TopSysOps — IT-аутсорсинг и техподдержка с 2023 года. Команда 20+ специалистов, штаб-квартира в Санкт-Петербурге, работаем с клиентами по всей России.',
|
||||||
|
alternates: { canonical: '/o-kompanii' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const FACTS = [
|
const FACTS = [
|
||||||
@@ -47,6 +48,7 @@ export default async function AboutPage() {
|
|||||||
const orgSchema = {
|
const orgSchema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'Organization',
|
'@type': 'Organization',
|
||||||
|
'@id': `${process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru'}/#organization`,
|
||||||
name: 'TopSysOps',
|
name: 'TopSysOps',
|
||||||
url: process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru',
|
url: process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru',
|
||||||
foundingDate: '2023',
|
foundingDate: '2023',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { serverApi } from '../../lib/serverApi.js';
|
|||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Отзывы клиентов TopSysOps',
|
title: 'Отзывы клиентов TopSysOps',
|
||||||
description: 'Отзывы клиентов об IT-аутсорсинге, техподдержке и других услугах TopSysOps. Оставьте свой отзыв.',
|
description: 'Отзывы клиентов об IT-аутсорсинге, техподдержке и других услугах TopSysOps. Оставьте свой отзыв.',
|
||||||
|
alternates: { canonical: '/otzyvy' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function ReviewsPage() {
|
export default async function ReviewsPage() {
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import Reveal from '../components/Reveal.jsx';
|
|||||||
import SectionHeading from '../components/SectionHeading.jsx';
|
import SectionHeading from '../components/SectionHeading.jsx';
|
||||||
import { serverApi } from '../lib/serverApi.js';
|
import { serverApi } from '../lib/serverApi.js';
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
alternates: { canonical: '/' },
|
||||||
|
};
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const [services, promotions, reviews, cases, contact] = await Promise.all([
|
const [services, promotions, reviews, cases, contact] = await Promise.all([
|
||||||
serverApi.getServices(),
|
serverApi.getServices(),
|
||||||
@@ -25,6 +29,7 @@ export default async function HomePage() {
|
|||||||
const business = {
|
const business = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'LocalBusiness',
|
'@type': 'LocalBusiness',
|
||||||
|
'@id': `${process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru'}/#organization`,
|
||||||
name: 'TopSysOps',
|
name: 'TopSysOps',
|
||||||
url: process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru',
|
url: process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru',
|
||||||
...(contact?.address ? { address: { '@type': 'PostalAddress', streetAddress: contact.address, addressCountry: 'RU' } } : {}),
|
...(contact?.address ? { address: { '@type': 'PostalAddress', streetAddress: contact.address, addressCountry: 'RU' } } : {}),
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ export async function generateMetadata({ params }) {
|
|||||||
const category = await findCategory(params.slug);
|
const category = await findCategory(params.slug);
|
||||||
if (!category) return { title: 'Услуга не найдена' };
|
if (!category) return { title: 'Услуга не найдена' };
|
||||||
const copy = getServiceLandingCopy(category);
|
const copy = getServiceLandingCopy(category);
|
||||||
return { title: copy.metaTitle, description: copy.metaDescription };
|
return {
|
||||||
|
title: copy.metaTitle,
|
||||||
|
description: copy.metaDescription,
|
||||||
|
alternates: { canonical: `/uslugi/${category.slug}` },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ServiceLandingPage({ params }) {
|
export default async function ServiceLandingPage({ params }) {
|
||||||
@@ -33,7 +37,7 @@ export default async function ServiceLandingPage({ params }) {
|
|||||||
'@type': 'Service',
|
'@type': 'Service',
|
||||||
name: copy.h1,
|
name: copy.h1,
|
||||||
description: copy.metaDescription,
|
description: copy.metaDescription,
|
||||||
provider: { '@type': 'Organization', name: 'TopSysOps' },
|
provider: { '@id': `${process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru'}/#organization` },
|
||||||
areaServed: 'RU',
|
areaServed: 'RU',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const metadata = {
|
|||||||
title: 'Услуги IT-аутсорсинга и техподдержки — прайс-лист',
|
title: 'Услуги IT-аутсорсинга и техподдержки — прайс-лист',
|
||||||
description:
|
description:
|
||||||
'IT-аутсорсинг, техподдержка, кибербезопасность, администрирование серверов и баз данных, сетевая инфраструктура, облачные решения, ИИ и автоматизация. Актуальные цены.',
|
'IT-аутсорсинг, техподдержка, кибербезопасность, администрирование серверов и баз данных, сетевая инфраструктура, облачные решения, ИИ и автоматизация. Актуальные цены.',
|
||||||
|
alternates: { canonical: '/uslugi' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function ServicesPage() {
|
export default async function ServicesPage() {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const NAV_ITEMS = [
|
|||||||
{ href: '/uslugi', label: 'Услуги' },
|
{ href: '/uslugi', label: 'Услуги' },
|
||||||
{ href: '/aktsii', label: 'Акции' },
|
{ href: '/aktsii', label: 'Акции' },
|
||||||
{ href: '/keysy', label: 'Кейсы' },
|
{ href: '/keysy', label: 'Кейсы' },
|
||||||
|
{ href: '/otzyvy', label: 'Отзывы' },
|
||||||
|
{ href: '/faq', label: 'FAQ' },
|
||||||
{ href: '/o-kompanii', label: 'О компании' },
|
{ href: '/o-kompanii', label: 'О компании' },
|
||||||
{ href: '/blog', label: 'Блог' },
|
{ href: '/blog', label: 'Блог' },
|
||||||
{ href: '/kontakty', label: 'Контакты' },
|
{ href: '/kontakty', label: 'Контакты' },
|
||||||
@@ -29,12 +31,12 @@ export default function Header() {
|
|||||||
<span className="text-base font-bold tracking-tight">TopSysOps</span>
|
<span className="text-base font-bold tracking-tight">TopSysOps</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="hidden md:flex items-center gap-1">
|
<nav className="hidden lg:flex items-center gap-0.5 xl:gap-1">
|
||||||
{NAV_ITEMS.map((item) => (
|
{NAV_ITEMS.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="px-3 py-2 rounded-md text-sm font-medium transition-colors text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-900"
|
className="px-2.5 xl:px-3 py-2 rounded-md text-sm font-medium transition-colors text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-900"
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -52,7 +54,7 @@ export default function Header() {
|
|||||||
<button
|
<button
|
||||||
aria-label="Открыть меню"
|
aria-label="Открыть меню"
|
||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(!open)}
|
||||||
className="md:hidden p-2 rounded-md hover:bg-slate-100 dark:hover:bg-slate-900"
|
className="lg:hidden p-2 rounded-md hover:bg-slate-100 dark:hover:bg-slate-900"
|
||||||
>
|
>
|
||||||
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||||
</button>
|
</button>
|
||||||
@@ -60,7 +62,7 @@ export default function Header() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{open && (
|
{open && (
|
||||||
<nav className="md:hidden pb-4 flex flex-col gap-1">
|
<nav className="lg:hidden pb-4 flex flex-col gap-1">
|
||||||
{NAV_ITEMS.map((item) => (
|
{NAV_ITEMS.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
@@ -77,8 +79,13 @@ export default function Header() {
|
|||||||
|
|
||||||
<div className="border-t px-5 sm:px-8 py-1.5 text-xs flex items-center gap-4 max-w-6xl mx-auto border-slate-200 text-slate-500 dark:border-slate-900 dark:text-slate-400">
|
<div className="border-t px-5 sm:px-8 py-1.5 text-xs flex items-center gap-4 max-w-6xl mx-auto border-slate-200 text-slate-500 dark:border-slate-900 dark:text-slate-400">
|
||||||
<StatusDot tone="ok" pulse label="система активна" mono />
|
<StatusDot tone="ok" pulse label="система активна" mono />
|
||||||
<span className="hidden sm:inline font-mono">24/7</span>
|
<Link
|
||||||
<span className="hidden sm:inline font-mono">отклик ~15 мин</span>
|
href="/uslugi/support"
|
||||||
|
title="На тарифе «Премиум»: поддержка 24/7, среднее время реакции ~15 минут. На тарифах «Базовый» и «Бизнес» — по графику 3/4 или 5/2."
|
||||||
|
className="hidden sm:inline font-mono underline decoration-dotted underline-offset-2 hover:text-emerald-500"
|
||||||
|
>
|
||||||
|
24/7 · отклик ~15 мин*
|
||||||
|
</Link>
|
||||||
<a href="/admin" className="ml-auto underline decoration-dotted underline-offset-2 hover:text-emerald-500">
|
<a href="/admin" className="ml-auto underline decoration-dotted underline-offset-2 hover:text-emerald-500">
|
||||||
вход для сотрудников
|
вход для сотрудников
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ArrowRight } from 'lucide-react';
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
|
||||||
const LINES = ['$ status --infra', 'uptime: 99.98%', 'incidents_resolved: 1240+', 'avg_response_time: 15m'];
|
const LINES = ['$ status --infra', 'uptime: 99.98%', 'team_size: 20+', 'avg_response_time: 15m'];
|
||||||
|
|
||||||
function cx(...args) {
|
function cx(...args) {
|
||||||
return args.filter(Boolean).join(' ');
|
return args.filter(Boolean).join(' ');
|
||||||
|
|||||||
@@ -11,6 +11,27 @@ function cx(...args) {
|
|||||||
export default function Reviews({ reviews, limit, moreHref, showForm = true }) {
|
export default function Reviews({ reviews, limit, moreHref, showForm = true }) {
|
||||||
const list = limit ? reviews.slice(0, limit) : reviews;
|
const list = limit ? reviews.slice(0, limit) : reviews;
|
||||||
|
|
||||||
|
// Пустая витрина отзывов на тизере (главная) выглядит слабо - лучше не показывать
|
||||||
|
// секцию вовсе, чем пустой список без формы для его заполнения.
|
||||||
|
if (list.length === 0 && !showForm) return null;
|
||||||
|
|
||||||
|
if (list.length === 0) {
|
||||||
|
return (
|
||||||
|
<section id="reviews" className="py-20 bg-slate-100/60 dark:bg-slate-950/40">
|
||||||
|
<div className="max-w-2xl mx-auto px-5 sm:px-8">
|
||||||
|
<SectionHeading
|
||||||
|
eyebrow="reviews --approved"
|
||||||
|
title="Отзывы клиентов"
|
||||||
|
desc="Отзывов пока нет - будьте первым, кто расскажет о работе с нами. Публикуется после проверки менеджером."
|
||||||
|
/>
|
||||||
|
<Reveal>
|
||||||
|
<ReviewForm />
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="reviews" className="py-20 bg-slate-100/60 dark:bg-slate-950/40">
|
<section id="reviews" className="py-20 bg-slate-100/60 dark:bg-slate-950/40">
|
||||||
<div className="max-w-6xl mx-auto px-5 sm:px-8">
|
<div className="max-w-6xl mx-auto px-5 sm:px-8">
|
||||||
@@ -22,7 +43,6 @@ export default function Reviews({ reviews, limit, moreHref, showForm = true }) {
|
|||||||
<div className={cx('grid gap-10', showForm && 'md:grid-cols-2')}>
|
<div className={cx('grid gap-10', showForm && 'md:grid-cols-2')}>
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{list.length === 0 && <p className="text-sm text-slate-400">Пока нет опубликованных отзывов. Будьте первым!</p>}
|
|
||||||
{list.map((r) => (
|
{list.map((r) => (
|
||||||
<div key={r.id} className="rounded-lg border p-5 bg-white border-slate-200 dark:bg-slate-900 dark:border-slate-800">
|
<div key={r.id} className="rounded-lg border p-5 bg-white border-slate-200 dark:bg-slate-900 dark:border-slate-800">
|
||||||
<div className="flex items-center gap-1 mb-2">
|
<div className="flex items-center gap-1 mb-2">
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru';
|
||||||
|
|
||||||
|
export function getOrganizationSchema() {
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'Organization',
|
||||||
|
'@id': `${SITE_URL}/#organization`,
|
||||||
|
name: 'TopSysOps',
|
||||||
|
url: SITE_URL,
|
||||||
|
foundingDate: '2023',
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in new issue
Block a user