Initial commit: TopSysOps site + admin panel
Multi-page public site (Next.js), Node/Express backend with Postgres, and Vite admin/account app, restructured from a single-page layout into dedicated service, promotions, cases, about, FAQ, and blog pages for SEO. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
This commit is contained in:
commit
130c5382cd
108 files changed
+7146
No files matched your search
@@ -0,0 +1,22 @@
|
||||
import Header from '../../components/Header.jsx';
|
||||
import Footer from '../../components/Footer.jsx';
|
||||
import Promotions from '../../components/sections/Promotions.jsx';
|
||||
import { serverApi } from '../../lib/serverApi.js';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Акции на IT-аутсорсинг, техподдержку и почтовые серверы',
|
||||
description:
|
||||
'Актуальные акции и специальные предложения TopSysOps: скидки на IT-аутсорсинг при годовом договоре, почтовый сервер под ключ, бот с ИИ-ассистентом.',
|
||||
};
|
||||
|
||||
export default async function PromotionsPage() {
|
||||
const promotions = await serverApi.getPromotions();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Promotions promotions={promotions} showMailNote />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import Header from '../../../components/Header.jsx';
|
||||
import Footer from '../../../components/Footer.jsx';
|
||||
import Reveal from '../../../components/Reveal.jsx';
|
||||
import { serverApi } from '../../../lib/serverApi.js';
|
||||
|
||||
function formatDate(iso) {
|
||||
if (!iso) return '';
|
||||
return new Date(iso).toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const post = await serverApi.getBlogPost(params.slug);
|
||||
if (!post) return { title: 'Статья не найдена' };
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.excerpt || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({ params }) {
|
||||
const post = await serverApi.getBlogPost(params.slug);
|
||||
if (!post) notFound();
|
||||
|
||||
const articleSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: post.title,
|
||||
...(post.excerpt ? { description: post.excerpt } : {}),
|
||||
...(post.published_at ? { datePublished: post.published_at } : {}),
|
||||
author: { '@type': 'Organization', name: 'TopSysOps' },
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }} />
|
||||
|
||||
<Header />
|
||||
<article className="max-w-2xl mx-auto px-5 sm:px-8 py-20 min-h-[60vh]">
|
||||
<Link href="/blog" className="text-sm text-emerald-500 font-medium inline-flex items-center gap-1 mb-8">
|
||||
<ArrowLeft className="h-3.5 w-3.5" /> Ко всем статьям
|
||||
</Link>
|
||||
|
||||
<Reveal>
|
||||
<p className="text-xs text-slate-400 dark:text-slate-500 mb-2 font-mono">{formatDate(post.published_at)}</p>
|
||||
<h1 className="text-3xl font-bold mb-6 font-display">{post.title}</h1>
|
||||
<div
|
||||
className="prose prose-slate dark:prose-invert max-w-none prose-a:text-emerald-500"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: post.content || '' }}
|
||||
/>
|
||||
</Reveal>
|
||||
</article>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import Link from 'next/link';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import Header from '../../components/Header.jsx';
|
||||
import Footer from '../../components/Footer.jsx';
|
||||
import SectionHeading from '../../components/SectionHeading.jsx';
|
||||
import Reveal from '../../components/Reveal.jsx';
|
||||
import { serverApi } from '../../lib/serverApi.js';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Блог',
|
||||
description: 'Заметки об IT-инфраструктуре, кибербезопасности и автоматизации от TopSysOps.',
|
||||
};
|
||||
|
||||
function formatDate(iso) {
|
||||
if (!iso) return '';
|
||||
return new Date(iso).toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
}
|
||||
|
||||
export default async function BlogListPage() {
|
||||
const posts = await serverApi.getBlogPosts();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<section className="max-w-3xl mx-auto px-5 sm:px-8 py-20 min-h-[60vh]">
|
||||
<SectionHeading eyebrow="blog --articles" title="Блог" desc="Заметки об IT-инфраструктуре, кибербезопасности и автоматизации." />
|
||||
|
||||
{posts.length === 0 && <p className="text-sm text-slate-400">Пока нет опубликованных статей - загляните позже.</p>}
|
||||
|
||||
<div className="space-y-4">
|
||||
{posts.map((p, i) => (
|
||||
<Reveal key={p.id} delay={i * 60}>
|
||||
<Link
|
||||
href={`/blog/${p.slug}`}
|
||||
className="block rounded-lg border p-5 transition-colors hover:border-emerald-500 bg-white border-slate-200 dark:bg-slate-900 dark:border-slate-800"
|
||||
>
|
||||
<p className="text-xs text-slate-400 dark:text-slate-500 mb-1 font-mono">{formatDate(p.published_at)}</p>
|
||||
<h2 className="font-bold mb-2 font-display text-lg">{p.title}</h2>
|
||||
{p.excerpt && <p className="text-sm text-slate-600 dark:text-slate-400 mb-2">{p.excerpt}</p>}
|
||||
<span className="text-sm text-emerald-500 font-medium inline-flex items-center gap-1">
|
||||
Читать <ArrowRight className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</Link>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import Header from '../../components/Header.jsx';
|
||||
import Footer from '../../components/Footer.jsx';
|
||||
import Faq from '../../components/sections/Faq.jsx';
|
||||
import { serverApi } from '../../lib/serverApi.js';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Частые вопросы об IT-аутсорсинге и услугах',
|
||||
description: 'Ответы на частые вопросы об IT-аутсорсинге, техподдержке, оплате услуг и конфиденциальности данных TopSysOps.',
|
||||
};
|
||||
|
||||
export default async function FaqPage() {
|
||||
const faqs = await serverApi.getFaqs();
|
||||
|
||||
const faqSchema =
|
||||
faqs.length > 0
|
||||
? {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqs.map((f) => ({
|
||||
'@type': 'Question',
|
||||
name: f.question,
|
||||
acceptedAnswer: { '@type': 'Answer', text: f.answer },
|
||||
})),
|
||||
}
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{faqSchema && (
|
||||
// eslint-disable-next-line react/no-danger
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }} />
|
||||
)}
|
||||
<Header />
|
||||
<Faq faqs={faqs} />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Unbounded:wght@500;700;900&family=Manrope:wght@400;500;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Manrope', sans-serif;
|
||||
overflow-x: hidden;
|
||||
max-width: 100vw;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="7" fill="#0f172a"/>
|
||||
<path d="M11 9 L20 16 L11 23" fill="none" stroke="#10b981" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="21" y="20.4" width="6" height="3.2" rx="1" fill="#10b981"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1,22 @@
|
||||
import Header from '../../components/Header.jsx';
|
||||
import Footer from '../../components/Footer.jsx';
|
||||
import Cases from '../../components/sections/Cases.jsx';
|
||||
import { serverApi } from '../../lib/serverApi.js';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Кейсы и портфолио — сетевая инфраструктура, ИИ, кибербезопасность',
|
||||
description:
|
||||
'Реализованные проекты TopSysOps: миграция сетевой инфраструктуры, внедрение ИИ-ассистента, аудит кибербезопасности финтех-проекта.',
|
||||
};
|
||||
|
||||
export default async function CasesPage() {
|
||||
const cases = await serverApi.getCases();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Cases cases={cases} showDisclaimer />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Header from '../../components/Header.jsx';
|
||||
import Footer from '../../components/Footer.jsx';
|
||||
import Contact from '../../components/sections/Contact.jsx';
|
||||
import { serverApi } from '../../lib/serverApi.js';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Контакты',
|
||||
description: 'Контакты TopSysOps: адрес офиса в Санкт-Петербурге, телефон, email и режим работы. Работаем с клиентами по всей России.',
|
||||
};
|
||||
|
||||
export default async function ContactPage() {
|
||||
const contact = await serverApi.getContact();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Contact contact={contact} />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import './globals.css';
|
||||
import { ThemeProvider } from '../components/ThemeProvider.jsx';
|
||||
import SupportChat from '../components/SupportChat.jsx';
|
||||
import VisitTracker from '../components/VisitTracker.jsx';
|
||||
|
||||
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru';
|
||||
|
||||
export const metadata = {
|
||||
metadataBase: new URL(SITE_URL),
|
||||
title: {
|
||||
default: 'TopSysOps — IT-аутсорсинг и автоматизация',
|
||||
template: '%s | TopSysOps',
|
||||
},
|
||||
description:
|
||||
'Профессиональный IT-аутсорсинг: техподдержка, кибербезопасность, администрирование БД, сетевая инфраструктура, ИИ и автоматизация.',
|
||||
};
|
||||
|
||||
// Выставляет класс "dark" на <html> ДО гидратации React, чтобы не было
|
||||
// мигания светлой темы при заходе пользователя с сохранённой тёмной темой.
|
||||
const NO_FLASH_SCRIPT = `
|
||||
(function () {
|
||||
try {
|
||||
var theme = localStorage.getItem('ts-theme');
|
||||
var dark = theme ? theme === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (dark) document.documentElement.classList.add('dark');
|
||||
} catch (e) {}
|
||||
})();
|
||||
`;
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="ru">
|
||||
<head>
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<script dangerouslySetInnerHTML={{ __html: NO_FLASH_SCRIPT }} />
|
||||
</head>
|
||||
<body className="bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100">
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
<SupportChat />
|
||||
<VisitTracker />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import Link from 'next/link';
|
||||
import { ArrowRight, Users, CalendarDays, MapPin, ShieldCheck, Clock, Bot } from 'lucide-react';
|
||||
import Header from '../../components/Header.jsx';
|
||||
import Footer from '../../components/Footer.jsx';
|
||||
import Reveal from '../../components/Reveal.jsx';
|
||||
import SectionHeading from '../../components/SectionHeading.jsx';
|
||||
import { serverApi } from '../../lib/serverApi.js';
|
||||
|
||||
export const metadata = {
|
||||
title: 'О компании',
|
||||
description:
|
||||
'TopSysOps — IT-аутсорсинг и техподдержка с 2023 года. Команда 20+ специалистов, штаб-квартира в Санкт-Петербурге, работаем с клиентами по всей России.',
|
||||
};
|
||||
|
||||
const FACTS = [
|
||||
{ icon: CalendarDays, label: 'На рынке с', value: '2023 года' },
|
||||
{ icon: Users, label: 'В команде', value: '20+ специалистов' },
|
||||
{ icon: MapPin, label: 'Штаб-квартира', value: 'Санкт-Петербург' },
|
||||
];
|
||||
|
||||
const DIFFERENTIATORS = [
|
||||
{
|
||||
icon: Clock,
|
||||
title: 'Быстрый отклик',
|
||||
text: 'На тарифе «Премиум» среднее время реакции на заявку — около 15 минут, поддержка доступна 24/7.',
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: 'Конфиденциальность',
|
||||
text: 'Перед началом работ подписываем NDA и используем защищённые каналы доступа к инфраструктуре клиента.',
|
||||
},
|
||||
{
|
||||
icon: Bot,
|
||||
title: 'Личный кабинет с ИИ',
|
||||
text: 'Клиенты получают личный кабинет с перепиской с командой и ИИ-ассистентом, отвечающим на вопросы 24/7.',
|
||||
},
|
||||
{
|
||||
icon: MapPin,
|
||||
title: 'Вся Россия удалённо',
|
||||
text: 'Большинство работ выполняем удалённо для клиентов по всей стране, выезд специалиста — по Санкт-Петербургу.',
|
||||
},
|
||||
];
|
||||
|
||||
export default async function AboutPage() {
|
||||
const contact = await serverApi.getContact();
|
||||
|
||||
const orgSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
name: 'TopSysOps',
|
||||
url: process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru',
|
||||
foundingDate: '2023',
|
||||
numberOfEmployees: { '@type': 'QuantitativeValue', minValue: 20 },
|
||||
...(contact?.address ? { address: { '@type': 'PostalAddress', streetAddress: contact.address, addressCountry: 'RU' } } : {}),
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(orgSchema) }} />
|
||||
|
||||
<Header />
|
||||
<section className="max-w-4xl mx-auto px-5 sm:px-8 pt-16 pb-10">
|
||||
<Reveal>
|
||||
<p className="text-xs uppercase tracking-widest text-emerald-500 mb-4 font-mono">> top-sysops.ru --about-company</p>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold mb-6 font-display">
|
||||
TopSysOps — IT-аутсорсинг и техподдержка для бизнеса
|
||||
</h1>
|
||||
<p className="text-base text-slate-600 dark:text-slate-400 max-w-2xl">
|
||||
Мы занимаемся IT-аутсорсингом, техподдержкой инфраструктуры, кибербезопасностью, администрированием
|
||||
серверов и баз данных, а также автоматизацией процессов с помощью ИИ. Наша цель — чтобы за IT-инфраструктуру
|
||||
бизнеса отвечала команда, а не один перегруженный сисадмин.
|
||||
</p>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
<section className="max-w-4xl mx-auto px-5 sm:px-8 pb-16">
|
||||
<div className="grid sm:grid-cols-3 gap-5">
|
||||
{FACTS.map((f, i) => {
|
||||
const Icon = f.icon;
|
||||
return (
|
||||
<Reveal key={f.label} delay={i * 60}>
|
||||
<div className="h-full rounded-lg border p-5 bg-white border-slate-200 dark:bg-slate-900 dark:border-slate-800">
|
||||
<Icon className="h-5 w-5 text-emerald-500 mb-3" />
|
||||
<div className="text-xs text-slate-400 dark:text-slate-500 mb-1">{f.label}</div>
|
||||
<div className="font-bold font-display">{f.value}</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="max-w-4xl mx-auto px-5 sm:px-8 pb-20">
|
||||
<SectionHeading eyebrow="why --us" title="Почему выбирают нас" />
|
||||
<div className="grid sm:grid-cols-2 gap-5">
|
||||
{DIFFERENTIATORS.map((d, i) => {
|
||||
const Icon = d.icon;
|
||||
return (
|
||||
<Reveal key={d.title} delay={i * 60}>
|
||||
<div className="h-full rounded-lg border p-5 bg-white border-slate-200 dark:bg-slate-900 dark:border-slate-800">
|
||||
<Icon className="h-5 w-5 text-emerald-500 mb-3" />
|
||||
<h3 className="font-bold mb-1 font-display text-[15px]">{d.title}</h3>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{d.text}</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="max-w-4xl mx-auto px-5 sm:px-8 pb-20">
|
||||
<Reveal>
|
||||
<div className="rounded-lg border p-6 sm:p-8 flex flex-col sm:flex-row items-start sm:items-center gap-6 justify-between bg-emerald-50 border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-900">
|
||||
<div>
|
||||
<h3 className="font-bold text-lg mb-2 font-display">Готовы обсудить вашу задачу?</h3>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Посмотрите услуги и цены или сразу оставьте заявку — свяжемся в течение рабочего дня.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3 flex-shrink-0">
|
||||
<Link
|
||||
href="/uslugi"
|
||||
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-md text-sm font-medium border border-slate-300 dark:border-slate-700 hover:bg-white dark:hover:bg-slate-900 transition-colors whitespace-nowrap"
|
||||
>
|
||||
Услуги
|
||||
</Link>
|
||||
<Link
|
||||
href="/kontakty"
|
||||
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-md bg-emerald-500 text-white text-sm font-medium hover:bg-emerald-600 transition-colors whitespace-nowrap"
|
||||
>
|
||||
Оставить заявку <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</section>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Header from '../../components/Header.jsx';
|
||||
import Footer from '../../components/Footer.jsx';
|
||||
import Reviews from '../../components/sections/Reviews.jsx';
|
||||
import { serverApi } from '../../lib/serverApi.js';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Отзывы клиентов TopSysOps',
|
||||
description: 'Отзывы клиентов об IT-аутсорсинге, техподдержке и других услугах TopSysOps. Оставьте свой отзыв.',
|
||||
};
|
||||
|
||||
export default async function ReviewsPage() {
|
||||
const reviews = await serverApi.getReviews();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Reviews reviews={reviews} />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import Link from 'next/link';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import Header from '../components/Header.jsx';
|
||||
import Footer from '../components/Footer.jsx';
|
||||
import Hero from '../components/sections/Hero.jsx';
|
||||
import AccountPromo from '../components/sections/AccountPromo.jsx';
|
||||
import Promotions from '../components/sections/Promotions.jsx';
|
||||
import Services from '../components/sections/Services.jsx';
|
||||
import Cases from '../components/sections/Cases.jsx';
|
||||
import Reviews from '../components/sections/Reviews.jsx';
|
||||
import Contact from '../components/sections/Contact.jsx';
|
||||
import Reveal from '../components/Reveal.jsx';
|
||||
import SectionHeading from '../components/SectionHeading.jsx';
|
||||
import { serverApi } from '../lib/serverApi.js';
|
||||
|
||||
export default async function HomePage() {
|
||||
const [services, promotions, reviews, cases, contact] = await Promise.all([
|
||||
serverApi.getServices(),
|
||||
serverApi.getPromotions(),
|
||||
serverApi.getReviews(),
|
||||
serverApi.getCases(),
|
||||
serverApi.getContact(),
|
||||
]);
|
||||
|
||||
const business = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'LocalBusiness',
|
||||
name: 'TopSysOps',
|
||||
url: process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru',
|
||||
...(contact?.address ? { address: { '@type': 'PostalAddress', streetAddress: contact.address, addressCountry: 'RU' } } : {}),
|
||||
...(contact?.phone ? { telephone: contact.phone } : {}),
|
||||
...(contact?.email ? { email: contact.email } : {}),
|
||||
...(reviews.length > 0
|
||||
? {
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: (reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length).toFixed(1),
|
||||
reviewCount: reviews.length,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(services.length > 0
|
||||
? {
|
||||
hasOfferCatalog: {
|
||||
'@type': 'OfferCatalog',
|
||||
name: 'Услуги TopSysOps',
|
||||
itemListElement: services.flatMap((cat) =>
|
||||
cat.items.map((it) => ({
|
||||
'@type': 'Offer',
|
||||
itemOffered: { '@type': 'Service', name: it.name, description: it.note || cat.description },
|
||||
}))
|
||||
),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(business) }} />
|
||||
|
||||
<Header />
|
||||
<Hero />
|
||||
<AccountPromo />
|
||||
|
||||
<Services
|
||||
categories={services}
|
||||
compact
|
||||
limit={6}
|
||||
moreHref="/uslugi"
|
||||
eyebrow="services --overview"
|
||||
title="Ключевые услуги"
|
||||
desc="IT-аутсорсинг, кибербезопасность, администрирование серверов и баз данных, сетевая инфраструктура, облака и автоматизация с ИИ."
|
||||
/>
|
||||
|
||||
<Promotions promotions={promotions} limit={3} moreHref="/aktsii" />
|
||||
|
||||
<section className="max-w-6xl mx-auto px-5 sm:px-8 py-20">
|
||||
<SectionHeading
|
||||
eyebrow="about --company"
|
||||
title="О компании"
|
||||
desc="TopSysOps — IT-аутсорсинг и техподдержка с 2023 года. Команда 20+ специалистов, штаб-квартира в Санкт-Петербурге, работаем с клиентами по всей России."
|
||||
/>
|
||||
<Reveal>
|
||||
<Link href="/o-kompanii" className="inline-flex items-center gap-2 text-sm text-emerald-500 font-medium hover:underline">
|
||||
Узнать больше о компании <ArrowRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
<Cases cases={cases} limit={3} moreHref="/keysy" />
|
||||
<Reviews reviews={reviews} limit={2} moreHref="/otzyvy" showForm={false} />
|
||||
<Contact contact={contact} compact />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru';
|
||||
|
||||
export default function robots() {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
disallow: ['/admin', '/account'],
|
||||
},
|
||||
sitemap: `${SITE_URL}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { serverApi } from '../lib/serverApi.js';
|
||||
|
||||
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://top-sysops.ru';
|
||||
|
||||
export default async function sitemap() {
|
||||
const [posts, services] = await Promise.all([serverApi.getBlogPosts(), serverApi.getServices()]);
|
||||
|
||||
return [
|
||||
{ url: `${SITE_URL}/`, changeFrequency: 'weekly', priority: 1.0 },
|
||||
{ url: `${SITE_URL}/uslugi`, changeFrequency: 'weekly', priority: 0.9 },
|
||||
...services.map((cat) => ({
|
||||
url: `${SITE_URL}/uslugi/${cat.slug}`,
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.8,
|
||||
})),
|
||||
{ url: `${SITE_URL}/aktsii`, changeFrequency: 'weekly', priority: 0.7 },
|
||||
{ url: `${SITE_URL}/keysy`, changeFrequency: 'monthly', priority: 0.6 },
|
||||
{ url: `${SITE_URL}/o-kompanii`, changeFrequency: 'monthly', priority: 0.6 },
|
||||
{ url: `${SITE_URL}/otzyvy`, changeFrequency: 'weekly', priority: 0.5 },
|
||||
{ url: `${SITE_URL}/faq`, changeFrequency: 'monthly', priority: 0.5 },
|
||||
{ url: `${SITE_URL}/kontakty`, changeFrequency: 'monthly', priority: 0.6 },
|
||||
{ url: `${SITE_URL}/blog`, changeFrequency: 'weekly', priority: 0.7 },
|
||||
...posts.map((p) => ({
|
||||
url: `${SITE_URL}/blog/${p.slug}`,
|
||||
lastModified: p.published_at ? new Date(p.published_at) : undefined,
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.6,
|
||||
})),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ArrowLeft, ArrowRight, CheckCircle2 } from 'lucide-react';
|
||||
import Header from '../../../components/Header.jsx';
|
||||
import Footer from '../../../components/Footer.jsx';
|
||||
import Reveal from '../../../components/Reveal.jsx';
|
||||
import { ServicePriceList } from '../../../components/sections/Services.jsx';
|
||||
import { getIcon } from '../../../lib/icons.js';
|
||||
import { getServiceLandingCopy } from '../../../lib/serviceLandingCopy.js';
|
||||
import { serverApi } from '../../../lib/serverApi.js';
|
||||
|
||||
async function findCategory(slug) {
|
||||
const services = await serverApi.getServices();
|
||||
return services.find((c) => c.slug === slug) || null;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const category = await findCategory(params.slug);
|
||||
if (!category) return { title: 'Услуга не найдена' };
|
||||
const copy = getServiceLandingCopy(category);
|
||||
return { title: copy.metaTitle, description: copy.metaDescription };
|
||||
}
|
||||
|
||||
export default async function ServiceLandingPage({ params }) {
|
||||
const category = await findCategory(params.slug);
|
||||
if (!category) notFound();
|
||||
|
||||
const copy = getServiceLandingCopy(category);
|
||||
const Icon = getIcon(category.icon);
|
||||
|
||||
const serviceSchema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Service',
|
||||
name: copy.h1,
|
||||
description: copy.metaDescription,
|
||||
provider: { '@type': 'Organization', name: 'TopSysOps' },
|
||||
areaServed: 'RU',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(serviceSchema) }} />
|
||||
|
||||
<Header />
|
||||
<section className="max-w-4xl mx-auto px-5 sm:px-8 pt-16 pb-4">
|
||||
<Link href="/uslugi" className="text-sm text-emerald-500 font-medium inline-flex items-center gap-1 mb-8">
|
||||
<ArrowLeft className="h-3.5 w-3.5" /> Все услуги
|
||||
</Link>
|
||||
<Reveal>
|
||||
<Icon className="h-8 w-8 text-emerald-500 mb-4" />
|
||||
<h1 className="text-2xl sm:text-3xl font-bold mb-4 font-display">{copy.h1}</h1>
|
||||
<p className="text-base text-slate-600 dark:text-slate-400 max-w-2xl mb-6">{copy.intro}</p>
|
||||
</Reveal>
|
||||
|
||||
{copy.bullets.length > 0 && (
|
||||
<Reveal delay={80}>
|
||||
<ul className="space-y-2 mb-10">
|
||||
{copy.bullets.map((b) => (
|
||||
<li key={b} className="flex items-start gap-2 text-sm text-slate-700 dark:text-slate-300">
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-500 mt-0.5 flex-shrink-0" />
|
||||
{b}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Reveal>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="max-w-4xl mx-auto px-5 sm:px-8 pb-20">
|
||||
<Reveal delay={120}>
|
||||
<div className="rounded-lg border p-5 bg-white border-slate-200 dark:bg-slate-900 dark:border-slate-800">
|
||||
<h2 className="font-bold mb-1 font-display text-[15px]">{category.title}: цены</h2>
|
||||
<p className="text-xs mb-4 text-slate-500 dark:text-slate-400">{category.description}</p>
|
||||
<ServicePriceList category={category} />
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={160} className="mt-8">
|
||||
<Link
|
||||
href="/kontakty"
|
||||
className="inline-flex items-center gap-2 px-5 py-3 rounded-md bg-emerald-500 text-white font-medium text-sm hover:bg-emerald-600 transition-colors"
|
||||
>
|
||||
Обсудить задачу <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</Reveal>
|
||||
</section>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Header from '../../components/Header.jsx';
|
||||
import Footer from '../../components/Footer.jsx';
|
||||
import Services from '../../components/sections/Services.jsx';
|
||||
import { serverApi } from '../../lib/serverApi.js';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Услуги IT-аутсорсинга и техподдержки — прайс-лист',
|
||||
description:
|
||||
'IT-аутсорсинг, техподдержка, кибербезопасность, администрирование серверов и баз данных, сетевая инфраструктура, облачные решения, ИИ и автоматизация. Актуальные цены.',
|
||||
};
|
||||
|
||||
export default async function ServicesPage() {
|
||||
const services = await serverApi.getServices();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Services categories={services} />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user