feat(brand): 三站替换四色火焰 logo 与 favicon

官网/docs 嵌入新品牌图;sparkapi nginx 可在未重建镜像时提供 /favicon.ico 与页头 logo。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
DESKTOP-L8OUL9F\gfl0034 2026-09-24 10:25:44 +08:00
parent ade98eef4d
commit b10c5d63a3
25 changed files with 103 additions and 10 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
deploy/brand/favicon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

BIN
deploy/brand/favicon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
deploy/brand/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
deploy/brand/logo-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
deploy/brand/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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 '<head>' '<head><link rel="icon" href="/favicon.ico?v=20260924-flames" sizes="any"/><link rel="apple-touch-icon" href="/apple-touch-icon.png?v=20260924-flames"/>';
sub_filter '<strong>Spark IM</strong>' '<a href="/docs" style="display:inline-flex;align-items:center;gap:8px;color:inherit;text-decoration:none"><img src="/brand/logo.png?v=20260924-flames" width="28" height="28" alt="" style="border-radius:6px"/><strong>Spark IM</strong></a>';
}
}

View File

@ -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":

View File

@ -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"
}
}

View File

@ -3,6 +3,9 @@
<head>
<meta charset="utf-8"/>
<title>Spark IM API</title>
<link rel="icon" href="/favicon.ico?v=20260924-flames" sizes="any"/>
<link rel="icon" href="/docs/static/favicon-32.png?v=20260924-flames" type="image/png" sizes="32x32"/>
<link rel="apple-touch-icon" href="/apple-touch-icon.png?v=20260924-flames"/>
<script>
(function () {
var p = location.pathname.replace(/\/+$/, "") || "/docs";
@ -21,6 +24,13 @@
padding: 10px 20px; border-bottom: 1px solid #e0e0e0; background: #fafafa;
position: sticky; top: 0; z-index: 20;
}
.docs-brand {
display: inline-flex; align-items: center; gap: 8px; color: #3b4151;
font-weight: 700; text-decoration: none;
}
.docs-brand img {
width: 28px; height: 28px; border-radius: 6px; display: block;
}
.docs-bar strong { color: #3b4151; }
.docs-nav { display: flex; gap: 20px; margin: 0; padding-left: 22px; }
.docs-nav a { color: #3b4151; text-decoration: none; }
@ -98,7 +108,10 @@
</head>
<body>
<div class="docs-bar">
<strong>Spark IM</strong>
<a class="docs-brand" href="/docs">
<img src="/docs/static/logo.png?v=20260924-flames" width="28" height="28" alt=""/>
<strong>Spark IM</strong>
</a>
<ol class="docs-nav">
<li><a id="nav-api" href="/docs">API</a></li>
<li><a id="nav-enums" href="/docs/enums">enums</a></li>

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,7 +1,7 @@
package httpx
import (
_ "embed"
"embed"
)
//go:embed docs/openapi.yaml
@ -9,3 +9,6 @@ var openAPISpec []byte
//go:embed docs/index.html
var docsHTML []byte
//go:embed docs/static/*
var docsStatic embed.FS

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
website/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
website/assets/logo-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
website/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -86,9 +86,12 @@ a {
}
.mark-icon {
width: 1.35rem;
height: 1.35rem;
fill: var(--ember);
width: 1.75rem;
height: 1.75rem;
border-radius: 0.4rem;
object-fit: cover;
display: block;
box-shadow: 0 0 0 1px rgba(26, 34, 28, 0.08);
}
.top-nav {

View File

@ -6,7 +6,9 @@
<title>Spark Messenger — 即时沟通,火花相连</title>
<meta name="description" content="Spark Messenger 是面向好友、群聊与朋友圈的即时通讯应用。安全、简洁、跨端同步。" />
<link rel="canonical" href="https://sparkmessenger.wyrs365.com/" />
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/assets/favicon.ico?v=20260924-flames" sizes="any" />
<link rel="icon" href="/assets/favicon-32.png?v=20260924-flames" type="image/png" sizes="32x32" />
<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png?v=20260924-flames" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700;12..96,800&family=Source+Serif+4:opsz,wght@8..60,400;8..60,600&display=swap" rel="stylesheet" />
@ -17,9 +19,7 @@
<header class="top">
<a class="mark" href="/">
<svg class="mark-icon" viewBox="0 0 32 32" aria-hidden="true">
<path d="M16 2.5 19.8 12.2 30 14.5 21.8 20.8 24.5 30 16 24.6 7.5 30 10.2 20.8 2 14.5 12.2 12.2Z" />
</svg>
<img class="mark-icon" src="/assets/logo.png?v=20260924-flames" width="32" height="32" alt="" />
<span>Spark Messenger</span>
</a>
<nav class="top-nav" aria-label="主导航">