209 lines
6.9 KiB
TypeScript
209 lines
6.9 KiB
TypeScript
import { lazy, Suspense } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { CheckIcon } from '../ui/icons/CheckIcon';
|
|
import HomeFinalCta from '../home/HomeFinalCta';
|
|
import Footer from '../ui/Footer';
|
|
import { usePageMeta } from '../../hooks/usePageMeta';
|
|
import {
|
|
getLocalizedSeoContentPage,
|
|
type SeoContentKey,
|
|
type SeoFaq,
|
|
type SeoLink,
|
|
type SeoSection,
|
|
} from '../../lib/seoLandingPages';
|
|
import { safeJsonLd } from '../../lib/json-ld';
|
|
|
|
const PUBLIC_URL = 'https://perfect-postcode.co.uk';
|
|
const ProductShowcase = lazy(() => import('../home/ProductShowcase'));
|
|
|
|
function ProductShowcaseFallback() {
|
|
return (
|
|
<div className="mx-auto min-h-[28rem] max-w-6xl rounded-lg bg-navy-900" aria-hidden="true" />
|
|
);
|
|
}
|
|
|
|
function JsonLd({ data }: { data: unknown }) {
|
|
return (
|
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: safeJsonLd(data) }} />
|
|
);
|
|
}
|
|
|
|
function FaqJsonLd({ faq }: { faq: SeoFaq[] }) {
|
|
if (faq.length === 0) return null;
|
|
return (
|
|
<JsonLd
|
|
data={{
|
|
'@context': 'https://schema.org',
|
|
'@type': 'FAQPage',
|
|
mainEntity: faq.map((item) => ({
|
|
'@type': 'Question',
|
|
name: item.question,
|
|
acceptedAnswer: {
|
|
'@type': 'Answer',
|
|
text: item.answer,
|
|
},
|
|
})),
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function SectionList({ sections }: { sections: SeoSection[] }) {
|
|
return (
|
|
<div className="grid gap-5">
|
|
{sections.map((section) => (
|
|
<article
|
|
key={section.title}
|
|
className="rounded-lg border border-warm-200 bg-white p-6 dark:border-warm-700 dark:bg-warm-800"
|
|
>
|
|
<h2 className="text-xl font-bold">{section.title}</h2>
|
|
<p className="mt-3 leading-relaxed text-warm-700 dark:text-warm-300">{section.body}</p>
|
|
{section.bullets && (
|
|
<ul className="mt-4 space-y-2">
|
|
{section.bullets.map((bullet) => (
|
|
<li key={bullet} className="flex gap-2 text-sm text-warm-700 dark:text-warm-300">
|
|
<CheckIcon className="mt-0.5 h-4 w-4 shrink-0 text-teal-600 dark:text-teal-400" />
|
|
<span>{bullet}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</article>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RelatedLinks({ links }: { links: SeoLink[] }) {
|
|
return (
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
{links.map((link) => (
|
|
<a
|
|
key={link.path}
|
|
href={link.path}
|
|
className="rounded-lg border border-warm-200 bg-white p-4 transition-colors hover:border-teal-300 hover:bg-teal-50 dark:border-warm-700 dark:bg-warm-800 dark:hover:border-teal-500/60 dark:hover:bg-teal-950/30"
|
|
>
|
|
<h3 className="font-bold text-navy-950 dark:text-warm-100">{link.label}</h3>
|
|
<p className="mt-2 text-sm leading-relaxed text-warm-600 dark:text-warm-300">
|
|
{link.description}
|
|
</p>
|
|
</a>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function SeoContentPage({
|
|
pageKey,
|
|
onOpenDashboard,
|
|
}: {
|
|
pageKey: SeoContentKey;
|
|
onOpenDashboard: () => void;
|
|
}) {
|
|
const { t, i18n } = useTranslation();
|
|
const page = getLocalizedSeoContentPage(pageKey, i18n.language);
|
|
const url = `${PUBLIC_URL}${page.path}`;
|
|
usePageMeta(page.metaTitle, page.metaDescription);
|
|
|
|
return (
|
|
<main className="flex-1 overflow-y-auto bg-warm-50 text-navy-950 dark:bg-navy-950 dark:text-warm-100">
|
|
<FaqJsonLd faq={page.faq} />
|
|
<JsonLd
|
|
data={{
|
|
'@context': 'https://schema.org',
|
|
'@type': 'BreadcrumbList',
|
|
itemListElement: [
|
|
{
|
|
'@type': 'ListItem',
|
|
position: 1,
|
|
name: 'Perfect Postcode',
|
|
item: `${PUBLIC_URL}/`,
|
|
},
|
|
{
|
|
'@type': 'ListItem',
|
|
position: 2,
|
|
name: page.title,
|
|
item: url,
|
|
},
|
|
],
|
|
}}
|
|
/>
|
|
|
|
<section className="bg-navy-950 text-white">
|
|
<div className="mx-auto max-w-5xl px-6 py-16 md:px-10 md:py-20">
|
|
<nav className="mb-6 text-sm font-medium text-warm-400" aria-label={t('seo.breadcrumb')}>
|
|
<a href="/" className="hover:text-teal-200">
|
|
{t('mobileMenu.home')}
|
|
</a>
|
|
<span className="mx-2">/</span>
|
|
<span className="text-warm-200">{page.eyebrow}</span>
|
|
</nav>
|
|
<p className="mb-4 text-sm font-semibold uppercase tracking-wide text-teal-300">
|
|
{page.eyebrow}
|
|
</p>
|
|
<h1 className="text-3xl font-extrabold leading-tight md:text-5xl">{page.title}</h1>
|
|
<p className="mt-6 max-w-3xl text-lg leading-relaxed text-warm-300">{page.intro}</p>
|
|
{page.cta && (
|
|
<button
|
|
onClick={onOpenDashboard}
|
|
className="mt-8 rounded-lg border border-[#d27a11] bg-[#f09a22] px-6 py-3 font-semibold text-navy-950 shadow-lg shadow-[#7a3905]/25 transition-colors hover:bg-[#df8614]"
|
|
>
|
|
{page.cta}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="bg-navy-950 px-6 py-12 md:px-10 md:py-16">
|
|
<div className="mx-auto max-w-6xl">
|
|
<Suspense fallback={<ProductShowcaseFallback />}>
|
|
<ProductShowcase className="mx-auto" />
|
|
</Suspense>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="mx-auto max-w-5xl px-6 py-14 md:px-10">
|
|
<SectionList sections={page.sections} />
|
|
</section>
|
|
|
|
{page.faq.length > 0 && (
|
|
<section className="bg-white py-14 dark:bg-navy-900">
|
|
<div className="mx-auto max-w-5xl px-6 md:px-10">
|
|
<h2 className="text-2xl font-bold md:text-3xl">{t('seo.frequentlyAskedQuestions')}</h2>
|
|
<div className="mt-7 grid gap-4">
|
|
{page.faq.map((item) => (
|
|
<details
|
|
key={item.question}
|
|
className="rounded-lg border border-warm-200 bg-warm-50 p-5 dark:border-warm-700 dark:bg-warm-800"
|
|
>
|
|
<summary className="cursor-pointer font-bold">{item.question}</summary>
|
|
<p className="mt-3 leading-relaxed text-warm-700 dark:text-warm-300">
|
|
{item.answer}
|
|
</p>
|
|
</details>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<section className="mx-auto max-w-5xl px-6 py-14 md:px-10">
|
|
<h2 className="text-2xl font-bold md:text-3xl">{t('seo.relatedPages')}</h2>
|
|
<p className="mt-3 max-w-3xl leading-relaxed text-warm-600 dark:text-warm-300">
|
|
{t('seo.relatedPagesDesc')}
|
|
</p>
|
|
<div className="mt-7">
|
|
<RelatedLinks links={page.relatedLinks} />
|
|
</div>
|
|
</section>
|
|
|
|
<section className="mx-auto max-w-5xl px-6 pb-16 md:px-10">
|
|
<HomeFinalCta
|
|
onOpenDashboard={onOpenDashboard}
|
|
trackingLocation={`seo_${pageKey}_bottom`}
|
|
/>
|
|
</section>
|
|
<Footer />
|
|
</main>
|
|
);
|
|
}
|