feat(flags): put /about and /blog behind flags, dark by default
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
This commit is contained in:
TudorandClaude Opus 5 committed 2026-09-08 17:12:25 +01:00
1 parent eb6d918650
commit 6fc7fce948
15 files changed
+338 -38

No files matched your search

+31 -12
View File
@@ -10,7 +10,17 @@
import { LogoMark } from './Logo';
import styles from './Footer.module.css';
export function Footer() {
/**
* 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 (
@@ -94,17 +104,26 @@ export function Footer() {
</ul>
</div>
<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. */}
<li><a href="/about" className={styles.link}>Who&apos;s behind this</a></li>
<li><a href="/blog" className={styles.link}>Blog</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&apos;s behind this</a></li>
)}
{blogEnabled && (
<li><a href="/blog" className={styles.link}>Blog</a></li>
)}
</ul>
</div>
)}
</div>
<div className={styles.bottom}>