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:
@@ -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<string, string> = { 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) => {
|
||||
<span class="kicker">{t.join.s2.kicker}</span>
|
||||
<h3>{t.join.s2.h.replace("{name}", launcherName)}</h3>
|
||||
<p>{t.join.s2.p.replace(/\{name\}/g, launcherName)}</p>
|
||||
{launcherBuilds.length > 0 && (
|
||||
<div class="actions">
|
||||
{launcherBuilds.map((b) => (
|
||||
<a class="mc-btn sm" href={b.url}>
|
||||
{b.label} <span class="hint">· {b.hint}</span>
|
||||
</a>
|
||||
))}
|
||||
<div class="actions" id="launcher-dl" data-src={launcherJsonUrl}>
|
||||
<span class="mc-btn sm dl-skeleton" aria-hidden="true"></span>
|
||||
<span class="mc-btn sm dl-skeleton" aria-hidden="true"></span>
|
||||
<span class="mc-btn sm dl-skeleton" aria-hidden="true"></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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<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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user