PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m15s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 20s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m21s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 12s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m39s
Both features ship dark. Neither is reachable in an environment where its flag is off, and every flag in this system starts off, so a deploy of this commit makes both disappear until someone turns them on deliberately. Two independent flags rather than one, which makes blog-on-about-off a reachable state. That state is the whole reason the change is larger than four notFound() calls: the blog leans on the About page for its author identity. The Person entity is anchored at /about#tudor, and that URL 404s while about_page is dark, so a post published in that state would claim an author resolving to nothing. Worse than having no named author. Both bylines fall back to unlinked text and the BlogPosting attributes to the publisher instead, so every combination of the two flags renders something correct. Gated: /about, /blog, /blog/[slug], the RSS feed, both footer links, and the matching content-sitemap entries. A sitemap must never advertise a URL that 404s. With both dark it emits a valid empty urlset rather than a 404, because robots.txt names it unconditionally. Not gated: /admin. Posts have to be writable before the blog is worth switching on, so flagging the panel would make the flag unflippable. getFlags takes a revalidate rather than always using the 300s constant. Reading a flag pins the calling route to the lowest revalidate among its fetches, and the footer links live in the root layout, so a naive gate there would have dropped every school and place page from a weekly cache to a 5-minute one. The layout passes 604800, the floor those routes already declare, and the build confirms all four SSG route families still prerender. The cost is one-way latency: pages follow a flip in minutes, footer links within a week. The e2e journeys follow the existing paired shape from the admission_distance flag: a lit journey and a dark one for each flag, reading state from whether /about and /blog respond rather than from /api/flags, which another journey asserts is not publicly reachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXnXQKnPpZBBP61fBQiFkq
138 lines
5.3 KiB
TypeScript
138 lines
5.3 KiB
TypeScript
/**
|
|
* Footer Component
|
|
*
|
|
* The footer is the guideline's "brand in action" band: a deep teal anchor
|
|
* that stays teal in both themes. Everything inside it therefore reads from
|
|
* the --on-sunken-* family rather than the page's --text-* tokens, which
|
|
* invert and would go dark against the teal in one theme or the other.
|
|
*/
|
|
|
|
import { LogoMark } from './Logo';
|
|
import styles from './Footer.module.css';
|
|
|
|
/**
|
|
* Both default to false so a caller that forgets a prop hides the link rather
|
|
* than pointing it at a page that 404s. Same reasoning as backend/flags.py:
|
|
* "Every flag defaults to False."
|
|
*/
|
|
interface FooterProps {
|
|
aboutEnabled?: boolean;
|
|
blogEnabled?: boolean;
|
|
}
|
|
|
|
export function Footer({ aboutEnabled = false, blogEnabled = false }: FooterProps = {}) {
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
return (
|
|
<footer className={styles.footer}>
|
|
<div className={styles.container}>
|
|
<div className={styles.content}>
|
|
<div className={styles.section}>
|
|
{/*
|
|
On the sunken band the mark inverts: the pin takes the band's
|
|
foreground and the leaf is knocked out in the band itself. The
|
|
header's defaults (brand pin, card leaf) would disappear here.
|
|
*/}
|
|
<h3 className={styles.lockup}>
|
|
<span className={styles.lockupMark}>
|
|
<LogoMark variant="onDark" size={34} />
|
|
</span>
|
|
<span className={styles.wordmark}>
|
|
school<span className={styles.wordmarkAccent}>compare</span>
|
|
</span>
|
|
</h3>
|
|
<p className={styles.tagline}>Find the right school. For their future.</p>
|
|
<p className={styles.description}>
|
|
Compare primary and secondary schools across England.
|
|
</p>
|
|
{/*
|
|
Says once, plainly, what the landing page's value props only
|
|
imply: we publish official data, we are not an official body.
|
|
Cheap to state and expensive to be wrong about.
|
|
*/}
|
|
<p className={styles.independence}>
|
|
An independent site. Not affiliated with the Department for Education or Ofsted.
|
|
</p>
|
|
<a
|
|
href="mailto:contact@schoolcompare.co.uk"
|
|
className={styles.link}
|
|
data-umami-event="contact_email_clicked"
|
|
>
|
|
contact@schoolcompare.co.uk
|
|
</a>
|
|
</div>
|
|
|
|
<div className={styles.section}>
|
|
<h4 className={styles.sectionTitle}>Product</h4>
|
|
<ul className={styles.links}>
|
|
<li><a href="/" className={styles.link}>Search schools</a></li>
|
|
<li><a href="/rankings" className={styles.link}>Rankings</a></li>
|
|
{/* "Compare", not "shortlist" — the nav, this link and the
|
|
landing page all name the same feature the same way. */}
|
|
<li><a href="/compare" className={styles.link}>Compare schools</a></li>
|
|
<li><a href="/admissions" className={styles.link}>Admissions guide</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div className={styles.section}>
|
|
<h4 className={styles.sectionTitle}>Resources</h4>
|
|
<ul className={styles.links}>
|
|
<li>
|
|
<a
|
|
href="https://www.gov.uk/school-performance-tables"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={styles.link}
|
|
data-umami-event="external_link_clicked"
|
|
data-umami-event-target="dfe"
|
|
>
|
|
School Performance Tables
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a
|
|
href="https://reports.ofsted.gov.uk/"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={styles.link}
|
|
data-umami-event="external_link_clicked"
|
|
data-umami-event-target="ofsted"
|
|
>
|
|
Ofsted reports
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{/* Dropped entirely when both flags are dark, rather than left as an
|
|
empty heading: shipping dark means the footer renders as it did
|
|
before the feature existed. */}
|
|
{(aboutEnabled || blogEnabled) && (
|
|
<div className={styles.section}>
|
|
<h4 className={styles.sectionTitle}>About</h4>
|
|
<ul className={styles.links}>
|
|
{/* The only route to a named human. Deliberately not in the nav:
|
|
the mobile bottom bar already carries four items, and both of
|
|
these are lower intent than any of them. Post bylines link
|
|
here too, which is where a reader actually asks the question. */}
|
|
{aboutEnabled && (
|
|
<li><a href="/about" className={styles.link}>Who's behind this</a></li>
|
|
)}
|
|
{blogEnabled && (
|
|
<li><a href="/blog" className={styles.link}>Blog</a></li>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className={styles.bottom}>
|
|
<p className={styles.copyright}>
|
|
© {currentYear} schoolcompare.co.uk
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
);
|
|
}
|