From 39b9262ee9255a0568581d2720e7e78ee1a2d209 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Thu, 11 Jun 2026 02:03:22 +0200 Subject: [PATCH] fix(landing): fetch launcher downloads from live launcher.json on load The build-time download buttons baked stale/placeholder URLs (the host has no synced launcher.json, so the build fell back to launcher.example.json). Render a skeleton at build time instead and fetch the live launcher.json client-side once on page load (no polling) from https://distribution.${BASE_DOMAIN}/launcher/launcher.json, building per-OS buttons from files[]. On fetch error the skeleton clears. Co-Authored-By: Claude Opus 4.8 --- landing/src/pages/[...lang].astro | 56 +++++++++++++++++++++---------- landing/src/styles/main.css | 20 +++++++++++ 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/landing/src/pages/[...lang].astro b/landing/src/pages/[...lang].astro index 7d32c26..637ef95 100644 --- a/landing/src/pages/[...lang].astro +++ b/landing/src/pages/[...lang].astro @@ -25,14 +25,11 @@ const launcherName = launcherManifest.product; const registerPath = LANG_REGISTER_PATH[locale]; const accountPath = LANG_ACCOUNT_PATH[locale]; -// Per-OS download buttons (join step 2) from the synced manifest. Missing -// manifest → empty builds → no buttons (the build never fails on it). -const OS_LABELS: Record = { windows: "Windows", macos: "macOS", linux: "Linux" }; -const launcherBuilds = launcherManifest.builds.map((b) => ({ - label: b.label ?? `${OS_LABELS[b.os] ?? b.os}`, - hint: `${b.arch} · ${b.format}`, - url: b.url, -})); +// Per-OS download buttons (join step 2) are fetched client-side from the live +// launcher.json on page load (no build-time baking, no polling) — see the +// script below. We only pass the URL; buttons render into the #launcher-dl +// skeleton. URL derives from the distribution subdomain. +const launcherJsonUrl = `https://distribution.${site.baseDomain}/launcher/launcher.json`; // Build-time floating dust bits (index-derived, no runtime RNG). const dustBits = Array.from({ length: 16 }, (_, i) => { @@ -158,15 +155,11 @@ const dustBits = Array.from({ length: 16 }, (_, i) => { {t.join.s2.kicker}

{t.join.s2.h.replace("{name}", launcherName)}

{t.join.s2.p.replace(/\{name\}/g, launcherName)}

- {launcherBuilds.length > 0 && ( -
- {launcherBuilds.map((b) => ( - - {b.label} · {b.hint} - - ))} -
- )} +
+ + + +
@@ -323,6 +316,35 @@ const dustBits = Array.from({ length: 16 }, (_, i) => { ); els.forEach((el) => io.observe(el)); } + + // Launcher downloads — fetch the live launcher.json once on load (no + // polling) and render per-OS buttons into the #launcher-dl skeleton. + // launcher.json shape: { files: [{ name, os, arch, ext, url }] }. + const dl = document.getElementById("launcher-dl"); + const src = dl?.dataset.src; + if (dl && src) { + const OS: Record = { windows: "Windows", macos: "macOS", linux: "Linux" }; + fetch(src, { cache: "no-store" }) + .then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))) + .then((data) => { + const files = Array.isArray(data?.files) ? data.files : []; + const frag = document.createDocumentFragment(); + for (const f of files) { + if (!f?.url) continue; + const a = document.createElement("a"); + a.className = "mc-btn sm"; + a.href = f.url; + a.append(`${OS[f.os] ?? f.os ?? "Download"} `); + const hint = document.createElement("span"); + hint.className = "hint"; + hint.textContent = `· ${[f.arch, f.ext].filter(Boolean).join(" · ")}`; + a.append(hint); + frag.append(a); + } + dl.replaceChildren(frag); // empty list → no buttons (skeleton cleared) + }) + .catch(() => dl.replaceChildren()); // unreachable/error → clear skeleton + } diff --git a/landing/src/styles/main.css b/landing/src/styles/main.css index 6892822..1099b90 100644 --- a/landing/src/styles/main.css +++ b/landing/src/styles/main.css @@ -207,6 +207,26 @@ section { position: relative; z-index: 1; } } .mc-btn.sm { font-size: 14px; padding: 10px 16px 12px; } +/* Launcher download skeleton (placeholder while launcher.json loads). */ +.dl-skeleton { + min-width: 9.5rem; + color: transparent; + pointer-events: none; + background-image: linear-gradient( + 90deg, + oklch(1 0 0 / 0.04), + oklch(1 0 0 / 0.14), + oklch(1 0 0 / 0.04) + ); + background-size: 200% 100%; + animation: dl-shimmer 1.2s linear infinite; +} +.dl-skeleton::after { content: "\00a0"; } /* keep height when text is empty */ +@keyframes dl-shimmer { + from { background-position: 200% 0; } + to { background-position: -200% 0; } +} + /* ---- Copy-IP chip ---- */ .ip-chip { display: inline-flex;