PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 18s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m18s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 3m48s
Review caught _places_payload documenting a `phase_url` the function never returned. The docstring was not stray prose: the approved design included the phase variant — "Primary schools in Beccles" was one of its four example links — and it was dropped during implementation without being mentioned. Deleting the sentence would have closed the report while losing the feature, so the links are built instead. These are the pages that most needed them. ~950 phase variants were once reachable by nothing at all: absent from every sitemap and unlinked from the place page. "Primary schools in brentwood" is the query they exist to answer. Membership is read from the registry's own `phase_urns` rather than re-derived from the school's phase string. The registry already decides which phases a place publishes and which schools are listed on each, so asking it is both shorter and the only way the link cannot disagree with the page it points at. It also means outcodes need no special case: they carry empty `phase_urns` by design, because nobody searches "primary schools in SW11", so they report no phase links on their own. `phases` is a list rather than a single url. An all-through school is listed on both the primary and secondary pages, so there is no tie to break and no reason to invent one. Each entry renders directly after its own place, so "22 primary schools in Brentwood" reads as part of Brentwood rather than as an unrelated link further along the row. The e2e journey now follows a phase link where the town publishes one and asserts it resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXnXQKnPpZBBP61fBQiFkq
68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
import Link from 'next/link';
|
|
import type { SchoolPlace, SchoolPhasePage } from '@/lib/jsonld';
|
|
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}`;
|
|
}
|
|
|
|
/** "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}`;
|
|
}
|
|
|
|
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}>
|
|
{sorted.flatMap((place) => [
|
|
<li key={`${place.kind}:${place.slug}`}>
|
|
<Link href={place.url} className={styles.link}>{label(place)}</Link>
|
|
</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>
|
|
)),
|
|
])}
|
|
</ul>
|
|
</section>
|
|
);
|
|
}
|