PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 17s
PR Checks / Build Frontend (no push) (pull_request) Successful in 45s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 2m39s
/schools/[place]/[phase] shipped as routes but reached nothing. The sitemap emitted one URL per registry entry and the registry had no phase dimension, so ~950 pages were absent from every sitemap — and PlaceView did not link them either, leaving them reachable by nothing at all. That is the query shape the baseline actually showed: 'primary schools in beccles', 'secondary schools in brentwood'. Publishing the routes without a path in meant building for the demand and then hiding from it. Place now carries phase_urns so the per-phase threshold can be applied without re-querying, the sitemap emits a variant wherever a phase clears the threshold on its own, and the API exposes the qualifying phases so the place page links only variants that exist. Outcodes are excluded: nobody searches 'primary schools in SW11' and those routes do not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
61 lines
1.9 KiB
TypeScript
61 lines
1.9 KiB
TypeScript
/**
|
|
* Client for the places API.
|
|
*
|
|
* Two namespaces, matching the backend: towns and localities share
|
|
* /schools/[place]; authorities take /schools/authority/[la]. 67 town names
|
|
* collide with an authority name and neither set contains the other, so one
|
|
* namespace would publish near-duplicate pages.
|
|
*/
|
|
import type { School } from '@/lib/types';
|
|
|
|
export interface PlaceSummary {
|
|
kind: string;
|
|
slug: string;
|
|
name: string;
|
|
count: number;
|
|
/** Phases that clear the threshold on their own, so the page links
|
|
* variants that exist rather than 404s. Absent on the registry listing. */
|
|
phases?: string[];
|
|
}
|
|
|
|
export interface PlaceDetail {
|
|
place: PlaceSummary & { parent_authority: string | null };
|
|
schools: School[];
|
|
averages: {
|
|
rwm_expected_pct: number | null;
|
|
attainment_8_score: number | null;
|
|
};
|
|
}
|
|
|
|
export function placeUrl(kind: string, slug: string, phase?: string): string {
|
|
const base =
|
|
kind === 'authority' ? `/schools/authority/${slug}`
|
|
: kind === 'outcode' ? `/schools/near/${slug}`
|
|
: `/schools/${slug}`;
|
|
return phase ? `${base}/${phase}` : base;
|
|
}
|
|
|
|
/** An authority name as it appears in a URL. */
|
|
export function authoritySlug(name: string): string {
|
|
return name.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-');
|
|
}
|
|
|
|
const API = process.env.FASTAPI_URL || process.env.NEXT_PUBLIC_API_URL
|
|
|| 'http://localhost:8000/api';
|
|
|
|
export async function fetchPlaces(): Promise<PlaceSummary[]> {
|
|
const res = await fetch(`${API}/places`, { next: { revalidate: 604800 } });
|
|
if (!res.ok) return [];
|
|
return (await res.json()).places ?? [];
|
|
}
|
|
|
|
export async function fetchPlace(
|
|
kind: string, slug: string, phase?: string,
|
|
): Promise<PlaceDetail | null> {
|
|
const q = phase ? `?phase=${encodeURIComponent(phase)}` : '';
|
|
const res = await fetch(`${API}/places/${kind}/${slug}${q}`,
|
|
{ next: { revalidate: 604800 } });
|
|
if (!res.ok) return null;
|
|
return res.json();
|
|
}
|