From 611bf7f15366b8a48e8aa032feaa982ce40b9f03 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Fri, 21 Aug 2026 16:29:13 -0400 Subject: [PATCH] refactor(player-profile)!: a player's page is at /profile The prefix, the shell, the module and its tests are all named for what the page is rather than for a plural noun, and the api host's unfurl route moves with them. infra's viewer function has to be applied before this build is served. Co-Authored-By: Claude Opus 5 (1M context) Change-Id: I2610de1924fd9161d85e3864793181dddca2b9ac --- plan/player-profile.md | 14 +++--- services/api/src/atproto/mod.rs | 2 +- services/api/src/routes.rs | 6 +-- services/api/src/unfurl.rs | 24 +++++----- web/astro.config.mjs | 14 +++--- ...players.test.mjs => profile-page.test.mjs} | 48 +++++++++---------- web/src/account.ts | 4 +- web/src/challenge-stash.ts | 2 +- web/src/destinations.ts | 18 +++---- web/src/identity.ts | 2 +- .../pages/{players => profile}/index.astro | 8 ++-- web/src/{players.ts => profile-page.ts} | 16 +++---- 12 files changed, 79 insertions(+), 79 deletions(-) rename web/scripts/{players.test.mjs => profile-page.test.mjs} (86%) rename web/src/pages/{players => profile}/index.astro (78%) rename web/src/{players.ts => profile-page.ts} (97%) diff --git a/plan/player-profile.md b/plan/player-profile.md index 608ddd4..6b78b63 100644 --- a/plan/player-profile.md +++ b/plan/player-profile.md @@ -11,7 +11,7 @@ exitCriterion: > # player-profile -There is a page now, at `/players/` and `/players/`, and it is +There is a page now, at `/profile/` and `/profile/`, and it is the same page for anybody: your own account, an opponent from a lineup, or a handle somebody pasted into a post. What is on it is what can be read without asking us — the identity, and the camo in the player's own repository — plus @@ -117,13 +117,13 @@ now, mounted beside the account's and waiting on nothing. the page waits for it and a failure shows nothing at all. Both directions are read; only "you follow them" is drawn today. -- [x] **A page at a real address**, `/players/`, rather than a +- [x] **A page at a real address**, `/profile/`, rather than a hash route. A profile is what people link to about each other, so it has to be a path a reader can copy out of the bar — and, more than that, an address the distribution can route on its own, since that is what lets a card fetcher be answered per player without the page running. - [x] **One shell, not a page per player.** Astro has no list of players to - build from, so `web/src/pages/players/index.astro` is built once and a + build from, so `web/src/pages/profile/index.astro` is built once and a viewer-request rewrite serves it for every address under the prefix. `astro.config.mjs` carries the same rewrite for the dev server, which is otherwise the one place a player's page 404s. @@ -144,8 +144,8 @@ now, mounted beside the account's and waiting on nothing. will be earned rather than rated, at which point it goes in the title and on the card together. - [x] **The address is the handle, with its @ and nothing escaped.** - `/players/@alice.example` is an address somebody can read out; - `/players/did%3Aplc%3Ah7k...` is one nobody can. Both forms answer, and + `/profile/@alice.example` is an address somebody can read out; + `/profile/did%3Aplc%3Ah7k...` is one nobody can. Both forms answer, and the handle's is what the site writes, what the canonical link names and what a card advertises as its own. The DID form covers the case the handle cannot - an account with no handle anybody can confirm - and a @@ -162,7 +162,7 @@ now, mounted beside the account's and waiting on nothing. running out of it, the picture under corner ticks, a standing read as instruments and counts read as a ruled row - and they are not the same object: the card is one fixed 1200x630 drawing and the page reflows, - links onwards and has a match history coming. `players.test.mjs` holds + links onwards and has a match history coming. `profile-page.test.mjs` holds the two to the same figures in the same two kinds, since nothing else links a TypeScript screen to a Rust drawing. - [x] **A figure is one of two kinds, and is drawn as what it is.** A rating @@ -176,7 +176,7 @@ now, mounted beside the account's and waiting on nothing. how a person does. What it was taking is the bio's room. - [x] **A shared link previews as the player, not as the site.** An unfurler runs no JavaScript, so one shell would preview as one generic card for - everybody. `services/api/src/unfurl.rs` answers `/unfurl/players/<...>` + everybody. `services/api/src/unfurl.rs` answers `/unfurl/profile/<...>` with the tags and a card drawn for whoever the address names, and the distribution sends card fetchers and crawlers there while everybody else is served the page from the bucket. The card is type on the site's diff --git a/services/api/src/atproto/mod.rs b/services/api/src/atproto/mod.rs index c76b98f..650272b 100644 --- a/services/api/src/atproto/mod.rs +++ b/services/api/src/atproto/mod.rs @@ -340,7 +340,7 @@ impl Atproto { } } - /// Who a `/players/<...>` address names, resolved from either form. + /// Who a `/profile/<...>` address names, resolved from either form. /// /// The web app does exactly this in `web/src/identity.ts`, and it has to /// be done here as well rather than shared: the page is drawn in the diff --git a/services/api/src/routes.rs b/services/api/src/routes.rs index 5e88bbd..ebfd504 100644 --- a/services/api/src/routes.rs +++ b/services/api/src/routes.rs @@ -123,12 +123,12 @@ pub fn app(state: AppState) -> Router { .route("/reports/{id}/card.png", get(crate::share::card)) .route("/reports/fonts/{name}", get(crate::share::font)) // What a link to a player unfurls as. A second prefix rather than - // /players/, because that address belongs to the site and is served + // /profile/, because that address belongs to the site and is served // from the bucket: the distribution sends a card fetcher here and // everybody else there. See unfurl.rs. - .route("/unfurl/players/{actor}", get(crate::unfurl::player)) + .route("/unfurl/profile/{actor}", get(crate::unfurl::player)) .route( - "/unfurl/players/{actor}/card.png", + "/unfurl/profile/{actor}/card.png", get(crate::unfurl::player_card), ) .route("/match/{id}", get(crate::proxy::entry)) diff --git a/services/api/src/unfurl.rs b/services/api/src/unfurl.rs index a3d2b00..0dee90c 100644 --- a/services/api/src/unfurl.rs +++ b/services/api/src/unfurl.rs @@ -1,6 +1,6 @@ //! What a link to a player unfurls as. //! -//! `/players/` is a page the site builds once and the browser +//! `/profile/` is a page the site builds once and the browser //! fills in, which is right for a reader and useless to a card service: an //! unfurler runs no JavaScript, so one shell would preview as the same //! generic card for every player on the site. @@ -44,7 +44,7 @@ struct Player { /// card prints and what the tags say. name: String, /// The same handle without its @, or None. What the address is written - /// with: `/players/@alice.example` carries one @ and not two. + /// with: `/profile/@alice.example` carries one @ and not two. handle: Option, did: String, /// The account's own picture, as it uploaded it. None where it has none. @@ -134,12 +134,12 @@ impl Player { /// The address of the page this is a preview of. fn here(&self, origin: &str) -> String { - format!("{origin}/players/{}", self.address()) + format!("{origin}/profile/{}", self.address()) } /// The picture's own address. /// - /// Under this prefix and not under the page's: `/players/...` is the + /// Under this prefix and not under the page's: `/profile/...` is the /// site's, and the distribution answers all of it from the bucket - a /// card asked for there would come back as the shell's HTML. /// @@ -148,7 +148,7 @@ impl Player { /// than on the name it currently goes by means a rename does not strand /// the card a post is already carrying. fn card(&self, origin: &str) -> String { - format!("{origin}/unfurl/players/{}/card.png", self.did) + format!("{origin}/unfurl/profile/{}/card.png", self.did) } } @@ -308,10 +308,10 @@ mod tests { fn the_page_names_the_site_and_the_card_names_this_service() { let player = player(); let origin = "https://lance.blue"; - assert_eq!(player.here(origin), "https://lance.blue/players/a.example"); + assert_eq!(player.here(origin), "https://lance.blue/profile/a.example"); assert_eq!( player.card(origin), - "https://lance.blue/unfurl/players/did:plc:abc123/card.png" + "https://lance.blue/unfurl/profile/did:plc:abc123/card.png" ); } @@ -323,16 +323,16 @@ mod tests { let html = document(&player, &player.here(origin), &player.card(origin)); assert!(html.contains("")); assert!(html.contains( - "" + "" )); assert!(html.contains( - "" + "" )); assert!(html.contains("")); // The canonical address a crawler routed here should index is the // page's, never this document's own. assert!( - html.contains("") + html.contains("") ); } @@ -344,7 +344,7 @@ mod tests { let player = player(); let html = document( &player, - "https://lance.blue/players/a.example", + "https://lance.blue/profile/a.example", "https://x/c.png", ); assert!(html.contains(&format!( @@ -369,7 +369,7 @@ mod tests { }; let html = document( &hostile, - "https://lance.blue/players/x", + "https://lance.blue/profile/x", "https://lance.blue/c.png", ); assert!(!html.contains(" diff --git a/web/src/players.ts b/web/src/profile-page.ts similarity index 97% rename from web/src/players.ts rename to web/src/profile-page.ts index aa3ba28..258ead8 100644 --- a/web/src/players.ts +++ b/web/src/profile-page.ts @@ -1,5 +1,5 @@ /** - * A player's page: /players/ and /players/, the same page. + * A player's page: /profile/ and /profile/, the same page. * * It is a page rather than a hash route on purpose. A profile is the thing * people link to about each other, and an address a reader can copy out of @@ -24,7 +24,7 @@ import { currentSession, listMatches, type Session } from "./api"; import { stashChallenge } from "./challenge-stash"; import { footer, pageHead } from "./chrome"; import { el, render } from "./dom"; -import { actorFromPath, playerHref } from "./destinations"; +import { actorFromPath, profileHref } from "./destinations"; import { resolveActor, type Actor } from "./identity"; import { relationship } from "./relationships"; import { avatarUrl, monogram } from "./profile"; @@ -147,7 +147,7 @@ function face(actor: Actor): HTMLElement { image.addEventListener("load", () => box.replaceChildren(image)); }) .catch((error: unknown) => { - console.warn("players: the picture could not be found", error); + console.warn("profile: the picture could not be found", error); }); return box; @@ -188,7 +188,7 @@ function figure(spec: Figure, actor: Actor, self: boolean): HTMLElement { .catch((error: unknown) => { // A figure that cannot be read stays an em dash. Nothing on this page is // worth an error screen, and the rest of it may have arrived. - console.warn(`players: ${spec.label} could not be counted`, error); + console.warn(`profile: ${spec.label} could not be counted`, error); row.title = "This could not be read just now."; }); @@ -248,7 +248,7 @@ function challengeButton( // this page is read from the network the account lives on and is // still true - but not silence either: a press that appears to do // nothing is the worst of the three answers. - console.warn("players: the challenge could not be started", error); + console.warn("profile: the challenge could not be started", error); button.disabled = false; trouble.textContent = "A challenge could not be started just now."; }); @@ -422,7 +422,7 @@ function profile(actor: Actor, session: Session | null): Node[] { * this page at all: the distribution sends it to the unfurl route instead. */ function markCanonical(actor: Actor): void { - const href = new URL(playerHref(actor), window.location.origin).href; + const href = new URL(profileHref(actor), window.location.origin).href; const existing = document.querySelector( "link[rel=canonical]", ); @@ -436,11 +436,11 @@ async function start(): Promise { const asked = actorFromPath(window.location.pathname); if (!asked) { - // /players with nobody after it. Your own page is the only sensible + // /profile with nobody after it. Your own page is the only sensible // answer, and a stranger is told what the address is for. const session = await currentSession().catch(() => null); if (session) { - window.location.replace(playerHref(session)); + window.location.replace(profileHref(session)); return; } render(...nobody(window.location.pathname)); -- 2.51.2