feat(website): offer the beta channel on the download page

Beta testers had to find prerelease assets on GitHub by hand. The
download page now carries a Stable/Beta switch, with stable staying the
default and the canonical URL.

The channel lives in the URL (?channel=beta) rather than client state so
maintainers can link someone straight to it. That link outlives the beta
it was shared for, so the switch only renders while the newest
prerelease leads stable on its core version, and a stale ?channel=beta
falls back to stable instead of serving a promoted version twice.

"What's new" deep-links the minor group anchor (/changelog#release-0.3),
not the exact entry: promotion rewrites the -beta.N entry in place, so
that anchor dies while the group survives — and the group is the
stable-to-beta diff a beta downloader came to read. A version missing
from the bundled changelog links plain /changelog rather than a dead
anchor, which covers the window between a tag going public and the site
redeploying.

Homebrew, the app stores, and the web app have no beta, so the Beta view
drops those rows instead of showing inert placeholders.
This commit is contained in:
Mohamed Boudra
2026-08-05 16:39:59 +02:00
parent aae9a474c2
commit 1c43e4eea1
8 changed files with 428 additions and 208 deletions
+19 -16
View File
@@ -1,25 +1,22 @@
import type { ReactNode } from "react";
import { createContext, useContext } from "react";
import { Outlet, createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
import type { ReleaseChannels, ReleaseInfo } from "~/latest-release";
import { getLatestRelease } from "~/release";
import { getStarCount } from "~/stars";
interface ReleaseContext {
version: string;
linuxAppImageAsset: string;
windowsX64Asset: string | null;
windowsArm64Asset: string | null;
}
interface StarsContext {
stars: string;
}
const ReleaseCtx = createContext<ReleaseContext>({
version: "",
linuxAppImageAsset: "",
windowsX64Asset: null,
windowsArm64Asset: null,
const ReleaseCtx = createContext<ReleaseChannels>({
stable: {
version: "",
linuxAppImageAsset: "",
windowsX64Asset: null,
windowsArm64Asset: null,
},
beta: null,
});
const StarsCtx = createContext<StarsContext>({ stars: "" });
@@ -27,8 +24,14 @@ const PLAUSIBLE_INIT_SCRIPT = {
__html: `window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};plausible.init()`,
};
export function useRelease(): ReleaseContext {
return useContext(ReleaseCtx);
/** The latest stable release. Everything on the site points here by default. */
export function useRelease(): ReleaseInfo {
return useContext(ReleaseCtx).stable;
}
/** The current beta, or null when there is no beta ahead of stable. */
export function useBetaRelease(): ReleaseInfo | null {
return useContext(ReleaseCtx).beta;
}
export function useStars(): StarsContext {
@@ -38,7 +41,7 @@ export function useStars(): StarsContext {
export const Route = createRootRoute({
loader: async () => {
const [release, stars] = await Promise.all([getLatestRelease(), getStarCount()]);
return { ...release, ...stars };
return { release, ...stars };
},
head: () => ({
meta: [
@@ -63,7 +66,7 @@ export const Route = createRootRoute({
function RootComponent() {
const data = Route.useLoaderData();
return (
<ReleaseCtx value={data}>
<ReleaseCtx value={data.release}>
<StarsCtx value={data}>
<RootDocument>
<Outlet />