Files
runicgateway.com/src/pages/community.astro
wtclaude 2d19ee4220
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
feat(marketing): phase 4 — the marketing pages
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>
2026-08-24 01:53:11 -05:00

289 lines
9.7 KiB
Plaintext

---
import Base from '../layouts/Base.astro';
import PageHeader from '../components/PageHeader.astro';
import platform from '../data/platform.json';
import { brand } from '../lib/brand.mjs';
/**
* `/community/` — PLAN.md §10 and §14 N3, built in phase 4.
*
* ---------------------------------------------------------------------------------------
* WHY IT IS IN THIS PHASE AT ALL
* ---------------------------------------------------------------------------------------
* §13's phase table never assigned it one. §10 specifies the page and §14 N3 specifies its
* contents, and the header nav and footer have both linked it since phase 1 — so it was a
* page the site pointed at and no phase built. The org lead folded it into phase 4 on
* 2026-08-20 rather than leaving it to be discovered by the link checker (D23). It is a
* marketing page with no new machinery, so this is where it fits.
*
* ---------------------------------------------------------------------------------------
* THE HONEST SPLIT, WHICH IS THE WHOLE POINT OF THE PAGE
* ---------------------------------------------------------------------------------------
* §14 N3 is explicit: this page describes a split rather than a single channel, because the
* obvious sentence — "found a bug? open an issue" — is currently false. Gitea registration
* is disabled on this instance, so the code is publicly readable and nobody outside the org
* can file anything against it. Discord is therefore the front door in fact, not just in
* preference (D10), and saying so is cheaper for a reader than letting them find the
* sign-up page and its refusal.
*
* N3 also records that this page is written the same way whether or not registration is
* later reopened — only one sentence changes. That sentence is marked below, so whoever
* changes the Gitea configuration can find it without rereading the page.
*
* The security address comes from `brand.json` and appears nowhere in this file. D13
* publishes a personal address on the understanding that moving to a role address later is
* an edit to a mounted file, and `checkFacts.mjs` fails the build if an address is typed
* into any source file — including this one, which is the file most likely to want to.
*/
const title = 'Community';
const description =
'Where to ask, where the code is, and how to report a security problem.';
const gitea = `${platform.gitea.base}/${platform.gitea.org}`;
---
<Base title={title} description={description}>
<PageHeader eyebrow="Getting in touch" title="Three doors, and which one to use">
<p>
This is a small project run by people with day jobs. There is no support desk and no
ticket queue, which is worth knowing before you choose where to put a question — one of
these channels answers in minutes and one of them may not answer at all.
</p>
</PageHeader>
<section class="page section chan">
<ul class="chan__grid">
<li class="panel chan__card chan__card--primary">
<div class="chan__head">
<h2>Discord</h2>
<span class="chip chip--live">The front door</span>
</div>
<p class="chan__lede">
Questions, bug reports, help getting an install working, and where the Android beta
is announced. No account with us to make, nothing to be approved for, and the
fastest way to reach somebody who has run this software.
</p>
<p class="chan__use">
<span class="chan__use-label">Use it for</span>
Anything you would otherwise open an issue for, and everything you would not.
</p>
<a class="btn btn--primary" href={brand.discordInvite} rel="noopener noreferrer">
Join the Discord
</a>
</li>
<li class="panel chan__card">
<div class="chan__head">
<h2>The code</h2>
<span class="chip">Read freely</span>
</div>
<p class="chan__lede">
Every repository is public and readable without signing in to anything — the
website, the bridge, the game plugin, the installer, the module, the app and all of
the documentation. Clone it, read it, run it.
</p>
<p class="chan__use">
<span class="chan__use-label">One caveat</span>
{/*
THE SENTENCE §14 N3 SAYS WILL CHANGE. If Gitea registration is reopened —
manual confirm, Turnstile, no repository creation by default — this becomes
"issues and pull requests are open to anyone with an account", and nothing else
on the page moves.
*/}
Registration on our Gitea is closed at the moment, so filing an issue needs an
account we would have to create for you. Ask on Discord and it will reach the same
place.
</p>
<a class="btn btn--ghost" href={gitea} rel="noopener noreferrer">Browse the source</a>
</li>
<li class="panel chan__card">
<div class="chan__head">
<h2>Security</h2>
<span class="chip chip--draft">Private</span>
</div>
<p class="chan__lede">
If you have found something that should not be discussed in a public channel, email
it. You will get a human, not a form, and there is no bounty programme to game —
just an acknowledgement and a fix.
</p>
<p class="chan__use">
<span class="chan__use-label">Use it for</span>
Anything that would let somebody reach a deployment, an account or a game server
they should not.
</p>
<a class="btn btn--ghost" href={`mailto:${brand.contactEmail}`}>{brand.contactEmail}</a>
</li>
</ul>
</section>
<section class="page section contrib">
<div class="panel contrib__panel">
<p class="eyebrow">Contributing</p>
<h2>What is useful, in order</h2>
<ol class="contrib__list">
<li>
<h3>Run it and say what broke</h3>
<p>
The install path is the priority of this whole project, and the most valuable
thing anyone outside it can do is walk it on a machine we have never seen and
report where it stopped making sense.
</p>
</li>
<li>
<h3>Build a module for another game</h3>
<p>
There is one module and it is Ultima Online, so the claim that this platform is
game-agnostic is currently an argument rather than a demonstration. The
<a href="/modules/">Integration Kit</a> exists to be followed by somebody outside
this project — and it stays marked draft until it has been.
</p>
</li>
<li>
<h3>Fix the documentation</h3>
<p>
Documentation is versioned alongside the code it describes and a change is not
finished until the docs match it. If something you read was wrong, that is a bug
of the same kind as any other.
</p>
</li>
</ol>
<p class="contrib__note">
All of it is free software under the GPL-3.0-or-later, and contributions carry one
house rule worth knowing before you start: work done with AI assistance has to say so
— a box on the pull request and a trailer on the commit. Undisclosed AI-generated
contributions get closed. Every repository's <code>CONTRIBUTING.md</code> has the
details.
</p>
</div>
</section>
</Base>
<style>
.chan__grid {
display: grid;
gap: 1rem;
margin: 0;
padding: 0;
list-style: none;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}
.chan__card {
display: flex;
flex-direction: column;
padding: clamp(1.25rem, 3vw, 1.75rem);
}
/* The one channel that actually answers gets the portal edge — the live signal, used
here for the same reason it is used on a running shard. */
.chan__card--primary {
border-color: color-mix(in srgb, var(--portal) 40%, transparent);
}
.chan__head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.85rem;
}
.chan__card h2 {
margin: 0;
font-size: 1.25rem;
}
.chan__lede {
margin: 0;
color: var(--muted);
font-size: 0.95rem;
}
/* Takes the slack so the button sits at the foot of every card in the row. */
.chan__use {
flex: 1;
margin: 1rem 0 1.5rem;
color: var(--dim);
font-size: 0.9rem;
}
.chan__use-label {
display: block;
color: var(--muted);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.11em;
text-transform: uppercase;
}
.chan__card .btn {
align-self: flex-start;
}
.contrib__panel {
padding: clamp(1.5rem, 4vw, 2.75rem);
}
.contrib h2 {
margin: 0 0 1.5rem;
font-size: clamp(1.5rem, 3vw, 2rem);
}
.contrib__list {
margin: 0;
padding: 0;
list-style: none;
counter-reset: item;
}
.contrib__list li {
position: relative;
padding-left: 3.25rem;
counter-increment: item;
}
.contrib__list li + li {
margin-top: 1.5rem;
}
.contrib__list li::before {
content: counter(item);
position: absolute;
left: 0;
top: 0;
display: grid;
place-items: center;
width: 2.25rem;
height: 2.25rem;
border: 1px solid var(--gold-deep);
border-radius: var(--radius-pill);
color: var(--gold);
font-family: var(--display);
font-size: 1rem;
}
.contrib__list h3 {
margin: 0.3rem 0 0.4rem;
font-size: 1.06rem;
}
.contrib__list p {
margin: 0;
max-width: var(--measure);
color: var(--muted);
}
.contrib__note {
margin: 2rem 0 0;
padding-top: 1.25rem;
border-top: 1px solid var(--line-soft);
max-width: var(--measure);
color: var(--dim);
font-size: 0.9rem;
}
</style>