@@ -2,8 +2,11 @@ import type * as Preset from "@docusaurus/preset-classic";
22import type { Config } from "@docusaurus/types" ;
33import { themes as prismThemes } from "prism-react-renderer" ;
44
5+ import type { LatestRelease } from "./src/lib/release" ;
6+
57const SITE_URL = "https://getopenscreen.com" ;
6- const REPO_URL = "https://github.com/getopenscreen/openscreen" ;
8+ const REPO_SLUG = "getopenscreen/openscreen" ;
9+ const REPO_URL = `https://github.com/${ REPO_SLUG } ` ;
710const UPSTREAM_REPO_URL = "https://github.com/siddharthvaddem/openscreen" ;
811const DISCORD_URL = "https://discord.gg/VvT6Vtnyh" ;
912
@@ -64,8 +67,73 @@ async function fetchStarCount(): Promise<number | null> {
6467 }
6568}
6669
70+ const MONTHS = [
71+ "January" ,
72+ "February" ,
73+ "March" ,
74+ "April" ,
75+ "May" ,
76+ "June" ,
77+ "July" ,
78+ "August" ,
79+ "September" ,
80+ "October" ,
81+ "November" ,
82+ "December" ,
83+ ] ;
84+
85+ /**
86+ * The published assets for the current stable release, resolved at build time
87+ * so /download can link each platform to its actual file instead of bouncing
88+ * everyone through the releases list.
89+ *
90+ * This is only safe because .github/workflows/docs.yml also rebuilds on
91+ * `release: published`. Without that trigger the data would go stale silently —
92+ * the workflow otherwise only fires on website/** changes, so shipping a new
93+ * version would leave this page advertising the previous one indefinitely.
94+ *
95+ * Returns null on any failure (the API is called unauthenticated, so a
96+ * rate-limited runner is a real possibility); the page falls back to
97+ * /releases/latest links, which are always correct.
98+ */
99+ async function fetchLatestRelease ( ) : Promise < LatestRelease > {
100+ try {
101+ const res = await fetch ( `https://api.github.com/repos/${ REPO_SLUG } /releases/latest` , {
102+ headers : { Accept : "application/vnd.github+json" } ,
103+ signal : AbortSignal . timeout ( 5000 ) ,
104+ } ) ;
105+ if ( ! res . ok ) return null ;
106+ const data = ( await res . json ( ) ) as {
107+ tag_name ?: unknown ;
108+ published_at ?: unknown ;
109+ assets ?: unknown ;
110+ } ;
111+ if ( typeof data . tag_name !== "string" || ! Array . isArray ( data . assets ) ) return null ;
112+
113+ const assets = data . assets . flatMap ( ( raw ) => {
114+ const a = raw as { name ?: unknown ; browser_download_url ?: unknown ; size ?: unknown } ;
115+ if ( typeof a . name !== "string" || typeof a . browser_download_url !== "string" ) return [ ] ;
116+ return [ { name : a . name , url : a . browser_download_url , size : Number ( a . size ) || 0 } ] ;
117+ } ) ;
118+ if ( assets . length === 0 ) return null ;
119+
120+ // Formatted here rather than in the component: toLocaleDateString would
121+ // resolve against the visitor's locale and time zone on hydration and
122+ // mismatch the server-rendered string.
123+ let published = "" ;
124+ if ( typeof data . published_at === "string" ) {
125+ const [ y , m , d ] = data . published_at . slice ( 0 , 10 ) . split ( "-" ) ;
126+ if ( y && m && d ) published = `${ Number ( d ) } ${ MONTHS [ Number ( m ) - 1 ] } ${ y } ` ;
127+ }
128+
129+ return { tag : data . tag_name , published, assets } ;
130+ } catch {
131+ return null ;
132+ }
133+ }
134+
67135export default async function createConfig ( ) : Promise < Config > {
68- const starCount = await fetchStarCount ( ) ;
136+ const [ starCount , latestRelease ] = await Promise . all ( [ fetchStarCount ( ) , fetchLatestRelease ( ) ] ) ;
69137 const starBadge =
70138 starCount !== null
71139 ? `<span class="navbar-github-stars">${ STAR_SVG } ${ formatStarCount ( starCount ) } </span>`
@@ -93,6 +161,10 @@ export default async function createConfig(): Promise<Config> {
93161 organizationName : "getopenscreen" ,
94162 projectName : "openscreen" ,
95163
164+ // Read back by src/pages/download.tsx. Serialized into the client bundle,
165+ // so it stays plain JSON.
166+ customFields : { latestRelease } ,
167+
96168 onBrokenLinks : "throw" ,
97169 onBrokenAnchors : "throw" ,
98170
0 commit comments