--- /** * Every page: the head, the masthead, and a stack for whatever the page is. * * The masthead used to be built by main.ts. It is rendered here instead * because the blog has pages the app never runs on, and a nav that only * appears once JavaScript has loaded would be missing from all of them. */ import { NAV_GROUPS } from "../destinations"; import { BANNER_KEY, SITE_BANNER } from "../site-banner"; import "../styles.css"; type Props = { title: string; description: string; /** Marks a masthead link as the current page. Omit where the app decides. */ current?: string; /** "article" for a post, so unfurlers stop calling it a site. */ ogType?: "website" | "article"; /** Absolute or site-relative. Falls back to the site's own card. */ image?: string; imageAlt?: string; /** AT-URI of the record this page was rendered from, if it has one. */ atUri?: string; }; const { title, description, current, atUri, ogType = "website", image = "/social-card.png", imageAlt = "The lance.blue logo and the text 'Find your mutuals. Salvage their mechs.'", } = Astro.props; const canonical = new URL(Astro.url.pathname, Astro.site); const imageUrl = new URL(image, Astro.site); // The site's own card is a known size and says so. A page that brings its own // image does not, because nothing here has measured it, and dimensions that // disagree with the file are worse than dimensions that are missing. const DEFAULT_CARD = "/social-card.png"; const cardSize = image === DEFAULT_CARD ? { width: 1200, height: 630 } : null; --- {title} { /* The link preview. Prerendered pages carry their own; the app's client routes all share the tags on / , because no unfurler runs the page's JavaScript and there is one file behind every one of those addresses. Keep this block near the top of the head. Slackbot reads only the first 32KB of the HTML, so anything large inlined above it costs the unfurl. og:image has to be an absolute URL. Relative paths are resolved by some unfurlers and dropped by the rest, which is worse than a plain link. public/social-card.png comes from `npm run social-card`. */ } { /* The canonical link's counterpart in ATProto. Only a page with a record carries it: a verifier resolves the href, so an empty one is worse than a missing tag. */ } {atUri && } { cardSize && ( <> ) } { /* twitter:* take name, not property; both spellings are common and one of them is wrong. There is no twitter:title or twitter:description here on purpose: X falls back to the og:* pair, and the tags that are left are the ones nothing falls back to. twitter:card is what tells Discord to show the image large instead of as a thumbnail. */ } { /* Where a reader looks for the feed. On every page rather than only the blog's: autodiscovery is how a subscribe button finds one at all, and it is asked of whatever address the reader was handed. */ } { /* The saved colour scheme, before anything is painted. account.ts is a module and runs after the first paint, so leaving this to it would show a light-mode reader a dark page first on every navigation — and the blog is a fresh page load per link. Inline, so it cannot import web/src/theme.ts and cannot share the key with it. scripts/theme.test.mjs keeps the two spellings together. */ } { /* The site-wide notice, when there is one. Above the masthead and not sticky with it: it is a thing to read once and scroll past, not chrome. Rendered here rather than by the app for the same reason the nav is — the blog's pages never run the app, and that is where most first visits land. site-banner.ts is where one is turned on. */ } { SITE_BANNER && ( ) } { /* Before the paint, not in the module script: a banner this browser has already closed must never be seen again, and one removed a moment after the page draws is seen every time — and shoves the masthead down on its way out. Same trick and same reason as the theme's script in the head. Unconditional, and it finds nothing on a page with no banner: the element it looks for is the only thing that decides, so this does not have to be told twice whether there is one. */ } { /* Brand and navigation. Outside #app because they outlive every screen change; on the app page main.ts only moves the current-page marker. */ }
{/* Decorative: the wordmark beside it is the link's text. */} lance.blue { /* What the site is: alpha. Outside the link rather than in it — inside, the anchor's aria-label would swallow it and a screen reader would never hear the word at all. This is the copy that is announced; the hero's and the footer's are hidden, because they repeat it on the same page. alpha-stamp.ts writes those two, and this one is spelled out here because the blog's pages never run the app. */ } Alpha
{ /* Where account.ts says that the filled link changed from Play to Resume. Shipped empty and on every page, because a live region has to be on the page before the text it is to announce arrives — one added and written in the same breath announces nothing. */ } { /* Filled in by theme.ts. Nothing here waits on the session: the colour scheme is a setting about this browser, and a reader who never signs in has the same say in it as a player. */ }
{ /* Filled in by account.ts once it knows whether anyone is signed in. Empty rather than a Login button the layout guessed at: a signed-in player would watch it turn into their own avatar. */ } { /* ph-no-capture: account.ts puts the player's handle and avatar in here, and autocapture sends the visible text of what was clicked. The class stops it at this subtree. */ }
{ /* Every page, including the blog's, which the app never runs on. Its own script rather than main.ts's job for that reason. */ }