+3
appview/pages/templates/layouts/base.html
+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
+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
+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
-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