Commit Graph

4 Commits

Author SHA1 Message Date
e71ff4acd4 feat(polish): phase 10 — search, accessibility, SEO and a real CSP
All checks were successful
PR checks / checks (pull_request) Successful in 9m36s
PLAN.md §13 phase 10, with four decisions of record — D47-D50, taking the count
to fifty. Three were straightforward; the CSP turned into the phase's real work,
because the thing meant to be a configuration flag was broken in a dependency and
broken silently.

D47 — search reaches the marketing pages, and the header gets a box.
Base.astro marks its <main> as a Pagefind body, so all ten join the index the
docs already query, and Search.astro opens it in a <dialog>. Nothing is fetched
until the dialog is opened (the bundle is 120 kB and these pages otherwise ship
almost no JavaScript). Pagefind titles a result from the first <h1>, and these
pages have editorial ones — "The app for a deployment you already use" — so the
index is given the page's short name instead. applyBrand.mjs now re-indexes after
a rewrite, closing a note phase 2 left for this phase.

D48 — the CSP is a real response header, sent by the container. Not a <meta>,
which ignores frame-ancestors, and not advice for someone's reverse proxy, which
puts the strictest promise in §6 outside what this repo tests. Three things
fought it, all the same shape — correct build, broken page, no error:

  * Astro does not hash <script is:inline>, and Starlight ships six per docs
    page, so the first build with CSP on had a strict header and a dead theme
    switcher. The hashes are now generated into src/config/cspHashes.mjs and
    checkCsp.mjs verifies every inline block against its own page's policy.
  * Expressive Code writes ~3,700 inline style ATTRIBUTES, which cannot be
    hashed, hence style-src-attr 'unsafe-inline' — scoped to that directive, so
    script-src is untouched.
  * @astrojs/node matched a request to a policy with pathname.includes(), a
    substring test: /modules/ was served /docs/modules/building-a-module's
    policy and rendered with its own stylesheet refused. scripts/serve.mjs keeps
    the same _headers.json and matches by equality; test/headers.test.mjs starts
    the server and reads the responses, because nothing that reads dist/ can see
    this.

D49 — robots.txt allows everything and names the sitemap (there was no way to
find it: no robots.txt, and D9 rules out a search console). D50 — Organization
and SoftwareApplication, no ratings and no docs-wide Article markup.

checkA11y.mjs is the eleventh check: seven structural rules over all fifty pages,
verified by breaking each in turn. The walk at 390/768/1280 found no overflow
anywhere, the CSP violations above, a 17x17 consent checkbox (WCAG 2.2 SC 2.5.8
wants 24), and a skip link that moved the scroll but not the focus.

npm run verify is green: fourteen steps, both test suites, all eleven checks.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 14:14:53 -05:00
c29ec94f46 feat(screens): phase 9 — real screenshots, from a real shard
All checks were successful
PR checks / checks (pull_request) Successful in 1m25s
D4 asked for screenshots of the review stack rather than placeholders. Seventeen
of them: eleven of the site in a browser, six of the app on a phone, all from one
demo deployment wired to a running ServUO shard over a real sidecar, captured on
one day (D42).

The deployment is branded "Runic Gateway Demo" rather than a real community (D43),
and the captures sit beside the claims they support — the homepage, /features/, and
five of the administration pages phase 7 could describe but not show (D44).

The rig is committed rather than remembered (D45):

  scripts/seedDemo.mjs        content, by driving the site's own API — never SQL,
                              because a row the product could not have produced is
                              a screenshot of a product that does not exist
  src/data/screens.mjs        every capture: route, viewport, scroll, alt, caption
  scripts/captureScreens.mjs  npm run screens:capture
  scripts/checkScreens.mjs    the ninth check script, in CI

Shard-side dressing is servuo-plugins' scaffolding (D46), never deployed.

The rig found five things nothing else had. One is fixed upstream — a fresh
module-uo install pinned wire protocol 3 against a sidecar speaking 4, released as
v1.0.2, which this repo's own facts check then caught in platform.json. Four are
raised as product observations and worked around in the rig: a renamed guild
member never reaches the site, a guild deleted while the shard is down is a ghost
row forever, "Houses in danger" cannot show a house that was already collapsing,
and the app's news list prints raw ISO timestamps.

Players online reads 0. Logging a character in needs a UO client driven by hand,
and that is where this stopped — PLAN.md §10 says exactly why, and how to retake
the two frames that would change.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 09:55:55 -05:00
556dee7355 feat(home): phase 3 — the homepage
All checks were successful
PR checks / checks (pull_request) Successful in 49s
Replaces phase 1's scaffold with the real homepage: hero, the data path as
inline SVG, the self-hosted argument, all five capability groups, and the
get-started CTA. Three decisions the org lead took first are recorded in
PLAN.md as D17-D19.

The data path is drawn generically and captioned specifically (D17): the nodes
say "your game server" and "sidecar", the sub-labels and caption name ServUO and
uo-link. The SVG is aria-hidden because the four numbered steps beside it carry
the same path in prose — one telling, not two.

The capability list is data with a check behind it (D18). Every Game-intelligence
item names the module-uo capability slug it comes from, and the build fails if
the page and platform.json disagree either way. That needed a fifteenth fact in
checkFacts.mjs: §12 named the capability list as an externally-sourced fact and
nothing re-read it, so the chain rested on someone remembering. It also found
that the site was omitting two of the module's eight capabilities — guilds and
city governors are now listed, in the page and in §10.

The hero leads with the emblem (D19), derived from whichever logo.png is in
force so one file still changes the hero, header, tab icon and app icon
together.

Also here, both found by standing the build up rather than by review:

  - checkBrand.mjs now enforces the demo slot's markup contract. applyBrand.mjs
    reveals the demo link by replacing an exact pair of empty attributes; an
    attribute inserted between them produces a build where the mount sets a demo
    URL, the boot log says nothing and the link never appears. Both halves are
    checked and the literal is derived from the expression applyBrand.mjs uses,
    so they cannot drift.

  - The header nav overflowed at 390px — four links plus the lockup measured
    433px against a 390px viewport, so every phone got a horizontally scrolling
    page. Phase 1 left this to phase 3 expecting a disclosure control; it got a
    wrap instead, because with four links there is nothing to disclose and a
    hamburger costs state, script and duplicate markup.

Verified on a clean checkout of this commit: all four checks, astro check, a
production build, a live /brand/* smoke, and a demo URL mounted and reverted.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 00:03:04 -05:00
66187dde5d feat(site): phase 1 — the foundation
Some checks failed
PR checks / checks (pull_request) Failing after 4m19s
Astro 7 with the Node adapter, Starlight mounted at /docs, the token file, both
self-hosted typefaces, the layout shell, and the two build-time checks from §12.

The palette's gold and cyan are sampled from runic-emblem.png rather than
guessed, per §11: 494,059 opaque pixels binned by hue, each value annotated with
its measured contrast against the ground, and restricted rather than brightened
where a ratio fails.

- checkTokens.mjs fails the build on any colour literal outside tokens.css,
  which is what keeps §7's "recolouring is a file copy" promise true.
- checkFacts.mjs re-reads all 14 externally-sourced facts from their authorities
  over the Gitea API and fails on disagreement. It also enforces D13: no email
  address in the source outside brand-default/brand.json.
- Both were negative-tested; neither has ever been allowed to pass by default.

§6 asks for output:'server' with per-page prerender=true. Astro 7 expresses the
same runtime shape as output:'static' with an adapter, opting individual routes
out — so the default is static rather than accidentally server-rendered.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 19:08:52 -05:00