From b41e8f0386f265036f09c3e1dae292dd62bd2128 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Wed, 19 Nov 2025 15:22:53 +0900 Subject: [PATCH] appview: support safari icons (favicon, bookmark) Change-Id: pqokmrkoyyptnmtkmyxmnxluqqutxsll Safari < 26.0 doesn't support svg favicon and even after 26.0, it defaults to #000 color, resulting to black favicon on white background. It is better to have actual ico image hosted on `/favicon.ico` To prevent chrome and other browsers choosing ico image for favicon over svg, we are restricting the `favicon.ico` size to `48x48`. 180x180 `apple-touch-icon.png` is also needed to support Safari bookmark icons. Commonly known as "Favorites". Because Safari bookmark icons is pretty large, we are using logo-dolly (dolly with eyes) here. reference: Fix: Signed-off-by: Seongmin Lee --- appview/middleware/middleware.go | 2 +- appview/pages/assets/apple-touch-icon.png | Bin 0 -> 6179 bytes appview/pages/assets/favicon.ico | Bin 0 -> 15086 bytes appview/pages/assets/favicon.svg | 83 ++++++++++++++++++++++ appview/pages/pages.go | 13 ++-- appview/pages/templates/layouts/base.html | 5 ++ appview/state/router.go | 4 +- appview/state/state.go | 13 ---- 8 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 appview/pages/assets/apple-touch-icon.png create mode 100644 appview/pages/assets/favicon.ico create mode 100644 appview/pages/assets/favicon.svg diff --git a/appview/middleware/middleware.go b/appview/middleware/middleware.go index 65c252ac..7c5d1190 100644 --- a/appview/middleware/middleware.go +++ b/appview/middleware/middleware.go @@ -176,7 +176,7 @@ func (mw Middleware) RepoPermissionMiddleware(requiredPerm string) middlewareFun } func (mw Middleware) ResolveIdent() middlewareFunc { - excluded := []string{"favicon.ico"} + excluded := []string{"favicon.ico", "favicon.svg"} return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { diff --git a/appview/pages/assets/apple-touch-icon.png b/appview/pages/assets/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d0f9b60c07f7ec6afa4d18a28a3887d5deb102ce GIT binary patch literal 6179 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 x#AssMwHs@!L1P5P z78@oS63w}ZfJ@FG|U%HVRatuH6KCU7S3)s3nL zt)O=`7~qSUMNXW-;8O4dh@)3fPIhC{Ujy6`=M=^kU)GwfJ$ZHpKWEHm%^#yN1^jIVIrw3=rnH<~n}Dx#CSWz5b{|Fy2;YSMl&L;Y{&s9EFBci^`iF```G&rtvGQ2`QU zKcPPZ47Yh`j|O^g{0Z{X2R3dJCz0NNS?DW_xA4V9r*}x+vFtft{IHx{%Rm-7ARHsy zCz!^BU$X=a*FeY7S;#l8yAa;G_9bv3xRuXukWJCzzfM zjggji_r>waal8@S3Yx49?Y)7I=AuZww!wBXSR4wroc`fJ^WhD_Z9v58H5=hG9Q2yc zAgcf$y@O(GQYLK91yOUxxnW=I^gIp@%j@kE>roI#T7PWki;pgDu6~x!uTgUH-xF7S zRj2pJ$Z$?%2PXL581`v&pZp6FYOw0Z-P{PZKGZujjuyE-ebFYl-X&>iH(LV^r>1n; z(fTx+lfNxx4c6=)TLXeutuTQGs`pMKj)i!;WURrOYtL?+?PTDy8c}!kybmMqhZ%Careo=has?^dn!hyU zStd@_``(tQds*Y=K#+D{&-#jMjW{mvbs)x73RVX#w7Kzpx(gKOIA_aWZY)t{t^&p7FxJd<>tEZ;)F@o;I;sz)c{}dk6Par1{vyJoWl~iMu^` z7<7Ut(ws>9Q{#Z<4OvNh_j~*DH%d;9Az%-1A~+o!3G@!<3yf}}t!t*=a?-vN-^Mog zoYv-V6sIPwGj0Z7fiUUVw5HR$Ki62(sCs>Is88!{t(T^N>0n1N#Ap9skaPS{u>S9C z?dFwc@k*giy46ybbDB)(E7rQMhn;)NRTtoD0zwvDmtG{g&dd%4Oh-MrYbiW9ff z6g$g-UGc9*ms_4=H|?SH#KNAx<5^aZz7z{6(D)$Z|SCGV!uf!Ep@Et0WyM=)pyjD*26r{8q5 zoU>D7^3New9KC^((X%HZ+Yh8Y7c+W#*LuWAdY0O2);(MJUJTOE=$@Xn9?lbE0%%_A z-D5zf>!IHwbuILbmCjX|7T2;xw{UpNanhKkHQKM>y=ae`6q|iOFh_=~9*sFJy+>iyH769$?JlV1*6&11U4_%WA!|<8o;bSRKGONsoOv&w0dkFT zo47I4w(-;NM@eJCLD=Zs9Y=Sk&FR_^P~&0}&BEaLBqQ`kxUrMk*lX`U4IPM))+|Y+ zu_w!SKBc69RsCrAn&8_M_RFyO2rL15Ugmn>Th}`%@!S zZeQ74Ds(oPR<;*}mWqp(QcI<^EVOnMzGx}W!MbZ@lVG~oxhS%0Beb)dQ3!T4no4Xt zfaWrBh$m?Qivrf|Y)=%sFC!0{;eX6P8_2s)!GVki+BZEMtomM>&^-v9zOVZLd;v7( zX&+x}*}s9MuW3x^;YbJBmDa%FAJdMSYkY>h$;O~P2xv|dCO2~j z=Z18)>-vwg!YaSc#ui6*3-|^ou1N2xwXF^9F+lTYtr1QG`&nJpe|==bz>z?td9Ga# zjG+Cagi!1^cj%`DR^5N}Zruu8?Z`Cl^jZhnTF_nK*s6Ie&@<^$$=bUdgY|%}y-V}y z=>Bj?*X#7%IoN4Bqm0jd$M^ZPJJOSfSd_}XJsAH0wv zfaZJ~SwEK}9|Fz*uK=Cbvw`j#OS + + + + + + + + + + + + + + + + + + + + + diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 7e806392..726dda6f 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -210,10 +210,6 @@ func (p *Pages) executeProfile(name string, w io.Writer, params any) error { return tpl.ExecuteTemplate(w, "layouts/base", params) } -func (p *Pages) Favicon(w io.Writer) error { - return p.executePlain("fragments/dolly/silhouette", w, nil) -} - type LoginParams struct { ReturnUrl string ErrorCode string @@ -1414,14 +1410,21 @@ func (p *Pages) Static() http.Handler { return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) } +func (p *Pages) StaticRedirect(target string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, target, http.StatusMovedPermanently) + } +} + func Cache(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := strings.Split(r.URL.Path, "?")[0] if strings.HasSuffix(path, ".css") { - // on day for css files + // one day for css files w.Header().Set("Cache-Control", "public, max-age=86400") } else { + // one year for others w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") } h.ServeHTTP(w, r) diff --git a/appview/pages/templates/layouts/base.html b/appview/pages/templates/layouts/base.html index f357201d..59675e98 100644 --- a/appview/pages/templates/layouts/base.html +++ b/appview/pages/templates/layouts/base.html @@ -7,6 +7,11 @@ + + + + + diff --git a/appview/state/router.go b/appview/state/router.go index b11f12c2..27d16e75 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -32,8 +32,8 @@ func (s *State) Router() http.Handler { s.pages, ) - router.Get("/favicon.svg", s.Favicon) - router.Get("/favicon.ico", s.Favicon) + router.Get("/favicon.ico", s.pages.StaticRedirect("/static/favicon.ico")) + router.Get("/favicon.svg", s.pages.StaticRedirect("/static/favicon.svg")) router.Get("/pwa-manifest.json", s.PWAManifest) router.Get("/robots.txt", s.RobotsTxt) diff --git a/appview/state/state.go b/appview/state/state.go index 11d94a5d..ab1f1ae4 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -202,19 +202,6 @@ func (s *State) Close() error { return s.db.Close() } -func (s *State) Favicon(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "image/svg+xml") - w.Header().Set("Cache-Control", "public, max-age=31536000") // one year - w.Header().Set("ETag", `"favicon-svg-v1"`) - - if match := r.Header.Get("If-None-Match"); match == `"favicon-svg-v1"` { - w.WriteHeader(http.StatusNotModified) - return - } - - s.pages.Favicon(w) -} - func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.Header().Set("Cache-Control", "public, max-age=86400") // one day -- 2.43.0