summaryrefslogtreecommitdiffhomepage
path: root/packages/console/app/src
diff options
context:
space:
mode:
authoradamelmore <[email protected]>2026-01-26 09:53:03 -0600
committeradamelmore <[email protected]>2026-01-26 09:53:10 -0600
commit3dce6a6608a4745cbf05670dbb57f2312f950acc (patch)
tree790d2dc3412522f85c456cd95e3750eb6dee3e3b /packages/console/app/src
parent39a73d4894bf7bda69a95b7d5572d5c7c24dd7ee (diff)
downloadopencode-3dce6a6608a4745cbf05670dbb57f2312f950acc.tar.gz
opencode-3dce6a6608a4745cbf05670dbb57f2312f950acc.zip
chore: gen changelog page off changelog json
Diffstat (limited to 'packages/console/app/src')
-rw-r--r--packages/console/app/src/routes/changelog.json.ts14
-rw-r--r--packages/console/app/src/routes/changelog/index.tsx134
2 files changed, 42 insertions, 106 deletions
diff --git a/packages/console/app/src/routes/changelog.json.ts b/packages/console/app/src/routes/changelog.json.ts
index a9667ac6d..9e3b75e5c 100644
--- a/packages/console/app/src/routes/changelog.json.ts
+++ b/packages/console/app/src/routes/changelog.json.ts
@@ -98,19 +98,23 @@ export async function GET() {
cacheTtl: 60 * 5,
cacheEverything: true,
},
- } as any)
+ } as any).catch(() => undefined)
- if (!response.ok) {
- return new Response(JSON.stringify({ releases: [] }), {
+ const fail = () =>
+ new Response(JSON.stringify({ releases: [] }), {
status: 503,
headers: {
"Content-Type": "application/json",
"Cache-Control": error,
},
})
- }
- const releases = (await response.json()) as Release[]
+ 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({
diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx
index 87e021ec8..e05ad42e6 100644
--- a/packages/console/app/src/routes/changelog/index.tsx
+++ b/packages/console/app/src/routes/changelog/index.tsx
@@ -1,44 +1,12 @@
import "./index.css"
import { Title, Meta, Link } from "@solidjs/meta"
-import { createAsync, query } from "@solidjs/router"
+import { createAsync } from "@solidjs/router"
import { Header } from "~/component/header"
import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
import { config } from "~/config"
import { For, Show, createSignal } from "solid-js"
-
-type Release = {
- tag_name: string
- name: string
- body: string
- published_at: string
- html_url: string
-}
-
-const getReleases = query(async () => {
- "use server"
- 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)
- if (!response.ok) return []
- return response.json() as Promise<Release[]>
-}, "releases.get")
-
-function formatDate(dateString: string) {
- const date = new Date(dateString)
- return date.toLocaleDateString("en-US", {
- year: "numeric",
- month: "short",
- day: "numeric",
- })
-}
+import { getRequestEvent } from "solid-js/web"
type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string }
@@ -54,68 +22,33 @@ type HighlightGroup = {
items: HighlightItem[]
}
-function parseHighlights(body: string): HighlightGroup[] {
- const groups = new Map<string, HighlightItem[]>()
- const regex = /<highlight\s+source="([^"]+)">([\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>([^<]+)<\/h2>/)
- const pMatch = content.match(/<p(?:\s+short="([^"]*)")?>([^<]+)<\/p>/)
- const imgMatch = content.match(/<img\s+width="([^"]+)"\s+height="([^"]+)"\s+alt="[^"]*"\s+src="([^"]+)"/)
- const videoMatch = content.match(/^\s*(https:\/\/github\.com\/user-attachments\/assets\/[a-f0-9-]+)\s*$/m)
-
- let media: HighlightMedia | undefined
- if (videoMatch) {
- media = { type: "video", src: videoMatch[1] }
- } else if (imgMatch) {
- media = { type: "image", src: imgMatch[3], width: imgMatch[1], height: imgMatch[2] }
- }
-
- if (titleMatch && media) {
- const item: HighlightItem = {
- title: titleMatch[1],
- description: pMatch?.[2] || "",
- shortDescription: pMatch?.[1],
- media,
- }
-
- if (!groups.has(source)) {
- groups.set(source, [])
- }
- groups.get(source)!.push(item)
- }
- }
-
- return Array.from(groups.entries()).map(([source, items]) => ({ source, items }))
+type ChangelogRelease = {
+ tag: string
+ name: string
+ date: string
+ url: string
+ highlights: HighlightGroup[]
+ sections: { title: string; items: string[] }[]
}
-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
+async function getReleases() {
+ const event = getRequestEvent()
+ const url = event ? new URL("/changelog.json", event.request.url).toString() : "/changelog.json"
- 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 response = await fetch(url).catch(() => undefined)
+ if (!response?.ok) return []
- const highlights = parseHighlights(body)
+ const json = await response.json().catch(() => undefined)
+ return Array.isArray(json?.releases) ? (json.releases as ChangelogRelease[]) : []
+}
- return { sections, highlights }
+function formatDate(dateString: string) {
+ const date = new Date(dateString)
+ return date.toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ })
}
function ReleaseItem(props: { item: string }) {
@@ -217,28 +150,27 @@ export default function Changelog() {
<section data-component="releases">
<For each={releases()}>
{(release) => {
- const parsed = () => parseMarkdown(release.body || "")
return (
<article data-component="release">
<header>
<div data-slot="version">
- <a href={release.html_url} target="_blank" rel="noopener noreferrer">
- {release.tag_name}
+ <a href={release.url} target="_blank" rel="noopener noreferrer">
+ {release.tag}
</a>
</div>
- <time dateTime={release.published_at}>{formatDate(release.published_at)}</time>
+ <time dateTime={release.date}>{formatDate(release.date)}</time>
</header>
<div data-slot="content">
- <Show when={parsed().highlights.length > 0}>
+ <Show when={release.highlights.length > 0}>
<div data-component="highlights">
- <For each={parsed().highlights}>{(group) => <HighlightSection group={group} />}</For>
+ <For each={release.highlights}>{(group) => <HighlightSection group={group} />}</For>
</div>
</Show>
- <Show when={parsed().highlights.length > 0 && parsed().sections.length > 0}>
- <CollapsibleSections sections={parsed().sections} />
+ <Show when={release.highlights.length > 0 && release.sections.length > 0}>
+ <CollapsibleSections sections={release.sections} />
</Show>
- <Show when={parsed().highlights.length === 0}>
- <For each={parsed().sections}>
+ <Show when={release.highlights.length === 0}>
+ <For each={release.sections}>
{(section) => (
<div data-component="section">
<h3>{section.title}</h3>