- Merge the duplicate Organization/LocalBusiness JSON-LD blocks (home's LocalBusiness + o-kompanii's Organization) into one shared getOrganizationSchema() call rendered on every page. Deliberately NOT wired through the root layout: fetches there get frozen into the static shell at build time (backend unreachable during image build) and never revalidate, silently serving stale data to crawlers - each page calls it directly instead, where revalidation is proven to work. - Add optional ItemList schema on /uslugi and Offer/OfferCatalog schema on /aktsii; entity pages (/uslugi/[slug] Service, /blog/[slug] Article) reference the organization via @id instead of duplicating it. - Add 8 more real FAQ entries (pricing model, contract terms, onboarding time, hybrid IT staff setup, supported stacks, backups, 152-FZ compliance, escalation) and give every FAQ its own slug and dedicated /faq/[slug] page with its own title/H1/FAQPage schema, modeled on the one-topic-per-page pattern (h/t remontka.pro). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
97 lines
3.7 KiB
React
97 lines
3.7 KiB
React
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';
|
|
import { ORGANIZATION_ID } from '../../../lib/schema.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,
|
|
alternates: { canonical: `/uslugi/${category.slug}` },
|
|
};
|
|
}
|
|
|
|
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: { '@id': ORGANIZATION_ID },
|
|
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>
|
|
);
|
|
}
|