feat(site): 添加站点图标(沿用论坛 LS 品牌图标)

素材来源:论坛已有的 favicon-38shvf3b.png(64x64 LS 蓝底图标),
保证与论坛视觉一致,不另外画一个。

生成 4 个尺寸(tools/make_icons.py):
- favicon.ico        多尺寸 16/32/48,浏览器标签页
- icon-192.png       Android / PWA
- icon-512.png       PWA / 高分辨率
- apple-touch-icon   iOS 主屏(180x180,用图标自身背景填充,
                     因 iOS 不支持透明)

实现
- 图标放 assets/ 而非 data/:这是需要版本管理的品牌素材,
  不是可重建的缓存
- build_site() 复制到输出目录并生成 manifest.webmanifest
- 图标缺失只告警不报错,不因素材问题导致建站失败

验证:203 测试通过(新增 2 个,含「assets 缺失不影响建站」)。
This commit is contained in:
gungun88 committed 2026-10-17 22:25:56 +08:00
1 parent 0fcc93b816
commit 1eb834b1b4
8 files changed
+148

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+4
View File
@@ -39,6 +39,10 @@ DATA_DIR = Path(_env("LDR_DATA_DIR", str(ROOT / "data")))
SITE_DIR = Path(_env("LDR_SITE_DIR", str(ROOT / "site")))
REPLY_CACHE_DIR = DATA_DIR / "replies"
# 站点静态资源(图标等)。与抓取数据分开:这些是需要版本管理的品牌素材,
# 不是可重建的缓存。
ASSETS_DIR = Path(_env("LDR_ASSETS_DIR", str(ROOT / "assets")))
DEALS_JSON = DATA_DIR / "deals.json"
DEALS_MD = DATA_DIR / "deals.md"
FEED_CACHE = DATA_DIR / "feed.xml"
+59
View File
@@ -10,6 +10,7 @@ from __future__ import annotations
import html
import json
import os
import shutil
from datetime import datetime, timezone
from . import config
@@ -160,6 +161,13 @@ SITE_TEMPLATE = r"""<!DOCTYPE html>
<title>@@TITLE@@</title>
<meta name="description" content="@@DESC@@">
@@CANONICAL@@
<!-- 站点图标:沿用论坛的 LS 品牌图标,保持视觉一致。
ico 供浏览器标签页(多尺寸),PNG 供 Android/PWA 与 iOS 主屏。 -->
<link rel="icon" href="favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="192x192" href="icon-192.png">
<link rel="apple-touch-icon" href="apple-touch-icon.png">
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#75ADFB">
<style>
:root{
--bg:#f6f7f9; --card:#fff; --ink:#16181d; --muted:#6b7280; --line:#e5e7eb;
@@ -687,8 +695,59 @@ def build_site(payload: dict, out_dir=None, verbose: bool = True) -> str:
f' <url><loc>{loc}</loc><lastmod>{today}</lastmod></url>\n'
'</urlset>\n')
# 站点图标:从 assets/ 复制到输出目录。
# 图标是二进制资源,不适合内联进模板;放 assets/ 便于替换品牌素材。
_copy_icons(out_dir, verbose)
if verbose:
print(f"站点已生成 -> {os.path.abspath(out_dir)}")
print(" index.html %8.1f KB" % (os.path.getsize(index_path) / 1024))
print(" data/deals.json %8.1f KB" % (os.path.getsize(api_path) / 1024))
return out_dir
# 需要随站点发布的图标文件
ICON_FILES = ("favicon.ico", "icon-192.png", "icon-512.png",
"apple-touch-icon.png")
def _copy_icons(out_dir: str, verbose: bool = True) -> None:
"""把 assets/ 下的图标复制到站点目录,并生成 webmanifest。
缺失图标时只告警不报错 —— 不应因为图标问题导致建站失败。
"""
src_dir = config.ASSETS_DIR
copied = 0
for name in ICON_FILES:
src = os.path.join(str(src_dir), name)
if os.path.exists(src):
shutil.copyfile(src, os.path.join(out_dir, name))
copied += 1
elif verbose:
print(" [warn] 缺少图标 assets/%s(运行 tools/make_icons.py 生成)"
% name)
# webmanifest:让 Android/PWA 也能用上图标
name = (config.SITE_TITLE or "Deals").strip()
short = name[:12]
icons = []
for fn, size in (("icon-192.png", "192x192"),
("icon-512.png", "512x512")):
if os.path.exists(os.path.join(out_dir, fn)):
icons.append({"src": fn, "sizes": size, "type": "image/png",
"purpose": "any"})
manifest = {
"name": name,
"short_name": short,
"icons": icons,
"theme_color": "#75ADFB",
"background_color": "#ffffff",
"display": "standalone",
"start_url": "./",
}
with open(os.path.join(out_dir, "manifest.webmanifest"), "w",
encoding="utf-8") as f:
json.dump(manifest, f, ensure_ascii=False, indent=2)
if verbose and copied:
print(" 图标 %d 个" % copied)
+34
View File
@@ -301,6 +301,40 @@ def test_link_rendering_both_cases(tmp_path):
assert "if(r.forum_url)" in html
def test_icons_declared_and_copied(tmp_path):
"""站点图标:HTML 声明 + 文件复制 + manifest 生成。"""
render.build_site(_sample_payload(), str(tmp_path), verbose=False)
html = open(os.path.join(tmp_path, "index.html"), encoding="utf-8").read()
assert 'rel="icon"' in html, "缺少 favicon 声明"
assert "apple-touch-icon" in html, "缺少 iOS 主屏图标声明"
assert 'rel="manifest"' in html, "缺少 webmanifest 声明"
# 图标文件应随站点发布
for name in ("favicon.ico", "manifest.webmanifest"):
assert os.path.exists(os.path.join(tmp_path, name)), f"缺少 {name}"
# manifest 内容应可解析且含图标
m = json.load(open(os.path.join(tmp_path, "manifest.webmanifest"),
encoding="utf-8"))
assert m["name"]
assert isinstance(m["icons"], list)
def test_missing_assets_does_not_break_build(tmp_path, monkeypatch):
"""图标缺失时只告警,不应导致建站失败。"""
import pytest as _pytest
mp = _pytest.MonkeyPatch()
mp.setattr(render.config, "ASSETS_DIR", "/nonexistent-assets")
try:
out = render.build_site(_sample_payload(), str(tmp_path), verbose=False)
finally:
mp.undo()
assert os.path.exists(os.path.join(out, "index.html"))
# manifest 仍应生成(只是没有图标条目)
assert os.path.exists(os.path.join(out, "manifest.webmanifest"))
def test_vendor_signal_status_is_translated(tmp_path):
"""回帖证据里的状态码要翻成中文。
+51
View File
@@ -0,0 +1,51 @@
"""从论坛 favicon 生成站点所需的各种尺寸图标。
输入是论坛现有的 64x64 PNG(LS 品牌图标)。
输出:
favicon.ico 多尺寸 ICO(16/32/48),浏览器标签页用
icon-192.png Android / PWA
icon-512.png PWA / 高分辨率
apple-touch-icon.png iOS 主屏(180x180,需不透明背景)
注意:源图只有 64x64,放大到 512 会有插值模糊,但 favicon 实际
只用 16-32px,视觉上没问题;512 版本仅为兼容部分浏览器/PWA 规范。
"""
import os
import sys
from PIL import Image
sys.stdout.reconfigure(encoding="utf-8")
SRC = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
os.environ.get("TEMP", "."), "ls_favicon.png")
OUT = sys.argv[2] if len(sys.argv) > 2 else "site"
os.makedirs(OUT, exist_ok=True)
im = Image.open(SRC).convert("RGBA")
print(" 源图: %s %s" % (SRC, im.size))
# ---- favicon.ico(多尺寸,浏览器会挑最合适的)----
ico_path = os.path.join(OUT, "favicon.ico")
im.save(ico_path, format="ICO",
sizes=[(16, 16), (32, 32), (48, 48)])
print(" favicon.ico %d 字节" % os.path.getsize(ico_path))
# ---- PNG 图标 ----
for size, name in ((192, "icon-192.png"), (512, "icon-512.png")):
p = os.path.join(OUT, name)
im.resize((size, size), Image.LANCZOS).save(p, format="PNG",
optimize=True)
print(" %-18s %d 字节" % (name, os.path.getsize(p)))
# ---- apple-touch-icon:iOS 会自己加圆角,且不支持透明 ----
# 用图标自身的背景色填充,避免透明区域变黑。
bg = im.getpixel((im.width // 2, 4)) # 取顶部中间的背景色
canvas = Image.new("RGB", (180, 180), bg[:3])
icon = im.resize((180, 180), Image.LANCZOS)
canvas.paste(icon, (0, 0), icon)
at = os.path.join(OUT, "apple-touch-icon.png")
canvas.save(at, format="PNG", optimize=True)
print(" apple-touch-icon %d 字节(背景色 %s)"
% (os.path.getsize(at), bg[:3]))