chore: 上线前测试修复与部署配置更新

- fix(test): 添加 useSearchParams mock,修正联系链接断言
- style(nav): 将"联系我们"改为"联系"
- chore(deploy): 更新 Nginx 配置和部署文档
- style(logo): 更新 Logo SVG 文件
- feat(scripts): 添加字体处理和站点配置脚本
This commit was merged in pull request #9.
This commit is contained in:
张翔
2026-04-22 20:17:13 +08:00
parent 84f488a253
commit 96dddeb20b
20 changed files with 1300 additions and 560 deletions
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
PRODUCT_NAME=$1
FILES_SOURCE=$2
if [ -z "$PRODUCT_NAME" ] || [ -z "$FILES_SOURCE" ]; then
echo "用法: $0 <product-name> <html-files-directory>"
echo "示例: $0 product-a ./product-a-website"
exit 1
fi
DOMAIN="${PRODUCT_NAME}.novalon.cn"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NGINX_DIR="$(dirname "$SCRIPT_DIR")"
CONF_DIR="${NGINX_DIR}/conf.d"
SITES_DIR="${NGINX_DIR}/sites"
SSL_DIR="${NGINX_DIR}/ssl"
if [ ! -d "$FILES_SOURCE" ]; then
echo "错误: 源文件目录不存在: $FILES_SOURCE"
exit 1
fi
mkdir -p "${SITES_DIR}/${PRODUCT_NAME}"
mkdir -p "${SSL_DIR}/${DOMAIN}"
mkdir -p "${CONF_DIR}"
cp -r "${FILES_SOURCE}/"* "${SITES_DIR}/${PRODUCT_NAME}/" 2>/dev/null || true
cp -r "${FILES_SOURCE}/." "${SITES_DIR}/${PRODUCT_NAME}/" 2>/dev/null || true
cat > "${CONF_DIR}/${DOMAIN}.conf" << 'CONF_TEMPLATE'
server {
listen 80;
server_name {{DOMAIN}};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name {{DOMAIN}};
ssl_certificate /etc/nginx/ssl/{{DOMAIN}}/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/{{DOMAIN}}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
root /var/www/sites/{{PRODUCT_NAME}};
index index.html;
location ~* \.(css|js|jpg|jpeg|png|gif|webp|avif|svg|ico|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
try_files $uri =404;
}
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
limit_req zone=general burst=20 nodelay;
try_files $uri $uri/ =404;
}
error_page 404 /404.html;
access_log /var/log/nginx/{{PRODUCT_NAME}}-access.log;
error_log /var/log/nginx/{{PRODUCT_NAME}}-error.log;
}
CONF_TEMPLATE
sed -i.bak "s/{{DOMAIN}}/${DOMAIN}/g" "${CONF_DIR}/${DOMAIN}.conf"
sed -i.bak "s/{{PRODUCT_NAME}}/${PRODUCT_NAME}/g" "${CONF_DIR}/${DOMAIN}.conf"
rm -f "${CONF_DIR}/${DOMAIN}.conf.bak"
echo "✅ 产品站点 ${PRODUCT_NAME} 配置完成"
echo ""
echo "后续步骤:"
echo " 1. 添加 DNS A 记录: ${DOMAIN} -> 服务器IP"
echo " 2. 申请 SSL 证书: ./scripts/ssl-product-site.sh ${DOMAIN}"
echo " 3. 重载 Nginx: docker exec novalon-nginx-secure nginx -s reload"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""对比两个字体文件"""
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables import _h_m_t_x, _g_a_s_p
original_hmtx = _h_m_t_x.table__h_m_t_x.decompile
def patched_hmtx(self, data, ttFont):
try: return original_hmtx(self, data, ttFont)
except: self.metrics = {}
_h_m_t_x.table__h_m_t_x.decompile = patched_hmtx
original_gasp = _g_a_s_p.table__g_a_s_p.decompile
def patched_gasp(self, data, ttFont):
try: return original_gasp(self, data, ttFont)
except: self.gaspRanges = {}
_g_a_s_p.table__g_a_s_p.decompile = patched_gasp
print('=== public/fonts/AoyagiReisho.ttf ===')
f1 = TTFont('public/fonts/AoyagiReisho.ttf')
cmap1 = f1.getBestCmap()
print('U+9060 遠:', 0x9060 in cmap1)
print('U+8fdc 远:', 0x8fdc in cmap1)
print('字形数:', len(f1.getGlyphOrder()))
print('GSUB:', 'GSUB' in f1)
f1.close()
print()
print('=== src/app/fonts/AoyagiReisho.ttf ===')
f2 = TTFont('src/app/fonts/AoyagiReisho.ttf')
cmap2 = f2.getBestCmap()
print('U+9060 遠:', 0x9060 in cmap2)
print('U+8fdc 远:', 0x8fdc in cmap2)
print('字形数:', len(f2.getGlyphOrder()))
print('GSUB:', 'GSUB' in f2)
f2.close()
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""将青柳隷書字体中的文字转换为 SVG 路径"""
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables import _h_m_t_x, _g_a_s_p
import os
# 修补表解析
original_hmtx = _h_m_t_x.table__h_m_t_x.decompile
def patched_hmtx(self, data, ttFont):
try: return original_hmtx(self, data, ttFont)
except: self.metrics = {}
_h_m_t_x.table__h_m_t_x.decompile = patched_hmtx
original_gasp = _g_a_s_p.table__g_a_s_p.decompile
def patched_gasp(self, data, ttFont):
try: return original_gasp(self, data, ttFont)
except: self.gaspRanges = {}
_g_a_s_p.table__g_a_s_p.decompile = patched_gasp
def get_glyph_path(font, char):
"""获取字符的 SVG 路径"""
cmap = font.getBestCmap()
codepoint = ord(char)
if codepoint not in cmap:
print(f"警告: 字符 '{char}' (U+{codepoint:04X}) 不在字体中")
return None, None
glyph_name = cmap[codepoint]
# 获取 glyf 表
glyf_table = font['glyf']
glyph = glyf_table[glyph_name]
# 获取度量
hmtx = font['hmtx']
advance_width, lsb = hmtx[glyph_name]
# 获取边界框
if hasattr(glyph, 'xMin') and glyph.xMin is not None:
bbox = (glyph.xMin, glyph.yMin, glyph.xMax, glyph.yMax)
else:
bbox = (0, 0, advance_width, 1000)
# 获取字形轮廓
try:
coords, endPts, flags = glyph.getCoordinates(glyf_table)
except:
print(f" 无法获取轮廓: {glyph_name}")
return None, None
# 构建 SVG 路径
path_parts = []
start_idx = 0
for end_pt in endPts:
contour_coords = coords[start_idx:end_pt + 1]
contour_flags = flags[start_idx:end_pt + 1]
if len(contour_coords) > 0:
path_parts.append(f"M {contour_coords[0][0]:.2f} {-contour_coords[0][1]:.2f}")
for i in range(1, len(contour_coords)):
x, y = contour_coords[i]
path_parts.append(f"L {x:.2f} {-y:.2f}")
path_parts.append("Z")
start_idx = end_pt + 1
return " ".join(path_parts), {'advance': advance_width, 'lsb': lsb, 'bbox': bbox}
# 加载字体
font_path = 'public/fonts/AoyagiReisho.ttf'
font = TTFont(font_path)
print("=" * 60)
print("青柳隷書 字形路径提取")
print("=" * 60)
chars = ['', '', '', '']
glyphs_data = []
for char in chars:
print(f"\n字符: {char} (U+{ord(char):04X})")
path, metrics = get_glyph_path(font, char)
if path and metrics:
print(f" Advance: {metrics['advance']}, LSB: {metrics['lsb']}")
print(f" BBox: {metrics['bbox']}")
print(f" Path length: {len(path)} chars")
glyphs_data.append({
'char': char,
'path': path,
'metrics': metrics
})
font.close()
# 生成 SVG
print("\n" + "=" * 60)
print("生成 SVG 文件...")
print("=" * 60)
# 计算总宽度
total_width = sum(g['metrics']['advance'] for g in glyphs_data)
scale = 48 / 1000 # 缩放因子
svg_paths = []
x_offset = 0
for g in glyphs_data:
m = g['metrics']
# 计算字符居中偏移
char_width = m['advance'] * scale
path = g['path']
# 缩放路径
scaled_path = path
for coord in [('M', 'L')]:
pass # 路径已经是正确的格式
svg_paths.append(f''' <!-- {g['char']} -->
<g transform="translate({x_offset:.2f}, 0) scale({scale})">
<path d="{path}" fill="currentColor"/>
</g>''')
x_offset += char_width
print(f"\n总宽度: {total_width * scale:.2f}px")
print("\nSVG 路径组:")
print("\n".join(svg_paths[:2]))
print("...")
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成包含繁体''的字体子集"""
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables import _h_m_t_x, _g_a_s_p
from fontTools.subset import Subsetter, Options
# 修补表解析以跳过损坏的数据
original_hmtx = _h_m_t_x.table__h_m_t_x.decompile
def patched_hmtx(self, data, ttFont):
try:
return original_hmtx(self, data, ttFont)
except:
self.metrics = {}
_h_m_t_x.table__h_m_t_x.decompile = patched_hmtx
original_gasp = _g_a_s_p.table__g_a_s_p.decompile
def patched_gasp(self, data, ttFont):
try:
return original_gasp(self, data, ttFont)
except:
self.gaspRanges = {}
_g_a_s_p.table__g_a_s_p.decompile = patched_gasp
# 加载字体
font = TTFont('src/app/fonts/AoyagiReisho.ttf')
# 删除损坏的表
for t in ['vmtx', 'gasp', 'VORG', 'mort', 'morx']:
if t in font:
del font[t]
print(f'Deleted table: {t}')
# 创建子集器
subsetter = Subsetter()
options = Options()
options.drop_tables = ['gasp', 'vmtx', 'VORG', 'mort', 'morx', 'GSUB', 'GPOS', 'GDEF']
subsetter.options = options
# 目标字符: 睿(0x777f), 新(0x65b0), 致(0x81f4), 遠(0x9060), 空格(0x20)
unicodes = [0x20, 0x777f, 0x65b0, 0x81f4, 0x9060]
print(f'Target Unicode: {[hex(u) for u in unicodes]}')
subsetter.populate(unicodes=unicodes)
# 执行子集化
try:
subsetter.subset(font)
except Exception as e:
print(f'Warning during subsetting: {e}')
# 保存
output_path = 'src/app/fonts/AoyagiReisho-subset.ttf'
font.save(output_path)
font.close()
print(f'Saved to: {output_path}')
# 验证
verify_font = TTFont(output_path)
cmap = verify_font.getBestCmap()
chars = [chr(k) for k in sorted(cmap.keys())]
codes = [hex(k) for k in sorted(cmap.keys())]
print(f'Subset characters: {chars}')
print(f'Unicode codes: {codes}')
print(f'Contains U+9060 (遠): {0x9060 in cmap}')
verify_font.close()
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""将青柳隷書字体中的文字转换为 SVG 路径并生成 logo"""
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables import _h_m_t_x, _g_a_s_p
import os
# 修补表解析
original_hmtx = _h_m_t_x.table__h_m_t_x.decompile
def patched_hmtx(self, data, ttFont):
try: return original_hmtx(self, data, ttFont)
except: self.metrics = {}
_h_m_t_x.table__h_m_t_x.decompile = patched_hmtx
original_gasp = _g_a_s_p.table__g_a_s_p.decompile
def patched_gasp(self, data, ttFont):
try: return original_gasp(self, data, ttFont)
except: self.gaspRanges = {}
_g_a_s_p.table__g_a_s_p.decompile = patched_gasp
def get_glyph_path(font, char):
"""获取字符的 SVG 路径"""
cmap = font.getBestCmap()
codepoint = ord(char)
if codepoint not in cmap:
return None, None
glyph_name = cmap[codepoint]
glyf_table = font['glyf']
glyph = glyf_table[glyph_name]
hmtx = font['hmtx']
advance_width, lsb = hmtx[glyph_name]
try:
coords, endPts, flags = glyph.getCoordinates(glyf_table)
except:
return None, None
path_parts = []
start_idx = 0
for end_pt in endPts:
contour_coords = coords[start_idx:end_pt + 1]
if len(contour_coords) > 0:
path_parts.append(f"M {contour_coords[0][0]:.2f} {-contour_coords[0][1]:.2f}")
for i in range(1, len(contour_coords)):
x, y = contour_coords[i]
path_parts.append(f"L {x:.2f} {-y:.2f}")
path_parts.append("Z")
start_idx = end_pt + 1
return " ".join(path_parts), {'advance': advance_width, 'lsb': lsb}
# 加载字体
font = TTFont('public/fonts/AoyagiReisho.ttf')
chars = ['', '', '', '']
glyphs_data = []
for char in chars:
path, metrics = get_glyph_path(font, char)
if path and metrics:
glyphs_data.append({'char': char, 'path': path, 'metrics': metrics})
font.close()
# 生成主标题 SVG 路径
scale = 48 / 1000
total_width = sum(g['metrics']['advance'] for g in glyphs_data) * scale
svg_title_paths = []
x_offset = 0
for g in glyphs_data:
svg_title_paths.append(f''' <g transform="translate({x_offset:.2f}, 0) scale({scale})">
<path d="{g['path']}" fill="currentColor"/>
</g>''')
x_offset += g['metrics']['advance'] * scale
# 生成印章内文字 (较小尺寸)
scale_seal = 26 / 1000
# 睿新
svg_seal_line1 = []
x_offset = 0
for char in ['', '']:
g = next((x for x in glyphs_data if x['char'] == char), None)
if g:
svg_seal_line1.append(f''' <g transform="translate({x_offset:.2f}, 0) scale({scale_seal})">
<path d="{g['path']}" fill="white"/>
</g>''')
x_offset += g['metrics']['advance'] * scale_seal
# 致遠
svg_seal_line2 = []
x_offset = 0
for char in ['', '']:
g = next((x for x in glyphs_data if x['char'] == char), None)
if g:
svg_seal_line2.append(f''' <g transform="translate({x_offset:.2f}, 0) scale({scale_seal})">
<path d="{g['path']}" fill="white"/>
</g>''')
x_offset += g['metrics']['advance'] * scale_seal
# 计算印章文字居中偏移
line1_width = sum(g['metrics']['advance'] for g in glyphs_data if g['char'] in ['', '']) * scale_seal
line2_width = sum(g['metrics']['advance'] for g in glyphs_data if g['char'] in ['', '']) * scale_seal
seal_center = 43 # 印章中心 x 坐标
line1_x = seal_center - line1_width / 2
line2_x = seal_center - line2_width / 2
# 生成完整 SVG
svg_content = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 120" width="480" height="120">
<defs>
<!-- 印章纹理滤镜 -->
<filter id="sealTexture" x="0%" y="0%" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="0.1" numOctaves="3" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="2" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<!-- 红色印章 - 手绘不规则风格 -->
<g transform="translate(12, 12)">
<!-- 印章外框 - 不规则手绘路径 -->
<path d="M8,2
C25,-2 45,-2 72,3
C82,5 85,12 84,25
C83,40 85,55 84,70
C83,82 78,88 65,89
C45,91 25,90 10,88
C2,86 -2,78 1,65
C3,50 2,35 1,20
C0,10 3,4 8,2 Z"
fill="#C41E3A"/>
<!-- 印章内框 - 手绘风格 -->
<path d="M14,10
C28,8 55,8 72,12
C78,14 79,20 78,30
C77,45 78,60 77,72
C76,80 72,84 62,85
C45,86 28,85 16,83
C10,82 8,76 9,65
C10,50 9,35 8,22
C7,15 10,11 14,10 Z"
fill="none" stroke="#fff" stroke-width="1.5" opacity="0.5"/>
<!-- 睿新 - 书法字体路径 -->
<g transform="translate({line1_x:.2f}, 38)">
{chr(10).join(svg_seal_line1)}
</g>
<!-- 致遠 - 书法字体路径 -->
<g transform="translate({line2_x:.2f}, 70)">
{chr(10).join(svg_seal_line2)}
</g>
</g>
<!-- 公司名称 -->
<g transform="translate(110, 60)">
<!-- 睿新致遠 - 书法字体路径 -->
<g transform="translate(0, 0)">
{chr(10).join(svg_title_paths)}
</g>
<!-- NOVALON - 英文字体 -->
<text x="24" y="42" font-family="Arial, sans-serif" font-size="14.5" font-weight="500" fill="currentColor" letter-spacing="10.5">NOVALON</text>
</g>
</svg>'''
# 写入文件
with open('public/logo.svg', 'w', encoding='utf-8') as f:
f.write(svg_content)
print("✅ 已生成 public/logo.svg")
# 生成白色版本 (logo-white.svg)
svg_white = svg_content.replace('fill="#C41E3A"', 'fill="currentColor"')
with open('public/logo-white.svg', 'w', encoding='utf-8') as f:
f.write(svg_white)
print("✅ 已生成 public/logo-white.svg")
print(f"\n标题总宽度: {total_width:.2f}px")
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
DOMAIN=$1
if [ -z "$DOMAIN" ]; then
echo "用法: $0 <subdomain>.novalon.cn"
echo "示例: $0 product-a.novalon.cn"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NGINX_DIR="$(dirname "$SCRIPT_DIR")"
CERTBOT_DIR="/home/novalon/docker-app/certbot"
SSL_DIR="${NGINX_DIR}/ssl"
mkdir -p "${SSL_DIR}/${DOMAIN}"
echo "正在为 ${DOMAIN} 申请 SSL 证书..."
echo ""
docker run --rm \
-v "${CERTBOT_DIR}:/var/www/certbot" \
-v "/etc/letsencrypt:/etc/letsencrypt" \
certbot/certbot certonly \
--webroot \
--webroot-path /var/www/certbot \
-d "${DOMAIN}" \
--email admin@novalon.cn \
--agree-tos \
--no-eff-email
if [ $? -eq 0 ]; then
echo ""
echo "证书申请成功,正在复制到 Nginx SSL 目录..."
cp "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" "${SSL_DIR}/${DOMAIN}/"
cp "/etc/letsencrypt/live/${DOMAIN}/privkey.pem" "${SSL_DIR}/${DOMAIN}/"
echo ""
echo "${DOMAIN} 证书申请完成"
echo ""
echo "后续步骤:"
echo " 1. 验证配置: docker exec novalon-nginx-secure nginx -t"
echo " 2. 重载 Nginx: docker exec novalon-nginx-secure nginx -s reload"
echo " 3. 验证访问: curl -I https://${DOMAIN}"
else
echo ""
echo "❌ 证书申请失败"
echo ""
echo "请检查:"
echo " 1. DNS 解析是否正确: ${DOMAIN} -> 服务器IP"
echo " 2. Nginx 配置是否正确加载"
echo " 3. certbot 目录权限是否正确"
exit 1
fi
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""验证字体子集与原始字体的字形一致性"""
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables import _h_m_t_x, _g_a_s_p
import os
# 修补表解析
original_hmtx = _h_m_t_x.table__h_m_t_x.decompile
def patched_hmtx(self, data, ttFont):
try: return original_hmtx(self, data, ttFont)
except: self.metrics = {}
_h_m_t_x.table__h_m_t_x.decompile = patched_hmtx
original_gasp = _g_a_s_p.table__g_a_s_p.decompile
def patched_gasp(self, data, ttFont):
try: return original_gasp(self, data, ttFont)
except: self.gaspRanges = {}
_g_a_s_p.table__g_a_s_p.decompile = patched_gasp
base = 'src/app/fonts'
# 加载字体
original = TTFont(f'{base}/AoyagiReisho.ttf')
subset = TTFont(f'{base}/AoyagiReisho-subset.ttf')
print("=" * 50)
print("字体对比验证")
print("=" * 50)
# 文件大小
orig_size = os.path.getsize(f'{base}/AoyagiReisho.ttf')
sub_size = os.path.getsize(f'{base}/AoyagiReisho-subset.ttf')
print(f"\n原始字体大小: {orig_size / 1024:.1f} KB ({orig_size} bytes)")
print(f"子集字体大小: {sub_size / 1024:.1f} KB ({sub_size} bytes)")
# CMAP 对比
orig_cmap = original.getBestCmap()
sub_cmap = subset.getBestCmap()
target_chars = [0x20, 0x777f, 0x65b0, 0x81f4, 0x9060]
char_names = {0x20: '空格', 0x777f: '', 0x65b0: '', 0x81f4: '', 0x9060: ''}
print("\n字符映射对比:")
for code in target_chars:
name = char_names[code]
orig_glyph = orig_cmap.get(code, 'MISSING')
sub_glyph = sub_cmap.get(code, 'MISSING')
match = "" if orig_glyph == sub_glyph else ""
print(f" U+{code:04X} ({name}): 原始={orig_glyph}, 子集={sub_glyph} {match}")
# 字形数量
print(f"\n字形数量:")
print(f" 原始: {len(original.getGlyphOrder())}")
print(f" 子集: {len(subset.getGlyphOrder())}")
# 表对比
print("\n字体表:")
orig_tables = set(original.keys())
sub_tables = set(subset.keys())
print(f" 原始表: {sorted(orig_tables)}")
print(f" 子集表: {sorted(sub_tables)}")
original.close()
subset.close()
print("\n" + "=" * 50)