fix: stars count rendered server-side with 30min cache, no client-side API calls

This commit is contained in:
juanatsap
2026-05-04 13:52:53 +01:00
parent 76d9649ec2
commit 7322c1a158
4 changed files with 69 additions and 19 deletions
+66 -1
View File
@@ -1,11 +1,14 @@
package handlers
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/go-git/go-git/v5"
@@ -327,9 +330,12 @@ func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, er
)
}
// Process projects for dynamic dates
// Process projects for dynamic dates and fetch GitHub stars
for i := range cv.Projects {
processProjectDates(&cv.Projects[i], lang)
if cv.Projects[i].OpenSource && cv.Projects[i].GitRepoUrl != "" {
cv.Projects[i].Stars = getGitHubStars(cv.Projects[i].GitRepoUrl)
}
}
// Split skills between left and right sidebars
@@ -383,6 +389,65 @@ func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, er
return data, nil
}
// ==============================================================================
// GITHUB STARS
// ==============================================================================
var (
starsCache = make(map[string]starsCacheEntry)
starsCacheMu sync.RWMutex
)
type starsCacheEntry struct {
count int
fetched time.Time
}
// getGitHubStars fetches the star count for a GitHub repo URL, cached for 30 minutes.
func getGitHubStars(repoURL string) int {
// Extract "owner/repo" from URL
repoURL = strings.TrimSuffix(repoURL, "/")
parts := strings.SplitN(repoURL, "github.com/", 2)
if len(parts) != 2 {
return 0
}
repo := parts[1]
// Check cache
starsCacheMu.RLock()
if entry, ok := starsCache[repo]; ok && time.Since(entry.fetched) < 30*time.Minute {
starsCacheMu.RUnlock()
return entry.count
}
starsCacheMu.RUnlock()
// Fetch from GitHub API
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get(fmt.Sprintf("https://api.github.com/repos/%s", repo))
if err != nil {
return 0
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != 200 {
return 0
}
var result struct {
Stars int `json:"stargazers_count"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return 0
}
// Cache the result
starsCacheMu.Lock()
starsCache[repo] = starsCacheEntry{count: result.Stars, fetched: time.Now()}
starsCacheMu.Unlock()
return result.Stars
}
// ==============================================================================
// COOKIE HELPERS
// ==============================================================================
+1
View File
@@ -112,6 +112,7 @@ type Project struct {
// Computed fields (not stored in JSON)
ComputedStartDate string `json:"-"` // Dynamically calculated from git repo or system
DynamicDate string `json:"-"` // Current date for ongoing projects
Stars int `json:"-"` // GitHub star count (fetched at runtime)
}
type Award struct {
+1
View File
@@ -117,6 +117,7 @@ type Project struct {
// Computed fields (not stored in JSON)
ComputedStartDate string `json:"-"` // Dynamically calculated from git repo or system
DynamicDate string `json:"-"` // Current date for ongoing projects
Stars int `json:"-"` // GitHub star count (fetched at runtime)
}
type Award struct {