Fix favicon fetching and caching improvements

- Introduced functionality to fetch favicons from website HTML and cache them.
- Updated default favicon creation to use a new feed.png file, with a fallback to inline PNG data if the file is missing.
- Improve favicon handling in the service to improve reliability and user experience.
This commit is contained in:
2025-09-25 04:00:11 -05:00
parent 68fc1b218a
commit 7243af462b
6 changed files with 84 additions and 19 deletions

View File

@@ -117,12 +117,12 @@ func Run(params Params) {
return func(c echo.Context) error {
method := c.Request().Method
path := c.Request().URL.Path
// Allow feed refresh in demo mode (read-only operation)
if method == "POST" && path == "/api/feeds/refresh" {
return next(c)
}
if method == "POST" || method == "PATCH" || method == "DELETE" {
return echo.NewHTTPError(http.StatusForbidden, "Demo mode: write operations not allowed")
}
@@ -167,7 +167,6 @@ func Run(params Params) {
authed.GET("/config", configAPIHandler.Get)
authed.PATCH("/config", configAPIHandler.Update)
var err error
addr := fmt.Sprintf("%s:%d", params.Host, params.Port)
if params.TLSCert != "" {

View File

@@ -32,11 +32,11 @@ func (f *faviconAPI) ServeFavicon(c echo.Context) error {
}
faviconPath := filepath.Join(f.cacheDir, filename)
// If favicon doesn't exist, try to fetch it from the actual feed
if _, err := os.Stat(faviconPath); os.IsNotExist(err) {
requestedHash := strings.TrimSuffix(filename, ".png")
// Find all feeds and check which one matches this hash
feeds, err := f.feedRepo.FindByFaviconHash(requestedHash)
if err == nil {
@@ -54,13 +54,13 @@ func (f *faviconAPI) ServeFavicon(c echo.Context) error {
}
}
}
// If we couldn't find and fetch a real favicon, create a default as last resort
if _, err := f.faviconSvc.CreateDefaultFavicon(faviconPath); err != nil {
return echo.NewHTTPError(http.StatusNotFound, "favicon not found")
}
}
c.Response().Header().Set("Cache-Control", "public, max-age=86400")
return c.File(faviconPath)
}

BIN
feed.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -26,7 +26,7 @@
--color-success-content: oklch(100% 0 0);
--color-warning: oklch(76% 0.188 70.08);
--color-warning-content: oklch(100% 0 0);
--color-error: oklch(57% 0.245 27.325);
--color-error: oklch(65% 0.25 25);
--color-error-content: oklch(100% 0 0);
--radius-selector: 0.25rem;
--radius-field: 0.5rem;
@@ -61,7 +61,7 @@
--color-success-content: oklch(100% 0 0);
--color-warning: oklch(79% 0.184 86.047);
--color-warning-content: oklch(100% 0 0);
--color-error: oklch(57% 0.245 27.325);
--color-error: oklch(65% 0.25 25);
--color-error-content: oklch(100% 0 0);
--radius-selector: 0.25rem;
--radius-field: 0.5rem;

BIN
frontend/static/feed.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -7,6 +7,7 @@ import (
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
@@ -120,6 +121,11 @@ func (s *Service) fetchAndCacheFavicon(hostname, cachePath string) (string, erro
fmt.Sprintf("https://www.google.com/s2/favicons?sz=32&domain=%s", hostname),
}
// First try to find favicons from the website's HTML
if feedFavicons := s.findFaviconsFromWebsite(hostname); len(feedFavicons) > 0 {
faviconURLs = append(feedFavicons, faviconURLs...)
}
for _, faviconURL := range faviconURLs {
if err := s.downloadFavicon(faviconURL, cachePath); err == nil {
return cachePath, nil
@@ -152,16 +158,76 @@ func (s *Service) downloadFavicon(faviconURL, cachePath string) error {
}
func (s *Service) CreateDefaultFavicon(cachePath string) (string, error) {
defaultFaviconData := []byte{
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x91, 0x68,
0x36, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 0x6F, 0x66, 0x74, 0x77, 0x61, 0x72,
0x65, 0x00, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x49, 0x6D, 0x61, 0x67, 0x65, 0x52, 0x65, 0x61,
0x64, 0x79, 0x71, 0xC9, 0x65, 0x3C, 0x00, 0x00, 0x00, 0x32, 0x49, 0x44, 0x41, 0x54, 0x78, 0xDA,
0x62, 0xFC, 0x3F, 0x95, 0x9F, 0x01, 0x37, 0x60, 0x62, 0xC0, 0x0B, 0x46, 0xAA, 0x34, 0x40, 0x80,
0x01, 0x00, 0x06, 0x50, 0x4E, 0x20, 0x3E, 0x28, 0x84, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E,
0x44, 0xAE, 0x42, 0x60, 0x82,
// Copy the feed.png file as the default favicon
sourcePath := "feed.png"
sourceFile, err := os.Open(sourcePath)
if err != nil {
// Fallback to a simple inline PNG if feed.png doesn't exist
defaultFaviconData := []byte{
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x91, 0x68,
0x36, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72,
0x65, 0x00, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x61,
0x64, 0x79, 0x71, 0xc9, 0x65, 0x3c, 0x00, 0x00, 0x00, 0x32, 0x49, 0x44, 0x41, 0x54, 0x78, 0x44,
0x62, 0xfc, 0x3f, 0x95, 0x9f, 0x01, 0x37, 0x60, 0x62, 0xc0, 0x0b, 0x46, 0xaa, 0x34, 0x40, 0x80,
0x01, 0x00, 0x06, 0x50, 0x4e, 0x20, 0x3e, 0x28, 0x84, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
0x44, 0xae, 0x42, 0x60, 0x82,
}
return cachePath, os.WriteFile(cachePath, defaultFaviconData, 0600)
}
defer sourceFile.Close()
// #nosec G304 - cachePath is constructed from sanitized hostname hash
destFile, err := os.Create(cachePath)
if err != nil {
return "", err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
return cachePath, err
}
func (s *Service) findFaviconsFromWebsite(hostname string) []string {
websiteURL := fmt.Sprintf("https://%s", hostname)
resp, err := s.client.Get(websiteURL)
if err != nil {
return nil
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil
}
return cachePath, os.WriteFile(cachePath, defaultFaviconData, 0600)
// Read only the first 50KB to avoid reading entire large pages
body := make([]byte, 51200)
n, err := io.ReadFull(resp.Body, body)
if err != nil && err != io.ErrUnexpectedEOF {
return nil
}
content := string(body[:n])
// Look for favicon links in HTML head
faviconRegex := regexp.MustCompile(`(?i)<link[^>]*rel=["'](?:icon|shortcut icon)["'][^>]*href=["']([^"']+)["'][^>]*>`)
matches := faviconRegex.FindAllStringSubmatch(content, -1)
var faviconURLs []string
for _, match := range matches {
if len(match) > 1 {
faviconURL := match[1]
// Convert relative URLs to absolute
if !strings.HasPrefix(faviconURL, "http") {
baseURL, err := url.Parse(websiteURL)
if err != nil {
continue
}
faviconURL = baseURL.ResolveReference(&url.URL{Path: faviconURL}).String()
}
faviconURLs = append(faviconURLs, faviconURL)
}
}
return faviconURLs
}