diff --git a/cache-images.go b/cache-images.go
index 692476e..84a4256 100644
--- a/cache-images.go
+++ b/cache-images.go
@@ -36,7 +36,7 @@ var (
imageURLMapMu sync.RWMutex
)
-func cacheImage(imageURL, imageID string, isThumbnail bool) (string, bool, error) {
+func cacheImage(imageURL, imageID string, imageType string) (string, bool, error) {
if imageURL == "" {
recordInvalidImageID(imageID)
return "", false, fmt.Errorf("empty image URL for image ID %s", imageID)
@@ -44,10 +44,15 @@ func cacheImage(imageURL, imageID string, isThumbnail bool) (string, bool, error
// Construct the filename based on the image ID and type
var filename string
- if isThumbnail {
+ switch imageType {
+ case "thumb":
filename = fmt.Sprintf("%s_thumb.webp", imageID)
- } else {
+ case "icon":
+ filename = fmt.Sprintf("%s_icon.webp", imageID)
+ case "full":
filename = fmt.Sprintf("%s_full.webp", imageID)
+ default:
+ return "", false, fmt.Errorf("unknown image type: %s", imageType)
}
// Make sure we store inside: config.DriveCache.Path / images
@@ -228,29 +233,23 @@ func handleImageServe(w http.ResponseWriter, r *http.Request) {
// Adjust to read from config.DriveCache.Path / images
cachedImagePath := filepath.Join(config.DriveCache.Path, "images", filename)
- if hasExtension && imageType == "thumb" {
- // Requesting cached image (thumbnail or full)
+ if hasExtension && (imageType == "thumb" || imageType == "icon") {
if _, err := os.Stat(cachedImagePath); err == nil {
- // Update the modification time to now
- err := os.Chtimes(cachedImagePath, time.Now(), time.Now())
- if err != nil {
- printWarn("Failed to update modification time for %s: %v", cachedImagePath, err)
- }
-
- // Determine content type based on file extension
- contentType := "image/webp"
- w.Header().Set("Content-Type", contentType)
+ // Update the modification time
+ _ = os.Chtimes(cachedImagePath, time.Now(), time.Now())
+ w.Header().Set("Content-Type", "image/webp")
w.Header().Set("Cache-Control", "public, max-age=31536000")
http.ServeFile(w, r, cachedImagePath)
return
} else {
- // Cached image not found
if config.DriveCacheEnabled {
- // Thumbnail should be cached, but not found
- serveMissingImage(w, r)
+ if imageType == "icon" {
+ serveGlobeImage(w, r)
+ } else {
+ serveMissingImage(w, r)
+ }
return
}
- // Else, proceed to proxy if caching is disabled
}
}
@@ -326,8 +325,12 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) {
invalidImageIDsMu.Unlock()
if isInvalid {
- // Image is invalid; inform the frontend by setting the missing image URL
- statusMap[id] = "/static/images/missing.svg"
+ // Image is invalid; provide appropriate fallback
+ if strings.HasSuffix(id, "_icon.webp") || strings.HasSuffix(id, "_icon") {
+ statusMap[id] = "/images/globe.svg"
+ } else {
+ statusMap[id] = "/images/missing.svg"
+ }
continue
}
@@ -335,11 +338,15 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) {
extensions := []string{"webp", "svg"} // Extensions without leading dots
imageReady := false
- // Check thumbnail first
for _, ext := range extensions {
- thumbFilename := fmt.Sprintf("%s_thumb.%s", id, ext)
- thumbPath := filepath.Join(config.DriveCache.Path, "images", thumbFilename)
+ thumbPath := filepath.Join(config.DriveCache.Path, "images", fmt.Sprintf("%s_thumb.%s", id, ext))
+ iconPath := filepath.Join(config.DriveCache.Path, "images", fmt.Sprintf("%s_icon.%s", id, ext))
+ if _, err := os.Stat(iconPath); err == nil {
+ statusMap[id] = fmt.Sprintf("/image/%s_icon.%s", id, ext)
+ imageReady = true
+ break
+ }
if _, err := os.Stat(thumbPath); err == nil {
statusMap[id] = fmt.Sprintf("/image/%s_thumb.%s", id, ext)
imageReady = true
@@ -363,11 +370,13 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) {
// If neither is ready and image is not invalid
if !imageReady {
- if !config.DriveCacheEnabled {
- // Hard cache is disabled; use the proxy URL
- statusMap[id] = fmt.Sprintf("/image/%s_thumb", id)
+ // Distinguish favicon vs image fallback
+ if strings.HasSuffix(id, "_icon.webp") || strings.HasSuffix(id, "_icon") {
+ statusMap[id] = "/images/globe.svg"
+ } else if !config.DriveCacheEnabled {
+ statusMap[id] = "/images/missing.svg"
}
- // Else, do not set statusMap[id]; the frontend will keep checking
+ // else: leave it unset — frontend will retry
}
}
@@ -520,8 +529,25 @@ func serveMissingImage(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
- if config.DriveCacheEnabled {
- w.WriteHeader(http.StatusNotFound)
- }
http.ServeFile(w, r, missingImagePath)
}
+
+func serveGlobeImage(w http.ResponseWriter, r *http.Request) {
+ globePath := filepath.Join("static", "images", "globe.svg")
+
+ // Set error code FIRST
+ w.WriteHeader(http.StatusNotFound)
+
+ // Now read the file and write it manually, to avoid conflict with http.ServeFile
+ data, err := os.ReadFile(globePath)
+ if err != nil {
+ http.Error(w, "globe.svg not found", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "image/svg+xml")
+ w.Header().Set("Cache-Control", "no-store, must-revalidate")
+ w.Header().Set("Pragma", "no-cache")
+ w.Header().Set("Expires", "0")
+ _, _ = w.Write(data)
+}
diff --git a/favicon.go b/favicon.go
index 6b9dbf2..37c7d94 100644
--- a/favicon.go
+++ b/favicon.go
@@ -325,22 +325,22 @@ func findFaviconInHTML(pageURL string) string {
func getFaviconProxyURL(rawFavicon, pageURL string) string {
if pageURL == "" {
- return "/static/images/missing.svg"
+ return "/static/images/globe.svg"
}
cacheID := faviconIDFromURL(pageURL)
- filename := fmt.Sprintf("%s_thumb.webp", cacheID)
+ filename := fmt.Sprintf("%s_icon.webp", cacheID)
cachedPath := filepath.Join(config.DriveCache.Path, "images", filename)
if _, err := os.Stat(cachedPath); err == nil {
- return fmt.Sprintf("/image/%s_thumb.webp", cacheID)
+ return fmt.Sprintf("/image/%s_icon.webp", cacheID)
}
// Resolve URL
faviconURL, _ := resolveFaviconURL(rawFavicon, pageURL)
if faviconURL == "" {
recordInvalidImageID(cacheID)
- return "/static/images/missing.svg"
+ return "/static/images/globe.svg"
}
// Check if already downloading
@@ -361,10 +361,10 @@ func getFaviconProxyURL(rawFavicon, pageURL string) string {
}
}
- return fmt.Sprintf("/image/%s_thumb.webp", cacheID)
+ return fmt.Sprintf("/image/%s_icon.webp", cacheID)
}
-// Caches favicon, always saving *_thumb.webp
+// Caches favicon, always saving *_icon.webp
func cacheFavicon(imageURL, imageID string) (string, bool, error) {
// if imageURL == "" {
// recordInvalidImageID(imageID)
@@ -374,7 +374,7 @@ func cacheFavicon(imageURL, imageID string) (string, bool, error) {
// Debug
fmt.Printf("Downloading favicon [%s] for ID [%s]\n", imageURL, imageID)
- filename := fmt.Sprintf("%s_thumb.webp", imageID)
+ filename := fmt.Sprintf("%s_icon.webp", imageID)
imageCacheDir := filepath.Join(config.DriveCache.Path, "images")
if err := os.MkdirAll(imageCacheDir, 0755); err != nil {
return "", false, fmt.Errorf("couldn't create images folder: %v", err)
diff --git a/images.go b/images.go
index be7ed6e..ef03f8b 100755
--- a/images.go
+++ b/images.go
@@ -174,7 +174,7 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I
if config.DriveCacheEnabled {
// Cache the thumbnail image asynchronously
go func(imgResult ImageSearchResult) {
- _, success, err := cacheImage(imgResult.Thumb, imgResult.ID, true)
+ _, success, err := cacheImage(imgResult.Thumb, imgResult.ID, "thumb")
if err != nil || !success {
printWarn("Failed to cache thumbnail image %s: %v", imgResult.Thumb, err)
removeImageResultFromCache(query, page, safe == "active", lang, imgResult.ID)
@@ -233,7 +233,7 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I
if config.DriveCacheEnabled {
// Cache the thumbnail image asynchronously
go func(imgResult ImageSearchResult) {
- _, success, err := cacheImage(imgResult.Thumb, imgResult.ID, true)
+ _, success, err := cacheImage(imgResult.Thumb, imgResult.ID, "thumb")
if err != nil || !success {
printWarn("Failed to cache thumbnail image %s: %v", imgResult.Thumb, err)
removeImageResultFromCache(query, page, safe == "active", lang, imgResult.ID)
diff --git a/static/css/style.css b/static/css/style.css
index f51b714..a477fa3 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -1850,4 +1850,8 @@ body, h1, p, a, input, button {
.leaflet-control-attribution a {
color: var(--link) !important;
}
-}
\ No newline at end of file
+}
+
+.favicon.globe-fallback {
+ color: var(--font-fg);
+}
diff --git a/static/images/globe.svg b/static/images/globe.svg
new file mode 100644
index 0000000..4837352
--- /dev/null
+++ b/static/images/globe.svg
@@ -0,0 +1,3 @@
+
diff --git a/static/js/dynamicscrollingtext.js b/static/js/dynamicscrollingtext.js
index 8181978..98811b5 100644
--- a/static/js/dynamicscrollingtext.js
+++ b/static/js/dynamicscrollingtext.js
@@ -28,26 +28,59 @@
imgElement.closest('.favicon-wrapper')?.classList.remove('loading');
if (title) title.classList.remove('title-loading');
- if (type === 'image' && imgElement.src.endsWith('/images/missing.svg')) {
+ if (type === 'image' && imgElement.src.endsWith('/images/globe.svg')) {
container.remove();
}
}
// Handle image/favicon loading errors
- function handleImageError(imgElement, retryCount = 10, retryDelay = 500) {
+ function handleImageError(imgElement, retryCount = 8, retryDelay = 500) {
+ const isFavicon = !!imgElement.closest('.favicon-wrapper');
const container = imgElement.closest(type === 'image' ? '.image' : '.result_item');
const titleSelector = type === 'image' ? '.img_title' : '.result-url';
const title = container?.querySelector(titleSelector);
-
- if (retryCount > 0) {
- setTimeout(() => {
- imgElement.src = imgElement.getAttribute('data-full');
- imgElement.onerror = () => handleImageError(imgElement, retryCount - 1, retryDelay);
- }, retryDelay);
+ const fullURL = imgElement.getAttribute('data-full');
+
+ if (retryCount > 0 && !imgElement.dataset.checked404) {
+ imgElement.dataset.checked404 = '1'; // avoid infinite loop
+
+ fetch(fullURL, { method: 'HEAD' })
+ .then(res => {
+ if (res.status === 404) {
+ fallbackToGlobe(imgElement);
+ } else {
+ setTimeout(() => {
+ imgElement.src = fullURL;
+ imgElement.onerror = () => handleImageError(imgElement, retryCount - 1, retryDelay);
+ }, retryDelay);
+ }
+ })
+ .catch(() => {
+ fallbackToGlobe(imgElement);
+ });
} else {
+ fallbackToGlobe(imgElement);
+ }
+
+ function fallbackToGlobe(imgElement) {
imgElement.closest('.favicon-wrapper')?.classList.remove('loading');
if (title) title.classList.remove('title-loading');
- if (type === 'image') container.style.display = 'none';
+
+ if (isFavicon) {
+ const wrapper = imgElement.closest('.favicon-wrapper') || imgElement.parentElement;
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
+ svg.setAttribute("viewBox", "0 -960 960 960");
+ svg.setAttribute("height", imgElement.height || "16");
+ svg.setAttribute("width", imgElement.width || "16");
+ svg.setAttribute("fill", "currentColor");
+ svg.classList.add("favicon", "globe-fallback");
+ svg.innerHTML = ``;
+ imgElement.remove();
+ wrapper.appendChild(svg);
+ } else if (type === 'image') {
+ container?.remove();
+ }
}
}
@@ -128,6 +161,7 @@
group.forEach(id => {
const elements = mediaMap.get(id);
const resolved = statusMap[id];
+ if (!elements) return;
if (resolved && resolved !== 'pending') {
elements.forEach(img => {
img.src = resolved;
diff --git a/templates/text.html b/templates/text.html
index 6d7246c..74566dd 100755
--- a/templates/text.html
+++ b/templates/text.html
@@ -149,7 +149,7 @@
diff --git a/text.go b/text.go
index 63ce269..7cce267 100755
--- a/text.go
+++ b/text.go
@@ -103,7 +103,7 @@ func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string
func ensureFaviconIsCached(faviconID, rootURL string) {
// Check if already exists in cache
- filename := fmt.Sprintf("%s_thumb.webp", faviconID)
+ filename := fmt.Sprintf("%s_icon.webp", faviconID)
cachedPath := filepath.Join(config.DriveCache.Path, "images", filename)
if _, err := os.Stat(cachedPath); err == nil {