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:
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 |
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
"""回帖证据里的状态码要翻成中文。
|
||||
|
||||
|
||||
@@ -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]))
|
||||
Reference in new issue
Block a user