seo rubbish
This commit is contained in:
parent
7a1696541f
commit
7cba369308
5 changed files with 3633 additions and 0 deletions
234
frontend/src/components/landing/SeoLandingPage.tsx
Normal file
234
frontend/src/components/landing/SeoLandingPage.tsx
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import { CheckIcon } from '../ui/icons/CheckIcon';
|
||||
import { LogoIcon } from '../ui/icons/LogoIcon';
|
||||
import {
|
||||
SEO_LANDING_PAGES,
|
||||
type SeoFaq,
|
||||
type SeoLandingKey,
|
||||
type SeoLink,
|
||||
type SeoSection,
|
||||
} from '../../lib/seoLandingPages';
|
||||
|
||||
const PUBLIC_URL = 'https://perfect-postcode.co.uk';
|
||||
|
||||
function JsonLd({ data }: { data: unknown }) {
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(data).replace(/</g, '\\u003c') }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FaqJsonLd({ faq }: { faq: SeoFaq[] }) {
|
||||
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 LinkGrid({ 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>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionGrid({ sections }: { sections: SeoSection[] }) {
|
||||
return (
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SeoLandingPage({
|
||||
pageKey,
|
||||
onOpenDashboard,
|
||||
}: {
|
||||
pageKey: SeoLandingKey;
|
||||
onOpenDashboard: () => void;
|
||||
}) {
|
||||
const page = SEO_LANDING_PAGES[pageKey];
|
||||
const url = `${PUBLIC_URL}${page.path}`;
|
||||
|
||||
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-6xl px-6 py-16 md:px-10 md:py-20">
|
||||
<nav className="mb-6 text-sm font-medium text-warm-400" aria-label="Breadcrumb">
|
||||
<a href="/" className="hover:text-teal-200">
|
||||
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="max-w-4xl 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>
|
||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||
<button
|
||||
onClick={onOpenDashboard}
|
||||
className="rounded-lg bg-coral-500 px-6 py-3 font-semibold text-white shadow-lg shadow-coral-500/25 transition-colors hover:bg-coral-600"
|
||||
>
|
||||
{page.cta}
|
||||
</button>
|
||||
<a
|
||||
href="/data-sources"
|
||||
className="rounded-lg border-2 border-teal-400 px-6 py-[10px] text-center font-semibold text-teal-300 transition-colors hover:bg-teal-400/10"
|
||||
>
|
||||
Review the data sources
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto grid max-w-6xl gap-8 px-6 py-12 md:grid-cols-[0.85fr_1.15fr] md:px-10 md:py-16">
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-2 rounded-md border border-teal-200 bg-teal-50 px-3 py-1.5 text-sm font-semibold text-teal-700 dark:border-teal-400/30 dark:bg-teal-400/10 dark:text-teal-200">
|
||||
<LogoIcon className="h-4 w-4" />
|
||||
Perfect Postcode
|
||||
</div>
|
||||
<h2 className="mt-5 text-2xl font-bold md:text-3xl">What you can compare</h2>
|
||||
<p className="mt-3 leading-relaxed text-warm-600 dark:text-warm-300">
|
||||
Each page is built around real shortlisting work: removing impossible places, comparing
|
||||
the remaining postcodes, and deciding what to validate next.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
{page.points.map((point) => (
|
||||
<div
|
||||
key={point}
|
||||
className="flex gap-3 rounded-lg border border-warm-200 bg-white p-4 dark:border-warm-700 dark:bg-warm-800"
|
||||
>
|
||||
<CheckIcon className="mt-0.5 h-5 w-5 shrink-0 text-teal-600 dark:text-teal-400" />
|
||||
<p className="leading-relaxed text-warm-700 dark:text-warm-300">{point}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-6xl px-6 pb-16 md:px-10">
|
||||
<div className="mb-7 max-w-3xl">
|
||||
<h2 className="text-2xl font-bold md:text-3xl">How to use it</h2>
|
||||
<p className="mt-3 leading-relaxed text-warm-600 dark:text-warm-300">
|
||||
Use these workflows to make the page useful before you open a listing portal or book a
|
||||
viewing.
|
||||
</p>
|
||||
</div>
|
||||
<SectionGrid sections={page.workflows} />
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-6xl px-6 pb-16 md:px-10">
|
||||
<SectionGrid sections={page.sections} />
|
||||
</section>
|
||||
|
||||
<section className="bg-white py-14 dark:bg-navy-900">
|
||||
<div className="mx-auto max-w-6xl px-6 md:px-10">
|
||||
<div className="mb-7 max-w-3xl">
|
||||
<h2 className="text-2xl font-bold md:text-3xl">Method and limitations</h2>
|
||||
<p className="mt-3 leading-relaxed text-warm-600 dark:text-warm-300">
|
||||
The data is designed for comparison and shortlisting. Important decisions still need
|
||||
current listings, professional checks, and direct local validation.
|
||||
</p>
|
||||
</div>
|
||||
<SectionGrid sections={page.methodology} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-6xl px-6 py-14 md:px-10">
|
||||
<div className="mb-7 max-w-3xl">
|
||||
<h2 className="text-2xl font-bold md:text-3xl">Questions buyers ask</h2>
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
{page.faq.map((item) => (
|
||||
<details
|
||||
key={item.question}
|
||||
className="rounded-lg border border-warm-200 bg-white 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>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-6xl px-6 pb-16 md:px-10">
|
||||
<div className="mb-7 max-w-3xl">
|
||||
<h2 className="text-2xl font-bold md:text-3xl">Related guides</h2>
|
||||
<p className="mt-3 leading-relaxed text-warm-600 dark:text-warm-300">
|
||||
Continue through the indexed public pages using canonical internal links.
|
||||
</p>
|
||||
</div>
|
||||
<LinkGrid links={page.relatedLinks} />
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue