Files
runicgateway.com/src/layouts/Base.astro
wtclaude de9d25bbe7
All checks were successful
PR checks / checks (pull_request) Successful in 1m26s
feat(validation): phase 11 — the walk that found what the checks could not
The checks were green before this phase started and are green now. What found
anything was the part no script does: fifty pages at three widths in a real
browser, a signup walked against its store, and a full brand mount applied and
restarted.

D51 — the chrome and the head follow the mount; the consent sentence does not.
With a complete brand.json mounted, forty-nine pages came back rebranded and
/beta did not. applyBrand.mjs rewrites files in dist/client and /beta renders
per request, so its HTML never exists as a file to rewrite; liveBrand() was
there for exactly that and was used for betaOptInUrl alone. Everything around
the form — title, OG tags, header lockup, footer Source and Discord links —
came from the shared chrome, and the shared chrome was baked. renderBrand()
picks by Astro.isPrerendered, in one place, so the other forty-nine keep taking
the value the boot rewrite will replace. CONSENT_TEXT stays a constant: it is
stored verbatim in a person's row, so following a mounted name would change the
recorded text of a consent already given.

D52 — the documentation half gets phase 10's skip-link fix. Starlight's skip
link targets the page <h1>, which is no more focusable than the <main> phase 10
fixed, so following it moved the viewport and not the focus on forty pages. A
PageTitle override adds tabindex="-1".

D53 — no twelfth check. The external-link sweep (73 of 74 alive) and the
brand-mount walk stay throwaway scripts: one would make the build depend on
other people's uptime and the other needs Chrome on the runner.

Also recorded and deliberately not fixed: Starlight's heading anchor links
measure under 24px at 390, and are exempt under SC 2.5.8's Equivalent clause
because the mobile table of contents links to every one of the same anchors.

npm run verify green — fourteen steps, both suites, all eleven checks.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 15:16:08 -05:00

130 lines
5.6 KiB
Plaintext

---
import '../styles/tokens.css';
import '../styles/global.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import { renderBrand } from '../lib/brand.mjs';
import { token } from '../lib/tokens.mjs';
interface Props {
title: string;
description: string;
/** Suppress the site name suffix — the homepage sets its own full title. */
bareTitle?: boolean;
}
const { title, description, bareTitle = false } = Astro.props;
// Per-request routes read the mount; prerendered pages take the stock value and let the
// boot rewrite carry the mount in. See `renderBrand` in src/lib/brand.mjs (phase 11).
const brand = renderBrand(Astro);
const fullTitle = bareTitle ? title : `${title} — ${brand.siteName}`;
const canonical = new URL(Astro.url.pathname, Astro.site);
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{fullTitle}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={brand.siteName} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={new URL('/brand/og-image.png', Astro.site)} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content={`${brand.siteName} — ${brand.tagline}`} />
<meta name="twitter:card" content="summary_large_image" />
<!--
No analytics, no third-party requests, no cookie banner (D9), and the fonts are
self-hosted (§11) — so there is nothing here to preconnect to, and §6's
`default-src 'self'` holds with no exception to argue about. The CSP header itself
is set at the adapter in phase 10; this comment is here so nobody adds a CDN link
in the meantime and quietly breaks the promise.
-->
<meta name="theme-color" content={token('--bg')} />
<!--
Icons, like every other brand asset, come from the mount (§7). Only `favicon.ico` is
strictly needed — browsers ask for it at that exact path whether or not a page links
it, which is why the route derives one rather than 404ing — but naming the PNG and
the apple-touch icon explicitly means a device picks the size it wants instead of
downscaling a 48px .ico.
None of these files has to exist in `brand-default/`. They are derived on request
from whichever `logo.png` is in force, so an operator replaces exactly one file.
-->
<link rel="icon" href="/brand/favicon.ico" sizes="16x16 32x32 48x48" />
<link rel="icon" href="/brand/favicon-32.png" type="image/png" sizes="32x32" />
<link rel="apple-touch-icon" href="/brand/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.webmanifest" />
<!--
Last in the head, and that position is the mechanism (§7). This is the operator's
stylesheet: it only ever redefines custom properties, and a custom property
redefined on `:root` wins by being later, not by being more specific. Moving this
above the site's own stylesheet would silently turn every override into a no-op.
The stock copy declares nothing, so this costs one 304 and changes no pixels until
somebody mounts a real one.
-->
<link rel="stylesheet" href="/brand/theme.css" />
<slot name="head" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<div class="site">
<Header />
<!--
`data-pagefind-body` is what puts the marketing pages into the search index the
documentation already had (D47). It is on `<main>` and not on `<body>` deliberately:
the header and footer are on all ten pages, so indexing them would make every page
a result for "Discord", "Privacy" and the product's own name.
Pagefind indexes a page only if it finds this attribute, which is why the docs were
the whole index before now — Starlight marks its own content and nothing else did.
The explicit title is worth the second attribute. Pagefind titles a result from the
first <h1> it finds, and these pages have EDITORIAL h1s — /app/'s is "The app for a
deployment you already use", /terms/'s is "Short, and only about what we run". Read
on the page under an eyebrow that says "Android app" those are right; read as four
rows in a result list they are unscannable, and the first walk of this search turned
up exactly that. The page's short name — the one in the nav and the browser tab —
is what a reader is looking for in a list.
-->
<!--
`tabindex="-1"` is what makes the skip link above actually skip. Following it moves
the SCROLL to this element, but a container is not focusable, so focus stays where
it was; Chrome papers over that by moving its sequential-navigation point, and not
every browser or screen reader does. `-1` makes the element focusable by script and
fragment only — never by Tab — so the link lands here for everyone and the tab order
is unchanged. (Negative, so it is not the positive `tabindex` checkA11y refuses.)
-->
<main
id="main"
tabindex="-1"
data-pagefind-body
data-pagefind-meta={`title:${bareTitle ? brand.siteName : title}`}
>
<slot />
</main>
<Footer />
</div>
</body>
</html>