v3.2.0 (Primary colors, remote URLs)

This commit is contained in:
selfhst-bot
2026-02-18 08:16:46 -05:00
parent 97d60985bc
commit d980408689
5 changed files with 58 additions and 31 deletions

View File

@@ -1,3 +1,12 @@
# v3.2.0
## What's Changed
* [Feature] Added PRIMARY_COLOR variable to easily apply a single custom color to all icons (see Wiki for additional details)
* [Feature] Added REMOTE_URL to allow users to serve icons from their own remote sources (see Wiki for additional details) ([#690](https://github.com/selfhst/icons/issues/690))
* Reduced remote icon load time by removing redundant existence checks
* Updated Go version to [v1.26](https://go.dev/doc/go1.26)
# v3.1.1 # v3.1.1
## What's Changed ## What's Changed
@@ -19,7 +28,7 @@ The legacy custom color URL ```domain.com/icon.svg?color=000000``` is no longer
## What's Changed ## What's Changed
* Added support for the new AVIF and ICO formats * [Feature] Added support for the new AVIF and ICO formats
* Updated Go version to [v1.25](https://go.dev/doc/go1.25) * Updated Go version to [v1.25](https://go.dev/doc/go1.25)
* Removed unmaintained [gorilla/mux](https://github.com/gorilla/mux) external dependency in favor of [net/http enhancements introduced in Go 1.22](https://go.dev/blog/routing-enhancements) * Removed unmaintained [gorilla/mux](https://github.com/gorilla/mux) external dependency in favor of [net/http enhancements introduced in Go 1.22](https://go.dev/blog/routing-enhancements)
@@ -33,7 +42,7 @@ The legacy custom color URL ```domain.com/icon.svg?color=000000``` is no longer
## What's Changed ## What's Changed
* Added optional volume for custom (non-selfh.st) icons ([#495](https://github.com/selfhst/icons/issues/495)) * [Feature] Added optional volume for custom (non-selfh.st) icons ([#495](https://github.com/selfhst/icons/issues/495))
# v2.0.0 # v2.0.0

View File

@@ -1,4 +1,4 @@
FROM golang:1.25-alpine AS builder FROM golang:1.26-alpine AS builder
WORKDIR /app WORKDIR /app

View File

@@ -1 +1 @@
3.1.1 3.2.0

View File

@@ -1,3 +1,3 @@
module selfhst-icons module selfhst-icons
go 1.25 go 1.26

View File

@@ -17,9 +17,10 @@ import (
type Config struct { type Config struct {
Port string Port string
IconSource string IconSource string
JSDelivrURL string RemoteURL string
LocalPath string LocalPath string
StandardIconFormat string StandardIconFormat string
PrimaryColor string
CacheTTL time.Duration CacheTTL time.Duration
CacheSize int CacheSize int
} }
@@ -110,12 +111,20 @@ func loadConfig() *Config {
standardFormat = "svg" standardFormat = "svg"
} }
remoteURL := os.Getenv("REMOTE_URL")
if remoteURL == "" {
remoteURL = "https://cdn.jsdelivr.net/gh/selfhst/icons@main"
}
primaryColor := strings.TrimPrefix(os.Getenv("PRIMARY_COLOR"), "#")
return &Config{ return &Config{
Port: port, Port: port,
IconSource: iconSource, IconSource: iconSource,
JSDelivrURL: "https://cdn.jsdelivr.net/gh/selfhst/icons@main", RemoteURL: remoteURL,
LocalPath: "/app/icons", LocalPath: "/app/icons",
StandardIconFormat: standardFormat, StandardIconFormat: standardFormat,
PrimaryColor: primaryColor,
CacheTTL: time.Hour, CacheTTL: time.Hour,
CacheSize: 500, CacheSize: 500,
} }
@@ -131,14 +140,6 @@ func fileExists(path string) bool {
return err == nil return err == nil
} }
func urlExists(url string) bool {
resp, err := http.Head(url)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
func readLocalFile(path string) (string, error) { func readLocalFile(path string) (string, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
@@ -225,6 +226,14 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
return return
} }
primaryFallback := false
if colorCode == "primary" {
if config.PrimaryColor == "" {
primaryFallback = true
}
colorCode = config.PrimaryColor
}
if colorCode != "" && !isValidHexColor(colorCode) { if colorCode != "" && !isValidHexColor(colorCode) {
log.Printf("[ERROR] Invalid color code for icon \"%s\": %s", iconName, colorCode) log.Printf("[ERROR] Invalid color code for icon \"%s\": %s", iconName, colorCode)
http.Error(w, "Invalid color code. Use 6-digit hex without #", http.StatusBadRequest) http.Error(w, "Invalid color code. Use 6-digit hex without #", http.StatusBadRequest)
@@ -288,28 +297,31 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
} }
} else { } else {
if colorCode != "" { if colorCode != "" {
lightURL := config.JSDelivrURL + "/svg/" + iconName + "-light.svg" lightURL := config.RemoteURL + "/svg/" + iconName + "-light.svg"
if urlExists(lightURL) { iconContent, err = fetchRemoteFile(lightURL)
iconContent, err = fetchRemoteFile(lightURL) if err == nil {
if err == nil { iconContent = applySVGColor(iconContent, colorCode)
iconContent = applySVGColor(iconContent, colorCode) } else {
} iconContent = ""
err = nil
} }
} else { } else {
var standardURL string var standardURL string
if formatToServe == "svg" { if formatToServe == "svg" {
standardURL = config.JSDelivrURL + "/svg/" + iconName + ".svg" standardURL = config.RemoteURL + "/svg/" + iconName + ".svg"
} else { } else {
standardURL = config.JSDelivrURL + "/" + formatToServe + "/" + iconName + "." + formatToServe standardURL = config.RemoteURL + "/" + formatToServe + "/" + iconName + "." + formatToServe
} }
if urlExists(standardURL) { iconContent, err = fetchRemoteFile(standardURL)
iconContent, err = fetchRemoteFile(standardURL) if err != nil {
iconContent = ""
err = nil
} }
} }
if iconContent == "" { if iconContent == "" {
svgURL := config.JSDelivrURL + "/svg/" + iconName + ".svg" svgURL := config.RemoteURL + "/svg/" + iconName + ".svg"
iconContent, err = fetchRemoteFile(svgURL) iconContent, err = fetchRemoteFile(svgURL)
contentType = "image/svg+xml" contentType = "image/svg+xml"
formatToServe = "svg" formatToServe = "svg"
@@ -326,8 +338,14 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
cache.Set(cacheKey, iconContent) cache.Set(cacheKey, iconContent)
log.Printf("[SUCCESS] Serving icon: \"%s\"%s (%s, source: %s)", iconName, log.Printf("[%s] Serving icon: \"%s\"%s (%s, source: %s)",
func() string { if colorCode != "" { return " with color " + colorCode } else { return "" } }(), func() string { if primaryFallback { return "WARN" } else { return "SUCCESS" } }(),
iconName,
func() string {
if colorCode != "" { return " with color " + colorCode }
if primaryFallback { return " (PRIMARY_COLOR not set, using standard format)" }
return ""
}(),
formatToServe, config.IconSource) formatToServe, config.IconSource)
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
@@ -412,7 +430,7 @@ func handleRoot(w http.ResponseWriter, r *http.Request) {
if config.IconSource == "local" { if config.IconSource == "local" {
return config.LocalPath return config.LocalPath
} }
return config.JSDelivrURL return config.RemoteURL
}(), }(),
}, },
} }
@@ -444,7 +462,7 @@ func main() {
if config.IconSource == "local" { if config.IconSource == "local" {
return "Local volume" return "Local volume"
} }
return "Remote CDN" return "Remote: " + config.RemoteURL
}()) }())
log.Printf("Cache settings: TTL %ds, Max %d items", int(config.CacheTTL.Seconds()), config.CacheSize) log.Printf("Cache settings: TTL %ds, Max %d items", int(config.CacheTTL.Seconds()), config.CacheSize)