fix: add favicon (SVG + PNG + ICO), fix static MIME types (#613)
Squash-merges PR #613. Adds favicon to the app (was missing entirely — blank tab icon). 1371 tests passing, QA harness green. Review by independent agent (see PR comments). Follow-up commit addresses all three reviewer notes: hoisted _STATIC_MIME to module scope, fixed charset=utf-8 being appended to binary MIME types, confirmed correct MIME types on all three favicon formats. Co-authored-by: tiansiyuan <tiansiyuan@users.noreply.github.com>
This commit is contained in:
@@ -420,6 +420,17 @@ def handle_get(handler, parsed) -> bool:
|
||||
return j(handler, {"auth_enabled": is_auth_enabled(), "logged_in": logged_in})
|
||||
|
||||
if parsed.path == "/favicon.ico":
|
||||
static_root = Path(__file__).parent.parent / "static"
|
||||
ico_path = (static_root / "favicon.ico").resolve()
|
||||
if ico_path.exists() and ico_path.is_file():
|
||||
data = ico_path.read_bytes()
|
||||
handler.send_response(200)
|
||||
handler.send_header("Content-Type", "image/x-icon")
|
||||
handler.send_header("Content-Length", str(len(data)))
|
||||
handler.send_header("Cache-Control", "public, max-age=86400")
|
||||
handler.end_headers()
|
||||
handler.wfile.write(data)
|
||||
else:
|
||||
handler.send_response(204)
|
||||
handler.end_headers()
|
||||
return True
|
||||
@@ -1313,6 +1324,25 @@ def handle_post(handler, parsed) -> bool:
|
||||
|
||||
# ── GET route helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
# MIME types for static file serving. Hoisted to module scope to avoid
|
||||
# rebuilding the dict on every request.
|
||||
_STATIC_MIME = {
|
||||
"css": "text/css",
|
||||
"js": "application/javascript",
|
||||
"html": "text/html",
|
||||
"svg": "image/svg+xml",
|
||||
"png": "image/png",
|
||||
"jpg": "image/jpeg",
|
||||
"jpeg": "image/jpeg",
|
||||
"ico": "image/x-icon",
|
||||
"gif": "image/gif",
|
||||
"webp": "image/webp",
|
||||
"woff": "font/woff",
|
||||
"woff2": "font/woff2",
|
||||
}
|
||||
# MIME types that are text-based and should carry charset=utf-8
|
||||
_TEXT_MIME_TYPES = {"text/css", "application/javascript", "text/html", "image/svg+xml", "text/plain"}
|
||||
|
||||
|
||||
def _serve_static(handler, parsed):
|
||||
static_root = (Path(__file__).parent.parent / "static").resolve()
|
||||
@@ -1326,11 +1356,10 @@ def _serve_static(handler, parsed):
|
||||
if not static_file.exists() or not static_file.is_file():
|
||||
return j(handler, {"error": "not found"}, status=404)
|
||||
ext = static_file.suffix.lower()
|
||||
ct = {"css": "text/css", "js": "application/javascript", "html": "text/html"}.get(
|
||||
ext.lstrip("."), "text/plain"
|
||||
)
|
||||
ct = _STATIC_MIME.get(ext.lstrip("."), "text/plain")
|
||||
ct_header = f"{ct}; charset=utf-8" if ct in _TEXT_MIME_TYPES else ct
|
||||
handler.send_response(200)
|
||||
handler.send_header("Content-Type", f"{ct}; charset=utf-8")
|
||||
handler.send_header("Content-Type", ct_header)
|
||||
handler.send_header("Cache-Control", "no-store")
|
||||
raw = static_file.read_bytes()
|
||||
handler.send_header("Content-Length", str(len(raw)))
|
||||
|
||||
BIN
static/favicon-32.png
Normal file
BIN
static/favicon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
20
static/favicon.svg
Normal file
20
static/favicon.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="12" fill="#1a1a1a"/>
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#F5C542;stop-opacity:1"/>
|
||||
<stop offset="100%" style="stop-color:#D4961C;stop-opacity:1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="30" y="10" width="4" height="46" rx="2" fill="url(#g)"/>
|
||||
<path d="M30 18 C24 14, 14 14, 10 18 C14 16, 22 16, 28 20" fill="#F5C542" opacity="0.9"/>
|
||||
<path d="M30 22 C26 19, 18 19, 14 22 C18 20, 24 20, 28 24" fill="#D4961C" opacity="0.8"/>
|
||||
<path d="M34 18 C40 14, 50 14, 54 18 C50 16, 42 16, 36 20" fill="#F5C542" opacity="0.9"/>
|
||||
<path d="M34 22 C38 19, 46 19, 50 22 C46 20, 40 20, 36 24" fill="#D4961C" opacity="0.8"/>
|
||||
<path d="M32 48 C22 44, 20 38, 26 34 C20 36, 18 42, 24 46 C18 40, 22 30, 30 28 C24 32, 22 38, 28 42"
|
||||
fill="none" stroke="#F5C542" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M32 48 C42 44, 44 38, 38 34 C44 36, 46 42, 40 46 C46 40, 42 30, 34 28 C40 32, 42 38, 36 42"
|
||||
fill="none" stroke="#D4961C" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<circle cx="32" cy="10" r="4" fill="#F5C542"/>
|
||||
<circle cx="32" cy="10" r="2" fill="#FFF8E1" opacity="0.7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -4,6 +4,9 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Hermes</title>
|
||||
<link rel="icon" type="image/svg+xml" href="static/favicon.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="static/favicon-32.png">
|
||||
<link rel="shortcut icon" href="static/favicon.ico">
|
||||
<!-- base href enables subpath mount support; all static paths must stay relative (no leading slash) -->
|
||||
<script>(function(){var p=location.pathname.endsWith('/')?location.pathname:(location.pathname.replace(/\/[^\/]*$/,'/')||'/');document.write('<base href="'+location.origin+p+'">');})()</script>
|
||||
<script>(function(){var t=localStorage.getItem('hermes-theme');if(t==='system'){t=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';}if(t&&t!=='dark')document.documentElement.dataset.theme=t;})()</script>
|
||||
|
||||
Reference in New Issue
Block a user