v3.0.0 (New icon formats, dropped legacy URL)

This commit is contained in:
selfhst-bot
2026-01-03 07:00:06 -05:00
parent 16c7e97709
commit 986dcb1fac
7 changed files with 42 additions and 94 deletions

View File

@@ -88,10 +88,10 @@ jobs:
- name: Output image info
run: |
echo "🐳 Images built and pushed:"
echo "Images built and pushed:"
echo "${{ steps.meta.outputs.tags }}" | sed 's/^/ - /'
echo ""
echo "🏗️ Supported architectures:"
echo " - linux/amd64 (Intel/AMD x86_64)"
echo " - linux/arm64 (ARM 64-bit, Apple M1, Raspberry Pi 4)"
echo " - linux/arm/v7 (ARM 32-bit, Raspberry Pi 2/3)"
echo "Supported architectures:"
echo " - linux/amd64"
echo " - linux/arm64"
echo " - linux/arm/v7"

View File

@@ -1,3 +1,15 @@
# v3.0.0
## Breaking Changes
The legacy custom color URL ```domain.com/icon.svg?color=000000``` is no longer supported. See the [supported methods for building URLs](https://github.com/selfhst/icons/wiki#building-links) in the project's wiki if this change impacts you.
## What's Changed
* Added support for the new AVIF and ICO formats
* 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)
# v2.2.0
## What's Changed

View File

@@ -1,13 +1 @@
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.git
.gitignore
README.md
.env
.nyc_output
coverage
.vscode
.DS_Store
*.log
VERSION

View File

@@ -1,17 +1,14 @@
FROM golang:1.21-alpine AS builder
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY go.mod main.go ./
RUN go mod tidy && \
go mod download && \
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server .
RUN CGO_ENABLED=0 go build -o server .
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
USER 65534:65534

View File

@@ -1 +1 @@
2.2.0
3.0.0

View File

@@ -1,7 +1,3 @@
module selfhst-icons
go 1.21
require (
github.com/gorilla/mux v1.8.1
)
go 1.25

View File

@@ -12,8 +12,6 @@ import (
"strings"
"sync"
"time"
"github.com/gorilla/mux"
)
type Config struct {
@@ -67,7 +65,6 @@ func (c *Cache) Set(key, value string) {
c.mutex.Lock()
defer c.mutex.Unlock()
// Remove oldest item if cache is full
if len(c.items) >= c.max {
var oldestKey string
var oldestTime time.Time
@@ -109,8 +106,8 @@ func loadConfig() *Config {
if standardFormat == "" {
standardFormat = "svg"
}
// Validate format
if standardFormat != "svg" && standardFormat != "png" && standardFormat != "webp" {
if standardFormat != "svg" && standardFormat != "png" && standardFormat != "webp" && standardFormat != "avif" && standardFormat != "ico" {
standardFormat = "svg"
}
@@ -173,14 +170,12 @@ func fetchRemoteFile(url string) (string, error) {
func applySVGColor(svgContent, colorCode string) string {
color := "#" + colorCode
// Replace style fill attributes
re1 := regexp.MustCompile(`style="[^"]*fill:\s*#fff[^"]*"`)
svgContent = re1.ReplaceAllStringFunc(svgContent, func(match string) string {
re2 := regexp.MustCompile(`fill:\s*#fff`)
return re2.ReplaceAllString(match, "fill:"+color)
})
// Replace direct fill attributes
re3 := regexp.MustCompile(`fill="#fff"`)
svgContent = re3.ReplaceAllString(svgContent, `fill="`+color+`"`)
@@ -193,6 +188,10 @@ func getContentType(format string) string {
return "image/png"
case "webp":
return "image/webp"
case "avif":
return "image/avif"
case "ico":
return "image/x-icon"
case "svg":
return "image/svg+xml"
default:
@@ -208,16 +207,14 @@ func getCacheKey(iconName, colorCode string) string {
}
func handleIcon(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
iconName := vars["iconname"]
colorCode := vars["colorcode"]
iconName := r.PathValue("iconname")
colorCode := r.PathValue("colorcode")
if iconName == "" {
http.Error(w, "Icon name is required", http.StatusBadRequest)
return
}
// Validate color if provided
if colorCode != "" && !isValidHexColor(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)
@@ -226,21 +223,17 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
cacheKey := getCacheKey(iconName, colorCode)
// Determine content type and format to serve
var contentType string
var formatToServe string
if colorCode != "" {
// Always use SVG for colorization
contentType = "image/svg+xml"
formatToServe = "svg"
} else {
// Use standard format when no color specified
contentType = getContentType(config.StandardIconFormat)
formatToServe = config.StandardIconFormat
}
// Check cache first
if cached, found := cache.Get(cacheKey); found {
log.Printf("[CACHE] Serving cached icon: \"%s\"%s (%s)", iconName,
func() string { if colorCode != "" { return " with color " + colorCode } else { return "" } }(),
@@ -254,9 +247,7 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
var err error
if config.IconSource == "local" {
// Use local volume
if colorCode != "" {
// Try to find -light version for colorization (always SVG)
lightPath := filepath.Join(config.LocalPath, "svg", iconName+"-light.svg")
if fileExists(lightPath) {
iconContent, err = readLocalFile(lightPath)
@@ -265,12 +256,10 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
}
}
} else {
// No color - try to serve standard format
var standardPath string
if formatToServe == "svg" {
standardPath = filepath.Join(config.LocalPath, "svg", iconName+".svg")
} else {
// For PNG/WebP, use format-specific directories
standardPath = filepath.Join(config.LocalPath, formatToServe, iconName+"."+formatToServe)
}
@@ -279,7 +268,6 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
}
}
// Fall back to SVG if standard format not found
if iconContent == "" {
svgPath := filepath.Join(config.LocalPath, "svg", iconName+".svg")
if fileExists(svgPath) {
@@ -289,9 +277,7 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
}
}
} else {
// Use remote CDN
if colorCode != "" {
// Try to find -light version for colorization (always SVG)
lightURL := config.JSDelivrURL + "/svg/" + iconName + "-light.svg"
if urlExists(lightURL) {
iconContent, err = fetchRemoteFile(lightURL)
@@ -300,7 +286,6 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
}
}
} else {
// No color - try to serve standard format
var standardURL string
if formatToServe == "svg" {
standardURL = config.JSDelivrURL + "/svg/" + iconName + ".svg"
@@ -313,7 +298,6 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
}
}
// Fall back to SVG if standard format not found
if iconContent == "" {
svgURL := config.JSDelivrURL + "/svg/" + iconName + ".svg"
iconContent, err = fetchRemoteFile(svgURL)
@@ -330,7 +314,6 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
return
}
// Cache the result
cache.Set(cacheKey, iconContent)
log.Printf("[SUCCESS] Serving icon: \"%s\"%s (%s, source: %s)", iconName,
@@ -341,41 +324,19 @@ func handleIcon(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(iconContent))
}
func handleLegacyIcon(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
colorQuery := r.URL.Query().Get("color")
var colorCode string
if colorQuery != "" {
cleanColor := strings.TrimPrefix(colorQuery, "#")
if isValidHexColor(cleanColor) {
colorCode = cleanColor
}
}
// Redirect internally to new handler
vars["colorcode"] = colorCode
handleIcon(w, r)
}
func handleCustomIcon(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
filename := vars["filename"]
filename := r.PathValue("filename")
if filename == "" {
http.Error(w, "Filename is required", http.StatusBadRequest)
return
}
// Build the path to the custom icon file
customPath := filepath.Join("/app/icons/custom", filename)
// Debug logging
log.Printf("[DEBUG] Looking for custom icon at: %s", customPath)
// Check if file exists
if !fileExists(customPath) {
// List directory contents for debugging
if files, err := os.ReadDir("/app/icons/custom"); err == nil {
var fileList []string
for _, file := range files {
@@ -390,7 +351,6 @@ func handleCustomIcon(w http.ResponseWriter, r *http.Request) {
return
}
// Read the file
data, err := os.ReadFile(customPath)
if err != nil {
log.Printf("[ERROR] Failed to read custom icon \"%s\": %v", filename, err)
@@ -398,7 +358,6 @@ func handleCustomIcon(w http.ResponseWriter, r *http.Request) {
return
}
// Determine content type based on file extension
ext := strings.ToLower(filepath.Ext(filename))
var contentType string
switch ext {
@@ -412,6 +371,8 @@ func handleCustomIcon(w http.ResponseWriter, r *http.Request) {
contentType = "image/svg+xml"
case ".webp":
contentType = "image/webp"
case ".avif":
contentType = "image/avif"
case ".ico":
contentType = "image/x-icon"
default:
@@ -454,20 +415,14 @@ func main() {
config = loadConfig()
cache = NewCache(config.CacheTTL, config.CacheSize)
r := mux.NewRouter()
// Custom icons route: /custom/filename
r.HandleFunc("/custom/{filename}", handleCustomIcon).Methods("GET")
// Main route: /iconname or /iconname/colorcode
r.HandleFunc("/{iconname}", handleIcon).Methods("GET")
r.HandleFunc("/{iconname}/{colorcode}", handleIcon).Methods("GET")
// Legacy route: /iconname.svg?color=colorcode
r.HandleFunc("/{iconname}.svg", handleLegacyIcon).Methods("GET")
// Root endpoint
r.HandleFunc("/", handleRoot).Methods("GET")
mux := http.NewServeMux()
mux.HandleFunc("GET /custom/{filename}", handleCustomIcon)
mux.HandleFunc("GET /{iconname}/{colorcode}", handleIcon)
mux.HandleFunc("GET /{iconname}", handleIcon)
mux.HandleFunc("GET /", handleRoot)
log.Printf("Icon server listening on port %s", config.Port)
log.Printf("Icon source: %s", func() string {
@@ -478,5 +433,5 @@ func main() {
}())
log.Printf("Cache settings: TTL %ds, Max %d items", int(config.CacheTTL.Seconds()), config.CacheSize)
log.Fatal(http.ListenAndServe(":"+config.Port, r))
log.Fatal(http.ListenAndServe(":"+config.Port, mux))
}