Boilerplate
Default Next.js app. Zero changes. Deployed to production.
The first thing I did was deploy. Not plan. Not design. Deploy.
Every version shipped to production. Every one.
Default Next.js app. Zero changes. Deployed to production.
The first thing I did was deploy. Not plan. Not design. Deploy.
Branded landing page with the mission and a preview of what's coming. Umami analytics wired in, tracking every visitor from this moment forward. robots.txt, sitemap.xml, Open Graph image, and app icons ship with it.
Before the first feature, I had analytics. SEO ships with every deploy, not after.
flagplays.org is live on Vercel with SSL. Middleware marks every non-canonical host noindex, so only the real domain gets indexed while every tagged version stays publicly reachable at its own URL.
This is when we stopped being a Vercel subdomain and became flagplays.org. That's not a code change — it's a commitment.
Web app manifest, a service worker caching the app shell for offline use, and an install button wired to Umami. The nav shell moved into the root layout so every future page inherits it.
Four deploys, still day one, still no features — but a coach can install it on the sideline and open it with no signal.
The nav was rendering the navy app tile on the navy bar. Derived a true white-on-transparent mark from the source art and put that in the nav instead. Brand source files moved into brand/.
A ten-minute fix still gets a tag and still goes to production. That's the whole discipline.
The icon carried a white fringe from the source art — invisible on a light tab bar, obvious on a dark one. Icons are now generated by a committed script that masks the corners and trims the fringe, with transparent corners for tabs and opaque ones for home screens.
Nobody files a bug about a favicon. You just look unfinished until you fix it.
Nine flag football plays for 5v5, 6v6 and 7v7, each with its own page, diagram, and breakdown of who does what. Plus the journey timeline itself, rendered from journey.json.
Now there's a product. Every play has its own URL, its own structured data, and its own Open Graph image.
The v0.2 service worker cached build chunks cache-first, which is safe in production where they're content-hashed but pinned stale JavaScript against the dev server. It now stays off locally and tears down any worker an earlier build left behind.
Offline caching that works in production can still be wrong on localhost. Ship the fix the same day you find it.
A full guide at /sports/flag-football — 11 rules as FAQ structured data, six positions with coaching tips, six formations illustrated by real plays, age group breakdowns, equipment, and an 18-term glossary that links into the plays library.
This is the page that ranks. Parents searching 'flag football rules for kids' land here, and every glossary term is a door into the plays.
A Buy Me a Coffee button in the footer with a scannable QR code, plus a quiet line on each play page. The footer moved into the root layout, so every page now carries it along with the Code43 attribution.
Ten deploys before we asked for anything. A plays library and a full rules guide first — you earn the ask.
The per-version deployment links are hidden behind a single flag while those Vercel deployments sit behind Deployment Protection. Every previewUrl stays recorded in journey.json — only the rendering is switched off.
A link that lands on a login wall is worse than no link. Hide it behind a flag, keep the data, flip it back when the wall comes down.
Create playbooks, add plays from any play page, reorder them and remove them — all saved in the browser with no account. Built on a lib/storage.ts abstraction whose methods are async on purpose, so the Firebase migration later replaces one file.
The first version with real user data, and still no login, no database, no backend. Not needing an account is a feature.
The first server action. A playbook exports to a printable PDF with a cover sheet and one page per play, drawn as vectors from the same scene builder that renders the screen — so the printout matches the app and stays sharp at any size.
The fourth renderer over the same play data — canvas, SVG, Open Graph, and now print. Build the data right once and every new output is a small change.
Six defensive plays join the library — man coverage, Cover 2, Cover 3, Cover 4, a double rush and man-free. Plays now carry a side, defenders draw as red triangles against a faded offensive alignment, and the library filters by side as well as team size.
A whole side of the ball, and the play editor hasn't been built yet. The data model took a side field and all four renderers followed.
On a defensive play the faded offense still read as full-strength players, so the two units blurred together. Defenders are now brighter and larger, the reference unit sits further back, and a legend under the diagram names which shape is which.
Shipping a feature isn't the same as shipping a readable one. Someone looked at it and couldn't tell the sides apart — that's a bug.
Create your own plays. Pick a side and a team size, drag players wherever you want them, rename positions, and save to your browser. Saved plays list underneath and reopen for editing.
The editor draws through the same scene builder as everything else, so what you build is exactly what you'll see everywhere else.
A play you draw can now be added to a playbook, shows up there alongside library plays, and prints in the PDF. Playbooks resolve ids from both sources, and the export action validates every play sent from the browser before drawing it.
Shipping the editor without this made a play you could create but couldn't use. The gap was obvious the moment someone tried it.
Switch to route mode, tap a player, then tap the field to plot where they go. Pick the assignment type in the language of the side running it, undo a point, or clear a route. Moving a player carries the start of their route with them.
A tap on the field could mean move or route. Rather than guess at intent, the mode is explicit — guessing wrong is worse than one extra button.
Opening a play you drew from a playbook returned a 404: /play/[id] is generated from the static library, so a custom id had nowhere to land. Custom plays now share the same URL space and render the same page, resolved in the browser.
Two storage locations behind one URL space. The fix was to share the page body, not to invent a second kind of play page.
Copy any of the 15 library plays into the editor as the basis for your own — from the play page or from a picker in the editor. The copy is deep, so editing it never touches the original.
Most coaches don't want a blank field. They want a play that works with one thing changed.
The homepage still promised a donate button that shipped in v1.2, advertised drills that don't exist, and headed a mostly-live feature list with "What's coming". Copy now describes what the site actually is, and the live count is computed from the list.
Ship fast enough and your own homepage becomes the last place telling the truth. Copy is code — it goes stale the same way.
Twenty-one deploys of SEO shipped feature by feature, never checked as a whole. An audit across all 19 indexable URLs found the sport guide had silently lost its Open Graph image and the site description had grown past the length Google shows. Both fixed.
Shipping SEO with every version isn't the same as knowing it still works. Nineteen pages, two real problems, neither visible from any single deploy.
Press play and the players run their routes. Pause, reset, and 0.5x/1x/2x speed. Players interpolate by arc length so a long leg takes longer than a short cut, and the routes stay drawn so you can see where the rest of the play is going.
v1.0 chose Canvas over SVG for a reason it didn't need yet. Three versions later the static play became the first frame — no rewrite.
Every route now runs on its own clock — a blocker is done in a fifth of the play, a back waits for the handoff. And the ball is on the field: snapped, handed off, or thrown to where the receiver will be rather than where they are.
A thrown ball leads its receiver. That's hard to explain standing still and obvious once you watch it — which is the whole reason to animate a play for kids.
A play you drew could be viewed and run but never edited — the editor was only reachable from its own page. Play pages and playbook rows now link straight into editing, and library plays link into a copy.
Every screen that shows a play should answer 'and how do I change it?'. Three screens showed plays; one answered.
From any play, tap a player to drop the diagram to the quarterback, the centre and just them, with the throw to that player running in isolation and a sentence explaining what the timing means. It's a filter over the real play, so it can never disagree with the full version.
Four routes at once is unreadable when you're eight. One route, with the ball leaving before the cut, teaches the thing a static diagram never could.
The ball always went to whoever the play was written for. Now you pick, and the throw is retimed for that target — a deep ball leaves earlier and leads further than a slant, which is the lesson. Isolating a receiver aims it at them automatically.
One play became four. Same routes, different read — which is how a quarterback actually sees it.
The editor now lets you say who the play is designed to get the ball, and writes that into the play. Leave it unset and the play keeps deciding for itself, which stays right while you're still drawing routes.
A play isn't just where everyone runs. It's who it's for — and until now only the built-in plays could say so.
Five nav links had been crammed into a phone-width header since v1.0. They now collapse behind a menu button below 768px and stack full-width with 48px touch targets. The desktop row is untouched.
Every deploy went to production, and every one of them shipped a header I'd only ever looked at on a laptop.
The app has been installable since v0.2, but the only button that said so needed an event Safari never fires — so no iPhone was ever told. A dismissable bar now gives iOS the Share-sheet directions and everyone else a one-tap install.
Shipping a feature and shipping a feature people can find are two different deploys. This one took eighteen versions to notice.
Accounts arrive, and do nothing yet. Signing in creates or links an account keyed on the verified email, so a coach who signs in with Google today and an emailed link later lands on the same account. Everything on the site still works signed out.
Thirty deploys in and this is the first one that needed a backend. It ships doing nothing except letting you sign in — because the next version needs somewhere to put data.
Email a link, click it, you're in. It lands on the same account as Google if the address matches, because accounts are keyed on the verified email. Tokens are hashed, single use, expire in 30 minutes, and one address can only be mailed once a minute.
Planned last, built second — because it depended on nothing. The account model from the version before did the work, and the emailed link found the account Google had already made.
Animated plays still said "Coming soon" four versions after they shipped. Now marked live and linked to a play you can actually run, with the ball and the one-player view mentioned.
Second time the homepage has fallen behind the product. The live count updates itself; the card statuses still don't.
New signups post to Discord, and there's a feedback form at /feedback that resists bots without a captcha — a honeypot, a signed timestamp that makes instant submissions impossible to fake, and a per-sender rate limit.
A form that tells a bot why it was rejected is a form that teaches bots to get past it. Ours thanks them and throws it away.
Signed in, playbooks live in your account and appear on any device you use. Signed out, nothing changed. The storage interface has been async since v2.0 for exactly this, so the swap touched no component logic — only which module they import.
The bet from v2.0 paid. Every call site was already awaiting something; none of them had to learn where the data now lives.
Plays you draw now live in your account when signed in, the same way playbooks do. They keep their ids on import, because a playbook already refers to them by id — renaming them would empty every playbook pointing at one.
This had to land before sharing. A playbook sent to a parent can only carry plays the server can see, and until now half of them lived in one browser.
The import offer kept reappearing after a successful import, because local copies are deliberately never deleted — so "there is local data" is not the same question as "is anything left to move". It now remembers what it already moved.
Two correct decisions — keep the local copy, offer whenever there's local data — combined into a prompt that nagged forever.
Each imported playbook now records the local id it came from, so the offer asks the server what is left rather than trusting a note in the browser — and pressing Import twice, or in two tabs, cannot produce two copies.
The first fix trusted the client to remember. The right one made the server the only party that could be wrong about it.
Importing drawn plays returned a 500. Firestore rejects an explicit undefined, and a play with no ball had one — an optional field that TypeScript is perfectly happy with. Fixed at the source and guarded so no other optional field can do it again.
The types said the field was optional. The database disagreed about what optional means, and only the plays without a ball found out.
A coach can publish a playbook to a link, send it by email or show a QR code, and turn it off again. Parents open it with no account and nothing installed — every play travels in the link, animations and printable PDF included, and nothing on the page can be changed.
The first thing this site has ever made for somebody other than the coach. A parent who never signs in, never edits, and just wants to see what their kid is being taught.
At rest the ball drew at exactly its holder's position, covering that player's label — most obviously the centre before the snap, in every static diagram including the ones parents now see. It sits beside them instead.
Correct behaviour and correct rendering are different questions. The ball really is at the centre's feet; drawing it there hid the centre.
The ball was drawn wider than tall, lying across the field. Rotated so its long axis points the way the play is going, with the shape now defined once in the scene instead of the same ratio hardcoded in three renderers.
Three renderers each knew the ball was 1.35 times wider than tall. Now one does, and the other three ask.
Opening a play from a playbook offered to send you back to the full play library instead — somewhere you had never been. A play can be reached from five places, and the breadcrumb now reflects which one, with the origin whitelisted so it can't become a link off the site.
A back button that goes somewhere you have never been is worse than no back button. It was wrong from every route except the one it was written for.
The add-to-playbook panel promised that signing in made playbooks follow you to any device. It doesn't — signing in switches you to your account's playbooks, and anything made in the browser stays there until you move it across. The copy now says which.
The site claimed a migration it never performs. There is a whole component whose only job is asking whether to perform it.
Sharing is a once-a-season job that was taking up permanent residence above the plays, so it became an accordion that still says whether a link is live. On a 414px phone the play rows refused to wrap — the controls held their width and squeezed play names into a column of single words.
The bug was invisible on a laptop and obvious on a phone. Someone had to actually look at it on a phone.
The roadmap had 'describe a play and Claude draws it' penciled in since day one. Building the editor first is what killed it — drawing plays turned out to be the fun part of the product, and automating away the fun is a bad trade. The roster takes its place: who's on the team, and what each kid does well.
The play model is better as AI input than AI output. I found that out by using the thing I built.
A roster: who's on the team, and what each kid does well right now. Strengths are tags rather than scores — no numeric scale, nothing sortable, because a sorted roster is a ranked list of children. Local when signed out, in your account when signed in, and never attached to a shared playbook.
The feature that replaced AI play generation. "Who's quick" is a set, not an order — and a set is everything play design needs.
Ten routes on the learn page — go, slant, out, in, curl, post, corner, comeback, drag and wheel. Each one has how to run it, the sentence to say at practice, what it beats, and a diagram you can press play on. The demonstration is a real play object, so every renderer already knew how to draw it.
The route tree cost a data file and a play button. Everything that draws it was written months ago for something else.
Put the roster on a play. The diagram wears real shirt numbers, and 'Suggest who goes where' fills the form from what you noted about each kid — the quarterback slot goes to someone who can throw, the deep route to someone quick. Every suggestion is a suggestion; nothing applies itself.
The roster finally does something. And the play it decorates never learns a child's name — that is the whole reason assignments live on the team.
Re-cut the homepage around the sequence a volunteer coach moves through — learn the game, find plays, draw your own, build the playbook, put your team on it, show the kids and the parents. Reading it as a sequence caught the Team builder card still saying 'Coming soon' three versions after v6.0 shipped it.
The homepage was a list of what I built. Nobody wakes up wanting an editor — they want eleven kids who know where to run.
The feedback form now asks one optional question: here is something a coach has to do — how much does it matter, and how well does this handle it today. One outcome sampled per page load out of ten, so twenty visitors answer one question each instead of one visitor answering twenty.
I had six versions of opinions about what to build next and zero numbers. This is the cheapest instrument that turns one into the other.
The app reads a play against your roster: what it asks for, and who never gets the ball across a whole playbook. Nothing is hand-tagged — every check falls out of geometry the play already carried.
Six versions of data model finally paid out. I authored zero content to get here — the app knew what a route demanded because it already knew where the route went.
Strengths can be ticked while adding a kid instead of after, the form stays open for the next one, and the newly added card is marked so a coach can find where the sort put them. Removing a player now asks in the app's own dialog rather than a browser alert.
Nobody reported this as a bug. It was just annoying — and a feature people find annoying is a feature people stop using.
Every destructive action — deleting a team, a playbook, a custom play, removing a player, turning off a share link — now asks in the app's own dialog. Built on native <dialog>, so focus trapping and Escape come from the platform.
Five window.confirm calls, five different chances for the app to stop looking like an app. The component that replaced them is forty lines.
The footer and the journey page both name the talk this project is built for. Now the title is a link to the Commit Your Code session page, from a single shared constant.
The site has said the name of the talk since v0.1 and never once pointed at it. A deploy is allowed to be one link.
Changing team size adds or removes a player instead of wiping the field, so a 5v5 play copied into a 6v6 league keeps every route. Preview runs the play in place, and saving offers to file it in the playbook you were last working in.
Three things that were never bugs. Every one of them made a coach leave the screen they were working on, and leaving is where people stop.
Every playbook picker now asks the hook for one sport's playbooks, so a soccer play can't be filed into a football playbook. The sport the UI exposes has one name instead of a literal in every component that creates a record.
Soccer isn't built yet, and that is exactly when this was cheap to fix. A week from now it's a data migration.
Record a game and mark which plays you called and how they went — from the playbook while it's happening, or from the recap screen afterwards. The playbook then says which plays have worked and which have never been called at all.
The job map had eight steps and nothing served step six. Every competitor builds the two steps you can see; this is the one where the coach was on their own.
"Review and finish" now leads to a finish button. Marking a game done puts the playbook back to normal instead of leaving it in sideline mode until midnight, a second game the same day is a tournament rather than a mistake, and the games list says which one is still in progress.
I shipped a game with a start and no end, and the tester found it in about four minutes. Every version does this. The point is that the next deploy is four minutes away too.
A playbook with plays in it had no way to add another one — the only route was to leave, find a play, and remember that "add to playbook" lives on its page. There is now a picker on the playbook itself, listing your own plays first, and a link to draw a new one.
The empty playbook told you where to go. The full one said nothing, because the version that filled it was written by someone who already knew the way.
A playbook can name the team it's for, and the link is walkable both ways — the team card and the team page list the playbooks, and the playbook links back to the roster. The fit read follows the link instead of a separate browser setting.
The roster and the plays it runs were two screens with no door between them, and a coach walks between them all season.
A play you drew could be edited but not copied, so a second version of “Bunch Right Go” with one route changed meant drawing the whole thing again on an empty field. Duplicate is a button on every play you own, and the editor says out loud when what's on screen is a copy.
Copying a play shipped in v3.3 — for the library's plays. The coach's own plays, the ones actually worth a second version, never got it, because the version that built copying was looking at the library.
Every play has a mirror image, and drawing it by hand meant placing every player and every waypoint again in reverse. Flip is a button in the editor and on every play you own, and it renames as it goes — “Bunch Right Go” comes back as “Bunch Left Go”, ready to save as its own play.
One line of arithmetic doubles the size of every playbook on the site. The version that took three days to build was the one that decided a play is data, not a picture.
The first juice deploy is the least glamorous one. Duration and easing tokens are defined once, a global reduced-motion rule is in place before the first animated pixel, and loading states are shape-matched skeletons that reserve the space the content is about to land in. Opening a play you drew used to grow the page by two thousand pixels; the diagram's box is now held from the first frame.
Juice starts with subtraction. Before anything is allowed to move on purpose, everything has to stop moving by accident.
The playbook skeleton was a grid of tall cards standing in for a list of short rows, and one shared row component was serving two lists that end differently — a text button on one, a 44px icon button on the other. Every box is now measured off the markup it replaces: the thumbnail carries the ratio its SVG is generated at, and the roster reserves the strength chips, which are what actually make a player card tall.
A placeholder that is roughly the right shape still moves the page when it lands. Roughly right and wrong are the same thing here.
Hovering a play card now runs its routes, which turns a thumbnail of a formation into a preview of the play — browsing fifteen plays used to mean opening fifteen pages to find out what any of them did. The landing page gets a hero play that loops: it draws the routes on, holds, runs the play, and throws to a different receiver each time round.
The cards do it in CSS with no JavaScript at all; the hero does it on the same Canvas engine the play page uses. Two mechanisms, because one of them had to move a ball to real timing and the other had to cost nothing.
The overview page has said since v1.1 that contact blocking is a penalty and the rule new coaches get wrong most often — and then the library shipped six plays with blocking assignments in them, the roster offered a "Blocks well" tag, and the editor let you draw one. All three are gone. A player with no route is now told they have no route, rather than that they stay in to protect.
The site was teaching the rule correctly on one page and breaking it on another. Content and data disagreeing is not a copy problem — it is the product being wrong twice.
At the first session a coach asks every kid what they want out of the season — play quarterback, get an interception, score once. The roster now holds that answer in the kid's own words, with a tick for when they have had their shot at it and a quiet count of how many still haven't. Free text rather than a picker, because "not be scared of the big kids" is the answer a picker loses.
A strength is the coach's read on a child. A goal is the child's own words about themselves, so it travels even less far — and it is tracked as 'had their shot', never 'achieved', because only one of those is the half a coach controls.
One goal per kid was wrong the moment you asked a real nine-year-old, who names three things and means all of them. Each roster card now has a collapsed Goals section holding several, each ticked off on its own, and the roster counts things rather than kids: "5 of 12 things they asked for have happened, and 3 kids haven't had a go at anything yet."
The single-goal version shipped an hour earlier and was already carrying data, so the list reads the old shape and folds it in. A migration nobody had to run is still a migration.
Twelve plays added to the library, two in every bucket — a coach who filters to 5v5 defense now sees four options instead of two. Curl-flat, jet sweep, wheel, smash, levels and a draw on offense; three deep, a prevent look, Cover 2 man, an edge blitz, two-deep-four-under and a bracket on defense.
The plays were proofread the way a coach reads one — by looking at it. The OG image endpoint built for sharing turned out to be the review tool: four of the twelve had a route running through a player's marker, and only the picture said so.
The play diagram in a playbook is a link now, not decoration — the name was the only way in, and only on hover. Reordering swaps a pair of arrow buttons for a drag handle built on Motion: the held row lifts and the rest slide around it.
A coach said the arrows felt like a spreadsheet. Removing them meant the replacement had to work with a finger on a tablet and with a keyboard for anyone without one — the arrows were carrying both jobs quietly.
One toast system replaces four kinds of homemade feedback — an inline red paragraph, a word that swapped itself for 'Copied', and two deletes that said nothing at all. Buttons give under a tap, the export button counts while it works, and the first exported playbook gets eight dots of confetti.
The delete that said nothing was the bug. A row disappearing looks identical whether it worked or the app dropped it, and the coach who can't tell will check twice every time.
A play card no longer vanishes so a play page can appear — the thumbnail travels across the navigation and grows into the diagram, blurring slightly mid-flight. Pages slide left going deeper and right coming back, and the header holds still while they do.
The biggest visual change in the app cost zero kilobytes. React's ViewTransition ships inside the React that Next vendors, so this is browser machinery with a name attached — the animation library that landed a version later cannot do cross-route morphing at any price.
Filtering the play library moves the cards to their new places instead of cutting to them, and the active filter is one pill that travels between buttons rather than a colour switching on and off. Playbook and team rows arrive and leave. The nav marks the section you are in with an underline that slides.
Two bugs only a screenshot could find: the travelling pill was painting behind the page because a negative z-index escapes its button's stacking context, and the label it sits under turned white the instant it was clicked — white text on a white page for the length of the animation.
Open a play and the routes draw themselves on. Point at a player on the field and their row in 'Who does what' lights up; point at the row and the rest of the field drops back so only that assignment is left. It works with a mouse, a finger and the tab key.
The hit test was written against the canvas backing store instead of its box — correct on a 1× display and wrong by exactly the pixel ratio on every retina screen. It passed its first test because the test browser was 1×.
Seventy-five deploys is a long page. Each version now arrives as you reach it, and the line down the left fills as you read — the page moves like the thing it is describing, one deploy after another, in order.
A reveal-on-scroll hides content by default, which is the one thing the juice phase promised never to do. It took three separate guarantees to keep the promise: server HTML, no hiding style until hydration, and a print rule loud enough to beat an inline style.
A pass now leaves the quarterback's hand, leads the receiver to where they are about to be, arrives over the shoulder it was thrown from, and keeps the angle it was thrown at while it is carried. It used to fly to the middle of the receiver and hop sideways on the frame it landed.
Four rounds of 'closer, but still wonky', and every round was a different thing: the hop, the side, the swing, the spin. The one that took longest to see was a heading normalised in field units — right on paper and twenty degrees off on a screen, because a diagram framed to its play is not square.
The ball and watch pickers share one row, and become dropdowns on a phone. Every select in the app got its own arrow. Moving players and drawing routes stopped being two modes — drag to place, tap to draw — and the editor learned to undo.
Nobody asks for this version. It has no feature you can name. It is the one where the thing you use every day stops fighting you — and it took four deploys' worth of work to make the app feel like nothing happened.
The editor draws like a finger does. Select a player and drag on the field to sketch their route as a smooth curve; a quick tap still drops the next waypoint. Each stroke simplifies to a handful of waypoints fit with bezier curves, so the arc stays a curve everywhere — the editor, the animation, the thumbnails, the open-graph images and the printed playbook all draw it curved, and flipping a play mirrors it.
Fingers don't draw with corners. An arc is one gesture to sketch and one bezier to store — and it stays a curve in every renderer at once.
The editor was grabbing at a player whenever a stroke started anywhere close to them. Now only the marker itself moves a player — open grass next to them starts a route, which is what a coach is usually reaching for.
Proximity decided the gesture. Now the player is the target, and everything around them is the line.
In the editor preview, the saved 'ball goes to' control sat in a section below the whole play, stranded under the run controls. It now rides inside the animator's picker band next to 'watch one player' — the same shared row a play detail page gives them — stacking on a phone and sitting side by side on a screen.
Two controls that aim the same diagram should sit next to each other, not at opposite ends of the page. One band, two ways to aim — the preview finally reads like the play page.
In the editor preview, 'ball goes to' now matches the animator's own ball picker on a phone — a dropdown that can hand the ball back to 'let the play decide' — and grows back into the chip row when there is width to spare.
The same control, one shape on a phone and another on a screen. Narrow is a dropdown because an armful of chips is a chore to tap; wide is a row again because that is where the field is.
The sitemap stopped stamping every page with the build time, so a deploy no longer claims all thirty-three changed. The editor is indexable — it was hidden as user-generated content, but the drawings live elsewhere and it is the page that answers "play designer". A dead share link answers 404 instead of 200.
The sitemap said every page changed every deploy. A date you can't trust is worse than no date at all — so it says nothing now, and means it.