Procházet zdrojové kódy

feat(console): add SWR caching to changelog endpoints

- Add Cloudflare edge caching (cf.cacheTtl) for GitHub API requests
- Add Cache-Control headers with s-maxage=300 and stale-while-revalidate=600
- Refactor /changelog to fetch from /changelog.json instead of GitHub directly
- Remove duplicate parsing logic from changelog page
Ryan Vogel před 2 měsíci
rodič
revize
dda80da2c6

+ 22 - 15
packages/console/app/src/routes/changelog.json.ts

@@ -90,25 +90,32 @@ export async function GET() {
       Accept: "application/vnd.github.v3+json",
       Accept: "application/vnd.github.v3+json",
       "User-Agent": "OpenCode-Console",
       "User-Agent": "OpenCode-Console",
     },
     },
-  })
+    cf: {
+      cacheTtl: 60 * 5,
+      cacheEverything: true,
+    },
+  } as RequestInit)
 
 
   if (!response.ok) {
   if (!response.ok) {
-    return { releases: [] }
+    return Response.json({ releases: [] }, { status: 502 })
   }
   }
 
 
   const releases = (await response.json()) as Release[]
   const releases = (await response.json()) as Release[]
 
 
-  return {
-    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,
-      }
-    }),
-  }
+  return Response.json(
+    {
+      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: { "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600" } },
+  )
 }
 }

+ 54 - 128
packages/console/app/src/routes/changelog/index.tsx

@@ -7,39 +7,6 @@ import { Legal } from "~/component/legal"
 import { config } from "~/config"
 import { config } from "~/config"
 import { For, Show, createSignal } from "solid-js"
 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",
-  })
-}
-
 type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string }
 type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string }
 
 
 type HighlightItem = {
 type HighlightItem = {
@@ -54,68 +21,30 @@ type HighlightGroup = {
   items: HighlightItem[]
   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 ParsedRelease = {
+  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
-
-  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)
+const getReleases = query(async () => {
+  "use server"
+  const response = await fetch(`${config.baseUrl}/changelog.json`)
+  if (!response.ok) return []
+  const data = (await response.json()) as { releases: ParsedRelease[] }
+  return data.releases
+}, "releases.get")
 
 
-  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 }) {
 function ReleaseItem(props: { item: string }) {
@@ -216,43 +145,40 @@ export default function Changelog() {
 
 
           <section data-component="releases">
           <section data-component="releases">
             <For each={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>
-                      </div>
-                      <time dateTime={release.published_at}>{formatDate(release.published_at)}</time>
-                    </header>
-                    <div data-slot="content">
-                      <Show when={parsed().highlights.length > 0}>
-                        <div data-component="highlights">
-                          <For each={parsed().highlights}>{(group) => <HighlightSection group={group} />}</For>
-                        </div>
-                      </Show>
-                      <Show when={parsed().highlights.length > 0 && parsed().sections.length > 0}>
-                        <CollapsibleSections sections={parsed().sections} />
-                      </Show>
-                      <Show when={parsed().highlights.length === 0}>
-                        <For each={parsed().sections}>
-                          {(section) => (
-                            <div data-component="section">
-                              <h3>{section.title}</h3>
-                              <ul>
-                                <For each={section.items}>{(item) => <ReleaseItem item={item} />}</For>
-                              </ul>
-                            </div>
-                          )}
-                        </For>
-                      </Show>
+              {(release) => (
+                <article data-component="release">
+                  <header>
+                    <div data-slot="version">
+                      <a href={release.url} target="_blank" rel="noopener noreferrer">
+                        {release.tag}
+                      </a>
                     </div>
                     </div>
-                  </article>
-                )
-              }}
+                    <time dateTime={release.date}>{formatDate(release.date)}</time>
+                  </header>
+                  <div data-slot="content">
+                    <Show when={release.highlights.length > 0}>
+                      <div data-component="highlights">
+                        <For each={release.highlights}>{(group) => <HighlightSection group={group} />}</For>
+                      </div>
+                    </Show>
+                    <Show when={release.highlights.length > 0 && release.sections.length > 0}>
+                      <CollapsibleSections sections={release.sections} />
+                    </Show>
+                    <Show when={release.highlights.length === 0}>
+                      <For each={release.sections}>
+                        {(section) => (
+                          <div data-component="section">
+                            <h3>{section.title}</h3>
+                            <ul>
+                              <For each={section.items}>{(item) => <ReleaseItem item={item} />}</For>
+                            </ul>
+                          </div>
+                        )}
+                      </For>
+                    </Show>
+                  </div>
+                </article>
+              )}
             </For>
             </For>
           </section>
           </section>