From 65870b9b264f350404e0329392efe9783cbc422d Mon Sep 17 00:00:00 2001 From: Ryan Vogel Date: Mon, 26 Jan 2026 11:30:25 -0500 Subject: [PATCH] fix: changelog page SSR not fetching releases on Cloudflare Workers Internal fetch to /changelog.json during SSR fails silently on Cloudflare Workers. Refactor to use SolidStart query() with 'use server' directive to fetch directly from GitHub API during SSR. - Extract shared release fetching logic to lib/changelog.ts - Update changelog page to use the new changelog() query - Simplify changelog.json.ts to use shared fetchReleases() - Add error logging for debugging --- packages/console/app/src/lib/changelog.ts | 141 ++++++++++++++++++ .../console/app/src/routes/changelog.json.ts | 135 ++--------------- .../app/src/routes/changelog/index.tsx | 24 +-- 3 files changed, 153 insertions(+), 147 deletions(-) create mode 100644 packages/console/app/src/lib/changelog.ts diff --git a/packages/console/app/src/lib/changelog.ts b/packages/console/app/src/lib/changelog.ts new file mode 100644 index 0000000000..ba10363b9d --- /dev/null +++ b/packages/console/app/src/lib/changelog.ts @@ -0,0 +1,141 @@ +import { query } from "@solidjs/router" + +type Release = { + tag_name: string + name: string + body: string + published_at: string + html_url: string +} + +type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string } + +type HighlightItem = { + title: string + description: string + shortDescription?: string + media: HighlightMedia +} + +type HighlightGroup = { + source: string + items: HighlightItem[] +} + +export type ChangelogRelease = { + tag: string + name: string + date: string + url: string + highlights: HighlightGroup[] + sections: { title: string; items: string[] }[] +} + +function parseHighlights(body: string): HighlightGroup[] { + const groups = new Map() + const regex = /([\s\S]*?)<\/highlight>/g + let match + + while ((match = regex.exec(body)) !== null) { + const source = match[1] + const content = match[2] + + const titleMatch = content.match(/

([^<]+)<\/h2>/) + const pMatch = content.match(/([^<]+)<\/p>/) + const imgMatch = content.match(/ ({ source, items })) +} + +function parseMarkdown(body: string) { + const lines = body.split("\n") + const sections: { title: string; items: string[] }[] = [] + let current: { title: string; items: string[] } | null = null + let skip = false + + for (const line of lines) { + if (line.startsWith("## ")) { + if (current) sections.push(current) + const title = line.slice(3).trim() + current = { title, items: [] } + skip = false + } else if (line.startsWith("**Thank you")) { + skip = true + } else if (line.startsWith("- ") && !skip) { + current?.items.push(line.slice(2).trim()) + } + } + if (current) sections.push(current) + + const highlights = parseHighlights(body) + + return { sections, highlights } +} + +export async function fetchReleases(): Promise { + const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "OpenCode-Console", + }, + cf: { + cacheTtl: 60 * 5, + cacheEverything: true, + }, + } as any).catch((e) => { + console.error("[changelog] Failed to fetch releases:", e) + return undefined + }) + + if (!response?.ok) { + if (response) { + console.warn(`[changelog] GitHub API returned ${response.status}`) + } + return [] + } + + const data = await response.json().catch(() => undefined) + if (!Array.isArray(data)) return [] + + const releases = data as Release[] + + return releases.map((release) => { + const parsed = parseMarkdown(release.body || "") + return { + tag: release.tag_name, + name: release.name, + date: release.published_at, + url: release.html_url, + highlights: parsed.highlights, + sections: parsed.sections, + } + }) +} + +export const changelog = query(async () => { + "use server" + return fetchReleases() +}, "changelog") diff --git a/packages/console/app/src/routes/changelog.json.ts b/packages/console/app/src/routes/changelog.json.ts index 9e3b75e5cd..b355e9300e 100644 --- a/packages/console/app/src/routes/changelog.json.ts +++ b/packages/console/app/src/routes/changelog.json.ts @@ -1,140 +1,25 @@ -type Release = { - tag_name: string - name: string - body: string - published_at: string - html_url: string -} - -type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string } - -type HighlightItem = { - title: string - description: string - shortDescription?: string - media: HighlightMedia -} - -type HighlightGroup = { - source: string - items: HighlightItem[] -} +import { fetchReleases } from "~/lib/changelog" const ok = "public, max-age=1, s-maxage=300, stale-while-revalidate=86400, stale-if-error=86400" const error = "public, max-age=1, s-maxage=60, stale-while-revalidate=600, stale-if-error=86400" -function parseHighlights(body: string): HighlightGroup[] { - const groups = new Map() - const regex = /([\s\S]*?)<\/highlight>/g - let match - - while ((match = regex.exec(body)) !== null) { - const source = match[1] - const content = match[2] - - const titleMatch = content.match(/

([^<]+)<\/h2>/) - const pMatch = content.match(/([^<]+)<\/p>/) - const imgMatch = content.match(/ ({ source, items })) -} - -function parseMarkdown(body: string) { - const lines = body.split("\n") - const sections: { title: string; items: string[] }[] = [] - let current: { title: string; items: string[] } | null = null - let skip = false - - for (const line of lines) { - if (line.startsWith("## ")) { - if (current) sections.push(current) - const title = line.slice(3).trim() - current = { title, items: [] } - skip = false - } else if (line.startsWith("**Thank you")) { - skip = true - } else if (line.startsWith("- ") && !skip) { - current?.items.push(line.slice(2).trim()) - } - } - if (current) sections.push(current) - - const highlights = parseHighlights(body) - - return { sections, highlights } -} - export async function GET() { - const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", { - headers: { - Accept: "application/vnd.github.v3+json", - "User-Agent": "OpenCode-Console", - }, - cf: { - // best-effort edge caching (ignored outside Cloudflare) - cacheTtl: 60 * 5, - cacheEverything: true, - }, - } as any).catch(() => undefined) + const releases = await fetchReleases() - const fail = () => - new Response(JSON.stringify({ releases: [] }), { + if (releases.length === 0) { + return new Response(JSON.stringify({ releases: [] }), { status: 503, headers: { "Content-Type": "application/json", "Cache-Control": error, }, }) + } - if (!response?.ok) return fail() - - const data = await response.json().catch(() => undefined) - if (!Array.isArray(data)) return fail() - - const releases = data as Release[] - - return new Response( - JSON.stringify({ - releases: releases.map((release) => { - const parsed = parseMarkdown(release.body || "") - return { - tag: release.tag_name, - name: release.name, - date: release.published_at, - url: release.html_url, - highlights: parsed.highlights, - sections: parsed.sections, - } - }), - }), - { - headers: { - "Content-Type": "application/json", - "Cache-Control": ok, - }, + return new Response(JSON.stringify({ releases }), { + headers: { + "Content-Type": "application/json", + "Cache-Control": ok, }, - ) + }) } diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx index e05ad42e67..49c3af4caa 100644 --- a/packages/console/app/src/routes/changelog/index.tsx +++ b/packages/console/app/src/routes/changelog/index.tsx @@ -6,7 +6,7 @@ import { Footer } from "~/component/footer" import { Legal } from "~/component/legal" import { config } from "~/config" import { For, Show, createSignal } from "solid-js" -import { getRequestEvent } from "solid-js/web" +import { changelog, type ChangelogRelease } from "~/lib/changelog" type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string } @@ -22,26 +22,6 @@ type HighlightGroup = { items: HighlightItem[] } -type ChangelogRelease = { - tag: string - name: string - date: string - url: string - highlights: HighlightGroup[] - sections: { title: string; items: string[] }[] -} - -async function getReleases() { - const event = getRequestEvent() - const url = event ? new URL("/changelog.json", event.request.url).toString() : "/changelog.json" - - const response = await fetch(url).catch(() => undefined) - if (!response?.ok) return [] - - const json = await response.json().catch(() => undefined) - return Array.isArray(json?.releases) ? (json.releases as ChangelogRelease[]) : [] -} - function formatDate(dateString: string) { const date = new Date(dateString) return date.toLocaleDateString("en-US", { @@ -130,7 +110,7 @@ function CollapsibleSections(props: { sections: { title: string; items: string[] } export default function Changelog() { - const releases = createAsync(() => getReleases()) + const releases = createAsync(() => changelog()) return (