2026-09-14 20:45:50 +01:00
|
|
|
import Link from 'next/link';
|
2026-09-14 20:54:59 +01:00
|
|
|
import type { SchoolPlace, SchoolPhasePage } from '@/lib/jsonld';
|
2026-09-14 20:45:50 +01:00
|
|
|
import styles from './NearbyPlaces.module.css';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Links from a school page into the location layer.
|
|
|
|
|
*
|
|
|
|
|
* This exists for a structural reason rather than a decorative one. Before
|
|
|
|
|
* it, the only anchor on a school page pointed at the school's own website,
|
|
|
|
|
* so the ~27k pages that carry most of the site's inbound authority passed it
|
|
|
|
|
* straight off-site and none of it reached the place pages. These links are
|
|
|
|
|
* what circulate it instead.
|
|
|
|
|
*
|
|
|
|
|
* Every entry comes from the place registry via the API, so a link is only
|
|
|
|
|
* ever offered for a page that exists: a place below the publish threshold is
|
|
|
|
|
* absent from the registry and therefore absent here.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/** Narrowest first: a reader on a school page wants its town before its
|
|
|
|
|
* county. The API orders widest-first because that is what the breadcrumb
|
|
|
|
|
* reads, so the two orders are deliberately different. */
|
|
|
|
|
const ORDER: Record<string, number> = {
|
|
|
|
|
town: 0, locality: 0, outcode: 1, authority: 2,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function label(place: SchoolPlace): string {
|
|
|
|
|
const noun = place.count === 1 ? 'school' : 'schools';
|
|
|
|
|
const preposition = place.kind === 'outcode' ? 'near' : 'in';
|
|
|
|
|
return `${place.count} ${noun} ${preposition} ${place.name}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 20:54:59 +01:00
|
|
|
/** "22 primary schools in Brentwood" — the phrasing the query itself uses. */
|
|
|
|
|
function phaseLabel(place: SchoolPlace, page: SchoolPhasePage): string {
|
|
|
|
|
const noun = page.count === 1 ? 'school' : 'schools';
|
|
|
|
|
return `${page.count} ${page.phase} ${noun} in ${place.name}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 20:45:50 +01:00
|
|
|
export function NearbyPlaces({ places }: { places: SchoolPlace[] }) {
|
|
|
|
|
if (places.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
const sorted = [...places].sort(
|
|
|
|
|
(a, b) => (ORDER[a.kind] ?? 9) - (ORDER[b.kind] ?? 9),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className={styles.section} aria-labelledby="nearby-places">
|
|
|
|
|
<h2 id="nearby-places" className={styles.heading}>More schools near here</h2>
|
|
|
|
|
<ul className={styles.list}>
|
2026-09-14 20:54:59 +01:00
|
|
|
{sorted.flatMap((place) => [
|
2026-09-14 20:45:50 +01:00
|
|
|
<li key={`${place.kind}:${place.slug}`}>
|
|
|
|
|
<Link href={place.url} className={styles.link}>{label(place)}</Link>
|
2026-09-14 20:54:59 +01:00
|
|
|
</li>,
|
|
|
|
|
/* Immediately after its own place, so "22 primary schools in
|
|
|
|
|
Brentwood" reads as part of Brentwood rather than as an
|
|
|
|
|
unrelated link further down the row. */
|
|
|
|
|
...place.phases.map((page) => (
|
|
|
|
|
<li key={`${place.kind}:${place.slug}:${page.phase}`}>
|
|
|
|
|
<Link href={page.url} className={styles.link}>
|
|
|
|
|
{phaseLabel(place, page)}
|
|
|
|
|
</Link>
|
|
|
|
|
</li>
|
|
|
|
|
)),
|
|
|
|
|
])}
|
2026-09-14 20:45:50 +01:00
|
|
|
</ul>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|