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 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 02:03:22 +02:00
parent 037b1777d6
commit 39b9262ee9
2 changed files with 59 additions and 17 deletions

View File

@@ -25,14 +25,11 @@ const launcherName = launcherManifest.product;
const registerPath = LANG_REGISTER_PATH[locale]; const registerPath = LANG_REGISTER_PATH[locale];
const accountPath = LANG_ACCOUNT_PATH[locale]; const accountPath = LANG_ACCOUNT_PATH[locale];
// Per-OS download buttons (join step 2) from the synced manifest. Missing // Per-OS download buttons (join step 2) are fetched client-side from the live
// manifest → empty builds → no buttons (the build never fails on it). // launcher.json on page load (no build-time baking, no polling) — see the
const OS_LABELS: Record<string, string> = { windows: "Windows", macos: "macOS", linux: "Linux" }; // script below. We only pass the URL; buttons render into the #launcher-dl
const launcherBuilds = launcherManifest.builds.map((b) => ({ // skeleton. URL derives from the distribution subdomain.
label: b.label ?? `${OS_LABELS[b.os] ?? b.os}`, const launcherJsonUrl = `https://distribution.${site.baseDomain}/launcher/launcher.json`;
hint: `${b.arch} · ${b.format}`,
url: b.url,
}));
// Build-time floating dust bits (index-derived, no runtime RNG). // Build-time floating dust bits (index-derived, no runtime RNG).
const dustBits = Array.from({ length: 16 }, (_, i) => { const dustBits = Array.from({ length: 16 }, (_, i) => {
@@ -158,15 +155,11 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
<span class="kicker">{t.join.s2.kicker}</span> <span class="kicker">{t.join.s2.kicker}</span>
<h3>{t.join.s2.h.replace("{name}", launcherName)}</h3> <h3>{t.join.s2.h.replace("{name}", launcherName)}</h3>
<p>{t.join.s2.p.replace(/\{name\}/g, launcherName)}</p> <p>{t.join.s2.p.replace(/\{name\}/g, launcherName)}</p>
{launcherBuilds.length > 0 && ( <div class="actions" id="launcher-dl" data-src={launcherJsonUrl}>
<div class="actions"> <span class="mc-btn sm dl-skeleton" aria-hidden="true"></span>
{launcherBuilds.map((b) => ( <span class="mc-btn sm dl-skeleton" aria-hidden="true"></span>
<a class="mc-btn sm" href={b.url}> <span class="mc-btn sm dl-skeleton" aria-hidden="true"></span>
{b.label} <span class="hint">· {b.hint}</span> </div>
</a>
))}
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -323,6 +316,35 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
); );
els.forEach((el) => io.observe(el)); 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<string, string> = { 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
}
</script> </script>
</body> </body>
</html> </html>

View File

@@ -207,6 +207,26 @@ section { position: relative; z-index: 1; }
} }
.mc-btn.sm { font-size: 14px; padding: 10px 16px 12px; } .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 ---- */ /* ---- Copy-IP chip ---- */
.ip-chip { .ip-chip {
display: inline-flex; display: inline-flex;