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 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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user