Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
215 lines
8.1 KiB
TypeScript
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>
|
|
);
|
|
}
|