Monorepo for Tangled tangled.org

appview: serve ico favicons on /favicon.ico

this is recognized by safari. the only size this image is served at is
48x48. the dolly silhouette is used. cache/etag headers are identical to
the svg output.

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 16f7cd54 1d36aa92

verified
Changed files
+96 -15
appview
pages
templates
layouts
state
+3
appview/pages/templates/layouts/base.html
··· 11 11 <script defer src="/static/htmx-ext-ws.min.js"></script> 12 12 <script defer src="/static/actor-typeahead.js" type="module"></script> 13 13 14 + <link rel="icon" href="/favicon.ico" sizes="48x48"/> 15 + <link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml"/> 16 + 14 17 <!-- preconnect to image cdn --> 15 18 <link rel="preconnect" href="https://avatar.tangled.sh" /> 16 19 <link rel="preconnect" href="https://camo.tangled.sh" />
+91
appview/state/favicon.go
··· 1 + package state 2 + 3 + import ( 4 + "bytes" 5 + "fmt" 6 + "html/template" 7 + "image" 8 + "image/color" 9 + "net/http" 10 + 11 + "github.com/srwiley/oksvg" 12 + "github.com/srwiley/rasterx" 13 + "golang.org/x/image/draw" 14 + "tangled.org/core/appview/pages" 15 + "tangled.org/core/ico" 16 + ) 17 + 18 + func (s *State) FaviconSvg(w http.ResponseWriter, r *http.Request) { 19 + w.Header().Set("Content-Type", "image/svg+xml") 20 + w.Header().Set("Cache-Control", "public, max-age=31536000") // one year 21 + w.Header().Set("ETag", `"favicon-svg-v1"`) 22 + 23 + if match := r.Header.Get("If-None-Match"); match == `"favicon-svg-v1"` { 24 + w.WriteHeader(http.StatusNotModified) 25 + return 26 + } 27 + 28 + s.pages.Favicon(w) 29 + } 30 + 31 + func (s *State) FaviconIco(w http.ResponseWriter, r *http.Request) { 32 + w.Header().Set("Content-Type", "image/x-icon") 33 + w.Header().Set("Cache-Control", "public, max-age=31536000") // one year 34 + w.Header().Set("ETag", `"favicon-ico-v1"`) 35 + 36 + if match := r.Header.Get("If-None-Match"); match == `"favicon-ico-v1"` { 37 + w.WriteHeader(http.StatusNotModified) 38 + return 39 + } 40 + 41 + ico, err := dollyIco() 42 + if err != nil { 43 + s.logger.Error("failed to render ico", "err", err) 44 + w.WriteHeader(http.StatusNotFound) 45 + return 46 + } 47 + 48 + w.Write(ico) 49 + } 50 + 51 + func dollyIco() ([]byte, error) { 52 + // first, get the bytes from the svg 53 + tpl, err := template.New("dolly"). 54 + ParseFS(pages.Files, "templates/fragments/dolly/silhouette.html") 55 + if err != nil { 56 + return nil, err 57 + } 58 + 59 + var svgData bytes.Buffer 60 + if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/silhouette", "#000000"); err != nil { 61 + return nil, err 62 + } 63 + 64 + img, err := svgToImage(svgData.Bytes(), 48, 48) 65 + if err != nil { 66 + return nil, err 67 + } 68 + 69 + ico, err := ico.ImageToIco(img) 70 + if err != nil { 71 + return nil, err 72 + } 73 + 74 + return ico, nil 75 + } 76 + 77 + func svgToImage(svgData []byte, w, h int) (image.Image, error) { 78 + icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData)) 79 + if err != nil { 80 + return nil, fmt.Errorf("error parsing SVG: %v", err) 81 + } 82 + 83 + icon.SetTarget(0, 0, float64(w), float64(h)) 84 + rgba := image.NewRGBA(image.Rect(0, 0, w, h)) 85 + draw.Draw(rgba, rgba.Bounds(), &image.Uniform{color.Transparent}, image.Point{}, draw.Src) 86 + scanner := rasterx.NewScannerGV(w, h, rgba, rgba.Bounds()) 87 + raster := rasterx.NewDasher(w, h, scanner) 88 + icon.Draw(raster, 1.0) 89 + 90 + return rgba, nil 91 + }
+2 -2
appview/state/router.go
··· 32 32 s.pages, 33 33 ) 34 34 35 - router.Get("/favicon.svg", s.Favicon) 36 - router.Get("/favicon.ico", s.Favicon) 35 + router.Get("/favicon.svg", s.FaviconSvg) 36 + router.Get("/favicon.ico", s.FaviconIco) 37 37 router.Get("/pwa-manifest.json", s.PWAManifest) 38 38 router.Get("/robots.txt", s.RobotsTxt) 39 39
-13
appview/state/state.go
··· 202 202 return s.db.Close() 203 203 } 204 204 205 - func (s *State) Favicon(w http.ResponseWriter, r *http.Request) { 206 - w.Header().Set("Content-Type", "image/svg+xml") 207 - w.Header().Set("Cache-Control", "public, max-age=31536000") // one year 208 - w.Header().Set("ETag", `"favicon-svg-v1"`) 209 - 210 - if match := r.Header.Get("If-None-Match"); match == `"favicon-svg-v1"` { 211 - w.WriteHeader(http.StatusNotModified) 212 - return 213 - } 214 - 215 - s.pages.Favicon(w) 216 - } 217 - 218 205 func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) { 219 206 w.Header().Set("Content-Type", "text/plain") 220 207 w.Header().Set("Cache-Control", "public, max-age=86400") // one day