Rename all "crew" mentions (en/es/eu) to the untranslated proper name "El valle" across hero, members, and features copy in i18n/ui.ts. Rewire the homepage launcher download list to the custom-launcher build output launcher.json (productName/version/files[]) instead of the never-produced launcher-manifest.json shape. site.ts now reads launcher.json and normalizes files[] -> internal builds[]; launcher.example.json is the committed fallback. Drop the stale launcher-manifest.example/schema, update .gitignore and docs (CLAUDE.md, plan/18). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
239 lines
10 KiB
TypeScript
239 lines
10 KiB
TypeScript
// Single source of truth for the landing page's *non-translatable* config:
|
|
// domains, URLs, theme, launcher files, and the structural shape of the
|
|
// stat/feature lists. Translatable copy lives in src/i18n/ui.ts.
|
|
// BASE_DOMAIN is read at build time; falls back to ulicraft.local.
|
|
// @ts-ignore — node builtins (no @types/node in this Astro SSG; same reason
|
|
// `process` below is untyped). Used only at build time to read the manifest.
|
|
import { readFileSync } from "node:fs";
|
|
import { MODS } from "./mods";
|
|
|
|
const BASE_DOMAIN = process.env.BASE_DOMAIN ?? "ulicraft.local";
|
|
|
|
// Installed-pack mod count, fed by tooling/build-modlist.py via mods.json (see
|
|
// ./mods.ts). Feeds the homepage "Mods" stat tile. When the generated catalogue
|
|
// is absent/empty (fresh checkout), fall back to the honest "50+" estimate.
|
|
const MOD_COUNT_KEY = MODS.count > 0 ? `${MODS.count}` : "50+";
|
|
|
|
// NMSR render mode for avatar tiles. AVATAR_MODE is the ServerCard online row +
|
|
// account preview (heads); ROSTER_AVATAR_MODE is the Members grid (full body by
|
|
// default). Read at build time, same as BASE_DOMAIN. e.g. "headiso" | "head" |
|
|
// "fullbody" | "fullbodyiso".
|
|
// @ts-ignore — `process` is untyped here (no @types/node), same as BASE_DOMAIN.
|
|
const AVATAR_MODE = process.env.AVATAR_MODE ?? "headiso";
|
|
// @ts-ignore — `process` is untyped here (no @types/node).
|
|
const ROSTER_AVATAR_MODE = process.env.ROSTER_AVATAR_MODE ?? "fullbodyiso";
|
|
|
|
// UlicraftLauncher download list. Source of truth is the custom-launcher repo's
|
|
// build output `launcher.json` (shape: productName/version/baseUrl/files[]),
|
|
// published to https://distribution.${BASE}/launcher/launcher.json. The release
|
|
// flow syncs that file into this dir as `launcher.json` (gitignored, generated)
|
|
// — same pattern as sync-server-mods.sh. It MAY NOT EXIST at build time, so we
|
|
// read it at runtime with fs (a static `import` would hard-fail the build when
|
|
// absent). Fall back to the committed `launcher.example.json` (the documented
|
|
// shape); if even that is unreadable, expose an empty file list so the homepage
|
|
// simply renders no download buttons. The raw launcher.json is normalized into
|
|
// the internal shape the page renders against (builds[]).
|
|
type LauncherBuild = {
|
|
os: string; // "windows" | "macos" | "linux"
|
|
arch: string; // "x64" | "arm64" | "universal"
|
|
format: string; // file extension: "exe" | "dmg" | "AppImage" | "deb" | "tar.gz" | …
|
|
filename: string;
|
|
url: string;
|
|
size?: number;
|
|
};
|
|
type LauncherFile = { filename: string; url: string; size?: number };
|
|
type LauncherManifest = {
|
|
product: string;
|
|
version: string;
|
|
builds: LauncherBuild[];
|
|
// Not present in launcher.json; the Fjord page degrades gracefully when absent.
|
|
fjordPack?: LauncherFile;
|
|
};
|
|
|
|
// Raw shape as produced by the custom-launcher build (UlicraftLauncher/dist/launcher.json).
|
|
type RawLauncherJson = {
|
|
productName?: string;
|
|
version?: string;
|
|
baseUrl?: string;
|
|
files?: Array<{ name: string; os: string; arch: string; ext: string; url: string; size?: number }>;
|
|
};
|
|
|
|
function normalizeLauncher(raw: RawLauncherJson): LauncherManifest {
|
|
return {
|
|
product: raw.productName ?? "UlicraftLauncher",
|
|
version: raw.version ?? "0.0.0",
|
|
builds: (raw.files ?? []).map((f) => ({
|
|
os: f.os,
|
|
arch: f.arch,
|
|
format: f.ext,
|
|
filename: f.name,
|
|
url: f.url,
|
|
size: f.size,
|
|
})),
|
|
};
|
|
}
|
|
|
|
function loadLauncherManifest(): LauncherManifest {
|
|
const empty: LauncherManifest = { product: "UlicraftLauncher", version: "0.0.0", builds: [] };
|
|
// Resolve relative to this module first, then fall back to a cwd-relative path:
|
|
// Astro bundles this module so import.meta.url may not point at src/data/ at
|
|
// build time, but the build runs from the landing root, so <cwd>/src/data/*
|
|
// resolves. Prefer the synced launcher.json, then the committed example.
|
|
// @ts-ignore — `process` is untyped here (no @types/node).
|
|
const cwd: string = typeof process !== "undefined" ? process.cwd() : ".";
|
|
const candidates: Array<string | URL> = [];
|
|
for (const name of ["launcher.json", "launcher.example.json"]) {
|
|
try {
|
|
candidates.push(new URL(name, import.meta.url));
|
|
} catch {
|
|
// import.meta.url unavailable — skip the module-relative candidate.
|
|
}
|
|
candidates.push(`${cwd}/src/data/${name}`);
|
|
}
|
|
for (const c of candidates) {
|
|
try {
|
|
const raw = JSON.parse(readFileSync(c, "utf8")) as RawLauncherJson;
|
|
if (raw && Array.isArray(raw.files)) return normalizeLauncher(raw);
|
|
} catch {
|
|
// try the next candidate (synced file missing → fall back to example)
|
|
}
|
|
}
|
|
return empty;
|
|
}
|
|
|
|
const LAUNCHER_MANIFEST = loadLauncherManifest();
|
|
|
|
// REGISTRATION_MODE (invite|open) is read at build time, same .env as Drasl's
|
|
// render-config.sh. "invite" => the register form must collect an invite code.
|
|
const REGISTRATION_MODE = process.env.REGISTRATION_MODE ?? "invite";
|
|
|
|
// REGISTRATION_SHOW_INVITE (true|false, default false=hidden) ONLY toggles the
|
|
// visibility of the invite-code field on the register form. The backend invite
|
|
// gate stays on Drasl's RequireInvite (driven by REGISTRATION_MODE). Decoupled on
|
|
// purpose: invites are usually admin-minted and the field is hidden by default.
|
|
// CAVEAT: if REGISTRATION_MODE=invite (backend requires a code) but this is
|
|
// false, public self-registration can't supply one — set both together.
|
|
// @ts-ignore — `process` is untyped here (no @types/node).
|
|
const REGISTRATION_SHOW_INVITE = process.env.REGISTRATION_SHOW_INVITE ?? "false";
|
|
|
|
// Minecraft / pack facts, surfaced in a few places.
|
|
const MC_VERSION = "1.21.1";
|
|
|
|
export const site = {
|
|
name: "Ulicraft",
|
|
baseDomain: BASE_DOMAIN,
|
|
mcVersion: MC_VERSION,
|
|
|
|
// Register form behaviour, driven by REGISTRATION_MODE in .env.
|
|
// true => Drasl RequireInvite = true (backend gate)
|
|
// false => open self-registration
|
|
registrationRequiresInvite: REGISTRATION_MODE === "invite",
|
|
// Whether the invite-code FIELD is shown on the register form (default false).
|
|
// Independent of the backend gate above — see REGISTRATION_SHOW_INVITE note.
|
|
registrationShowInvite: REGISTRATION_SHOW_INVITE === "true",
|
|
|
|
// Design knobs (replace the design's in-browser TweaksPanel). Baked at build.
|
|
// mood: "grass" | "nether" | "end"
|
|
// hero: "centered" | "split" | "spotlight"
|
|
// headFont: "blockblueprint" | "pixelify" | "8bit" | "silkscreen"
|
|
// dust: floating pixel particles in the hero
|
|
theme: {
|
|
mood: "grass",
|
|
hero: "centered",
|
|
headFont: "blockblueprint",
|
|
dust: true,
|
|
},
|
|
|
|
// Connect target shown to guests. No port needed (defaults to 25565).
|
|
serverAddress: `${BASE_DOMAIN}`,
|
|
|
|
// Drasl auth web UI + authlib-injector API root (public HTTPS).
|
|
authUrl: `https://auth.${BASE_DOMAIN}`,
|
|
authlibUrl: `https://auth.${BASE_DOMAIN}/authlib-injector`,
|
|
// Drasl REST API v2 root — the /register form calls this from the browser.
|
|
// Requires CORSAllowOrigins to include the apex in drasl config.toml.
|
|
draslApiUrl: `https://auth.${BASE_DOMAIN}/drasl/api/v2`,
|
|
|
|
// NMSR avatar renderer root. Player heads come from OUR Drasl skins, not
|
|
// Mojang/Crafatar — head URL is `${avatarUrl}/headiso/<uuid>?size=64`.
|
|
avatarUrl: `https://avatar.${BASE_DOMAIN}`,
|
|
|
|
// NMSR render mode for avatar tiles. Avatar URL = `${avatarUrl}/<mode>/<uuid>?size=N`.
|
|
// avatarMode → ServerCard online row + account preview (heads)
|
|
// rosterAvatarMode → PlayerRoster members grid (full body)
|
|
avatarMode: AVATAR_MODE,
|
|
rosterAvatarMode: ROSTER_AVATAR_MODE,
|
|
|
|
// Uptime Kuma status page (external link in footer).
|
|
statusUrl: `https://status.${BASE_DOMAIN}`,
|
|
|
|
// Live server card (ServerCard.astro). `slots` is the SSG fallback cap shown
|
|
// before/without JS; live data comes from our SELF-HOSTED mc-status service
|
|
// (docker/mc-status, mcutil wrapper) reverse-proxied same-origin at the apex
|
|
// /api/mcstatus/* path — no CORS, no third-party calls. It emits mcstatus.io
|
|
// v2 JSON, so the URL stays drop-in compatible. Uptime Kuma stays the history
|
|
// + alerting backend (plan/10).
|
|
status: {
|
|
slots: 10,
|
|
statusApi: `/api/mcstatus/v2/status/java/${BASE_DOMAIN}`,
|
|
// Registered-players roster (PlayerRoster.astro). Same mc-status service,
|
|
// same-origin via caddy /api/mcstatus/* → internal /players. Returns a
|
|
// sanitized [{uuid,name}] of every Drasl player (admin login is server-side
|
|
// in mc-status; no admin token in the page).
|
|
rosterApi: `/api/mcstatus/players`,
|
|
},
|
|
|
|
// Honest stat tiles. Keys (numbers/versions) are language-neutral; labels
|
|
// come from ui.ts (same order).
|
|
stats: [
|
|
{ key: MOD_COUNT_KEY },
|
|
{ key: MC_VERSION },
|
|
{ key: "10" },
|
|
{ key: "6h" },
|
|
],
|
|
|
|
// Feature cards. glyph ∈ shield|diamond|orb|heart; gold = gold pixel fill.
|
|
// Text (h/p) comes from ui.ts (same order).
|
|
features: [
|
|
{ glyph: "diamond", gold: true },
|
|
{ glyph: "shield", gold: false },
|
|
{ glyph: "orb", gold: false },
|
|
{ glyph: "heart", gold: true },
|
|
],
|
|
|
|
// UlicraftLauncher download manifest — feeds the homepage join step 2
|
|
// per-OS buttons from `builds[]` (and the Fjord page's .mrpack via
|
|
// `fjordPack`). Synced from the custom-launcher repo; see loader above.
|
|
launcherManifest: LAUNCHER_MANIFEST,
|
|
|
|
// Fjord Launcher downloads (the alternative manual path on /fjord, OFF the
|
|
// homepage flow). Stable filenames are symlinks created by
|
|
// tooling/fetch-launcher.sh — keep these in sync with that script. OS names
|
|
// and hints are technical, kept language-neutral here.
|
|
fjord: {
|
|
name: "Fjord Launcher",
|
|
base: "/launcher/latest",
|
|
downloads: [
|
|
{ os: "Windows", file: "fjord-windows-setup.exe", hint: "x64 installer" },
|
|
{ os: "macOS", file: "fjord-macos.dmg", hint: "Apple Silicon / Intel" },
|
|
{ os: "Linux", file: "fjord-linux-x86_64.AppImage", hint: "x86_64 AppImage" },
|
|
],
|
|
},
|
|
} as const;
|
|
|
|
// Product / in-game UI literals — never translated (they match what guests see
|
|
// on screen or type verbatim).
|
|
export const LITERALS = {
|
|
authlib: "authlib-injector",
|
|
multiplayer: "Multiplayer",
|
|
addServer: "Add Server",
|
|
} as const;
|
|
|
|
// Head-font CSS stacks, keyed by theme.headFont.
|
|
export const HEAD_FONTS: Record<string, string> = {
|
|
blockblueprint: "'Block Blueprint', system-ui, sans-serif",
|
|
pixelify: "'Pixelify Sans', system-ui, sans-serif",
|
|
"8bit": "'Press Start 2P', monospace",
|
|
silkscreen: "'Silkscreen', system-ui, sans-serif",
|
|
};
|