feat: 添加站点设置中的 favicon URL 支持,更新相关接口和页面
All checks were successful
ui-regression / playwright-regression (push) Successful in 6m20s
docker-images / resolve-build-targets (push) Successful in 6s
docker-images / build-and-push (admin) (push) Successful in 25s
docker-images / build-and-push (backend) (push) Successful in 35s
docker-images / build-and-push (frontend) (push) Successful in 1m46s
docker-images / submit-indexnow (push) Successful in 15s
All checks were successful
ui-regression / playwright-regression (push) Successful in 6m20s
docker-images / resolve-build-targets (push) Successful in 6s
docker-images / build-and-push (admin) (push) Successful in 25s
docker-images / build-and-push (backend) (push) Successful in 35s
docker-images / build-and-push (frontend) (push) Successful in 1m46s
docker-images / submit-indexnow (push) Successful in 15s
This commit is contained in:
@@ -412,6 +412,7 @@ export interface AdminSiteSettingsResponse {
|
|||||||
media_r2_public_base_url: string | null
|
media_r2_public_base_url: string | null
|
||||||
media_r2_access_key_id: string | null
|
media_r2_access_key_id: string | null
|
||||||
media_r2_secret_access_key: string | null
|
media_r2_secret_access_key: string | null
|
||||||
|
seo_favicon_url: string | null
|
||||||
seo_default_og_image: string | null
|
seo_default_og_image: string | null
|
||||||
seo_default_twitter_handle: string | null
|
seo_default_twitter_handle: string | null
|
||||||
seo_wechat_share_qr_enabled: boolean
|
seo_wechat_share_qr_enabled: boolean
|
||||||
@@ -489,6 +490,7 @@ export interface SiteSettingsPayload {
|
|||||||
mediaR2PublicBaseUrl?: string | null
|
mediaR2PublicBaseUrl?: string | null
|
||||||
mediaR2AccessKeyId?: string | null
|
mediaR2AccessKeyId?: string | null
|
||||||
mediaR2SecretAccessKey?: string | null
|
mediaR2SecretAccessKey?: string | null
|
||||||
|
seoFaviconUrl?: string | null
|
||||||
seoDefaultOgImage?: string | null
|
seoDefaultOgImage?: string | null
|
||||||
seoDefaultTwitterHandle?: string | null
|
seoDefaultTwitterHandle?: string | null
|
||||||
seoWechatShareQrEnabled?: boolean
|
seoWechatShareQrEnabled?: boolean
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ function toPayload(form: AdminSiteSettingsResponse): SiteSettingsPayload {
|
|||||||
mediaR2PublicBaseUrl: form.media_r2_public_base_url,
|
mediaR2PublicBaseUrl: form.media_r2_public_base_url,
|
||||||
mediaR2AccessKeyId: form.media_r2_access_key_id,
|
mediaR2AccessKeyId: form.media_r2_access_key_id,
|
||||||
mediaR2SecretAccessKey: form.media_r2_secret_access_key,
|
mediaR2SecretAccessKey: form.media_r2_secret_access_key,
|
||||||
|
seoFaviconUrl: form.seo_favicon_url,
|
||||||
seoDefaultOgImage: form.seo_default_og_image,
|
seoDefaultOgImage: form.seo_default_og_image,
|
||||||
seoDefaultTwitterHandle: form.seo_default_twitter_handle,
|
seoDefaultTwitterHandle: form.seo_default_twitter_handle,
|
||||||
seoWechatShareQrEnabled: form.seo_wechat_share_qr_enabled,
|
seoWechatShareQrEnabled: form.seo_wechat_share_qr_enabled,
|
||||||
@@ -915,6 +916,22 @@ export function SiteSettingsPage() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-6 lg:grid-cols-2">
|
<CardContent className="grid gap-6 lg:grid-cols-2">
|
||||||
|
<Field label="Favicon URL" hint="浏览器标签页图标;支持外链,也支持上传 / 抓取 / 选择媒体库。">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Input
|
||||||
|
value={form.seo_favicon_url ?? ''}
|
||||||
|
onChange={(event) => updateField('seo_favicon_url', event.target.value)}
|
||||||
|
/>
|
||||||
|
<MediaUrlControls
|
||||||
|
value={form.seo_favicon_url ?? ''}
|
||||||
|
onChange={(seoFaviconUrl) => updateField('seo_favicon_url', seoFaviconUrl)}
|
||||||
|
prefix="seo-assets/"
|
||||||
|
contextLabel="站点 favicon 上传"
|
||||||
|
remoteTitle={form.site_name || form.site_title || '站点 favicon'}
|
||||||
|
dataTestIdPrefix="site-favicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
<Field label="默认 OG 图 URL" hint="文章未单独设置时作为分享图回退,也支持上传 / 抓取 / 选择媒体库。">
|
<Field label="默认 OG 图 URL" hint="文章未单独设置时作为分享图回退,也支持上传 / 抓取 / 选择媒体库。">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -60,3 +60,4 @@
|
|||||||
ai_system_prompt: "你是这个博客的站内 AI 助手。请优先依据检索到的站内内容回答问题,回答保持准确、简洁、清晰;如果上下文不足,请明确说明,不要编造。"
|
ai_system_prompt: "你是这个博客的站内 AI 助手。请优先依据检索到的站内内容回答问题,回答保持准确、简洁、清晰;如果上下文不足,请明确说明,不要编造。"
|
||||||
ai_top_k: 4
|
ai_top_k: 4
|
||||||
ai_chunk_size: 1200
|
ai_chunk_size: 1200
|
||||||
|
seo_favicon_url: null
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ mod m20260402_000036_create_worker_jobs;
|
|||||||
mod m20260402_000037_add_wechat_share_qr_setting_to_site_settings;
|
mod m20260402_000037_add_wechat_share_qr_setting_to_site_settings;
|
||||||
mod m20260402_000038_add_music_enabled_to_site_settings;
|
mod m20260402_000038_add_music_enabled_to_site_settings;
|
||||||
mod m20260402_000039_add_maintenance_mode_to_site_settings;
|
mod m20260402_000039_add_maintenance_mode_to_site_settings;
|
||||||
|
mod m20260403_000040_add_favicon_url_to_site_settings;
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -98,6 +99,7 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260402_000037_add_wechat_share_qr_setting_to_site_settings::Migration),
|
Box::new(m20260402_000037_add_wechat_share_qr_setting_to_site_settings::Migration),
|
||||||
Box::new(m20260402_000038_add_music_enabled_to_site_settings::Migration),
|
Box::new(m20260402_000038_add_music_enabled_to_site_settings::Migration),
|
||||||
Box::new(m20260402_000039_add_maintenance_mode_to_site_settings::Migration),
|
Box::new(m20260402_000039_add_maintenance_mode_to_site_settings::Migration),
|
||||||
|
Box::new(m20260403_000040_add_favicon_url_to_site_settings::Migration),
|
||||||
// inject-above (do not remove this comment)
|
// inject-above (do not remove this comment)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
let table = Alias::new("site_settings");
|
||||||
|
|
||||||
|
if !manager.has_column("site_settings", "seo_favicon_url").await? {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(table.clone())
|
||||||
|
.add_column(
|
||||||
|
ColumnDef::new(Alias::new("seo_favicon_url"))
|
||||||
|
.string()
|
||||||
|
.null(),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
let table = Alias::new("site_settings");
|
||||||
|
|
||||||
|
if manager.has_column("site_settings", "seo_favicon_url").await? {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(table)
|
||||||
|
.drop_column(Alias::new("seo_favicon_url"))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -397,6 +397,14 @@ impl Hooks for App {
|
|||||||
Some(trimmed.to_string())
|
Some(trimmed.to_string())
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let seo_favicon_url = settings["seo_favicon_url"].as_str().and_then(|value| {
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(trimmed.to_string())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let item = site_settings::ActiveModel {
|
let item = site_settings::ActiveModel {
|
||||||
id: Set(settings["id"].as_i64().unwrap_or(1) as i32),
|
id: Set(settings["id"].as_i64().unwrap_or(1) as i32),
|
||||||
@@ -441,6 +449,7 @@ impl Hooks for App {
|
|||||||
music_enabled: Set(music_enabled),
|
music_enabled: Set(music_enabled),
|
||||||
maintenance_mode_enabled: Set(maintenance_mode_enabled),
|
maintenance_mode_enabled: Set(maintenance_mode_enabled),
|
||||||
maintenance_access_code: Set(maintenance_access_code),
|
maintenance_access_code: Set(maintenance_access_code),
|
||||||
|
seo_favicon_url: Set(seo_favicon_url),
|
||||||
ai_enabled: Set(settings["ai_enabled"].as_bool()),
|
ai_enabled: Set(settings["ai_enabled"].as_bool()),
|
||||||
paragraph_comments_enabled: Set(settings["paragraph_comments_enabled"]
|
paragraph_comments_enabled: Set(settings["paragraph_comments_enabled"]
|
||||||
.as_bool()
|
.as_bool()
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ pub struct AdminSiteSettingsResponse {
|
|||||||
pub media_r2_public_base_url: Option<String>,
|
pub media_r2_public_base_url: Option<String>,
|
||||||
pub media_r2_access_key_id: Option<String>,
|
pub media_r2_access_key_id: Option<String>,
|
||||||
pub media_r2_secret_access_key: Option<String>,
|
pub media_r2_secret_access_key: Option<String>,
|
||||||
|
pub seo_favicon_url: Option<String>,
|
||||||
pub seo_default_og_image: Option<String>,
|
pub seo_default_og_image: Option<String>,
|
||||||
pub seo_default_twitter_handle: Option<String>,
|
pub seo_default_twitter_handle: Option<String>,
|
||||||
pub seo_wechat_share_qr_enabled: bool,
|
pub seo_wechat_share_qr_enabled: bool,
|
||||||
@@ -1068,6 +1069,7 @@ fn build_settings_response(
|
|||||||
media_r2_public_base_url: item.media_r2_public_base_url,
|
media_r2_public_base_url: item.media_r2_public_base_url,
|
||||||
media_r2_access_key_id: item.media_r2_access_key_id,
|
media_r2_access_key_id: item.media_r2_access_key_id,
|
||||||
media_r2_secret_access_key: item.media_r2_secret_access_key,
|
media_r2_secret_access_key: item.media_r2_secret_access_key,
|
||||||
|
seo_favicon_url: item.seo_favicon_url,
|
||||||
seo_default_og_image: item.seo_default_og_image,
|
seo_default_og_image: item.seo_default_og_image,
|
||||||
seo_default_twitter_handle: item.seo_default_twitter_handle,
|
seo_default_twitter_handle: item.seo_default_twitter_handle,
|
||||||
seo_wechat_share_qr_enabled: item.seo_wechat_share_qr_enabled.unwrap_or(false),
|
seo_wechat_share_qr_enabled: item.seo_wechat_share_qr_enabled.unwrap_or(false),
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ pub struct SiteSettingsPayload {
|
|||||||
pub media_r2_access_key_id: Option<String>,
|
pub media_r2_access_key_id: Option<String>,
|
||||||
#[serde(default, alias = "mediaR2SecretAccessKey")]
|
#[serde(default, alias = "mediaR2SecretAccessKey")]
|
||||||
pub media_r2_secret_access_key: Option<String>,
|
pub media_r2_secret_access_key: Option<String>,
|
||||||
|
#[serde(default, alias = "seoFaviconUrl")]
|
||||||
|
pub seo_favicon_url: Option<String>,
|
||||||
#[serde(default, alias = "seoDefaultOgImage")]
|
#[serde(default, alias = "seoDefaultOgImage")]
|
||||||
pub seo_default_og_image: Option<String>,
|
pub seo_default_og_image: Option<String>,
|
||||||
#[serde(default, alias = "seoDefaultTwitterHandle")]
|
#[serde(default, alias = "seoDefaultTwitterHandle")]
|
||||||
@@ -220,6 +222,7 @@ pub struct PublicSiteSettingsResponse {
|
|||||||
pub subscription_popup_title: String,
|
pub subscription_popup_title: String,
|
||||||
pub subscription_popup_description: String,
|
pub subscription_popup_description: String,
|
||||||
pub subscription_popup_delay_seconds: i32,
|
pub subscription_popup_delay_seconds: i32,
|
||||||
|
pub seo_favicon_url: Option<String>,
|
||||||
pub seo_default_og_image: Option<String>,
|
pub seo_default_og_image: Option<String>,
|
||||||
pub seo_default_twitter_handle: Option<String>,
|
pub seo_default_twitter_handle: Option<String>,
|
||||||
pub seo_wechat_share_qr_enabled: bool,
|
pub seo_wechat_share_qr_enabled: bool,
|
||||||
@@ -776,6 +779,9 @@ impl SiteSettingsPayload {
|
|||||||
item.media_r2_secret_access_key =
|
item.media_r2_secret_access_key =
|
||||||
normalize_optional_string(Some(media_r2_secret_access_key));
|
normalize_optional_string(Some(media_r2_secret_access_key));
|
||||||
}
|
}
|
||||||
|
if let Some(seo_favicon_url) = self.seo_favicon_url {
|
||||||
|
item.seo_favicon_url = normalize_optional_string(Some(seo_favicon_url));
|
||||||
|
}
|
||||||
if let Some(seo_default_og_image) = self.seo_default_og_image {
|
if let Some(seo_default_og_image) = self.seo_default_og_image {
|
||||||
item.seo_default_og_image = normalize_optional_string(Some(seo_default_og_image));
|
item.seo_default_og_image = normalize_optional_string(Some(seo_default_og_image));
|
||||||
}
|
}
|
||||||
@@ -942,6 +948,7 @@ fn default_payload() -> SiteSettingsPayload {
|
|||||||
media_r2_public_base_url: None,
|
media_r2_public_base_url: None,
|
||||||
media_r2_access_key_id: None,
|
media_r2_access_key_id: None,
|
||||||
media_r2_secret_access_key: None,
|
media_r2_secret_access_key: None,
|
||||||
|
seo_favicon_url: None,
|
||||||
seo_default_og_image: None,
|
seo_default_og_image: None,
|
||||||
seo_default_twitter_handle: None,
|
seo_default_twitter_handle: None,
|
||||||
seo_wechat_share_qr_enabled: Some(false),
|
seo_wechat_share_qr_enabled: Some(false),
|
||||||
@@ -1041,6 +1048,7 @@ fn public_response(model: Model) -> PublicSiteSettingsResponse {
|
|||||||
subscription_popup_delay_seconds: model
|
subscription_popup_delay_seconds: model
|
||||||
.subscription_popup_delay_seconds
|
.subscription_popup_delay_seconds
|
||||||
.unwrap_or_else(default_subscription_popup_delay_seconds),
|
.unwrap_or_else(default_subscription_popup_delay_seconds),
|
||||||
|
seo_favicon_url: model.seo_favicon_url,
|
||||||
seo_default_og_image: model.seo_default_og_image,
|
seo_default_og_image: model.seo_default_og_image,
|
||||||
seo_default_twitter_handle: model.seo_default_twitter_handle,
|
seo_default_twitter_handle: model.seo_default_twitter_handle,
|
||||||
seo_wechat_share_qr_enabled: model.seo_wechat_share_qr_enabled.unwrap_or(false),
|
seo_wechat_share_qr_enabled: model.seo_wechat_share_qr_enabled.unwrap_or(false),
|
||||||
|
|||||||
@@ -60,3 +60,4 @@
|
|||||||
ai_system_prompt: "你是这个博客的站内 AI 助手。请优先依据检索到的站内内容回答问题,回答保持准确、简洁、清晰;如果上下文不足,请明确说明,不要编造。"
|
ai_system_prompt: "你是这个博客的站内 AI 助手。请优先依据检索到的站内内容回答问题,回答保持准确、简洁、清晰;如果上下文不足,请明确说明,不要编造。"
|
||||||
ai_top_k: 4
|
ai_top_k: 4
|
||||||
ai_chunk_size: 1200
|
ai_chunk_size: 1200
|
||||||
|
seo_favicon_url: null
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ async fn sync_site_settings(ctx: &AppContext, base: &Path) -> Result<()> {
|
|||||||
let music_enabled = seed["music_enabled"].as_bool().or(Some(true));
|
let music_enabled = seed["music_enabled"].as_bool().or(Some(true));
|
||||||
let maintenance_mode_enabled = seed["maintenance_mode_enabled"].as_bool().or(Some(false));
|
let maintenance_mode_enabled = seed["maintenance_mode_enabled"].as_bool().or(Some(false));
|
||||||
let maintenance_access_code = as_optional_string(&seed["maintenance_access_code"]);
|
let maintenance_access_code = as_optional_string(&seed["maintenance_access_code"]);
|
||||||
|
let seo_favicon_url = as_optional_string(&seed["seo_favicon_url"]);
|
||||||
let comment_verification_mode = as_optional_string(&seed["comment_verification_mode"]);
|
let comment_verification_mode = as_optional_string(&seed["comment_verification_mode"]);
|
||||||
let subscription_verification_mode =
|
let subscription_verification_mode =
|
||||||
as_optional_string(&seed["subscription_verification_mode"]);
|
as_optional_string(&seed["subscription_verification_mode"]);
|
||||||
@@ -196,6 +197,9 @@ async fn sync_site_settings(ctx: &AppContext, base: &Path) -> Result<()> {
|
|||||||
if is_blank(&existing.maintenance_access_code) {
|
if is_blank(&existing.maintenance_access_code) {
|
||||||
model.maintenance_access_code = Set(maintenance_access_code.clone());
|
model.maintenance_access_code = Set(maintenance_access_code.clone());
|
||||||
}
|
}
|
||||||
|
if is_blank(&existing.seo_favicon_url) {
|
||||||
|
model.seo_favicon_url = Set(seo_favicon_url.clone());
|
||||||
|
}
|
||||||
if existing.ai_enabled.is_none() {
|
if existing.ai_enabled.is_none() {
|
||||||
model.ai_enabled = Set(seed["ai_enabled"].as_bool());
|
model.ai_enabled = Set(seed["ai_enabled"].as_bool());
|
||||||
}
|
}
|
||||||
@@ -278,6 +282,7 @@ async fn sync_site_settings(ctx: &AppContext, base: &Path) -> Result<()> {
|
|||||||
music_enabled: Set(music_enabled),
|
music_enabled: Set(music_enabled),
|
||||||
maintenance_mode_enabled: Set(maintenance_mode_enabled),
|
maintenance_mode_enabled: Set(maintenance_mode_enabled),
|
||||||
maintenance_access_code: Set(maintenance_access_code),
|
maintenance_access_code: Set(maintenance_access_code),
|
||||||
|
seo_favicon_url: Set(seo_favicon_url),
|
||||||
ai_enabled: Set(seed["ai_enabled"].as_bool()),
|
ai_enabled: Set(seed["ai_enabled"].as_bool()),
|
||||||
paragraph_comments_enabled: Set(seed["paragraph_comments_enabled"]
|
paragraph_comments_enabled: Set(seed["paragraph_comments_enabled"]
|
||||||
.as_bool()
|
.as_bool()
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ pub struct Model {
|
|||||||
#[sea_orm(column_type = "Text", nullable)]
|
#[sea_orm(column_type = "Text", nullable)]
|
||||||
pub media_r2_secret_access_key: Option<String>,
|
pub media_r2_secret_access_key: Option<String>,
|
||||||
#[sea_orm(column_type = "Text", nullable)]
|
#[sea_orm(column_type = "Text", nullable)]
|
||||||
|
pub seo_favicon_url: Option<String>,
|
||||||
|
#[sea_orm(column_type = "Text", nullable)]
|
||||||
pub seo_default_og_image: Option<String>,
|
pub seo_default_og_image: Option<String>,
|
||||||
pub seo_default_twitter_handle: Option<String>,
|
pub seo_default_twitter_handle: Option<String>,
|
||||||
pub seo_wechat_share_qr_enabled: Option<bool>,
|
pub seo_wechat_share_qr_enabled: Option<bool>,
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 655 B |
@@ -148,7 +148,7 @@ const i18nPayload = JSON.stringify({ locale, messages });
|
|||||||
title={`${siteSettings.siteName} llms-full.txt`}
|
title={`${siteSettings.siteName} llms-full.txt`}
|
||||||
href={`${siteUrl}/llms-full.txt`}
|
href={`${siteUrl}/llms-full.txt`}
|
||||||
/>
|
/>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
{jsonLd && <script type="application/ld+json" set:html={jsonLd}></script>}
|
{jsonLd && <script type="application/ld+json" set:html={jsonLd}></script>}
|
||||||
<slot name="head" />
|
<slot name="head" />
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ export interface ApiSiteSettings {
|
|||||||
subscription_popup_title: string | null;
|
subscription_popup_title: string | null;
|
||||||
subscription_popup_description: string | null;
|
subscription_popup_description: string | null;
|
||||||
subscription_popup_delay_seconds: number | null;
|
subscription_popup_delay_seconds: number | null;
|
||||||
|
seo_favicon_url: string | null;
|
||||||
seo_default_og_image: string | null;
|
seo_default_og_image: string | null;
|
||||||
seo_default_twitter_handle: string | null;
|
seo_default_twitter_handle: string | null;
|
||||||
seo_wechat_share_qr_enabled: boolean;
|
seo_wechat_share_qr_enabled: boolean;
|
||||||
@@ -492,6 +493,7 @@ export const DEFAULT_SITE_SETTINGS: SiteSettings = {
|
|||||||
webPushVapidPublicKey: undefined,
|
webPushVapidPublicKey: undefined,
|
||||||
},
|
},
|
||||||
seo: {
|
seo: {
|
||||||
|
faviconUrl: undefined,
|
||||||
defaultOgImage: undefined,
|
defaultOgImage: undefined,
|
||||||
defaultTwitterHandle: undefined,
|
defaultTwitterHandle: undefined,
|
||||||
wechatShareQrEnabled: false,
|
wechatShareQrEnabled: false,
|
||||||
@@ -669,6 +671,7 @@ const normalizeSiteSettings = (settings: ApiSiteSettings): SiteSettings => {
|
|||||||
undefined,
|
undefined,
|
||||||
},
|
},
|
||||||
seo: {
|
seo: {
|
||||||
|
faviconUrl: settings.seo_favicon_url ?? undefined,
|
||||||
defaultOgImage: settings.seo_default_og_image ?? undefined,
|
defaultOgImage: settings.seo_default_og_image ?? undefined,
|
||||||
defaultTwitterHandle: settings.seo_default_twitter_handle ?? undefined,
|
defaultTwitterHandle: settings.seo_default_twitter_handle ?? undefined,
|
||||||
wechatShareQrEnabled: Boolean(settings.seo_wechat_share_qr_enabled),
|
wechatShareQrEnabled: Boolean(settings.seo_wechat_share_qr_enabled),
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ export function shouldBypassMaintenance(pathname: string): boolean {
|
|||||||
pathname === '/maintenance' ||
|
pathname === '/maintenance' ||
|
||||||
pathname.startsWith('/api/maintenance') ||
|
pathname.startsWith('/api/maintenance') ||
|
||||||
pathname === '/healthz' ||
|
pathname === '/healthz' ||
|
||||||
pathname === '/favicon.svg' ||
|
pathname === '/favicon.ico' ||
|
||||||
|
pathname.startsWith('/favicon.') ||
|
||||||
|
pathname.startsWith('/apple-touch-icon') ||
|
||||||
pathname.startsWith('/_astro/') ||
|
pathname.startsWith('/_astro/') ||
|
||||||
pathname.startsWith('/_image') ||
|
pathname.startsWith('/_image') ||
|
||||||
pathname.startsWith('/_img')
|
pathname.startsWith('/_img')
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export interface SiteSettings {
|
|||||||
webPushVapidPublicKey?: string;
|
webPushVapidPublicKey?: string;
|
||||||
};
|
};
|
||||||
seo: {
|
seo: {
|
||||||
|
faviconUrl?: string;
|
||||||
defaultOgImage?: string;
|
defaultOgImage?: string;
|
||||||
defaultTwitterHandle?: string;
|
defaultTwitterHandle?: string;
|
||||||
wechatShareQrEnabled: boolean;
|
wechatShareQrEnabled: boolean;
|
||||||
|
|||||||
35
frontend/src/pages/favicon.ico.ts
Normal file
35
frontend/src/pages/favicon.ico.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
|
||||||
|
import { createApiClient, DEFAULT_SITE_SETTINGS } from '../lib/api/client'
|
||||||
|
|
||||||
|
function resolveFaviconTarget(requestUrl: URL, configured: string | undefined) {
|
||||||
|
const fallbackTarget = '/favicon.svg'
|
||||||
|
const candidate = configured?.trim()
|
||||||
|
|
||||||
|
if (!candidate) {
|
||||||
|
return fallbackTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resolved = new URL(candidate, requestUrl)
|
||||||
|
if (resolved.pathname === '/favicon.ico' && resolved.origin === requestUrl.origin) {
|
||||||
|
return fallbackTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved.toString()
|
||||||
|
} catch {
|
||||||
|
return fallbackTarget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ url, redirect }) => {
|
||||||
|
let siteSettings = DEFAULT_SITE_SETTINGS
|
||||||
|
|
||||||
|
try {
|
||||||
|
siteSettings = await createApiClient({ requestUrl: url }).getSiteSettings()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load site settings for favicon:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect(resolveFaviconTarget(url, siteSettings.seo.faviconUrl), 302)
|
||||||
|
}
|
||||||
@@ -20,9 +20,9 @@ const errorMessage =
|
|||||||
? '请先输入访问口令。'
|
? '请先输入访问口令。'
|
||||||
: errorCode === 'invalid'
|
: errorCode === 'invalid'
|
||||||
? '口令不正确,请重新输入。'
|
? '口令不正确,请重新输入。'
|
||||||
: errorCode === 'unavailable'
|
: errorCode === 'unavailable'
|
||||||
? '当前无法校验访问口令,请稍后再试。'
|
? '当前无法校验访问口令,请稍后再试。'
|
||||||
: ''
|
: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -31,6 +31,7 @@ const errorMessage =
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<title>{siteSettings.siteName} · 维护模式</title>
|
<title>{siteSettings.siteName} · 维护模式</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-[var(--bg)] text-[var(--text)]">
|
<body class="min-h-screen bg-[var(--bg)] text-[var(--text)]">
|
||||||
|
|||||||
@@ -824,6 +824,7 @@ function createSiteSettings() {
|
|||||||
media_r2_public_base_url: `${MOCK_ORIGIN}/media`,
|
media_r2_public_base_url: `${MOCK_ORIGIN}/media`,
|
||||||
media_r2_access_key_id: 'mock-access-key',
|
media_r2_access_key_id: 'mock-access-key',
|
||||||
media_r2_secret_access_key: 'mock-secret',
|
media_r2_secret_access_key: 'mock-secret',
|
||||||
|
seo_favicon_url: `${MOCK_ORIGIN}/favicon.svg`,
|
||||||
seo_default_og_image: `${MOCK_ORIGIN}/media-files/default-og.svg`,
|
seo_default_og_image: `${MOCK_ORIGIN}/media-files/default-og.svg`,
|
||||||
seo_default_twitter_handle: '@initcool',
|
seo_default_twitter_handle: '@initcool',
|
||||||
seo_wechat_share_qr_enabled: false,
|
seo_wechat_share_qr_enabled: false,
|
||||||
@@ -2768,6 +2769,7 @@ const server = createServer(async (req, res) => {
|
|||||||
mediaR2PublicBaseUrl: 'media_r2_public_base_url',
|
mediaR2PublicBaseUrl: 'media_r2_public_base_url',
|
||||||
mediaR2AccessKeyId: 'media_r2_access_key_id',
|
mediaR2AccessKeyId: 'media_r2_access_key_id',
|
||||||
mediaR2SecretAccessKey: 'media_r2_secret_access_key',
|
mediaR2SecretAccessKey: 'media_r2_secret_access_key',
|
||||||
|
seoFaviconUrl: 'seo_favicon_url',
|
||||||
seoDefaultOgImage: 'seo_default_og_image',
|
seoDefaultOgImage: 'seo_default_og_image',
|
||||||
seoDefaultTwitterHandle: 'seo_default_twitter_handle',
|
seoDefaultTwitterHandle: 'seo_default_twitter_handle',
|
||||||
seoWechatShareQrEnabled: 'seo_wechat_share_qr_enabled',
|
seoWechatShareQrEnabled: 'seo_wechat_share_qr_enabled',
|
||||||
|
|||||||
Reference in New Issue
Block a user