080456673632810806c9bfcbae58ff50f3307fab
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1fc1e07d21 |
fix(seo): keep staging out of the search index
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) Canceled after 13s
PR Checks / Build Pipeline (no push) (pull_request) Canceled after 0s
PR Checks / AI Code Review (Claude) (pull_request) Canceled after 0s
Staging serves the same image as production off stx., with robots.txt saying Allow: / and no noindex — a fully crawlable duplicate of the site. Nothing appears indexed today, most likely because the pages canonicalise across to production, but that is a side effect rather than a control. X-Robots-Tag, not a robots.txt Disallow. Disallow blocks crawling, which is not the same as blocking indexing: a disallowed URL can still be indexed from external links, and blocking the crawl means Google never fetches the page and so never sees a noindex at all. Staging stays crawlable and answers noindex. Matched on the staging host explicitly rather than 'any host that is not production'. The inverted form would cover future environments automatically, but its failure mode is deindexing production if the Host header ever arrives rewritten by a proxy — which cannot be verified from here. This form's failure mode is a new environment being indexable until someone adds it, which is recoverable. Any new non-production hostname must be added. The journeys only ever run against staging (deploy.yml passes STAGING_BASE_URL; promote.yml smoke-polls production without Playwright), so asserting the header there is safe. The two assertions live in one test because the halves only work together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj |
||
|
|
b11ee3c8de |
feat(brand): use the supplied logo artwork instead of a reconstruction
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m6s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 12s
PR Checks / Build Frontend (no push) (pull_request) Successful in 49s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m7s
The mark shipped so far was my SVG approximation, and it was wrong: the real mark is a teardrop pin with a white window and a path flowing out of its base, carrying three leaves — not a circle with a separate tail. Both colourways are extracted from the supplied sheet, which has a genuinely transparent background, so these are the artwork rather than a trace: public/brand/mark.png teal pin, white window and path, green leaves public/brand/mark-on-dark.png white pin with the counter knocked through Two colourways are needed, not one. Rendered against every real ground, the teal pin holds up on Warm White, white cards, Sand and both dark-theme grounds — but it vanishes on the teal footer band, where only the white window survives. The header therefore serves the teal artwork and swaps to the on-dark artwork for dark-theme viewers through a <picture> source, needing no JavaScript; the footer forces on-dark, because its band is teal in both themes. Everything downstream now derives from those two files: the favicon (app/icon.png, replacing icon.svg), the touch icon (app/apple-icon.png, replacing the generated apple-icon.tsx), the three PWA rasters, and the share card, which reads public/brand/mark.png off disk so it can never drift from the header. The logo sizing CSS keyed off a square box, which would have squashed a 153:189 artwork — height now drives and width follows. The wordmark stays live text in Manrope. The sheet's wordmark is a raster with visible edge fringing, and the written style guide specifies Manrope; live text also stays selectable, scales cleanly and recolours with the theme. KNOWN LIMITATION: the largest instance on the sheet is 153×189. That is ample for the header at 38px, the favicon and the share card, but short of the 512px PWA icon, which is upscaled and slightly soft. A vector would fix it and is a single swap — every consumer goes through components/Logo.tsx or public/brand. Verified: tsc clean, 159/159 tests, build green, /icon.png and /apple-icon.png emit as static routes, and the header, footer, share card and all five icons were rendered and inspected in both themes. |
||
|
|
8c3a5cc4e9 |
fix(design): keep below/attention off the brand hue, harden the share-card fonts
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
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 49s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 1m8s
Review follow-up on #86. The blind coral -> brand rename recreated the exact collision this PR set out to remove: coral had been both the primary CTA and the "below average" signal, so every negative indicator followed --primary onto iris. Sixteen rules moved back onto the status ramp — delta chips, trend-down arrows, progress-negative values, statusBad, chipBad/badgeBad, and the urgent deadline chips. The Ofsted scale had also lost its worst step, with grade 4 landing on brand while 1-2 were teal and 3 was amber. It now escalates by weight rather than by reaching for another hue: a tinted amber chip for "requires improvement", a solid amber one for "inadequate" (5.1:1 light, 7.7:1 dark). Report-card grade 5 follows the same rule. globals.css now describes status as valence — teal above/good, amber below/needs-attention — which is what it has to mean for an urgent deadline, rather than the narrower "comparison point only" the first draft claimed. On the share-card fonts: /opengraph-image is prerendered, so the font read happens in the builder stage where assets/ exists, and the baked PNG ships inside .next/standalone/.next/server/app/. File tracing independently places the fonts at .next/standalone/assets, which the existing standalone COPY carries to /app/assets. So the reported ENOENT doesn't occur — but it depends on the tracer resolving a runtime join(), and a miss would be a silent 500 rather than a build failure. Declared outputFileTracingIncludes for the route and made the Dockerfile COPY explicit so neither is left to inference. Also repointed the immutable Cache-Control rule from the deleted favicon.svg to app/icon.svg, where it was caching a 404. Verified: tsc clean, 159/159 tests, clean rebuild prerenders all three image routes with the fonts present in standalone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9556595800 |
feat(security): allow analytics subdomain to iframe the site
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
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 48s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 8s
Replace X-Frame-Options: SAMEORIGIN with a CSP frame-ancestors directive so analytics.schoolcompare.co.uk (Umami heatmap/recorder) can embed the site while all other origins stay blocked. X-Frame-Options cannot allow a specific sibling subdomain (ALLOW-FROM is deprecated/ignored by modern browsers), so frame-ancestors is the correct replacement. Also update the nginx snippet in DEPLOYMENT.md to match, so the reverse proxy doesn't re-inject a conflicting X-Frame-Options header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
95a5783da1 |
fix(frontend): proxy /api and /sitemap.xml at runtime, not via baked rewrites
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 9m38s
PR Checks / Backend Smoke (pull_request) Successful in 5s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
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 3m13s
next.config.js rewrites() bakes its destination into the build (routes-manifest.json), capturing FASTAPI_URL at build time. Because one frontend image is promoted staging->prod, the baked backend host forced every environment to name the backend service identically; staging names it 'backend_stg', so the browser's /api/* calls proxied to the baked 'http://backend' and failed with getaddrinfo ENOTFOUND backend. (SSR was unaffected because lib/api.ts reads FASTAPI_URL at runtime.) Replace the rewrites with route handlers that read FASTAPI_URL per request: - app/api/[...path]/route.ts — transparent proxy for all methods, streams the response, strips hop-by-hop headers, and returns 502 on upstream failure instead of crashing. - app/sitemap.xml/route.ts — proxies the backend sitemap (robots.ts points crawlers here). The same promoted image now adapts to whatever the backend is called in each environment. Verified: production build succeeds with /api/[...path] and /sitemap.xml as dynamic routes and an empty rewrites manifest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
62eeee5f7c |
perf: cache aggressively and trim client bundle
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 1m1s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 53s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 2m4s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Frontend - Dynamic-import Chart.js components on detail/compare views so Chart.js no longer ships in initial JS. - Drop force-dynamic on home, compare, rankings so internal data fetches reuse Next.js's per-call revalidate cache. - Switch /school/[slug] to ISR with a 7-day revalidate window (school data updates annually). - Preconnect to analytics + postcodes.io; remove redundant defer on the Umami Script tag (afterInteractive already covers it). - Bump images.minimumCacheTTL to 1 year. - Extract HowItWorks and Editorial sections as server components passed to HomeView via slot props so their JSX stays out of the client bundle. Backend - Add GZipMiddleware (min 512 bytes). - Add CacheAndETagMiddleware: per-path Cache-Control with long s-maxage + stale-while-revalidate, ETag generation, and 304 on If-None-Match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
b7bff7bf6b |
feat(seo): static sitemap generation job via Airflow
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 45s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m5s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m29s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
- Backend builds sitemap.xml from school data at startup (in-memory) - POST /api/admin/regenerate-sitemap refreshes it after data updates - New Airflow DAG (sitemap_generate) runs Sundays 05:00 and calls the endpoint - Next.js proxies /sitemap.xml to the backend; removes the slow dynamic sitemap.ts - docker-compose passes BACKEND_URL + ADMIN_API_KEY to Airflow env Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
6315f366c8 |
fix(build): single-brace JSX for schoolUrl, migrate images.domains to remotePatterns
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 32s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m10s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
53e11aca82 |
Fix: Append :path* to FASTAPI_URL in rewrites
The rewrite destination was using FASTAPI_URL directly, which replaced the entire destination including the :path* parameter. This caused /api/compare to rewrite to just http://backend:80/api instead of http://backend:80/api/compare. Now properly constructs: ${FASTAPI_URL}/:path* Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
ff7f5487e6 |
Complete Next.js migration with SSR and Docker deployment
- Migrate from vanilla JavaScript SPA to Next.js 16 with App Router - Add server-side rendering for all pages (Home, Compare, Rankings) - Create individual school pages with dynamic routing (/school/[urn]) - Implement Chart.js and Leaflet map integrations - Add comprehensive SEO with sitemap, robots.txt, and JSON-LD - Set up Docker multi-service architecture (PostgreSQL, FastAPI, Next.js) - Update CI/CD pipeline to build both backend and frontend images - Fix Dockerfile to include devDependencies for TypeScript compilation - Add Jest testing configuration - Implement performance optimizations (code splitting, caching) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> |