seo rubbish
This commit is contained in:
parent
7a1696541f
commit
7cba369308
5 changed files with 3633 additions and 0 deletions
182
frontend/src/components/landing/SeoContentPage.tsx
Normal file
182
frontend/src/components/landing/SeoContentPage.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { CheckIcon } from '../ui/icons/CheckIcon';
|
||||
import {
|
||||
SEO_CONTENT_PAGES,
|
||||
type SeoContentKey,
|
||||
type SeoFaq,
|
||||
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[] }) {
|
||||
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 page = SEO_CONTENT_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-5xl 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="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 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>
|
||||
)}
|
||||
</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">Frequently asked questions</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">Related pages</h2>
|
||||
<p className="mt-3 max-w-3xl leading-relaxed text-warm-600 dark:text-warm-300">
|
||||
Follow these internal links to compare the same property-search workflow from another
|
||||
angle.
|
||||
</p>
|
||||
<div className="mt-7">
|
||||
<RelatedLinks links={page.relatedLinks} />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
755
frontend/src/lib/seoLandingPages.ts
Normal file
755
frontend/src/lib/seoLandingPages.ts
Normal file
|
|
@ -0,0 +1,755 @@
|
|||
import type { SeoContentKey, SeoLandingKey } from './seoRoutes';
|
||||
|
||||
export type { SeoContentKey, SeoLandingKey, SeoPageKey } from './seoRoutes';
|
||||
|
||||
export interface SeoFaq {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
export interface SeoLink {
|
||||
label: string;
|
||||
path: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface SeoSection {
|
||||
title: string;
|
||||
body: string;
|
||||
bullets?: string[];
|
||||
}
|
||||
|
||||
export interface SeoLandingContent {
|
||||
path: string;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
metaTitle: string;
|
||||
metaDescription: string;
|
||||
intro: string;
|
||||
points: string[];
|
||||
workflows: SeoSection[];
|
||||
sections: SeoSection[];
|
||||
methodology: SeoSection[];
|
||||
faq: SeoFaq[];
|
||||
relatedLinks: SeoLink[];
|
||||
cta: string;
|
||||
}
|
||||
|
||||
export interface SeoContentPage {
|
||||
path: string;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
metaTitle: string;
|
||||
metaDescription: string;
|
||||
intro: string;
|
||||
sections: SeoSection[];
|
||||
faq: SeoFaq[];
|
||||
relatedLinks: SeoLink[];
|
||||
cta?: string;
|
||||
}
|
||||
|
||||
const COMMON_RELATED_LINKS: SeoLink[] = [
|
||||
{
|
||||
label: 'Data sources and coverage',
|
||||
path: '/data-sources',
|
||||
description: 'See which datasets sit behind the postcode filters and where they have limits.',
|
||||
},
|
||||
{
|
||||
label: 'Methodology',
|
||||
path: '/methodology',
|
||||
description:
|
||||
'Understand how the map is intended to support shortlisting, not replace due diligence.',
|
||||
},
|
||||
{
|
||||
label: 'Postcode checker',
|
||||
path: '/postcode-checker',
|
||||
description: 'Check one postcode before you spend time on a viewing.',
|
||||
},
|
||||
];
|
||||
|
||||
export const SEO_LANDING_PAGES: Record<SeoLandingKey, SeoLandingContent> = {
|
||||
'property-price-map': {
|
||||
path: '/property-price-map',
|
||||
eyebrow: 'Property price map',
|
||||
title: 'Compare property prices across every postcode in England',
|
||||
metaTitle: 'Property price map for England - Compare postcodes before viewing',
|
||||
metaDescription:
|
||||
'Compare sold prices, estimated current value, price per square metre and local context across English postcodes before searching listings.',
|
||||
intro:
|
||||
'Perfect Postcode maps sold prices, estimated current value, price per square metre, property type, floor area, tenure, and local context so buyers can find realistic search areas before opening listing portals.',
|
||||
points: [
|
||||
'Screen historical sale prices and current-value estimates by postcode.',
|
||||
'Compare value with commute, schools, broadband, crime, noise, and amenities.',
|
||||
'Build a shortlist before spending weekends on viewings.',
|
||||
],
|
||||
workflows: [
|
||||
{
|
||||
title: 'Find postcodes that fit the budget before listings appear',
|
||||
body: 'Start with a maximum price and property type, then colour the map by price per square metre or estimated current price. This helps reveal areas where similar homes have historically traded within reach, even when there are no live listings today.',
|
||||
bullets: [
|
||||
'Filter by last known sale price, estimated current value, property type, tenure, and floor area.',
|
||||
'Compare nearby postcodes using the same criteria instead of relying on area reputation.',
|
||||
'Use the results as a shortlist for listing alerts, local research, and viewings.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Separate cheap from good value',
|
||||
body: 'A lower price can reflect smaller homes, weaker transport, more noise, or fewer local services. The map keeps those trade-offs visible so the cheapest postcode is not automatically treated as the best option.',
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: 'Start from area value, not listing availability',
|
||||
body: 'Listing portals only show homes for sale today. A postcode-level property price map lets you compare wider areas, understand local price patterns, and avoid missing places where the next suitable listing might appear.',
|
||||
},
|
||||
{
|
||||
title: 'Use prices alongside real constraints',
|
||||
body: 'Budget rarely matters on its own. Perfect Postcode combines price filters with travel time, school quality, property size, energy performance, local environment, and services so your shortlist reflects how you actually want to live.',
|
||||
},
|
||||
],
|
||||
methodology: [
|
||||
{
|
||||
title: 'What the price data is for',
|
||||
body: 'Use the map to compare areas and spot search candidates. It is not a valuation, mortgage decision, survey, legal search, or live listing feed.',
|
||||
},
|
||||
{
|
||||
title: 'How to validate a promising area',
|
||||
body: 'Once a postcode looks promising, check current listings, sold-price comparables, agent details, flood searches, legal packs, surveys, and local authority information before making a decision.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
{
|
||||
question: 'Is this a replacement for Rightmove or Zoopla?',
|
||||
answer:
|
||||
'No. Use it before and alongside listing portals. Perfect Postcode helps decide where to look; listing portals show what is currently for sale.',
|
||||
},
|
||||
{
|
||||
question: 'Can I compare price with schools or commute time?',
|
||||
answer:
|
||||
'Yes. Price filters can be combined with travel-time, schools, crime, broadband, road-noise, amenities, and environment filters.',
|
||||
},
|
||||
{
|
||||
question: 'Does the map cover all of the UK?',
|
||||
answer:
|
||||
'The current product focuses on England because several core property and postcode datasets are England-specific.',
|
||||
},
|
||||
],
|
||||
relatedLinks: [
|
||||
{
|
||||
label: 'Birmingham property search guide',
|
||||
path: '/property-search/birmingham',
|
||||
description: 'A worked example for balancing price, commute, and family trade-offs.',
|
||||
},
|
||||
...COMMON_RELATED_LINKS,
|
||||
],
|
||||
cta: 'Explore the property map',
|
||||
},
|
||||
'postcode-property-search': {
|
||||
path: '/postcode-property-search',
|
||||
eyebrow: 'Postcode property search',
|
||||
title: 'Find postcodes that match your property search criteria',
|
||||
metaTitle: 'Postcode property search - Find areas that match your criteria',
|
||||
metaDescription:
|
||||
'Search every postcode by budget, property type, floor area, tenure, commute, schools, crime, broadband, noise, parks and local amenities.',
|
||||
intro:
|
||||
'Search every postcode by budget, property type, size, tenure, commute, schools, crime, broadband, noise, parks, and local amenities instead of checking areas one at a time.',
|
||||
points: [
|
||||
'Filter England-wide postcode data from one map.',
|
||||
'Shortlist unfamiliar areas with comparable evidence.',
|
||||
'Save and share search areas before booking viewings.',
|
||||
],
|
||||
workflows: [
|
||||
{
|
||||
title: 'Turn a broad brief into postcode candidates',
|
||||
body: 'Enter the practical constraints first: budget, property size, tenure, travel time, school needs, broadband, and tolerance for road noise or crime levels. The map removes places that fail those constraints and keeps the remaining options comparable.',
|
||||
},
|
||||
{
|
||||
title: 'Relax one constraint at a time',
|
||||
body: 'When the search becomes too narrow, loosen a single filter and watch which postcodes reappear. This makes compromise explicit instead of relying on guesswork.',
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: 'Turn vague areas into specific postcodes',
|
||||
body: 'Broad town or borough searches hide large differences between streets. Perfect Postcode helps you move from a general area to postcodes that satisfy your hard requirements.',
|
||||
},
|
||||
{
|
||||
title: 'Keep trade-offs visible',
|
||||
body: 'When there are too many or too few matches, adjust one constraint at a time and see exactly which postcodes reappear. That makes compromises explicit instead of relying on guesswork.',
|
||||
},
|
||||
],
|
||||
methodology: [
|
||||
{
|
||||
title: 'Why postcode-level comparison matters',
|
||||
body: 'Two nearby postcodes can differ on schools, road noise, transport access, property mix, and price. Comparing at postcode level reduces the chance of treating a whole town as one uniform market.',
|
||||
},
|
||||
{
|
||||
title: 'How to use the results',
|
||||
body: 'Treat matching postcodes as a research queue: check live listings, visit streets, confirm schools and admissions, and review current official sources.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
{
|
||||
question: 'Can I save a postcode property search?',
|
||||
answer:
|
||||
'Yes. Licensed users can save searches and return to them later. Saved searches are designed for shortlists and comparison notes.',
|
||||
},
|
||||
{
|
||||
question: 'Can I search without knowing the area?',
|
||||
answer:
|
||||
'Yes. The map is designed to surface unfamiliar areas that match practical constraints, not just places you already know.',
|
||||
},
|
||||
{
|
||||
question: 'Are the results live property listings?',
|
||||
answer:
|
||||
'No. The tool compares postcode data and historical/contextual property signals. You still need listing portals for current availability.',
|
||||
},
|
||||
],
|
||||
relatedLinks: [
|
||||
{
|
||||
label: 'Manchester property search guide',
|
||||
path: '/property-search/manchester',
|
||||
description: 'A regional guide for narrowing a broad search around Greater Manchester.',
|
||||
},
|
||||
...COMMON_RELATED_LINKS,
|
||||
],
|
||||
cta: 'Start a postcode search',
|
||||
},
|
||||
'commute-property-search': {
|
||||
path: '/commute-property-search',
|
||||
eyebrow: 'Commute property search',
|
||||
title: 'Search for places to live by commute time',
|
||||
metaTitle: 'Commute property search - Find places to live by travel time',
|
||||
metaDescription:
|
||||
'Filter postcodes by commute time, then compare price, schools, safety, broadband, road noise, parks and property data on one map.',
|
||||
intro:
|
||||
'Filter postcodes by modelled car, cycling, walking, and public transport travel times, then layer on property price, schools, crime, broadband, noise, and local amenities.',
|
||||
points: [
|
||||
'Compare reachable postcodes by realistic travel-time bands.',
|
||||
'Search by destination first, then filter for property and neighbourhood fit.',
|
||||
'Avoid areas that look close on a map but fail the daily journey.',
|
||||
],
|
||||
workflows: [
|
||||
{
|
||||
title: 'Start with the destination that matters',
|
||||
body: 'Choose a commute destination, transport mode, and time range, then add the property filters. This prevents a cheap-looking area from reaching the shortlist if the daily journey does not work.',
|
||||
},
|
||||
{
|
||||
title: 'Compare the commute against the rest of daily life',
|
||||
body: 'A fast commute is not enough if the property size, school context, safety threshold, broadband, or road-noise exposure do not fit. The map keeps those signals side by side.',
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: 'Commute from postcodes, not just place names',
|
||||
body: 'Two streets in the same town can have very different station access, road routes, and public transport options. Postcode-level travel-time filtering keeps that difference visible.',
|
||||
},
|
||||
{
|
||||
title: 'Balance journey time with the rest of the move',
|
||||
body: 'A fast commute only helps if the area also fits your budget, housing needs, school preferences, safety threshold, broadband requirement, and tolerance for road noise.',
|
||||
},
|
||||
],
|
||||
methodology: [
|
||||
{
|
||||
title: 'How travel-time filters should be interpreted',
|
||||
body: 'Travel-time modelling is useful for comparing areas consistently. Before committing, check current timetables, disruption patterns, parking, cycling conditions, and walking routes.',
|
||||
},
|
||||
{
|
||||
title: 'Why commute filters are combined with property data',
|
||||
body: 'Commute search is most useful when it removes impossible areas while still showing whether the remaining options are affordable and liveable.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
{
|
||||
question: 'Can I compare car, cycling, walking, and public transport?',
|
||||
answer:
|
||||
'The product supports multiple travel modes where precomputed destination data is available.',
|
||||
},
|
||||
{
|
||||
question: 'Are travel times exact?',
|
||||
answer:
|
||||
'No. Treat them as a consistent comparison model, then verify the real route before making viewing or purchase decisions.',
|
||||
},
|
||||
{
|
||||
question: 'Can I combine commute filters with schools and price?',
|
||||
answer:
|
||||
'Yes. The commute filter can be layered with property price, size, schools, broadband, crime, amenities, and environmental signals.',
|
||||
},
|
||||
],
|
||||
relatedLinks: [
|
||||
{
|
||||
label: 'Bristol property search guide',
|
||||
path: '/property-search/bristol',
|
||||
description: 'A worked example for balancing city access, price, and local context.',
|
||||
},
|
||||
...COMMON_RELATED_LINKS,
|
||||
],
|
||||
cta: 'Search by commute time',
|
||||
},
|
||||
'school-property-search': {
|
||||
path: '/school-property-search',
|
||||
eyebrow: 'Schools and property search',
|
||||
title: 'Find property search areas with schools and family trade-offs in view',
|
||||
metaTitle: 'School property search - Compare postcodes for family moves',
|
||||
metaDescription:
|
||||
'Compare nearby schools, property size, prices, parks, safety, commute and local amenities before building a viewing shortlist.',
|
||||
intro:
|
||||
'Compare nearby Ofsted ratings, education context, property size, budget, safety, parks, commute, and local amenities before narrowing your viewing shortlist.',
|
||||
points: [
|
||||
'Filter for nearby school quality alongside housing requirements.',
|
||||
'Compare family-friendly trade-offs across unfamiliar postcodes.',
|
||||
'Use the map as a shortlist tool before checking admissions and catchments.',
|
||||
],
|
||||
workflows: [
|
||||
{
|
||||
title: 'Use school context without ignoring the home',
|
||||
body: 'Start with property size, budget, and commute constraints, then layer in nearby school quality and local context. This prevents school-led searches from hiding affordability or daily-life problems.',
|
||||
},
|
||||
{
|
||||
title: 'Verify admissions before deciding',
|
||||
body: 'School data can point to promising areas, but admissions rules and catchments can change. Confirm current arrangements with schools and local authorities.',
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: 'School quality is one part of the shortlist',
|
||||
body: 'Perfect Postcode helps you compare nearby school data with the other practical constraints that shape a family move: space, price, commute, parks, safety, and local services.',
|
||||
},
|
||||
{
|
||||
title: 'Check catchments before making decisions',
|
||||
body: 'Admissions rules and catchment boundaries can change. Use postcode-level school data to find promising areas, then verify current admissions details with the school or local authority.',
|
||||
},
|
||||
],
|
||||
methodology: [
|
||||
{
|
||||
title: 'How to treat school filters',
|
||||
body: 'Use school filters to narrow research, not to assume admission eligibility. Ratings, distance, admissions criteria, and school capacity should all be checked with current official sources.',
|
||||
},
|
||||
{
|
||||
title: 'Family trade-offs to compare',
|
||||
body: 'Combine schools with parks, road noise, crime, property size, commute, broadband, and price so the shortlist reflects the whole move.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
{
|
||||
question: 'Does this show school catchment guarantees?',
|
||||
answer:
|
||||
'No. It helps identify promising areas, but catchments and admissions must be verified with the school or local authority.',
|
||||
},
|
||||
{
|
||||
question: 'Can I combine school filters with parks and safety?',
|
||||
answer:
|
||||
'Yes. School-aware search can be combined with crime, parks, commute, price, property size, and local services.',
|
||||
},
|
||||
{
|
||||
question: 'Is Ofsted the only school signal?',
|
||||
answer:
|
||||
'No single score should decide a move. Use the map as a starting point, then review current school information in detail.',
|
||||
},
|
||||
],
|
||||
relatedLinks: [
|
||||
{
|
||||
label: 'Data sources and coverage',
|
||||
path: '/data-sources',
|
||||
description: 'See where education, property, transport, and environment data comes from.',
|
||||
},
|
||||
...COMMON_RELATED_LINKS,
|
||||
],
|
||||
cta: 'Explore school-aware searches',
|
||||
},
|
||||
'postcode-checker': {
|
||||
path: '/postcode-checker',
|
||||
eyebrow: 'Postcode checker',
|
||||
title: 'Check postcode data before you book a viewing',
|
||||
metaTitle: 'Postcode checker - Property, crime, broadband, noise and schools',
|
||||
metaDescription:
|
||||
'Check postcode-level property prices, EPC data, crime, broadband, road noise, schools, council tax, amenities and travel-time context.',
|
||||
intro:
|
||||
'Review property prices, EPC context, crime, broadband, road noise, local amenities, schools, deprivation, council tax, and travel-time data from one postcode-first map.',
|
||||
points: [
|
||||
'Check multiple local signals before visiting a street.',
|
||||
'Use official and open datasets rather than reputation alone.',
|
||||
'Compare postcodes consistently across England.',
|
||||
],
|
||||
workflows: [
|
||||
{
|
||||
title: 'Check the street before spending a viewing slot',
|
||||
body: 'Use the postcode checker to review price history, local context, amenities, schools, and environment signals before you commit time to visiting.',
|
||||
},
|
||||
{
|
||||
title: 'Compare neighbouring postcodes',
|
||||
body: 'If one postcode looks promising, compare adjacent areas using the same filters. This often reveals whether a concern is street-specific or part of a wider pattern.',
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: 'Useful before and alongside listing portals',
|
||||
body: 'Listing photos rarely tell you enough about the surrounding street. Perfect Postcode gives you an evidence-led postcode check before you commit time to a viewing.',
|
||||
},
|
||||
{
|
||||
title: 'A screening tool, not professional advice',
|
||||
body: 'The data is designed for shortlisting and comparison. Any purchase still needs current listing checks, legal due diligence, flood searches, lender requirements, and survey findings.',
|
||||
},
|
||||
],
|
||||
methodology: [
|
||||
{
|
||||
title: 'What a postcode check can catch',
|
||||
body: 'A postcode check can surface price context, environmental signals, nearby amenities, and other local indicators that are easy to miss in a listing.',
|
||||
},
|
||||
{
|
||||
title: 'What a postcode check cannot prove',
|
||||
body: 'It cannot confirm the condition of a home, future development, legal title, lender requirements, or current street-level experience. Those still need direct checks.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
{
|
||||
question: 'Can I use the checker before a viewing?',
|
||||
answer:
|
||||
'Yes. That is one of the main use cases: screen the postcode first, then decide whether the viewing is worth the time.',
|
||||
},
|
||||
{
|
||||
question: 'Does the checker include exact property condition?',
|
||||
answer: 'No. Property condition requires listing details, surveys, and direct inspection.',
|
||||
},
|
||||
{
|
||||
question: 'Can I compare multiple postcodes?',
|
||||
answer: 'Yes. The map is designed for consistent comparison across postcodes.',
|
||||
},
|
||||
],
|
||||
relatedLinks: COMMON_RELATED_LINKS,
|
||||
cta: 'Check postcodes on the map',
|
||||
},
|
||||
};
|
||||
|
||||
export const SEO_CONTENT_PAGES: Record<SeoContentKey, SeoContentPage> = {
|
||||
'birmingham-property-search': {
|
||||
path: '/property-search/birmingham',
|
||||
eyebrow: 'Regional guide',
|
||||
title: 'How to compare Birmingham postcodes before a property search',
|
||||
metaTitle: 'Birmingham property search - Compare postcodes by price and commute',
|
||||
metaDescription:
|
||||
'Use postcode-level data to compare Birmingham property prices, commute trade-offs, schools, crime, broadband and local amenities before viewings.',
|
||||
intro:
|
||||
'Birmingham searches can change quickly from street to street. Use postcode-level evidence to compare budget, commute, schools, noise, crime, and local services before deciding where to watch listings.',
|
||||
sections: [
|
||||
{
|
||||
title: 'Start with commute corridors',
|
||||
body: 'Choose the destination that matters, such as a workplace, station, university, or hospital, then compare reachable postcodes by transport mode and travel-time band.',
|
||||
bullets: [
|
||||
'Use commute time as a hard filter before judging price.',
|
||||
'Compare public transport with car, cycling, or walking where available.',
|
||||
'Check the route manually before booking viewings.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Compare price with property type',
|
||||
body: 'Median prices alone can be misleading if the local property mix changes. Add property type, tenure, floor area, and price filters so similar areas are compared fairly.',
|
||||
},
|
||||
{
|
||||
title: 'Keep family and environment trade-offs visible',
|
||||
body: 'Layer school context, parks, road noise, broadband, and crime signals on top of the property filters. That makes it easier to decide which compromises are acceptable.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
{
|
||||
question: 'Can Perfect Postcode tell me the best area in Birmingham?',
|
||||
answer:
|
||||
'No tool can decide the best area for every buyer. It helps compare postcodes against your own constraints so you can build a better shortlist.',
|
||||
},
|
||||
{
|
||||
question: 'Should I use this instead of local knowledge?',
|
||||
answer:
|
||||
'No. Use it to find and compare candidates, then validate them with visits, local advice, listings, and official checks.',
|
||||
},
|
||||
],
|
||||
relatedLinks: [
|
||||
{
|
||||
label: 'Property price map',
|
||||
path: '/property-price-map',
|
||||
description: 'Compare price patterns before looking at live listings.',
|
||||
},
|
||||
{
|
||||
label: 'Commute property search',
|
||||
path: '/commute-property-search',
|
||||
description: 'Search by travel time and then layer on property requirements.',
|
||||
},
|
||||
{
|
||||
label: 'Methodology',
|
||||
path: '/methodology',
|
||||
description: 'Understand how to interpret filters and limitations.',
|
||||
},
|
||||
],
|
||||
cta: 'Compare Birmingham postcodes',
|
||||
},
|
||||
'manchester-property-search': {
|
||||
path: '/property-search/manchester',
|
||||
eyebrow: 'Regional guide',
|
||||
title: 'How to compare Manchester postcodes for a property search',
|
||||
metaTitle: 'Manchester property search - Compare postcodes before viewing',
|
||||
metaDescription:
|
||||
'Compare Manchester-area postcodes by budget, commute, property type, schools, broadband, crime, noise and amenities before booking viewings.',
|
||||
intro:
|
||||
'A Manchester-area search can span city-centre, suburban, and commuter options. Perfect Postcode helps keep each postcode comparable against the same property and daily-life constraints.',
|
||||
sections: [
|
||||
{
|
||||
title: 'Use travel time to define the real search area',
|
||||
body: 'Start from the destinations that matter, then compare reachable postcodes rather than assuming every nearby place has the same practical journey.',
|
||||
},
|
||||
{
|
||||
title: 'Compare housing requirements before lifestyle preferences',
|
||||
body: 'Filter by property type, floor area, tenure, and price before judging amenities. That keeps the shortlist grounded in homes that could realistically work.',
|
||||
},
|
||||
{
|
||||
title: 'Check local context consistently',
|
||||
body: 'Use broadband, crime, road noise, parks, schools, and amenities as comparable signals. Then validate the strongest candidates with current local checks.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
{
|
||||
question: 'Can I compare Manchester suburbs with city-centre postcodes?',
|
||||
answer:
|
||||
'Yes. Use the same budget, property, commute, and local-context filters across both so trade-offs remain visible.',
|
||||
},
|
||||
{
|
||||
question: 'Does this include live listings?',
|
||||
answer:
|
||||
'No. Use it to decide where to search, then use listing portals for current homes for sale.',
|
||||
},
|
||||
],
|
||||
relatedLinks: [
|
||||
{
|
||||
label: 'Postcode property search',
|
||||
path: '/postcode-property-search',
|
||||
description: 'Move from a broad search brief to specific postcode candidates.',
|
||||
},
|
||||
{
|
||||
label: 'Data sources',
|
||||
path: '/data-sources',
|
||||
description: 'Review the datasets used for property and local-context comparison.',
|
||||
},
|
||||
{
|
||||
label: 'Postcode checker',
|
||||
path: '/postcode-checker',
|
||||
description: 'Check a single postcode before arranging a viewing.',
|
||||
},
|
||||
],
|
||||
cta: 'Compare Manchester postcodes',
|
||||
},
|
||||
'bristol-property-search': {
|
||||
path: '/property-search/bristol',
|
||||
eyebrow: 'Regional guide',
|
||||
title: 'How to compare Bristol postcodes before a property search',
|
||||
metaTitle: 'Bristol property search - Compare postcodes by commute and price',
|
||||
metaDescription:
|
||||
'Compare Bristol postcodes by price, commute, property size, schools, broadband, crime, road noise, parks and amenities before viewings.',
|
||||
intro:
|
||||
'Bristol searches often involve sharp trade-offs between price, journey time, property size, and neighbourhood context. A postcode-first comparison keeps those trade-offs visible.',
|
||||
sections: [
|
||||
{
|
||||
title: 'Make commute constraints explicit',
|
||||
body: 'If access to the centre, a station, hospital, university, or business park matters, use travel-time filters first and then compare the remaining postcodes by property data.',
|
||||
},
|
||||
{
|
||||
title: 'Compare value, not just headline price',
|
||||
body: 'Use price, property type, and floor-area filters together. This helps distinguish lower-cost areas from areas that simply contain smaller or different homes.',
|
||||
},
|
||||
{
|
||||
title: 'Screen environmental and local-service signals',
|
||||
body: 'Road noise, parks, broadband, crime, and amenities can affect whether a property works day to day. Use them as screening criteria before booking viewings.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
{
|
||||
question: 'Can I use this for commuter villages around Bristol?',
|
||||
answer:
|
||||
'Yes, where the relevant postcode and travel-time data is available. Always verify routes and services manually before deciding.',
|
||||
},
|
||||
{
|
||||
question: 'Can this tell me whether a listing is good value?',
|
||||
answer:
|
||||
'It can provide area context, but a specific listing still needs comparable sales, condition checks, survey findings, and professional advice where appropriate.',
|
||||
},
|
||||
],
|
||||
relatedLinks: [
|
||||
{
|
||||
label: 'Commute property search',
|
||||
path: '/commute-property-search',
|
||||
description: 'Search by reachable postcodes before refining by budget and local context.',
|
||||
},
|
||||
{
|
||||
label: 'Property price map',
|
||||
path: '/property-price-map',
|
||||
description: 'Understand price patterns before setting listing alerts.',
|
||||
},
|
||||
{
|
||||
label: 'Privacy and security',
|
||||
path: '/privacy-security',
|
||||
description: 'How account and saved-search data is handled in the product.',
|
||||
},
|
||||
],
|
||||
cta: 'Compare Bristol postcodes',
|
||||
},
|
||||
'data-sources': {
|
||||
path: '/data-sources',
|
||||
eyebrow: 'Trust and coverage',
|
||||
title: 'Perfect Postcode data sources and coverage',
|
||||
metaTitle: 'Perfect Postcode data sources - Property, schools, commute and local context',
|
||||
metaDescription:
|
||||
'Review the public and official datasets used by Perfect Postcode, including property prices, EPC, schools, crime, broadband, noise and travel-time context.',
|
||||
intro:
|
||||
'Perfect Postcode combines property, transport, education, environment, and local-service datasets so buyers can compare postcodes consistently. This page explains what the data is for and where it should be verified.',
|
||||
sections: [
|
||||
{
|
||||
title: 'Property and housing context',
|
||||
body: 'The product uses property transaction and housing-context datasets to support filters such as sale price, property type, tenure, floor area, energy performance, and estimated current value.',
|
||||
bullets: [
|
||||
'Use these fields to compare areas, not as a formal valuation.',
|
||||
'Check current listings, title information, lender requirements, and survey results before buying.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Schools, safety, broadband, and environment',
|
||||
body: 'Local-context filters help compare postcodes on signals that affect daily life. They should be treated as screening data and checked against current official sources for decisions.',
|
||||
},
|
||||
{
|
||||
title: 'Travel-time data',
|
||||
body: 'Travel-time filters are designed for consistent area comparison. Route availability, disruption, parking, walking access, and timetable details should be verified before committing to an area.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
{
|
||||
question: 'Why does coverage focus on England?',
|
||||
answer:
|
||||
'Several core property, education, and local-context datasets are jurisdiction-specific. England coverage keeps comparisons more consistent.',
|
||||
},
|
||||
{
|
||||
question: 'How should I handle stale or missing data?',
|
||||
answer:
|
||||
'Use the map as a shortlist tool. If a postcode matters, verify the latest details with current official sources and direct local checks.',
|
||||
},
|
||||
],
|
||||
relatedLinks: [
|
||||
{
|
||||
label: 'Methodology',
|
||||
path: '/methodology',
|
||||
description: 'How filters and comparisons should be interpreted.',
|
||||
},
|
||||
{
|
||||
label: 'Postcode checker',
|
||||
path: '/postcode-checker',
|
||||
description: 'Review postcode-level context before a viewing.',
|
||||
},
|
||||
{
|
||||
label: 'Privacy and security',
|
||||
path: '/privacy-security',
|
||||
description: 'How saved searches and account data are handled.',
|
||||
},
|
||||
],
|
||||
},
|
||||
methodology: {
|
||||
path: '/methodology',
|
||||
eyebrow: 'How to use the map',
|
||||
title: 'Methodology for postcode property research',
|
||||
metaTitle: 'Perfect Postcode methodology - How to interpret postcode property data',
|
||||
metaDescription:
|
||||
'Understand how to use postcode filters, property estimates, travel-time data, school context and local signals as a home-buying shortlist tool.',
|
||||
intro:
|
||||
'Perfect Postcode is designed to make area shortlisting more evidence-led. It does not replace estate agents, surveyors, conveyancers, lenders, school admissions teams, or local authority checks.',
|
||||
sections: [
|
||||
{
|
||||
title: 'Start with hard constraints',
|
||||
body: 'Begin with non-negotiables such as budget, property type, floor area, commute time, and essential services. This removes impossible postcodes before softer preferences are considered.',
|
||||
},
|
||||
{
|
||||
title: 'Use colour layers for trade-offs',
|
||||
body: 'After filtering, colour the remaining map by one signal at a time: price per square metre, road noise, school context, commute time, broadband, or crime. This makes trade-offs easier to discuss.',
|
||||
},
|
||||
{
|
||||
title: 'Measure what is working',
|
||||
body: 'Use Search Console and analytics to track which public pages are indexed, which queries produce impressions, and which pages convert visitors into dashboard exploration. Review Core Web Vitals after every substantial frontend change.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
{
|
||||
question: 'Can the tool choose the right postcode for me?',
|
||||
answer:
|
||||
'No. It helps compare evidence and reduce the search area. The final decision needs direct visits, current listings, legal checks, surveys, and personal judgement.',
|
||||
},
|
||||
{
|
||||
question: 'How should I use estimates?',
|
||||
answer:
|
||||
'Use estimates as comparison signals, not as professional valuations or purchase advice.',
|
||||
},
|
||||
],
|
||||
relatedLinks: [
|
||||
{
|
||||
label: 'Data sources and coverage',
|
||||
path: '/data-sources',
|
||||
description: 'Understand where key filters come from.',
|
||||
},
|
||||
{
|
||||
label: 'Property price map',
|
||||
path: '/property-price-map',
|
||||
description: 'Apply the methodology to price-led area comparison.',
|
||||
},
|
||||
{
|
||||
label: 'Commute property search',
|
||||
path: '/commute-property-search',
|
||||
description: 'Apply the methodology to destination-led search.',
|
||||
},
|
||||
],
|
||||
},
|
||||
'privacy-security': {
|
||||
path: '/privacy-security',
|
||||
eyebrow: 'Trust',
|
||||
title: 'Privacy and security for saved property searches',
|
||||
metaTitle: 'Perfect Postcode privacy and security - Saved searches and account data',
|
||||
metaDescription:
|
||||
'Learn how Perfect Postcode treats saved searches, account data and property research workflows with privacy and security in mind.',
|
||||
intro:
|
||||
'Property research can reveal personal priorities, budgets, and locations. The product keeps public SEO pages separate from account-only areas and marks private dashboard/account routes as noindex.',
|
||||
sections: [
|
||||
{
|
||||
title: 'Public pages and private areas are separated',
|
||||
body: 'Marketing, methodology, guide, and support pages are indexable. Dashboard, account, saved searches, invites, and invitation routes are marked noindex or blocked from crawler access where appropriate.',
|
||||
},
|
||||
{
|
||||
title: 'Saved search data is account-scoped',
|
||||
body: 'Saved searches and properties are intended for signed-in use. They are not included in the public sitemap and should not be crawlable as public content.',
|
||||
},
|
||||
{
|
||||
title: 'Search measurement without exposing private data',
|
||||
body: 'SEO measurement should happen on public pages using aggregated analytics and Search Console data. Private query parameters and account views should not become indexable landing pages.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
{
|
||||
question: 'Are saved searches listed in the sitemap?',
|
||||
answer:
|
||||
'No. Public SEO pages are listed; account and saved-search routes are intentionally excluded.',
|
||||
},
|
||||
{
|
||||
question: 'Can private dashboard URLs appear in search?',
|
||||
answer:
|
||||
'They should not be indexed. The server marks private routes noindex and the sitemap only lists public pages.',
|
||||
},
|
||||
],
|
||||
relatedLinks: [
|
||||
{
|
||||
label: 'Methodology',
|
||||
path: '/methodology',
|
||||
description: 'How to use public postcode data responsibly.',
|
||||
},
|
||||
{
|
||||
label: 'Data sources and coverage',
|
||||
path: '/data-sources',
|
||||
description: 'What data powers the public comparisons.',
|
||||
},
|
||||
{
|
||||
label: 'Postcode property search',
|
||||
path: '/postcode-property-search',
|
||||
description: 'Explore public postcode-search workflows.',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
51
frontend/src/lib/seoRoutes.ts
Normal file
51
frontend/src/lib/seoRoutes.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
export type SeoLandingKey =
|
||||
| 'property-price-map'
|
||||
| 'postcode-property-search'
|
||||
| 'commute-property-search'
|
||||
| 'school-property-search'
|
||||
| 'postcode-checker';
|
||||
|
||||
export type SeoContentKey =
|
||||
| 'birmingham-property-search'
|
||||
| 'manchester-property-search'
|
||||
| 'bristol-property-search'
|
||||
| 'data-sources'
|
||||
| 'methodology'
|
||||
| 'privacy-security';
|
||||
|
||||
export type SeoPageKey = SeoLandingKey | SeoContentKey;
|
||||
|
||||
export const SEO_LANDING_PATHS: Record<SeoLandingKey, string> = {
|
||||
'property-price-map': '/property-price-map',
|
||||
'postcode-property-search': '/postcode-property-search',
|
||||
'commute-property-search': '/commute-property-search',
|
||||
'school-property-search': '/school-property-search',
|
||||
'postcode-checker': '/postcode-checker',
|
||||
};
|
||||
|
||||
export const SEO_CONTENT_PATHS: Record<SeoContentKey, string> = {
|
||||
'birmingham-property-search': '/property-search/birmingham',
|
||||
'manchester-property-search': '/property-search/manchester',
|
||||
'bristol-property-search': '/property-search/bristol',
|
||||
'data-sources': '/data-sources',
|
||||
methodology: '/methodology',
|
||||
'privacy-security': '/privacy-security',
|
||||
};
|
||||
|
||||
export function getSeoLandingPage(pathname: string): SeoLandingKey | null {
|
||||
const match = Object.entries(SEO_LANDING_PATHS).find(([, path]) => path === pathname);
|
||||
return (match?.[0] as SeoLandingKey | undefined) ?? null;
|
||||
}
|
||||
|
||||
export function getSeoContentPage(pathname: string): SeoContentKey | null {
|
||||
const match = Object.entries(SEO_CONTENT_PATHS).find(([, path]) => path === pathname);
|
||||
return (match?.[0] as SeoContentKey | undefined) ?? null;
|
||||
}
|
||||
|
||||
export function isSeoLandingKey(page: string): page is SeoLandingKey {
|
||||
return Object.prototype.hasOwnProperty.call(SEO_LANDING_PATHS, page);
|
||||
}
|
||||
|
||||
export function isSeoContentKey(page: string): page is SeoContentKey {
|
||||
return Object.prototype.hasOwnProperty.call(SEO_CONTENT_PATHS, page);
|
||||
}
|
||||
2411
server-rs/logs/server.log.2026-05-05
Normal file
2411
server-rs/logs/server.log.2026-05-05
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue