PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 50s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m22s
Staging audit of the Cohort identity found the whole site rendering in Times. Root cause: next/font's variable classes were on <body>, while globals.css declares --font-display/--font-ui/--font-prose on :root as `var(--font-schibsted), ...`. A custom property's var() references resolve on the element that declares it, so at :root --font-schibsted was undefined, --font-display computed to the guaranteed-invalid value, and every font-family referencing it fell back. Nothing threw and the build was green — the only symptom was visual. Verified the mechanism in a browser both ways round: class on <body> gives an empty token and a Times body font; class on <html> resolves to Schibsted Grotesk. The classes now sit on <html>. Two colour escapes from the same audit: * Form controls don't inherit font or colour from their parent, so the omni search input and the map's "Open full map" button rendered in the system font at pure black. Nearly invisible against --text-primary in light mode and completely invisible on the dark ground. Added a base inherit rule. * Leaflet ships its own palette — a #ddd tile backdrop, #333 attribution text and a #0078A8 link blue that was the most saturated colour anywhere on the site. The map chrome now uses tokens; the tiles stay as OSM renders them. Three e2e gates added, because the existing suite passed while the site was entirely in Times: * the font tokens resolve to a non-empty value and the applied family is Schibsted, not a serif fallback * no visible element renders in the browser's default black * every rendered colour comes from the token palette — the manual audit, turned into a gate Also made the og:image check environment-relative: metadataBase pins canonical URLs to the production host, so the absolute URL pointed off-environment when the suite ran against staging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
5.0 KiB
TypeScript
126 lines
5.0 KiB
TypeScript
import type { Metadata, Viewport } from 'next';
|
|
import { Schibsted_Grotesk, Literata } from 'next/font/google';
|
|
import Script from 'next/script';
|
|
import { Navigation } from '@/components/Navigation';
|
|
import { Footer } from '@/components/Footer';
|
|
import { ComparisonToast } from '@/components/ComparisonToast';
|
|
import { ComparisonProvider } from '@/context/ComparisonProvider';
|
|
import './globals.css';
|
|
|
|
// Schibsted Grotesk carries the whole interface — headings, labels, and every
|
|
// figure. It was drawn for news brands, so its tabular numerals hold a column
|
|
// properly, which matters on a page that is mostly percentages.
|
|
const schibsted = Schibsted_Grotesk({
|
|
subsets: ['latin'],
|
|
weight: ['400', '500', '600', '700'],
|
|
variable: '--font-schibsted',
|
|
display: 'swap',
|
|
});
|
|
|
|
// Literata is for prose only — the admissions guide, methodology notes, the
|
|
// editorial sections. It never touches a number or a control.
|
|
const literata = Literata({
|
|
subsets: ['latin'],
|
|
weight: ['400', '600'],
|
|
style: ['normal', 'italic'],
|
|
variable: '--font-literata',
|
|
display: 'swap',
|
|
});
|
|
|
|
export const viewport: Viewport = {
|
|
width: 'device-width',
|
|
initialScale: 1,
|
|
// viewport-fit=cover lets us paint behind the notch / Dynamic Island so
|
|
// env(safe-area-inset-*) values resolve to real numbers on iPhone.
|
|
viewportFit: 'cover',
|
|
// These must match --bg-primary in globals.css for each theme, or the
|
|
// browser chrome and the page disagree at the top edge of the screen.
|
|
themeColor: [
|
|
{ media: '(prefers-color-scheme: light)', color: '#f2f1ed' },
|
|
{ media: '(prefers-color-scheme: dark)', color: '#10151b' },
|
|
],
|
|
};
|
|
|
|
export const metadata: Metadata = {
|
|
appleWebApp: {
|
|
capable: true,
|
|
title: 'SchoolCompare',
|
|
statusBarStyle: 'default',
|
|
},
|
|
title: {
|
|
default: 'SchoolCompare | Compare School Performance',
|
|
template: '%s | SchoolCompare',
|
|
},
|
|
description: 'Compare primary and secondary school SATs and GCSE performance across England',
|
|
keywords: 'school comparison, KS2 results, KS4 results, primary school, secondary school, England schools, SATs results, GCSE results',
|
|
authors: [{ name: 'SchoolCompare' }],
|
|
manifest: '/manifest.json',
|
|
// No `icons` key on purpose: setting it here would override the file
|
|
// conventions. app/icon.svg and app/apple-icon.tsx are the source, and
|
|
// app/opengraph-image.tsx supplies og:image and twitter:image.
|
|
metadataBase: new URL('https://schoolcompare.co.uk'),
|
|
openGraph: {
|
|
type: 'website',
|
|
title: 'SchoolCompare | Compare School Performance',
|
|
description: 'Compare primary and secondary school SATs and GCSE performance across England',
|
|
url: 'https://schoolcompare.co.uk',
|
|
siteName: 'SchoolCompare',
|
|
},
|
|
twitter: {
|
|
// summary_large_image now that there is an image worth showing.
|
|
card: 'summary_large_image',
|
|
title: 'SchoolCompare | Compare School Performance',
|
|
description: 'Compare primary and secondary school SATs and GCSE performance across England',
|
|
},
|
|
};
|
|
|
|
export default function RootLayout({
|
|
children,
|
|
}: Readonly<{
|
|
children: React.ReactNode;
|
|
}>) {
|
|
return (
|
|
// The font variable classes must sit on <html>, not <body>. globals.css
|
|
// declares --font-display/--font-ui on :root as var(--font-schibsted),
|
|
// and a custom property's var() references resolve on the element that
|
|
// declares it. With the classes on <body>, --font-schibsted was undefined
|
|
// at :root, so --font-display computed to the guaranteed-invalid value and
|
|
// every font-family that referenced it silently fell back — the whole site
|
|
// rendered in Times.
|
|
<html lang="en" className={`${schibsted.variable} ${literata.variable}`}>
|
|
<head>
|
|
<link rel="preconnect" href="https://analytics.schoolcompare.co.uk" />
|
|
<link rel="preconnect" href="https://api.postcodes.io" />
|
|
{/* data-domains: the tracker only fires on the production hostnames,
|
|
so staging (same image, different host) never pollutes Umami */}
|
|
<Script
|
|
src="https://analytics.schoolcompare.co.uk/script.js"
|
|
data-website-id="d7fb0c95-bb6c-4336-8209-bd10077e50dd"
|
|
data-domains="schoolcompare.co.uk,www.schoolcompare.co.uk"
|
|
data-performance="true"
|
|
strategy="afterInteractive"
|
|
/>
|
|
{/* Heatmap / session recorder — same data-domains guard so staging
|
|
(same image, different host) never feeds the heatmap. */}
|
|
<Script
|
|
src="https://analytics.schoolcompare.co.uk/recorder.js"
|
|
data-website-id="d7fb0c95-bb6c-4336-8209-bd10077e50dd"
|
|
data-domains="schoolcompare.co.uk,www.schoolcompare.co.uk"
|
|
strategy="afterInteractive"
|
|
/>
|
|
</head>
|
|
<body>
|
|
<ComparisonProvider>
|
|
<a href="#main-content" className="skip-link">Skip to main content</a>
|
|
<Navigation />
|
|
<main id="main-content" className="main">
|
|
{children}
|
|
</main>
|
|
<ComparisonToast />
|
|
<Footer />
|
|
</ComparisonProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|