PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m11s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m17s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 53s
The staging E2E gate's one failure. Every link to the site pasted into a chat has been rendering bare. Staging serves og:title, og:description, og:url, og:site_name, og:type and twitter:card, and no og:image at all. So metadata from the layout reaches the page; only the file convention does not. app/opengraph-image.tsx does work — _not-found, which lives in the app root segment, carries an og:image from it in the build output. It does not reach the site's pages, which live in the (frontend) route group whose own layout.tsx is the root layout. The icon conventions are not affected: /icon.png and /apple-icon.png are both linked correctly on the same page, verified against staging. The asymmetry is the whole bug, and it arrived with the route-group split that Payload required. The file stays at the app root. Moving metadata files into a route group is what drops /robots.txt and hashes /icon.png, which CLAUDE.md records and which this must not undo — the build still emits all four of /robots.txt, /icon.png, /apple-icon.png and /opengraph-image. The root layout points at the route instead, and metadataBase makes it absolute, which the journey needs since it calls new URL() on the value. twitter.images is set for the same reason: the card is declared summary_large_image, and claiming a large-image card while supplying no image is worse than claiming a summary card. Checked before fixing that og:image was the only broken assertion in that journey: the test aborts at line 911, so its apple-touch-icon and maskable-icon assertions had never run. All four of those assets return 200 image/png from staging, so this does not simply move the failure further down the test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXnXQKnPpZBBP61fBQiFkq
SchoolCompare Next.js Application
Modern Next.js application for comparing primary school KS2 performance across England.
Features
- Server-Side Rendering (SSR): Fast initial page loads with pre-rendered content
- Individual School Pages: Dedicated pages for each school with full SEO optimization
- Side-by-Side Comparison: Compare up to 5 schools simultaneously
- School Rankings: Top-performing schools by various metrics
- Interactive Maps: Leaflet integration for geographic visualization
- Performance Charts: Chart.js visualizations for historical data
- Responsive Design: Mobile-first approach with full responsive support
- SEO Optimized: Dynamic sitemaps, meta tags, and structured data
Tech Stack
- Framework: Next.js 16 (App Router)
- Language: TypeScript 5
- Styling: CSS Modules + CSS Variables
- State Management: React Context API + URL state
- Data Fetching: SWR (client-side) + Next.js fetch (server-side)
- Charts: Chart.js + react-chartjs-2
- Maps: Leaflet + react-leaflet
- Testing: Jest + React Testing Library
- Validation: Zod
Getting Started
Prerequisites
- Node.js 24+ (using nvm recommended)
- FastAPI backend running on port 8000
Installation
# Install dependencies
npm install
# Copy environment variables
cp .env.example .env.local
# Update .env.local with your configuration
Development
# Start development server
npm run dev
# Open http://localhost:3000
Building
# Build for production
npm run build
# Start production server
npm start
Testing
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
Linting
# Run ESLint
npm run lint
Project Structure
nextjs-app/
├── app/ # App Router pages
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ ├── compare/ # Compare page
│ ├── rankings/ # Rankings page
│ ├── school/[urn]/ # Individual school pages
│ ├── sitemap.ts # Dynamic sitemap
│ └── robots.ts # Robots.txt
├── components/ # React components
│ ├── SchoolCard.tsx # School card component
│ ├── FilterBar.tsx # Search/filter controls
│ ├── ComparisonView.tsx # Comparison interface
│ ├── RankingsView.tsx # Rankings table
│ └── ...
├── lib/ # Utility libraries
│ ├── api.ts # API client
│ ├── types.ts # TypeScript types
│ └── utils.ts # Helper functions
├── hooks/ # Custom React hooks
├── context/ # React Context providers
├── styles/ # Global styles
├── public/ # Static assets
└── __tests__/ # Test files
Environment Variables
| Variable | Description | Default |
|---|---|---|
NEXT_PUBLIC_API_URL |
Public API endpoint (client-side) | http://localhost:8000/api |
FASTAPI_URL |
Server-side API endpoint | http://localhost:8000/api |
NODE_ENV |
Environment mode | development |
Performance Optimizations
- Server-Side Rendering: Initial HTML rendered on server
- Static Generation: Where possible, pages are pre-generated
- Image Optimization: Next.js Image component with AVIF/WebP support
- Code Splitting: Automatic route-based code splitting
- Dynamic Imports: Heavy components loaded on demand
- API Caching: Configurable revalidation for data fetching
- Bundle Optimization: Tree shaking and minification
- Compression: Gzip compression enabled
SEO Features
- Dynamic Meta Tags: Generated per page with Next.js Metadata API
- Open Graph: Social media optimization
- JSON-LD: Structured data for search engines
- Sitemap: Auto-generated from database
- Robots.txt: Search engine crawling rules
- Canonical URLs: Duplicate content prevention
Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
License
Proprietary - SchoolCompare
Support
For issues and questions, please contact the development team.