diff --git a/backend/src/main.rs b/backend/src/main.rs index db6ff88..8cfb807 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -304,6 +304,7 @@ async fn main() -> Result<()> { let app = Router::new() .route("/api/bootstrap", get(routes::public::bootstrap)) + .route("/api/favicon", get(routes::public::favicon)) .route("/api/history/{id}", get(routes::public::history)) .route("/api/latency/{id}", get(routes::public::latency_history)) .route("/api/exchange-rates", get(routes::public::exchange_rates)) diff --git a/backend/src/routes/public.rs b/backend/src/routes/public.rs index 6642f3d..cc0a426 100644 --- a/backend/src/routes/public.rs +++ b/backend/src/routes/public.rs @@ -5,7 +5,7 @@ use crate::models::PublicConfig; use axum::Json; use axum::extract::{Path, Query, State}; use axum::http::HeaderMap; -use axum::response::{IntoResponse, Response}; +use axum::response::{IntoResponse, Redirect, Response}; use serde::Serialize; use std::collections::HashMap; use std::sync::Arc; @@ -115,6 +115,21 @@ pub async fn bootstrap( })) } +pub async fn favicon(State(state): State>) -> Result { + let settings = crate::db::load_settings(&state.db) + .await + .map_err(ApiResponse::internal)?; + let logo_url = settings.logo_url.trim(); + let target = if logo_url.is_empty() { + "/logo.svg".to_string() + } else { + url::Url::parse(logo_url) + .map_err(ApiResponse::internal)? + .to_string() + }; + Ok(Redirect::temporary(&target).into_response()) +} + pub async fn history( State(state): State>, headers: HeaderMap, diff --git a/frontend/admin.html b/frontend/admin.html index d7b4f74..159f280 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -8,7 +8,7 @@ - + 管理面板 diff --git a/frontend/index.html b/frontend/index.html index f6a2868..d988ad0 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,7 +5,7 @@ - + diff --git a/frontend/src/AppProvider.tsx b/frontend/src/AppProvider.tsx index e493f88..84e2e87 100644 --- a/frontend/src/AppProvider.tsx +++ b/frontend/src/AppProvider.tsx @@ -94,7 +94,7 @@ export function AppProvider({ children }: { children: ReactNode }) { const background = resolveBackground(config.background_url, dark); const blur = themeToggle(config, "enableBlur"); const carrierLatency = config.show_latency && themeToggle(config, "showCarrierLatency", false); - useFavicon(config.logo_url); + useFavicon(configReady ? config.logo_url : undefined); if (!reloadQueueRef.current) { reloadQueueRef.current = createRefreshQueue(async (quiet) => { diff --git a/frontend/src/hooks/useBrowserAppearance.ts b/frontend/src/hooks/useBrowserAppearance.ts index 755ebca..23f7bc4 100644 --- a/frontend/src/hooks/useBrowserAppearance.ts +++ b/frontend/src/hooks/useBrowserAppearance.ts @@ -32,6 +32,8 @@ export function useSystemDark() { export function useFavicon(logoUrl: string | null | undefined) { useEffect(() => { + // Keep the server-resolved icon while the site configuration is loading. + if (logoUrl == null) return; let link = document.querySelector('link[rel="icon"]'); if (!link) { link = document.createElement("link"); @@ -39,6 +41,6 @@ export function useFavicon(logoUrl: string | null | undefined) { document.head.append(link); } link.removeAttribute("type"); - link.href = logoUrl || "/logo.svg"; + link.href = logoUrl.trim() || "/logo.svg"; }, [logoUrl]); }