feat(flags): put /about and /blog behind flags, dark by default #144

Merged
tudor merged 1 commits from feat/about-blog-flags into main 2026-09-08 16:47:18 +00:00
Owner

Both features ship dark. Every flag in this system starts off, so deploying this makes /about and /blog disappear until someone turns them on deliberately in Unleash.

Two flags, not one

about_page and blog are independent, which makes blog-on-about-off a reachable state. That state is why this is bigger 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 — a worse credibility signal than having no named author at all. So when About is dark:

  • both bylines render "Tudor" as plain text rather than a link into a 404
  • blogPostingJsonLd attributes to the publisher instead of the Person
  • the Person node is dropped from the post's @graph

Every combination of the two flags now renders something correct.

What's gated

/about, /blog, /blog/[slug], the RSS feed, both footer links, and the matching content-sitemap.xml 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.

The footer's whole "About" section is dropped when both are dark, rather than left as a heading with nothing under it.

Not gated: /admin. Posts have to be writable before the blog is worth switching on; flagging the panel would make the flag unflippable.

The revalidate problem

Reading a flag pins the calling route to the lowest revalidate among its fetches, and the footer links render in the root layout — so a naive gate there would have dropped every school and place page from a 7-day cache to a 5-minute one. That is a large origin-load regression to pay for two footer links.

getFlags therefore takes a revalidate instead of always using the 300s constant, and each call site passes its own route's floor:

Route Passes Effect
root layout 604800 matches what the SEO routes already declare, so nothing regresses
/blog/[slug] 3600 its own declared floor
/about, /blog, RSS, sitemap default 300 no floor of their own to protect

The build confirms all four SSG route families (/school/[slug], /schools/[place], /schools/authority/[la], /schools/near/[outcode]) still prerender as ● (SSG), and /about is now ISR at 5 minutes.

The cost is one-way latency: pages follow a flip within minutes, footer links within a week. Turning a feature on early shows the page before its footer link, which is harmless. Turning one off leaves a footer link to a 404 until the cache turns over, so a rollback that matters wants a purge.

Tests

The e2e journeys follow the paired shape the admission_distance flag already established: a lit journey and a dark one per flag, reading state from whether /about and /blog respond rather than from /api/flags, which another journey asserts is not publicly reachable. The dark journeys assert clean absence — 404, no footer link, no sitemap entry — rather than an empty page.

New Jest coverage for the footer's four link states, the JSON-LD author fallback, and the revalidate parameter. The backend's existing registry and 90-day staleness tests pick up both new flags without changes.

Verification

  • tsc --noEmit clean.
  • 412 Jest tests across 49 suites pass.
  • 11 backend flag tests pass.
  • npx playwright test --list compiles: 117 journeys, both new ones present.
  • next build succeeds with DATABASE_URL unset, as CI builds it, with the SSG route families intact.

Both flags are temporary scaffolding like every flag here: the 90-day tripwire starts failing on 2026-12-07, at which point either the feature is permanent and the flag comes out, or it was never going to ship.

docs/PUBLISHING.md gains a section up front, since a writer publishing to a dark blog would otherwise reasonably conclude that saving is broken.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DXnXQKnPpZBBP61fBQiFkq

Both features ship dark. Every flag in this system starts off, so deploying this makes `/about` and `/blog` disappear until someone turns them on deliberately in Unleash. ## Two flags, not one `about_page` and `blog` are independent, which makes blog-on-about-off a reachable state. That state is why this is bigger 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 — a worse credibility signal than having no named author at all. So when About is dark: - both bylines render "Tudor" as plain text rather than a link into a 404 - `blogPostingJsonLd` attributes to the publisher instead of the Person - the `Person` node is dropped from the post's `@graph` Every combination of the two flags now renders something correct. ## What's gated `/about`, `/blog`, `/blog/[slug]`, the RSS feed, both footer links, and the matching `content-sitemap.xml` 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. The footer's whole "About" section is dropped when both are dark, rather than left as a heading with nothing under it. **Not gated: `/admin`.** Posts have to be writable before the blog is worth switching on; flagging the panel would make the flag unflippable. ## The revalidate problem Reading a flag pins the calling route to the *lowest* revalidate among its fetches, and the footer links render in the root layout — so a naive gate there would have dropped every school and place page from a 7-day cache to a 5-minute one. That is a large origin-load regression to pay for two footer links. `getFlags` therefore takes a revalidate instead of always using the 300s constant, and each call site passes its own route's floor: | Route | Passes | Effect | |---|---|---| | root layout | `604800` | matches what the SEO routes already declare, so nothing regresses | | `/blog/[slug]` | `3600` | its own declared floor | | `/about`, `/blog`, RSS, sitemap | default `300` | no floor of their own to protect | The build confirms all four SSG route families (`/school/[slug]`, `/schools/[place]`, `/schools/authority/[la]`, `/schools/near/[outcode]`) still prerender as `● (SSG)`, and `/about` is now ISR at 5 minutes. The cost is one-way latency: pages follow a flip within minutes, footer links within a week. Turning a feature on early shows the page before its footer link, which is harmless. Turning one off leaves a footer link to a 404 until the cache turns over, so a rollback that matters wants a purge. ## Tests The e2e journeys follow the paired shape the `admission_distance` flag already established: a lit journey and a dark one per flag, reading state from whether `/about` and `/blog` respond rather than from `/api/flags`, which another journey asserts is not publicly reachable. The dark journeys assert clean absence — 404, no footer link, no sitemap entry — rather than an empty page. New Jest coverage for the footer's four link states, the JSON-LD author fallback, and the revalidate parameter. The backend's existing registry and 90-day staleness tests pick up both new flags without changes. ## Verification - `tsc --noEmit` clean. - 412 Jest tests across 49 suites pass. - 11 backend flag tests pass. - `npx playwright test --list` compiles: 117 journeys, both new ones present. - `next build` succeeds with `DATABASE_URL` unset, as CI builds it, with the SSG route families intact. Both flags are temporary scaffolding like every flag here: the 90-day tripwire starts failing on 2026-12-07, at which point either the feature is permanent and the flag comes out, or it was never going to ship. `docs/PUBLISHING.md` gains a section up front, since a writer publishing to a dark blog would otherwise reasonably conclude that saving is broken. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DXnXQKnPpZBBP61fBQiFkq
tudor added 1 commit 2026-09-08 16:12:53 +00:00
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
6fc7fce948
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

🤖 AI Code Review (Claude Code)

This PR gates the new /about and /blog features behind two Unleash flags (about_page, blog), making both pages, the RSS feed, sitemap entries, footer links, and JSON-LD author attribution independently conditional and defaulting to dark/off. It's a clean, well-tested change: the ISR revalidate floors passed to getFlags() at each call site match each route's actual declared revalidate (verified against about/page.tsx, blog/[slug]/page.tsx's revalidate=3600, and layout.tsx's 604800), the sole call site of blogPostingJsonLd was updated for its new required namedAuthor param, and the /api/flags endpoint is generic over the registry so the two new flags need no extra backend wiring.

✅ No issues found.

## 🤖 AI Code Review (Claude Code) This PR gates the new /about and /blog features behind two Unleash flags (about_page, blog), making both pages, the RSS feed, sitemap entries, footer links, and JSON-LD author attribution independently conditional and defaulting to dark/off. It's a clean, well-tested change: the ISR revalidate floors passed to getFlags() at each call site match each route's actual declared revalidate (verified against about/page.tsx, blog/[slug]/page.tsx's revalidate=3600, and layout.tsx's 604800), the sole call site of blogPostingJsonLd was updated for its new required namedAuthor param, and the /api/flags endpoint is generic over the registry so the two new flags need no extra backend wiring. ✅ No issues found.
tudor merged commit 47f3591ed8 into main 2026-09-08 16:47:18 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tudor/school_compare#144