diff --git a/deploy/brand/apple-touch-icon.png b/deploy/brand/apple-touch-icon.png
new file mode 100644
index 0000000..dc6cc30
Binary files /dev/null and b/deploy/brand/apple-touch-icon.png differ
diff --git a/deploy/brand/favicon-16.png b/deploy/brand/favicon-16.png
new file mode 100644
index 0000000..eb50ee9
Binary files /dev/null and b/deploy/brand/favicon-16.png differ
diff --git a/deploy/brand/favicon-32.png b/deploy/brand/favicon-32.png
new file mode 100644
index 0000000..d6ddbb7
Binary files /dev/null and b/deploy/brand/favicon-32.png differ
diff --git a/deploy/brand/favicon.ico b/deploy/brand/favicon.ico
new file mode 100644
index 0000000..98e3cc7
Binary files /dev/null and b/deploy/brand/favicon.ico differ
diff --git a/deploy/brand/logo-256.png b/deploy/brand/logo-256.png
new file mode 100644
index 0000000..7fcc125
Binary files /dev/null and b/deploy/brand/logo-256.png differ
diff --git a/deploy/brand/logo.png b/deploy/brand/logo.png
new file mode 100644
index 0000000..7fcc125
Binary files /dev/null and b/deploy/brand/logo.png differ
diff --git a/deploy/nginx/host/sparkapi.conf b/deploy/nginx/host/sparkapi.conf
index ccc37f4..75b0fac 100644
--- a/deploy/nginx/host/sparkapi.conf
+++ b/deploy/nginx/host/sparkapi.conf
@@ -67,11 +67,33 @@ server {
proxy_set_header X-Forwarded-Proto https;
}
+ # Brand icons (no app rebuild required)
+ location = /favicon.ico {
+ alias /var/www/sparkapi-brand/favicon.ico;
+ add_header Cache-Control "public, max-age=604800";
+ access_log off;
+ }
+ location = /apple-touch-icon.png {
+ alias /var/www/sparkapi-brand/apple-touch-icon.png;
+ add_header Cache-Control "public, max-age=604800";
+ access_log off;
+ }
+ location ^~ /brand/ {
+ alias /var/www/sparkapi-brand/;
+ add_header Cache-Control "public, max-age=604800";
+ access_log off;
+ }
+
location / {
proxy_pass http://spark_http;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
+ # Inject favicon + header logo until next spark-server image ships docs HTML.
+ sub_filter_once off;
+ sub_filter_types text/html;
+ sub_filter '
' '';
+ sub_filter 'Spark IM' '
Spark IM';
}
}
diff --git a/internal/transport/http/auth.go b/internal/transport/http/auth.go
index 25fcc21..d399084 100644
--- a/internal/transport/http/auth.go
+++ b/internal/transport/http/auth.go
@@ -86,7 +86,8 @@ func authLimitPath(path string) bool {
func publicPath(path string) bool {
switch path {
- case "/healthz", "/readyz", "/openapi.yaml", "/docs", "/docs/":
+ case "/healthz", "/readyz", "/openapi.yaml", "/docs", "/docs/",
+ "/favicon.ico", "/apple-touch-icon.png":
return true
case "/v1/meta/enums", "/v1/auth/login", "/v1/auth/token/refresh", "/v1/sms/codes",
"/v1/music/playlist", "/v1/music/settings":
diff --git a/internal/transport/http/docs.go b/internal/transport/http/docs.go
index e8322d1..b336d0a 100644
--- a/internal/transport/http/docs.go
+++ b/internal/transport/http/docs.go
@@ -1,7 +1,10 @@
package httpx
import (
+ "io/fs"
"net/http"
+ "path"
+ "strings"
"github.com/gin-gonic/gin"
)
@@ -19,4 +22,52 @@ func registerDocs(r *gin.Engine) {
r.GET("/docs/", func(c *gin.Context) {
c.Redirect(http.StatusFound, "/docs")
})
+
+ staticRoot, err := fs.Sub(docsStatic, "docs/static")
+ if err != nil {
+ panic("docs static embed: " + err.Error())
+ }
+ serveStaticFile := func(name, contentType string) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ b, err := fs.ReadFile(staticRoot, name)
+ if err != nil {
+ c.Status(http.StatusNotFound)
+ return
+ }
+ c.Header("Cache-Control", "public, max-age=604800")
+ c.Data(http.StatusOK, contentType, b)
+ }
+ }
+ r.GET("/favicon.ico", serveStaticFile("favicon.ico", "image/x-icon"))
+ r.GET("/apple-touch-icon.png", serveStaticFile("apple-touch-icon.png", "image/png"))
+ r.GET("/docs/static/*filepath", func(c *gin.Context) {
+ rel := strings.TrimPrefix(c.Param("filepath"), "/")
+ rel = path.Clean("/" + rel)
+ rel = strings.TrimPrefix(rel, "/")
+ if rel == "" || strings.Contains(rel, "..") {
+ c.Status(http.StatusNotFound)
+ return
+ }
+ b, err := fs.ReadFile(staticRoot, rel)
+ if err != nil {
+ c.Status(http.StatusNotFound)
+ return
+ }
+ ct := contentTypeFor(rel)
+ c.Header("Cache-Control", "public, max-age=604800")
+ c.Data(http.StatusOK, ct, b)
+ })
+}
+
+func contentTypeFor(name string) string {
+ switch strings.ToLower(path.Ext(name)) {
+ case ".png":
+ return "image/png"
+ case ".ico":
+ return "image/x-icon"
+ case ".svg":
+ return "image/svg+xml"
+ default:
+ return "application/octet-stream"
+ }
}
diff --git a/internal/transport/http/docs/index.html b/internal/transport/http/docs/index.html
index ba66521..af328d6 100644
--- a/internal/transport/http/docs/index.html
+++ b/internal/transport/http/docs/index.html
@@ -3,6 +3,9 @@
Spark IM API
+
+
+