---
/**
* 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. */
}