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>
This commit is contained in:
2026-08-19 19:08:52 -05:00
parent 650ea21ad4
commit 66187dde5d
25 changed files with 10462 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
---
import { brand } from '../lib/brand.mjs';
import platform from '../data/platform.json';
/**
* The footer is on every page, so it is the one place that must never quote a fact from
* memory. The version chip reads `platform.json` (§12); the contact address and the links
* read `brand.json` (§7, D13).
*
* `/privacy` and `/terms` are linked from every page (§9) — those pages land in phase 6,
* which is why they are the only two entries deliberately left out of the columns below
* until then.
*/
const year = new Date().getFullYear();
const columns = [
{
heading: 'Product',
links: [
{ href: '/features/', label: 'Features' },
{ href: '/architecture/', label: 'Architecture' },
{ href: '/modules/', label: 'Modules' },
{ href: '/app/', label: 'Android app' },
],
},
{
heading: 'Documentation',
links: [
{ href: '/docs/', label: 'Getting started' },
{ href: '/docs/', label: 'Administration' },
{ href: '/docs/', label: 'Building a module' },
],
},
{
heading: 'Project',
links: [
{ href: brand.giteaOrg, label: 'Source' },
{ href: brand.discordInvite, label: 'Discord' },
{ href: '/community/', label: 'Community' },
],
},
];
const isExternal = (href: string) => href.startsWith('http');
---
<footer class="site-footer">
<div class="page">
<div class="site-footer__cols">
{
columns.map((column) => (
<section>
<h2>{column.heading}</h2>
<ul>
{column.links.map((link) => (
<li>
<a
href={link.href}
rel={isExternal(link.href) ? 'noopener noreferrer' : undefined}
>
{link.label}
</a>
</li>
))}
</ul>
</section>
))
}
</div>
<div class="site-footer__legal">
<p>
{brand.siteName} is free software under the{' '}
<a href="https://www.gnu.org/licenses/gpl-3.0.html" rel="noopener noreferrer"
>GPL-3.0-or-later</a
>. &copy; {year}.
</p>
<p class="site-footer__meta">
<span class="chip chip--version">Protocol {platform.protocol}</span>
<span class="chip chip--version">Bundle {platform.bundle.tag}</span>
</p>
</div>
</div>
</footer>
<style>
.site-footer__meta {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
</style>