mirror of
https://github.com/horizon-rust/horizon-site.git
synced 2026-11-07 10:05:58 -10:00
Stand alone: read the game's files from its main, docs and setup for this repo
The site moves out of ludovic111/horizon (website/) into this repository, history kept. - build.mjs reads the README, launcher/links.json, docs/api/, the design tokens and the roadmap from the game repository's main on GitHub, all from one commit (raw files of a branch are cached for minutes), or from a local checkout with HORIZON_CHECKOUT. The tokens and the roadmap try their new paths in the game repository first (crates/horizon-theme/tokens.css, roadmap/roadmap.json), then the old website/ ones. - The local copies of tokens.css and roadmap.json go; the roadmap stays as a test fixture. - Docs pages of this (private) repo lose "Edit this page on GitHub"; the API pages keep theirs. - The benchmark fixtures are a copy of the telemetry server's; a test checks they still match. - railway.json: no watch patterns, every push deploys. README and CLAUDE.md for people and agents. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
6a20f759dc
commit
6d7848a1b1
15 files changed
+195
-220
No files matched your search
@@ -0,0 +1,54 @@
|
||||
# horizon-site: playhorizon.dev
|
||||
|
||||
The website of Horizon (Minecraft: Java Edition 26.3 rewritten in Rust). Private. The game, the launcher and their
|
||||
developer docs are in [ludovic111/horizon](https://github.com/ludovic111/horizon) (public), called "the game's
|
||||
repository" below. People edit it too (not only agents): [README.md](README.md) has "Working on it", the layout, the docs
|
||||
rules and the benchmarks; this page is the short map for agents.
|
||||
|
||||
## Build and test
|
||||
|
||||
```sh
|
||||
node build.mjs && node serve.mjs # dist/, served on http://localhost:4173
|
||||
node --test src/ scripts/ # all tests (npm test); no npm install needed
|
||||
HORIZON_CHECKOUT=~/code/horizon node build.mjs # read the game's files from a local checkout instead of GitHub
|
||||
```
|
||||
|
||||
`build.mjs` has no dependencies and fails on a broken docs link, a missing image or a README without its parity
|
||||
table. `npm install` only for `npm run assets` (fonts, screenshots, pixel art from the game's repository) and
|
||||
`npm run shots` (Playwright screenshots at every width).
|
||||
|
||||
## Where the data comes from
|
||||
|
||||
- **The game's `main` on GitHub**, all files from one commit (`horizonRef` in build.mjs): `README.md` (parity
|
||||
table, harness count), `launcher/links.json` (Discord, Sponsor), `docs/api/*.md` (the `/docs/api` pages),
|
||||
`crates/horizon-theme/tokens.css` (design tokens, generated by `cargo run -p xtask -- tokens` there),
|
||||
`roadmap/roadmap.json` (the hub, and `/data/roadmap.json`, which Kira reads; generated by `tools/roadmap.py build`
|
||||
there). Change those in the game's repository, never here.
|
||||
- **GitHub's API at build time**: releases, contributors, the roadmap's issues and pull requests.
|
||||
- **Other services**: the mod registry's index (ludovic111/horizon-mods), Kira's contributors list, the telemetry
|
||||
server (players' benchmarks, the hub's forms; `telemetry.playhorizon.dev`).
|
||||
- **This repository**: the pages (`src/`), the player docs (`docs/`), our benchmark runs (`data/benchmarks/`,
|
||||
`public/benchmarks.json`), assets (`public/`).
|
||||
|
||||
## Content-Security-Policy
|
||||
|
||||
`Caddyfile` serves a strict CSP. Inline `<script>`/`<style>` blocks are allowed by the hashes the build writes
|
||||
(`.csp-script`, `.csp-style`); inline handlers, `style="…"` attributes and `javascript:` URLs are blocked. A new
|
||||
origin (fetch, image, font) needs an entry in the Caddyfile's CSP, and third-party scripts, fonts and trackers are not
|
||||
allowed at all.
|
||||
|
||||
## Deploy
|
||||
|
||||
Railway project `horizon-site`, service `site`, environment `production`: every push to `main` here goes live within
|
||||
minutes (Dockerfile: Node builds, Caddy serves). When the files above change on the game's `main`, the site is
|
||||
redeployed from source: for now by `~/code/horizon-parity/tools/site_redeploy_watch.sh` on the Mac mini (every 10
|
||||
minutes), later by the game repository's `site-redeploy.yml` workflow. A failed build keeps the previous deployment live. After a deploy, check
|
||||
https://playhorizon.dev (/, /hub, /mods, /docs/, /releases, /benchmarks, /data/roadmap.json).
|
||||
|
||||
## Rules
|
||||
|
||||
- Branch work in a worktree under `.claude/worktrees/<name>`; keep the main checkout on `main`.
|
||||
- The site's rules (disclaimer, no Minecraft logo, no trackers, reduced motion): README "Rules the site keeps".
|
||||
- Agents' PRs start with `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, comments with
|
||||
`🤖 Agent:`; commits end with a `Co-Authored-By:` line for the agent.
|
||||
- Never commit Mojang's files, secrets, or `dist/` and `shots/`.
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
# Railway builds this with the service's root directory set to `website`.
|
||||
# Stage 1 renders dist/ from src/ and the README on main (no npm dependencies needed).
|
||||
# Railway builds this repository with the Dockerfile at its root.
|
||||
# Stage 1 renders dist/ from src/ and the game repository's files on its main (no npm dependencies needed).
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /site
|
||||
COPY build.mjs ./
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Horizon's website
|
||||
|
||||
Live at **https://playhorizon.dev**.
|
||||
Live at **https://playhorizon.dev**. The game, the launcher and their developer docs are in
|
||||
[ludovic111/horizon](https://github.com/ludovic111/horizon) (public); this repository (private) is only the site.
|
||||
|
||||
A landing page, a Releases page, a Benchmarks page and a documentation section in Horizon's design language (`docs/design-system.md`), with the tokens of
|
||||
`crates/horizon-theme` (generated into `src/tokens.css` by `cargo run -p xtask -- tokens`):
|
||||
`crates/horizon-theme` (generated into the game repository's `crates/horizon-theme/tokens.css` by
|
||||
`cargo run -p xtask -- tokens`, and read from there at build time):
|
||||
the Blocks look: Pixelify Sans headings over Geist text, Horizon's purple in a light and a dark
|
||||
appearance (the system's, or the toggle's), square panels with an outline, a bevel and a lip, a
|
||||
stone texture and a pixel landscape drawn in code (`scripts/art.mjs`), and short, eased motion. A headline and the download, then a scroll through what it is, the screenshots, the parity grid, the launcher, the newest
|
||||
@@ -13,6 +15,32 @@ no runtime dependencies.
|
||||
Home, Releases, Benchmarks and every Docs page share the header and phone menu rendered by
|
||||
`build.mjs`, with the same links, theme switch and Download button.
|
||||
|
||||
## Working on it
|
||||
|
||||
You need Node 22 or newer; nothing to install for the site itself.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/ludovic111/horizon-site && cd horizon-site
|
||||
node build.mjs && node serve.mjs # build dist/ and open http://localhost:4173
|
||||
node --test src/ scripts/ # the tests; run them before you push
|
||||
```
|
||||
|
||||
- **A push to `main` goes live.** Railway rebuilds and deploys https://playhorizon.dev within a few minutes of every
|
||||
push to `main`. If the build fails (a broken docs link, a missing image, an error in `build.mjs`), the previous
|
||||
version stays online and nothing breaks; fix it and push again. For anything bigger than a typo, push a branch and
|
||||
open a pull request first.
|
||||
- **Some content isn't in this repository.** The parity table and harness count (the game's `README.md`), the
|
||||
Discord and Sponsor links (`launcher/links.json`), the API pages under `/docs/api` (`docs/api/`), the colours and
|
||||
spacing (`crates/horizon-theme/tokens.css`) and the hub's roadmap (`roadmap/roadmap.json`) come from
|
||||
[ludovic111/horizon](https://github.com/ludovic111/horizon) at build time. Change them there; the site redeploys
|
||||
by itself when they change on its `main`.
|
||||
- **Content-Security-Policy.** The site is served with a strict CSP (`Caddyfile`). Inline `<script>` and `<style>`
|
||||
blocks in the page templates are fine: the build hashes each one and the server allows exactly those. Inline event
|
||||
handlers (`onclick="…"`), `style="…"` attributes and `javascript:` links are blocked: use a class and the
|
||||
stylesheet, or a listener in the page's script. Every script, style, font and image comes from this site; the only
|
||||
other origins allowed are GitHub (API, raw files, avatars), the mod registry's index and the telemetry server. A new
|
||||
origin needs a change to the CSP in the `Caddyfile`, and the site loads no third-party scripts, fonts or trackers.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
@@ -28,7 +56,6 @@ src/benchmarks.html Horizon against vanilla 26.3 on our machines, the hardware p
|
||||
src/hub.html the hub (/hub, /roadmap redirects to its Roadmap tab): Overview, Roadmap, Kanban, Journal, Contribute, a Simple / Technical switch
|
||||
src/mods.html the registry's list (/mods) and a page per project (/mods/<id>), one template
|
||||
src/docs.html the template of every documentation page (/docs/...), with its left navigation
|
||||
src/tokens.css the design tokens, generated from crates/horizon-theme (do not edit)
|
||||
src/style.css every style, on top of the tokens
|
||||
src/main.js the home page: download buttons, recent releases, links, contributors, nav, tabs, lightbox, motion
|
||||
src/chrome.js the light/dark switch, the phone menu and GitHub fetching, shared by the pages
|
||||
@@ -42,9 +69,9 @@ src/estimate.js estimates for unmeasured hardware from the nearest measured m
|
||||
src/markdown.js a small Markdown renderer (escapes everything), shared by build and page
|
||||
src/releases.js the home page's "Recently added" and the Releases page's markup, shared by build and page
|
||||
src/contributors.js the contributors' filtering and markup, shared by build and page
|
||||
src/hub.js the hub's roadmap, board and journal from data/roadmap.json, GitHub's issues and the releases, shared by build and page; /data/journal.json
|
||||
src/hub.js the hub's roadmap, board and journal from the game repository's roadmap/roadmap.json, GitHub's issues and the releases, shared by build and page; /data/journal.json
|
||||
src/hub-page.js the hub page: tabs by #hash, the Simple / Technical switch (localStorage), the area filter, live states
|
||||
data/roadmap.json the roadmap, generated by tools/roadmap.py from roadmap/roadmap.toml (do not edit); served at /data/roadmap.json
|
||||
src/fixtures/roadmap.json a snapshot of the roadmap for the hub's tests (the build reads the live one)
|
||||
public/ fonts, screenshots, icons, og.png, robots.txt, benchmarks.json: copied as is (sitemap.xml is written by the build)
|
||||
data/benchmarks/ our raw benchmark reports (tools/perf_bench.py) and fleet.json, the list scripts/benchmarks.mjs reads
|
||||
scripts/benchmarks.mjs benchmark reports -> public/benchmarks.json (ours, and players' groups of 3+ installs)
|
||||
@@ -60,11 +87,12 @@ shots/ (gitignored) full-page screenshots at 360–1920 px, a 1250×
|
||||
|
||||
- **Built in** (`build.mjs`): Horizon's registry (`/mods`, one page per project) from the index ludovic111/horizon-mods
|
||||
publishes (`HORIZON_MOD_INDEX=<file>` builds from a test index; docs/api/mod-index.md); the parity table, its score, harness count and audit date,
|
||||
all from `README.md`; the documentation pages (`/docs`) from `website/docs/`; the Discord and Sponsor links from
|
||||
`launcher/links.json`; the latest releases and the contributors (`/repos/ludovic111/horizon/contributors`,
|
||||
all from the game's `README.md`; the documentation pages (`/docs`) from `docs/` here; the Discord and Sponsor links
|
||||
from the game's `launcher/links.json`; the design tokens from its `crates/horizon-theme/tokens.css`; the hub's roadmap
|
||||
from its `roadmap/roadmap.json` (also served as `/data/roadmap.json`, which Kira reads); the latest releases and the contributors (`/repos/ludovic111/horizon/contributors`,
|
||||
bots left out, the owner first), when GitHub answers; without an answer the build still succeeds and
|
||||
the Contributors block and its menu entries stay hidden until the page loads them. Locally these come from
|
||||
the checkout; on Railway, which builds only this folder, from `main` on GitHub. The build
|
||||
the Contributors block and its menu entries stay hidden until the page loads them. The game's files come from its `main` on GitHub, all from
|
||||
the same commit (`HORIZON_CHECKOUT=<path>` reads a local checkout instead). The build
|
||||
fails if the README has no parity table, so a broken README never deploys.
|
||||
- **Fetched by the page**: the registry's index (a project merged after the build shows at once), the releases (the download button picks the newest
|
||||
`launcher-v*` release's asset for the visitor's OS, the home page lists the newest game releases, and
|
||||
@@ -84,7 +112,7 @@ between pages and every image, and fails with the list when one is broken. `/ser
|
||||
redirects to `/docs/hosting`.
|
||||
|
||||
The API portal is the repository's `docs/api/`, not a copy: `nav.json` lists its README as `api` (`/docs/api`) and
|
||||
its pages as `api/<page>` (`/docs/api/<page>`), read from the checkout or from `main` on GitHub like the README.
|
||||
its pages as `api/<page>` (`/docs/api/<page>`), read from the game's `main` on GitHub like the README.
|
||||
Links between its pages become the site's, every other relative link goes to the file on GitHub, and the same link
|
||||
check applies. Markdown tables render as tables. The other developer docs in `docs/` stay on GitHub and are linked
|
||||
from the Contributing page.
|
||||
@@ -92,8 +120,9 @@ from the Contributing page.
|
||||
## Run it locally
|
||||
|
||||
```sh
|
||||
cd website
|
||||
node build.mjs && node serve.mjs # http://localhost:4173
|
||||
node --test src/ scripts/ # the tests (npm test)
|
||||
HORIZON_CHECKOUT=../horizon node build.mjs # against a local checkout of the game (unmerged README, docs/api, roadmap)
|
||||
```
|
||||
|
||||
Asset and screenshot scripts need the dev dependencies (sharp, wawoff2, Playwright):
|
||||
@@ -101,7 +130,7 @@ Asset and screenshot scripts need the dev dependencies (sharp, wawoff2, Playwrig
|
||||
```sh
|
||||
npm install
|
||||
GITHUB_TOKEN=$(gh auth token) node build.mjs # optional token: bakes the releases past the API rate limit
|
||||
npm run assets # after a font, screenshot or logo changes in the repo
|
||||
npm run assets # after a font, screenshot or logo changes in the game's repo (../horizon or HORIZON_CHECKOUT)
|
||||
npm run shots # shots/*.png (gitignored; SHOTS=dir to write elsewhere) and public/og.png
|
||||
```
|
||||
|
||||
@@ -112,20 +141,23 @@ and characters per line, for 360 to 1920 px wide and a 1250×700 window.
|
||||
|
||||
## Railway
|
||||
|
||||
The service deploys `main` (Railway's GitHub integration; there is no `website` branch any more) when `/website/**`, `README.md` or `launcher/links.json` change, and answers on https://playhorizon.dev. Its
|
||||
Railway project `horizon-site`, service `site`, environment `production`. It deploys `main` of this repository on
|
||||
every push (Railway's GitHub integration, root directory `/`), and answers on https://playhorizon.dev. Its
|
||||
`*.up.railway.app` address redirects there with a 301 (the Caddyfile), so search engines index
|
||||
one URL; the page also names it as canonical, in its Open Graph tags and in `sitemap.xml`.
|
||||
Railway's healthcheck reaches `$PORT` under its own host name and is served normally.
|
||||
|
||||
Create a service from the GitHub repo with **root directory `website`**. `railway.json`
|
||||
selects the Dockerfile: a Node stage runs `build.mjs` (reading the README from `main`), and
|
||||
`railway.json` selects the Dockerfile: a Node stage runs `build.mjs` (reading the game's files from its `main`), and
|
||||
a Caddy stage serves `dist/` on `$PORT` with compression, cache headers, a strict
|
||||
Content-Security-Policy and the 404 page. Its watch patterns also redeploy when the README
|
||||
or `launcher/links.json` change on main. No variables are needed; an optional
|
||||
Content-Security-Policy and the 404 page. When the files the site reads change on the game's `main` (`README.md`,
|
||||
`launcher/links.json`, `docs/api/`, `roadmap/roadmap.json`, `crates/horizon-theme/tokens.css`), a watcher runs
|
||||
`railway redeploy --from-source`, so the site rebuilds from the new files (for now a loop on the maintainer's machine
|
||||
every 10 minutes; later the game repository's `site-redeploy.yml` workflow, with a project token scoped to this
|
||||
environment). No variables are needed; an optional
|
||||
`GITHUB_TOKEN` (read-only, public repos) lets the build bake the releases even when
|
||||
GitHub's anonymous rate limit is hit. `REGISTRY_UPSTREAM`
|
||||
(`horizon-mod-registry.railway.internal:8080`) sends `/api/mods/*` to the registry's submission
|
||||
service (`crates/horizon-mod-registry`); unset, `/mods/submit` says the form isn't open.
|
||||
service (`crates/horizon-mod-registry` in the game's repository); unset, `/mods/submit` says the form isn't open.
|
||||
|
||||
## Rules the site keeps
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
// the community links and contributors baked in, the documentation (docs/*.md, one page each, listed in
|
||||
// docs/nav.json), plus public/ and fingerprinted CSS and JS. No dependencies.
|
||||
//
|
||||
// Railway builds only website/, so the README and links.json come from the repo when they
|
||||
// are next to it (a local checkout) and from main on GitHub otherwise. The documentation lives in website/, except the
|
||||
// API portal (docs/api/), read the same way as the README. The latest releases and
|
||||
// the contributors are baked in when GitHub answers, so the page reads well before (or without) JavaScript.
|
||||
// Horizon's own files (the README, launcher/links.json, the API portal in docs/api/, the design tokens and the roadmap)
|
||||
// live in the game's repository, ludovic111/horizon, which is public: they come from its main on GitHub, pinned to one
|
||||
// commit so every file is from the same one, or from a local checkout with HORIZON_CHECKOUT=<path>. The rest of the
|
||||
// documentation lives here. The latest releases and the contributors are baked in when GitHub answers, so the page
|
||||
// reads well before (or without) JavaScript.
|
||||
import { readFile, writeFile, mkdir, rm, cp } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -23,15 +24,40 @@ import { INDEX_URL, KINDS, SORTS, SUBMIT, listHtml, projectHtml, readIndex, sort
|
||||
const site = dirname(fileURLToPath(import.meta.url));
|
||||
const dist = join(site, "dist");
|
||||
const REPO = "ludovic111/horizon";
|
||||
const RAW = `https://raw.githubusercontent.com/${REPO}/main/`;
|
||||
const DEFAULT_LINKS = { discord: "https://discord.gg/HcTeMZxe5E", sponsor: "https://github.com/sponsors/ludovic111" };
|
||||
|
||||
async function repoFile(path) {
|
||||
const local = join(site, "..", path);
|
||||
if (existsSync(local)) return [await readFile(local, "utf8"), "the checkout"];
|
||||
const res = await fetch(RAW + path, { signal: AbortSignal.timeout(15000) });
|
||||
if (!res.ok) throw new Error(`${path}: ${res.status} from GitHub`);
|
||||
return [await res.text(), "main on GitHub"];
|
||||
const CHECKOUT = process.env.HORIZON_CHECKOUT;
|
||||
if (CHECKOUT && !existsSync(join(CHECKOUT, "README.md"))) throw new Error(`HORIZON_CHECKOUT=${CHECKOUT}: no README.md there`);
|
||||
|
||||
// main's commit: raw.githubusercontent.com caches a branch's files for minutes, a commit's are fixed. Without an
|
||||
// answer from the API, main itself.
|
||||
let horizonRef = "main";
|
||||
if (!CHECKOUT) {
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${REPO}/commits/main`, {
|
||||
headers: { Accept: "application/vnd.github.sha", ...(process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}) },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`${res.status}`);
|
||||
horizonRef = (await res.text()).trim();
|
||||
} catch (e) {
|
||||
console.warn(`main's commit: ${e.message}; reading main`);
|
||||
}
|
||||
}
|
||||
|
||||
// A file of the game's repository: the first of `paths` that exists (a file that moved there lists its new path
|
||||
// first), with where it came from.
|
||||
async function repoFile(...paths) {
|
||||
for (const path of paths) {
|
||||
if (CHECKOUT) {
|
||||
if (existsSync(join(CHECKOUT, path))) return [await readFile(join(CHECKOUT, path), "utf8"), `the checkout ${CHECKOUT}`];
|
||||
continue;
|
||||
}
|
||||
const res = await fetch(`https://raw.githubusercontent.com/${REPO}/${horizonRef}/${path}`, { signal: AbortSignal.timeout(15000) });
|
||||
if (res.ok) return [await res.text(), horizonRef === "main" ? "main on GitHub" : `main on GitHub (${horizonRef.slice(0, 9)})`];
|
||||
if (res.status !== 404) throw new Error(`${path}: ${res.status} from GitHub`);
|
||||
}
|
||||
throw new Error(`${paths.join(" or ")}: not in ${CHECKOUT || `${REPO}@${horizonRef}`}`);
|
||||
}
|
||||
|
||||
// The README: required, a site with a stale or missing parity table is worse than no deploy.
|
||||
@@ -95,10 +121,12 @@ try {
|
||||
console.warn(`contributors: ${e.message}; the page will load them itself`);
|
||||
}
|
||||
|
||||
// The hub's roadmap (data/roadmap.json, generated from roadmap/roadmap.toml by tools/roadmap.py) and the live state
|
||||
// The hub's roadmap (roadmap/roadmap.json in the game's repository, generated from roadmap/roadmap.toml by
|
||||
// tools/roadmap.py) and the live state
|
||||
// of the issues and pull requests its items link: the open ones and those closed lately, then (with a token) the
|
||||
// older ones in one GraphQL request. Without GitHub the hub shows the links without their state.
|
||||
const roadmap = JSON.parse(await readFile(join(site, "data", "roadmap.json"), "utf8"));
|
||||
const [roadmapText] = await repoFile("roadmap/roadmap.json", "website/data/roadmap.json");
|
||||
const roadmap = JSON.parse(roadmapText);
|
||||
const linked = new Set(roadmap.items.flatMap((i) => (i.links || []).map((l) => Number((l.match(/^#(\d+)$/) || [])[1])).filter(Boolean)));
|
||||
let hubLive = [];
|
||||
try {
|
||||
@@ -248,9 +276,10 @@ for (const file of modules) {
|
||||
await writeFile(join(dist, "assets", names[file]), code);
|
||||
}
|
||||
const jsName = names["main.js"];
|
||||
// The design tokens come first: src/tokens.css is generated from crates/horizon-theme
|
||||
// The design tokens come first: the game's repository generates them from crates/horizon-theme
|
||||
// (`cargo run -p xtask -- tokens`), so the page shares the launcher's and the game's values.
|
||||
const css = (await readFile(join(site, "src/tokens.css"), "utf8")) + "\n" + (await readFile(join(site, "src/style.css"), "utf8"));
|
||||
const [tokens] = await repoFile("crates/horizon-theme/tokens.css", "website/src/tokens.css");
|
||||
const css = tokens + "\n" + (await readFile(join(site, "src/style.css"), "utf8"));
|
||||
// The stylesheet is inlined into the page, so the first paint needs no second request.
|
||||
const cssName = `style.${fingerprint(css)}.css`;
|
||||
await writeFile(join(dist, "assets", cssName), css);
|
||||
@@ -369,7 +398,7 @@ const values = {
|
||||
};
|
||||
|
||||
// --- The documentation ---------------------------------------------------------------------
|
||||
// website/docs/<slug>.md: "# Title", a one-paragraph lede, then the body. docs/nav.json orders them in groups.
|
||||
// docs/<slug>.md: "# Title", a one-paragraph lede, then the body. docs/nav.json orders them in groups.
|
||||
// The API portal is the repository's docs/api/ (the pages and their generated references): nav.json lists it as
|
||||
// `api` (its README, /docs/api) and `api/<page>` (/docs/api/<page>). Its links are written for GitHub, so links
|
||||
// between its pages become the site's and every other relative link goes to the file on GitHub.
|
||||
@@ -393,7 +422,7 @@ const docs = [];
|
||||
for (const { group, pages: slugs } of docsNav) {
|
||||
for (const slug of slugs) {
|
||||
const api = slug === "api" || slug.startsWith("api/");
|
||||
const file = api ? apiSource(slug) : `website/docs/${slug}.md`;
|
||||
const file = api ? apiSource(slug) : `docs/${slug}.md`;
|
||||
let text = (api ? (await repoFile(file))[0] : await readFile(join(site, "docs", `${slug}.md`), "utf8")).replace(/\r\n?/g, "\n");
|
||||
if (api) text = apiLinks(text.replace(/^<!--[\s\S]*?-->\n/, ""), file);
|
||||
const m = text.match(/^# (.+)\n\n((?:[^\n]+\n)+)\n/);
|
||||
@@ -454,7 +483,8 @@ const docPages = renderedDocs.map((d, i) => {
|
||||
DOC_TOC: toc.length >= 4 ? ` <nav class="docs-toc" aria-label="On this page"><p>On this page</p><ul>${toc.map((h) => `<li><a href="#${h.id}">${escape(h.text)}</a></li>`).join("")}</ul></nav>` : "",
|
||||
DOC_BODY: d.html,
|
||||
DOC_PAGER: pagerLink(renderedDocs[i - 1], "prev") + pagerLink(renderedDocs[i + 1], "next"),
|
||||
DOC_FILE: d.file,
|
||||
// The API portal is the game's public repository; the other pages are this private one's, so no edit link.
|
||||
DOC_EDIT: d.slug === "api" || d.slug.startsWith("api/") ? `<a href="https://github.com/${REPO}/blob/main/${d.file}">Edit this page on GitHub</a> · ` : "",
|
||||
SITE_NAV: siteNav("/docs/"),
|
||||
},
|
||||
};
|
||||
@@ -531,7 +561,7 @@ for (const { file, values: own } of docPages) {
|
||||
|
||||
// The roadmap for other readers (Kira, scripts): /data/roadmap.json, as tools/roadmap.py wrote it.
|
||||
await mkdir(join(dist, "data"), { recursive: true });
|
||||
await cp(join(site, "data", "roadmap.json"), join(dist, "data", "roadmap.json"));
|
||||
await writeFile(join(dist, "data", "roadmap.json"), roadmapText);
|
||||
// The journal for Kira's #updates and its X posts: /data/journal.json, rebuilt with every deploy.
|
||||
await writeFile(join(dist, "data", "journal.json"), JSON.stringify(journalJson(releases, hubLive, roadmap), null, 1) + "\n");
|
||||
|
||||
|
||||
@@ -6,11 +6,10 @@ Questions and ideas are welcome on [Discord](https://discord.gg/HcTeMZxe5E). Bug
|
||||
|
||||
## The repository
|
||||
|
||||
[github.com/ludovic111/horizon](https://github.com/ludovic111/horizon) holds the game, the launcher and this website, plus the tools, tests and docs that build, check and release them.
|
||||
[github.com/ludovic111/horizon](https://github.com/ludovic111/horizon) holds the game and the launcher, plus the tools, tests and docs that build, check and release them. This website is built in a separate repository; its API pages come from the game's `docs/api/`.
|
||||
|
||||
- `crates/` is the game, as Rust crates: the client, the server, the world and protocol code, the mod system, the design tokens and more.
|
||||
- `launcher/` is the launcher, a GPUI app with its own Cargo workspace.
|
||||
- `website/` is this site and its documentation, in `website/docs/`.
|
||||
- `tools/` holds the parity harness drivers, the parity runner, the release tools and the self-check.
|
||||
- `tests/` holds the Python tests, `benchmarks/` the published results and `packaging/` the installers.
|
||||
- `docs/` holds the developer documentation, [indexed here](https://github.com/ludovic111/horizon/blob/main/docs/README.md): the native client, the design system, world generation, the parity workflow, the release flow and more.
|
||||
|
||||
+1
-2
@@ -2,8 +2,7 @@
|
||||
"$schema": "https://railway.com/railway.schema.json",
|
||||
"build": {
|
||||
"builder": "DOCKERFILE",
|
||||
"dockerfilePath": "Dockerfile",
|
||||
"watchPatterns": ["/website/**", "/README.md", "/launcher/links.json", "/docs/api/**"]
|
||||
"dockerfilePath": "Dockerfile"
|
||||
},
|
||||
"deploy": {
|
||||
"healthcheckPath": "/",
|
||||
|
||||
+2
-1
@@ -8,7 +8,8 @@ import { fileURLToPath } from "node:url";
|
||||
import sharp from "sharp";
|
||||
|
||||
const site = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repo = join(site, "..");
|
||||
// The game's repository, for the design tokens and the fonts: HORIZON_CHECKOUT, or ../horizon beside this one.
|
||||
export const repo = process.env.HORIZON_CHECKOUT || join(site, "..", "horizon");
|
||||
|
||||
// The same small fixed random sequence as art.rs (xorshift), so the art is the same every time.
|
||||
function rng(seed) {
|
||||
|
||||
+3
-3
@@ -1,15 +1,15 @@
|
||||
// Copies the launcher's fonts and the repo's screenshots into public/, draws the pixel art,
|
||||
// converted for the web (woff2, webp in two widths). Run it after any of them change:
|
||||
// `npm run assets`. Its output is committed, because Railway builds only website/.
|
||||
// `npm run assets`, with the game's repository beside this one (or HORIZON_CHECKOUT=<path>). Its output is
|
||||
// committed, because Railway builds only this repository.
|
||||
import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import sharp from "sharp";
|
||||
import wawoff2 from "wawoff2";
|
||||
import { writeArt } from "./art.mjs";
|
||||
import { repo, writeArt } from "./art.mjs";
|
||||
|
||||
const site = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repo = join(site, "..");
|
||||
const pub = join(site, "public");
|
||||
|
||||
const fonts = [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// node --test (npm test in website/). The fixtures are shared with the telemetry server's summary test.
|
||||
// node --test (npm test). The fixtures are the telemetry server's (crates/horizon-telemetry-server/tests/fixtures/
|
||||
// benchmarks/ in the game's repository); the copy here must stay the same, which the last test checks when online.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
@@ -89,3 +90,28 @@ test("a display cap on our run is kept, and the comparison says at least", async
|
||||
assert.equal(compareRows(fps, h, { medians: { avg_fps: 120 } }).text, "at least 1.2x faster");
|
||||
assert.equal(compareRows(fps, h, { medians: { avg_fps: 200 } }).wins, "none");
|
||||
});
|
||||
|
||||
// The server's copy on the game's main: both sides must make the same groups from the same items. Skipped offline.
|
||||
test("the fixtures are the telemetry server's", async (t) => {
|
||||
const repo = process.env.HORIZON_CHECKOUT;
|
||||
for (const name of ["items.jsonl", "expected.json"]) {
|
||||
const ours = readFileSync(fixture(name), "utf8");
|
||||
let theirs = null;
|
||||
if (repo) {
|
||||
for (const dir of ["crates/horizon-telemetry-server/tests/fixtures/benchmarks", "website/scripts/fixtures/benchmarks"]) {
|
||||
try { theirs = readFileSync(`${repo}/${dir}/${name}`, "utf8"); break; } catch {}
|
||||
}
|
||||
} else {
|
||||
for (const dir of ["crates/horizon-telemetry-server/tests/fixtures/benchmarks", "website/scripts/fixtures/benchmarks"]) {
|
||||
try {
|
||||
const res = await fetch(`https://raw.githubusercontent.com/ludovic111/horizon/main/${dir}/${name}`, { signal: AbortSignal.timeout(10000) });
|
||||
if (res.ok) { theirs = await res.text(); break; }
|
||||
} catch {
|
||||
return t.skip("GitHub unreachable");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(theirs !== null, `${name}: not in the game's repository`);
|
||||
assert.equal(ours, theirs, `${name} differs from the telemetry server's: copy it from the game's repository`);
|
||||
}
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
# Benchmark summary fixtures
|
||||
|
||||
Shared by the website's aggregator test (`website/scripts/benchmarks.test.mjs`) and the telemetry server's
|
||||
`GET /v1/benchmarks/summary` test: the same `benchmark` items in, the same groups out.
|
||||
A copy of the telemetry server's (`crates/horizon-telemetry-server/tests/fixtures/benchmarks/` in the game's
|
||||
repository), used by `scripts/benchmarks.test.mjs`: the same `benchmark` items in, the same groups out as its
|
||||
`GET /v1/benchmarks/summary` test. The test checks that the copy matches the game's main.
|
||||
|
||||
- `items.jsonl`: one stored item per line (`install_id`, `day`, `version`, `data`: a benchmark report). Group
|
||||
RTX 4080 + i7-13700KF at render distance 12 has three installs (one with two runs); the same install at 32,
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@
|
||||
<nav class="docs-pager" aria-label="Next and previous pages">
|
||||
{{DOC_PAGER}}
|
||||
</nav>
|
||||
<p class="docs-edit"><a href="https://github.com/ludovic111/horizon/blob/main/{{DOC_FILE}}">Edit this page on GitHub</a> · Spotted something wrong? <a href="https://github.com/ludovic111/horizon/issues">Open an issue</a> or ask on <a href="{{DISCORD}}">Discord</a>.</p>
|
||||
<p class="docs-edit">{{DOC_EDIT}}Spotted something wrong? <a href="https://github.com/ludovic111/horizon/issues">Open an issue</a> or ask on <a href="{{DISCORD}}">Discord</a>.</p>
|
||||
<p class="disclaimer">Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft.</p>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
File renamed without changes.
+1
-1
@@ -1,4 +1,4 @@
|
||||
// The hub (/hub): the roadmap from website/data/roadmap.json (generated from roadmap/roadmap.toml by
|
||||
// The hub (/hub): the roadmap from the game repository's roadmap/roadmap.json (generated from roadmap/roadmap.toml by
|
||||
// tools/roadmap.py), with each item's issues and pull requests in their live GitHub state. Shared by build.mjs
|
||||
// (baked into the page) and hub-page.js (refreshed from GitHub in the browser). Every item says the same thing
|
||||
// twice: `plain` (.hub-plain) for players and `dev` (.hub-dev) for contributors; the page's Simple / Technical
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { boardFrom, boardHtml, creditFrom, creditHtml, fixesIn, itemOf, journalFrom, journalHtml, journalJson, overviewHtml, progress, roadmapHtml, slimIssue, suggestItemsHtml } from "./hub.js";
|
||||
|
||||
const roadmap = JSON.parse(readFileSync(new URL("../data/roadmap.json", import.meta.url), "utf8"));
|
||||
const roadmap = JSON.parse(readFileSync(new URL("./fixtures/roadmap.json", import.meta.url), "utf8"));
|
||||
|
||||
const pr = (number, extra = {}) => ({ number, title: `PR ${number}`, state: "open", pull_request: { merged_at: null }, user: { login: "a" }, labels: [], assignees: [], body: "", ...extra });
|
||||
const issue = (number, extra = {}) => ({ number, title: `Issue ${number}`, state: "open", user: { login: "b" }, labels: [{ name: "bug" }], assignees: [], ...extra });
|
||||
|
||||
-167
@@ -1,167 +0,0 @@
|
||||
/* Generated from crates/horizon-theme by `cargo run -p xtask -- tokens`; do not edit.
|
||||
docs/design-system.md describes the tokens. */
|
||||
|
||||
:root {
|
||||
--page: #ece7f7;
|
||||
--plate-soft: #e9e3f8;
|
||||
--plate: #ddd4f3;
|
||||
--surface: #f8f6fd;
|
||||
--rule: #e4ddf4;
|
||||
--ink: #1b1530;
|
||||
--ink-soft: #3b3354;
|
||||
--muted: #585071;
|
||||
--accent: #5333c9;
|
||||
--on-accent: #ffffff;
|
||||
--danger: #a3301d;
|
||||
--success: #136336;
|
||||
--warn: #7a4f00;
|
||||
--side: #e2dbf2;
|
||||
--panel: #f8f6fd;
|
||||
--raised: #e9e3f8;
|
||||
--button: #ddd4f3;
|
||||
--button-bevel: #f4f0fe;
|
||||
--well: #f3f0fb;
|
||||
--bevel: #ffffff;
|
||||
--lip: #c3b8e2;
|
||||
--outline: #2b2244;
|
||||
--face: #5b3fd6;
|
||||
--face-bevel: #8069ef;
|
||||
--face-lip: #3a2694;
|
||||
--on-face: #ffffff;
|
||||
color-scheme: light;
|
||||
--font: "Geist", ui-sans-serif, system-ui, sans-serif;
|
||||
--mono: "Geist Mono", ui-monospace, monospace;
|
||||
--pixel: "Pixelify Sans", "Geist", ui-sans-serif, sans-serif;
|
||||
--block-outline: 2px;
|
||||
--block-bevel: 2px;
|
||||
--block-lip: 4px;
|
||||
--text-micro: 11px;
|
||||
--text-micro-line: 1.5;
|
||||
--text-micro-tracking: 0.04em;
|
||||
--text-micro-weight: 400;
|
||||
--text-caption: 12px;
|
||||
--text-caption-line: 1.5;
|
||||
--text-caption-tracking: 0em;
|
||||
--text-caption-weight: 400;
|
||||
--text-label: 13px;
|
||||
--text-label-line: 1.5;
|
||||
--text-label-tracking: 0em;
|
||||
--text-label-weight: 400;
|
||||
--text-ui: 14px;
|
||||
--text-ui-line: 1.5;
|
||||
--text-ui-tracking: 0em;
|
||||
--text-ui-weight: 400;
|
||||
--text-body: clamp(15px, calc(15px + 2 * (100vw - 375px) / 1065), 17px);
|
||||
--text-body-line: 1.6;
|
||||
--text-body-tracking: 0em;
|
||||
--text-body-weight: 400;
|
||||
--text-heading: clamp(20px, calc(20px + 4 * (100vw - 375px) / 1065), 24px);
|
||||
--text-heading-line: 1.3;
|
||||
--text-heading-tracking: -0.01em;
|
||||
--text-heading-weight: 500;
|
||||
--text-title: clamp(32px, calc(32px + 16 * (100vw - 375px) / 1065), 48px);
|
||||
--text-title-line: 1.05;
|
||||
--text-title-tracking: -0.02em;
|
||||
--text-title-weight: 500;
|
||||
--text-display: clamp(34px, calc(34px + 30 * (100vw - 375px) / 1065), 64px);
|
||||
--text-display-line: 1.05;
|
||||
--text-display-tracking: -0.025em;
|
||||
--text-display-weight: 500;
|
||||
--text-wordmark: 18vw;
|
||||
--text-wordmark-line: 0.8;
|
||||
--text-wordmark-tracking: -0.04em;
|
||||
--text-wordmark-weight: 500;
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 24px;
|
||||
--space-6: 32px;
|
||||
--space-7: 48px;
|
||||
--space-8: 64px;
|
||||
--space-9: 96px;
|
||||
--gutter: 64px;
|
||||
--gutter-narrow: 16px;
|
||||
--radius: 8px;
|
||||
--radius-plate: 12px;
|
||||
--radius-pill: 999px;
|
||||
--rule-width: 1px;
|
||||
--control: 32px;
|
||||
--control-large: 40px;
|
||||
--segment: 28px;
|
||||
--field: 36px;
|
||||
--row: 86px;
|
||||
--index: 40px;
|
||||
--plate-bleed: 16px;
|
||||
--rise: 8px;
|
||||
--measure: 56ch;
|
||||
--quick: 150ms;
|
||||
--ease-quick: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--move: 240ms;
|
||||
--ease-move: cubic-bezier(0.23, 1, 0.32, 1);
|
||||
--enter: 600ms;
|
||||
--ease-enter: cubic-bezier(0.23, 1, 0.32, 1);
|
||||
--stagger: 40ms;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--page: #0f0d17;
|
||||
--plate-soft: #221e34;
|
||||
--plate: #2b2641;
|
||||
--surface: #1a1727;
|
||||
--rule: #120f1c;
|
||||
--ink: #ecedf3;
|
||||
--ink-soft: #c5c7d1;
|
||||
--muted: #a29cb8;
|
||||
--accent: #9a86ff;
|
||||
--on-accent: #ffffff;
|
||||
--danger: #f07a68;
|
||||
--success: #45c27a;
|
||||
--warn: #f0b84f;
|
||||
--side: #0b0a11;
|
||||
--panel: #1a1727;
|
||||
--raised: #221e34;
|
||||
--button: #2b2641;
|
||||
--button-bevel: #3d3560;
|
||||
--well: #0c0b12;
|
||||
--bevel: #342d52;
|
||||
--lip: #07060b;
|
||||
--outline: #05040a;
|
||||
--face: #5b3fd6;
|
||||
--face-bevel: #8a74ff;
|
||||
--face-lip: #3a2694;
|
||||
--on-face: #ffffff;
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--page: #0f0d17;
|
||||
--plate-soft: #221e34;
|
||||
--plate: #2b2641;
|
||||
--surface: #1a1727;
|
||||
--rule: #120f1c;
|
||||
--ink: #ecedf3;
|
||||
--ink-soft: #c5c7d1;
|
||||
--muted: #a29cb8;
|
||||
--accent: #9a86ff;
|
||||
--on-accent: #ffffff;
|
||||
--danger: #f07a68;
|
||||
--success: #45c27a;
|
||||
--warn: #f0b84f;
|
||||
--side: #0b0a11;
|
||||
--panel: #1a1727;
|
||||
--raised: #221e34;
|
||||
--button: #2b2641;
|
||||
--button-bevel: #3d3560;
|
||||
--well: #0c0b12;
|
||||
--bevel: #342d52;
|
||||
--lip: #07060b;
|
||||
--outline: #05040a;
|
||||
--face: #5b3fd6;
|
||||
--face-bevel: #8a74ff;
|
||||
--face-lip: #3a2694;
|
||||
--on-face: #ffffff;
|
||||
color-scheme: dark;
|
||||
}
|
||||
Reference in new issue
Block a user