feat(marketing): phase 4 — the marketing pages
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
PLAN.md §13 phase 4: /features/, /architecture/, /modules/, /integrations/, and /community/ — plus the two scope items the phase table never assigned to anyone. Six decisions taken by the org lead before coding, recorded in PLAN.md §10 as D20-D25: - D20 /features/ is the homepage's list with a `detail` line, not a second list. One data file, two renderings, so they cannot disagree about what exists. - D21 /architecture/ draws reasons, not reference: three new inline SVGs, one per boundary. No endpoint tables, no config keys — those are phase 8's and stay canonical in docs/. - D22 The deliberate absences of §2 become one tagged data file, rendered on the three pages that promise them. - D23 Phase 4 absorbs /community/ (specified in §10 and §14 N3, linked from the header since phase 1, built by no phase) and checkLinks.mjs. - D24 `needsModule`: writing the Teams detail exposed a false claim phase 3 shipped. Teams are module-sourced only — teams.module_id is NOT NULL, there is no create route, sync is gated on providerModuleId() — so the Community group no longer says a bare core does all of it. - D25 The per-capability demo affordance brand.json had promised since phase 2 is a deep link, filled at boot from data-demo-path. checkLinks.mjs reads the built HTML rather than src/, because half these links are assembled from data files and template literals. Its PLANNED_ROUTES list is checked in both directions, so it cannot rot into a permanent exemption. applyBrand.mjs gained a pass that recomputes deep links from their immutable path, making it idempotent and reversible; checkBrand.mjs lifts that pattern out and runs it against the stock markup so the two cannot drift. Both proved against a real mount, in both directions. Fixes a cascade bug the checks could not see: [data-demo-url=''] and a scoped component class are both specificity 0,1,0, so .demo-link's `display` beat the hide rule and twelve links to a nonexistent demo rendered, each resolving to the current page. The rule is now !important. The four diagrams' shared SVG vocabulary moved to src/styles/diagram.css. Verified from a clean checkout: npm ci, all five checks, astro check (0 errors), production build, and a live browser pass at desktop and 390px. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
118
PLAN.md
118
PLAN.md
@@ -234,6 +234,16 @@ Taken by the org lead (Colby Whitlock) on 2026-08-19. Recorded so they are not r
|
||||
| **D12** | **A public demo instance is planned but out of scope today** — a Proxmox VM running the full stack including ServUO, with restricted settings and an hourly automatic reset. | §15. The IA reserves the slot so it lands later without a restructure. |
|
||||
| **D13** | **Publish the existing address.** No mailbox has been created at the domain and the org lead elected not to wait for one: `whitlocktech@gmail.com` is the published contact wherever the site needs one — `/privacy`, `/terms`, `/community`, the Play listing — and `docs/SECURITY.md` keeps the address it already carries. | Taken 2026-08-19, superseding §14 N2 as a blocker. The address lives **only** in `brand.json` (§7), never in prose, so moving to `privacy@`/`security@` later is a file edit and a restart. Phase 0's PR 0.10 is withdrawn, not deferred. |
|
||||
|
||||
**Decisions after D13 are recorded where they were taken**, in the section describing the phase that
|
||||
raised them, rather than appended here — a decision is only re-litigated when its reasoning is
|
||||
somewhere other than the thing it decided. The count of record is **twenty-five**:
|
||||
|
||||
| # | Where | What it settled |
|
||||
|---|---|---|
|
||||
| D14–D16 | §7, "How phase 2 actually built it" | The branding pipeline: one raster in, brand text applied at boot, the mark is the real emblem |
|
||||
| D17–D19 | §10, "How phase 3 built the homepage" | The data-path diagram, all five groups on the homepage, the emblem-led hero |
|
||||
| D20–D25 | §10, "How phase 4 built the marketing pages" | `/features/` as the same list with detail, `/architecture/` as reasons not reference, the absences as data, the two absorbed scope items, `needsModule`, the demo deep links |
|
||||
|
||||
---
|
||||
|
||||
## 6. Runtime shape
|
||||
@@ -532,6 +542,11 @@ Guilds and city governors were added to Game intelligence in phase 3: `module-uo
|
||||
capabilities and the site was omitting two of the eight. That correction is now mechanical rather
|
||||
than editorial — see D18.
|
||||
|
||||
**Community is core machinery, but two of its six need a module to fill them.** Teams and Team
|
||||
forums are marked as such (D24). Core owns every part of the Team machinery and cannot create a
|
||||
Team: they arrive from the installed module, so on a deployment with no module the feature is
|
||||
present and permanently empty. The group's summary says so; `/features/` says why.
|
||||
|
||||
### How phase 3 built the homepage
|
||||
|
||||
Three decisions taken before the page was written (org lead, 2026-08-20).
|
||||
@@ -570,6 +585,87 @@ nothing has to be rewritten later. Links *into the documentation* are the except
|
||||
`/docs/`, because phases 7 and 8 own those slugs and a guessed one would be a stale URL nothing
|
||||
checks.
|
||||
|
||||
### How phase 4 built the marketing pages
|
||||
|
||||
Six decisions taken before coding (org lead, 2026-08-20), plus two scope items the phase table had
|
||||
never assigned to anyone.
|
||||
|
||||
**D20 — `/features/` is the homepage's list with a `detail` line, not a second list.** Every
|
||||
capability in `src/data/capabilities.mjs` gained a sentence or two of argument; `/` renders the
|
||||
label, `/features/` renders the label and the detail. Rejected: slicing the page by reader
|
||||
(players / staff / operators / builders), which reads better but makes the same capability appear
|
||||
twice and breaks the one-to-one mapping the coverage check depends on; and deep-diving only the
|
||||
differentiators, which would have left the page looking smaller than the homepage promised.
|
||||
`assertDetailCoverage()` fails the build on a capability with no detail — the homepage would still
|
||||
look right, and `/features/` would render a heading with nothing under it.
|
||||
|
||||
**D21 — `/architecture/` draws reasons, not reference.** Three new inline SVGs, each drawing one
|
||||
boundary: two hosts and two installs, the public/staff allowlist, and the core/module seam. It
|
||||
carries no endpoint tables, no configuration keys, no schema and no event catalog — phase 8 owns
|
||||
those, they are canonical in `docs/`, and a second copy here is a copy that goes stale (§1).
|
||||
Rejected: reusing the homepage's data-path diagram larger (a visitor arriving from `/` meets the
|
||||
same picture twice), and adding a component/version table (starts becoming the Reference section).
|
||||
The vocabulary the four diagrams now share moved to `src/styles/diagram.css`.
|
||||
|
||||
**D22 — the deliberate absences are one data file.** `src/data/notBuilt.mjs`, each entry tagged with
|
||||
the pages that render it, because the homepage already promises a reader they will find the list on
|
||||
both `/features/` and `/integrations/` and two hand-written copies is how the inconvenient half
|
||||
stops appearing on one of them. Every entry carries a `resolvedBy`: an absence with an exit
|
||||
condition is a position, an absence without one is a hole. That generalises what D8 already required
|
||||
of the Integration Kit's draft badge.
|
||||
|
||||
**D23 — phase 4 absorbs `/community/` and `checkLinks.mjs`.** Neither had a phase. §10 specifies the
|
||||
page and §14 N3 specifies its contents, and the header and footer have linked it since phase 1 — a
|
||||
page the site pointed at that no phase built. `checkLinks.mjs` is specified in §12 and phase 4 is
|
||||
what makes it load-bearing: it roughly quadrupled the internal link count and added the first
|
||||
outbound links into the repositories.
|
||||
|
||||
**D24 — `needsModule`, because "core" and "module-supplied" were not enough.** Writing the
|
||||
`/features/` detail for Teams exposed a claim phase 3 had shipped: the Community group said
|
||||
"everything here works on a deployment with no game module installed at all", and that is false.
|
||||
`teams.module_id` is `NOT NULL` on `website` `main`, there is no create route anywhere under
|
||||
`/api/v1/admin/teams`, and sync is gated on `teamProvider.providerModuleId()`. Core owns the whole
|
||||
Team machinery — tables, roster resolver, forums, notification streams, Discord bridge, voice,
|
||||
activity feed, `/admin/teams` — and deliberately cannot *originate* a Team, because core does not
|
||||
own the word for one. On a bare core the feature is present, correct and permanently empty. Teams
|
||||
and Team forums are marked; the group summary was requalified; the homepage changed by one sentence
|
||||
and kept D18's five named groups. Rejected: a sixth group for Teams (says it loudest, costs the
|
||||
five-group grid phase 3 tuned), and fixing only the wording (leaves the distinction one sentence
|
||||
deep and unguarded).
|
||||
|
||||
**D25 — the demo affordance on `/features/` is a per-capability deep link.** `brand.json` had
|
||||
promised one since phase 2 without defining it. Capabilities with a stable public route carry a link
|
||||
appended to the mounted `demoUrl`; the rest carry nothing, and that asymmetry is honest — a
|
||||
character sheet is reachable only by the account it belongs to, and a Team forum lives behind an id
|
||||
no static page can know. Paths are read from the real route tables on `main`, never guessed, which
|
||||
also means they are the *module's* routes: a deployment running a different module deep-links
|
||||
somewhere else.
|
||||
|
||||
That needed the branding pipeline extended, because the phase-3 slot could not express it. The slot
|
||||
is a literal swap of a whole URL, so it can only ever put the demo's root in an `href`, and
|
||||
reversing it would not even find a deep link — whose `href` is the root plus a path, matching no
|
||||
literal the script knows. `applyBrand.mjs` gained a second pass that **recomputes** all three
|
||||
attributes from the immutable `data-demo-path`, making it idempotent and exactly reversible, and
|
||||
`checkBrand.mjs` gained a guard that lifts the pattern out of `applyBrand.mjs` and runs it against
|
||||
the stock markup, so the two cannot drift. Both directions were proved against a real mount.
|
||||
|
||||
**One thing the checks caught about each other.** A scoped `:has([data-demo-url=''])` rule, added to
|
||||
hide the wrapper around a hidden demo link, made `checkBrand.mjs` fail: it cannot tell a CSS selector
|
||||
from an attribute, and it should not have to. The right fix was to delete the wrapper and let the
|
||||
link be the flex item, so the existing hide rule takes the margin with it — a case where the check
|
||||
being blunt pointed at simpler markup rather than at a needed exemption.
|
||||
|
||||
**And one thing no check caught.** `[data-demo-url=''] { display: none }` is specificity 0,1,0, and
|
||||
so is the scoped class Astro puts on the same element — so a component that sets `display` wins on
|
||||
source order, because component styles are emitted after `global.css`. `/features/`'s `.demo-link`
|
||||
set `display: inline-flex` for its arrow, and twelve links to a demo that does not exist rendered on
|
||||
the page, each pointing at `href=""` — which a browser resolves to the page it is already on.
|
||||
`checkBrand.mjs` was green throughout: the attributes were perfect and the defect was three files
|
||||
away, in the cascade. It was found by looking at the rendered page at 390px, which is not a
|
||||
mechanism, and it is the argument for keeping the live browser pass in every phase. The rule is now
|
||||
`!important` and says why in the stylesheet: while there is no demo these elements do not render,
|
||||
and no component may overrule that by accident.
|
||||
|
||||
### Documentation
|
||||
|
||||
```
|
||||
@@ -692,7 +788,23 @@ a mechanism rather than diligence:
|
||||
into a paragraph. Same argument as `checkTokens.mjs` and colour literals — the check is the
|
||||
mechanism, diligence is not.
|
||||
- **`scripts/checkLinks.mjs`** — every internal link resolves; every outbound link into a
|
||||
`RunicGateway` repo points at a branch path, not a commit permalink.
|
||||
`RunicGateway` repo points at a branch path, not a commit permalink. **Built in phase 4** (D23),
|
||||
and it reads `dist/client` rather than `src/`: half the links these pages carry are assembled from
|
||||
data files and template literals, and a source scan sees an expression rather than a URL. It runs
|
||||
after the build for that reason, in `verify` and in CI. It fetches nothing — the outbound rule is
|
||||
about the shape of a URL, and a check that fails when someone else's host is slow is a check
|
||||
people learn to ignore.
|
||||
|
||||
It carries one exemption list, `PLANNED_ROUTES`, because §10's convention is that the header,
|
||||
footer and homepage link the *final* routes rather than growing links phase by phase. That is safe
|
||||
only because the list is checked in both directions: a link to a route that is neither built nor
|
||||
listed fails, **and an entry whose route has since been built also fails**, so the list cannot rot
|
||||
into a permanent exemption once the page arrives.
|
||||
|
||||
- **`scripts/checkBrand.mjs`** also guards the demo slot and, since phase 4, the per-capability deep
|
||||
links (D25) — lifting the pattern out of `applyBrand.mjs` and running it against the stock markup,
|
||||
so a template and a script that share no code cannot drift apart. Both are invisible in a stock
|
||||
build, which is exactly why they need a check rather than a look.
|
||||
- **`scripts/checkTokens.mjs`** — no colour literal outside the token file (§7).
|
||||
- `astro check` plus a production build, in CI on every PR.
|
||||
|
||||
@@ -706,14 +818,14 @@ a mechanism rather than diligence:
|
||||
| **1** | Foundation: Astro + Node adapter scaffold, the token file, typography, layout shell, header/footer, docs theming and sidebar, `platform.json` + `checkFacts.mjs` + `checkTokens.mjs` |
|
||||
| **2** | Branding pipeline (§7): `/brand/*` resolution, `brand-default` contents, the emblem's web derivatives and lockup, `brand.json` wiring |
|
||||
| **3** | Homepage: hero, the data-path diagram as inline SVG, grouped capability sections, CTA, the reserved demo slot |
|
||||
| **4** | Marketing: `/features/`, `/architecture/`, `/modules/`, `/integrations/` |
|
||||
| **4** | Marketing: `/features/`, `/architecture/`, `/modules/`, `/integrations/`, **and `/community/`** — plus `checkLinks.mjs`, the capability `detail` lines, `notBuilt.mjs` and the demo deep links. See D20–D25 |
|
||||
| **5** | The app and the beta: `/app/`, `/beta/`, the signup endpoint, the SQLite store, rate limiting, the export CLI (§8) |
|
||||
| **6** | Legal: `/privacy/`, `/terms/`, footer links, and the Play Data Safety notes (§9) |
|
||||
| **7** | Docs — the journey: Getting started (7) + Administration (12). **The installation path is the priority of the whole project** |
|
||||
| **8** | Docs — builder and reference: Modules (8) + Architecture (5) + Reference (7) |
|
||||
| **9** | Screenshots (D4): stand up the local review stack, seed presentable content, capture the admin panel, Teams, forums, marketplace, spawn atlas and shard console; build the screenshot components |
|
||||
| **10** | Polish: responsive, accessibility, SEO/OpenGraph/sitemap/robots, full-text search, CSP headers |
|
||||
| **11** | Validation: `astro check`, production build, all four check scripts, mobile layout verified in a real browser, a signup walked end to end |
|
||||
| **11** | Validation: `astro check`, production build, **all five check scripts** (tokens, brand, links, facts, types), mobile layout verified in a real browser, a signup walked end to end |
|
||||
| **12** | Delivery: Dockerfile, `docker-compose.yml` with both bind mounts documented, Gitea Actions workflow publishing to the registry, README, CONTRIBUTING with the AI-disclosure requirement, and an operator note covering DNS, TLS and the reverse proxy (D6) |
|
||||
|
||||
Phases 5 and 6 are deliberately adjacent and early: the beta cannot start without `/privacy`, and
|
||||
|
||||
Reference in New Issue
Block a user