Files
2026-10-25 22:55:29 +04:00

215 lines
8.1 KiB
TypeScript

import { createT, isLocale, type Locale } from '@barev/i18n';
import type { Metadata } from 'next';
import { getCatalog } from '../lib/data';
import { languageAlternates, SITE } from '../lib/seo';
import { range, SERVICE_PAGES, type ServicePageId } from '../lib/services';
import { BotLink, BotLinkProvider } from './client';
import { HomeFooter } from './home';
import { RichText } from './rich-text';
import { VariantHeader } from './variant';
/** Title, description, canonical и hreflang страницы услуги; цены в описании — из админки */
export async function serviceMetadata(params: Promise<{ locale: string }>, id: ServicePageId): Promise<Metadata> {
const { locale } = await params;
if (!isLocale(locale)) return {};
const cfg = SERVICE_PAGES[id];
const t = createT(locale);
const vars = cfg.vars(await getCatalog(), locale);
const k = `landing.svc.${id}`;
const title = t(`${k}.meta.title`, vars);
const description = t(`${k}.meta.description`, vars);
return {
title,
description,
alternates: { canonical: `/${locale}${cfg.path}`, languages: languageAlternates(cfg.path) },
openGraph: { title, description, locale, type: 'website', siteName: 'Barev Club' },
twitter: { card: 'summary_large_image', title, description },
};
}
/**
* Страница услуги под поисковый запрос: прямой ответ с ценой в первом экране, факты, шаги,
* подробности, цены и FAQ. Так её удобно читать людям и цитировать поиску и ИИ.
*/
export async function ServicePage({ locale, id }: { locale: Locale; id: ServicePageId }) {
const cfg = SERVICE_PAGES[id];
const t = createT(locale);
const catalog = await getCatalog();
const vars = cfg.vars(catalog, locale);
const k = `landing.svc.${id}`;
const c = (key: string) => t(`landing.svc.common.${key}`);
const tv = (key: string) => t(`${k}.${key}`, vars);
const others = Object.values(SERVICE_PAGES).filter((p) => p.id !== id);
const url = `${SITE}/${locale}${cfg.path}`;
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'Service',
'@id': `${url}#service`,
name: tv('h1'),
serviceType: cfg.serviceType,
description: tv('lead'),
url,
inLanguage: locale,
provider: { '@id': `${SITE}/#org` },
areaServed: { '@type': 'City', name: 'Yerevan' },
offers: cfg.offers(catalog).map((o) => ({
'@type': 'Offer',
name: o.name,
priceCurrency: 'AMD',
...(o.minPrice ? { priceSpecification: { '@type': 'PriceSpecification', minPrice: o.price, priceCurrency: 'AMD' } } : { price: o.price }),
url,
})),
},
{
'@type': 'FAQPage',
'@id': `${url}#faq`,
mainEntity: range(cfg.faq).map((n) => ({
'@type': 'Question',
name: tv(`faq.q${n}.q`),
acceptedAnswer: { '@type': 'Answer', text: tv(`faq.q${n}.a`) },
})),
},
{
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: c('home'), item: `${SITE}/${locale}` },
{ '@type': 'ListItem', position: 2, name: tv('crumb'), item: url },
],
},
],
};
return (
<BotLinkProvider locale={locale} bot={catalog.botUsername}>
<div className="v2">
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c') }} />
<div className="v-wrap">
<VariantHeader locale={locale} path={cfg.path} className="vh--dark" logoColor="#F6C3D3" />
<nav className="s-crumbs" aria-label={c('breadcrumbs')}>
<a href={`/${locale}`}>{c('home')}</a>
<span aria-hidden="true">/</span>
<span aria-current="page">{tv('crumb')}</span>
</nav>
<section className="s-hero">
<div className="s-hero__text">
<h1 className="s-title">{tv('h1')}</h1>
<p className="s-lead">{tv('lead')}</p>
<p className="s-updated">{c('updated')}</p>
</div>
<aside className="s-price" aria-label={tv('priceLabel')}>
<span className="s-price__label">{tv('priceLabel')}</span>
<span className="s-price__value">{tv('priceValue')}</span>
<span className="s-price__note">{tv('priceNote')}</span>
<BotLink className="bc-btn s-price__cta">{c('cta')}</BotLink>
<a href={`/${locale}`} className="s-price__more">
{c('ctaMove')} →
</a>
</aside>
</section>
<section className="s-facts" aria-label={c('factsTitle')}>
<h2 className="v-sr">{c('factsTitle')}</h2>
<dl>
{range(4).map((n) => (
<div key={n} className="s-fact">
<dt>{tv(`facts.f${n}.label`)}</dt>
<dd>{tv(`facts.f${n}.value`)}</dd>
</div>
))}
</dl>
</section>
<section className="s-section" aria-labelledby="steps-title">
<h2 id="steps-title" className="s-h2">
{tv('stepsTitle')}
</h2>
<ol className="v2-line s-steps">
{range(cfg.steps).map((n) => (
<li key={n} className="v2-step" data-live={n === 1}>
<div className="v2-step__when">{tv(`steps.s${n}.when`)}</div>
<div className="v2-step__body">
<h3>{tv(`steps.s${n}.title`)}</h3>
<p>{tv(`steps.s${n}.text`)}</p>
</div>
</li>
))}
</ol>
</section>
<div className="s-body">
{range(cfg.blocks).map((n) => (
<section key={n} className="s-block">
<h2 className="s-h2">{tv(`blocks.b${n}.title`)}</h2>
<RichText text={tv(`blocks.b${n}.body`)} />
</section>
))}
</div>
<section className="s-section" aria-labelledby="prices-title">
<h2 id="prices-title" className="s-h2">
{c('pricesTitle')}
</h2>
<ul className="h-price__list">
{range(cfg.prices).map((n) => (
<li key={n} className="h-price__row">
<b className="h-price__name">{tv(`prices.p${n}.name`)}</b>
<span className="h-price__dots" aria-hidden="true" />
<span className="h-price__value">{tv(`prices.p${n}.value`)}</span>
<small className="h-price__hint">{tv(`prices.p${n}.note`)}</small>
</li>
))}
</ul>
</section>
<section className="s-section" aria-labelledby="faq-title">
<h2 id="faq-title" className="s-h2">
{c('faqTitle')}
</h2>
<div className="s-faq">
{range(cfg.faq).map((n) => (
<details key={n} open={n === 1}>
<summary>
<h3>{tv(`faq.q${n}.q`)}</h3>
</summary>
<p>{tv(`faq.q${n}.a`)}</p>
</details>
))}
</div>
</section>
<section className="v2-final">
<div>
<h2>{tv('finalTitle')}</h2>
<p>{tv('finalText')}</p>
</div>
<BotLink className="bc-btn v2-final__cta">{c('cta')}</BotLink>
</section>
<section className="s-meta">
<div>
<h2 className="s-h3">{c('relatedTitle')}</h2>
<ul className="s-related">
{others.map((p) => (
<li key={p.id}>
<a href={`/${locale}${p.path}`}>{t(`landing.svc.${p.id}.crumb`)}</a>
</li>
))}
<li>
<a href={`/${locale}/clean`}>{c('clean')}</a>
</li>
</ul>
</div>
</section>
<HomeFooter locale={locale} />
</div>
</div>
</BotLinkProvider>
);
}