feat: SEO 优化与浏览器自动化测试

- 为 admin-frontend、user-frontend、uni-app 添加完整 SEO meta 标签
- 添加结构化数据 (JSON-LD) 提升搜索引擎理解
- 创建 robots.txt 和 sitemap.xml 文件
- 优化移动端 viewport、PWA 支持、theme-color
- 添加 Open Graph 和 Twitter Card 元标签
- 创建浏览器自动化测试脚本 (11 项测试全部通过)
- 修复 Nginx charset 配置解决编码问题
This commit is contained in:
TradeMate Dev
2026-06-29 20:17:59 +08:00
parent ad329815fa
commit 9ca5d79d8a
8 changed files with 528 additions and 4 deletions
+347
View File
@@ -0,0 +1,347 @@
#!/usr/bin/env python3
"""
TradeMate 前端 SEO 与浏览器自动化测试脚本
使用 requests + BeautifulSoup 进行 SEO 审计
"""
import json
import re
import sys
from pathlib import Path
from urllib.parse import urljoin, urlparse
try:
import requests
from bs4 import BeautifulSoup
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
print("⚠️ requests/bs4 未安装,跳过网络测试")
BASE_URL = "https://trade.yuzhiran.com"
TEST_RESULTS = []
def run_test(name: str, test_func):
"""运行单个测试并记录结果"""
try:
result = test_func()
TEST_RESULTS.append({"name": name, "status": "PASS", "detail": result})
print(f"{name}")
except Exception as e:
TEST_RESULTS.append({"name": name, "status": "FAIL", "detail": str(e)})
print(f"{name}: {e}")
def get_page(url: str, timeout: int = 15) -> tuple:
"""获取页面内容"""
if not HAS_REQUESTS:
raise RuntimeError("requests not available")
response = requests.get(url, timeout=timeout, headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
response.raise_for_status()
return response
def parse_html(html: str) -> BeautifulSoup:
"""解析 HTML"""
return BeautifulSoup(html, 'html.parser', from_encoding='utf-8')
def test_homepage_seo():
"""测试首页 SEO 优化"""
response = get_page(BASE_URL)
soup = parse_html(response.text)
# 检查 title
title = soup.title.string if soup.title else ""
assert "TradeMate" in title, f"Title should contain TradeMate, got: {title}"
# 检查 meta description
desc_tag = soup.find('meta', attrs={'name': 'description'})
desc = desc_tag.get('content', '') if desc_tag else ""
assert len(desc) > 50, f"Meta description too short: {desc}"
# 检查 viewport
viewport_tag = soup.find('meta', attrs={'name': 'viewport'})
viewport = viewport_tag.get('content', '') if viewport_tag else ""
assert "width=device-width" in viewport, f"Viewport missing: {viewport}"
# 检查 canonical
canonical_tag = soup.find('link', attrs={'rel': 'canonical'})
canonical = canonical_tag.get('href', '') if canonical_tag else ""
assert canonical == BASE_URL + "/", f"Canonical incorrect: {canonical}"
# 检查 Open Graph
og_title_tag = soup.find('meta', attrs={'property': 'og:title'})
og_title = og_title_tag.get('content', '') if og_title_tag else ""
assert "TradeMate" in og_title, f"OG title missing: {og_title}"
# 检查结构化数据
ld_scripts = soup.find_all('script', attrs={'type': 'application/ld+json'})
assert len(ld_scripts) > 0, "No structured data found"
return {
"title": title,
"description": desc[:100] + "...",
"canonical": canonical,
"og_title": og_title,
"structured_data_count": len(ld_scripts)
}
def test_homepage_performance():
"""测试首页性能(简化版)"""
response = get_page(BASE_URL)
# 检查页面大小
html_size = len(response.text.encode('utf-8'))
assert html_size < 500000, f"HTML too large: {html_size} bytes"
# 检查 HTTP 状态
assert response.status_code == 200, f"Status code: {response.status_code}"
# 检查压缩
content_encoding = response.headers.get('Content-Encoding', '')
return {
"html_size": html_size,
"status_code": response.status_code,
"content_encoding": content_encoding,
"load_time_ms": response.elapsed.total_seconds() * 1000
}
def test_workspace_seo():
"""测试工作台 SEO"""
response = get_page(BASE_URL + "/workspace/")
soup = parse_html(response.text)
title = soup.title.string if soup.title else ""
assert "工作台" in title or "TradeMate" in title, f"Workspace title incorrect: {title}"
desc_tag = soup.find('meta', attrs={'name': 'description'})
desc = desc_tag.get('content', '') if desc_tag else ""
assert len(desc) > 30, f"Workspace description too short: {desc}"
# 检查 robots
robots_tag = soup.find('meta', attrs={'name': 'robots'})
robots = robots_tag.get('content', '') if robots_tag else ""
assert "noindex" in robots, f"Workspace should have noindex: {robots}"
return {"title": title, "description": desc[:100] + "...", "robots": robots}
def test_admin_seo():
"""测试管理后台 SEO"""
response = get_page(BASE_URL + "/admin/")
soup = parse_html(response.text)
title = soup.title.string if soup.title else ""
assert "管理后台" in title or "TradeMate" in title, f"Admin title incorrect: {title}"
# 检查 robots
robots_tag = soup.find('meta', attrs={'name': 'robots'})
robots = robots_tag.get('content', '') if robots_tag else ""
assert "noindex" in robots, f"Admin should have noindex: {robots}"
return {"title": title, "robots": robots}
def test_app_seo():
"""测试移动端 App SEO"""
response = get_page(BASE_URL + "/app/")
soup = parse_html(response.text)
title = soup.title.string if soup.title else ""
# Accept both full title and short title
assert "TradeMate" in title or "外贸" in title or "小助手" in title, f"App title incorrect: {title}"
desc_tag = soup.find('meta', attrs={'name': 'description'})
desc = desc_tag.get('content', '') if desc_tag else ""
assert len(desc) > 30, f"App description too short: {desc}"
# 检查 PWA meta
apple_capable_tag = soup.find('meta', attrs={'name': 'apple-mobile-web-app-capable'})
apple_capable = apple_capable_tag.get('content', '') if apple_capable_tag else ""
assert apple_capable == "yes", f"Apple web app capable missing: {apple_capable}"
# 检查 theme-color
theme_color_tag = soup.find('meta', attrs={'name': 'theme-color'})
theme_color = theme_color_tag.get('content', '') if theme_color_tag else ""
assert theme_color, f"Theme color missing: {theme_color}"
return {"title": title, "description": desc[:100] + "...", "apple_capable": apple_capable, "theme_color": theme_color}
def test_robots_txt():
"""测试 robots.txt"""
response = get_page(BASE_URL + "/robots.txt")
assert response.status_code == 200, f"robots.txt returned {response.status_code}"
content = response.text
assert "User-agent" in content, "robots.txt missing User-agent"
assert "Disallow" in content, "robots.txt missing Disallow"
assert "Sitemap" in content, "robots.txt missing Sitemap"
return {"status": response.status_code, "has_user_agent": True, "has_disallow": True, "has_sitemap": True}
def test_sitemap_xml():
"""测试 sitemap.xml"""
response = get_page(BASE_URL + "/sitemap.xml")
assert response.status_code == 200, f"sitemap.xml returned {response.status_code}"
content = response.text
assert "<urlset" in content, "sitemap.xml missing urlset"
assert "<url>" in content, "sitemap.xml missing url entries"
assert BASE_URL in content, "sitemap.xml missing base URL"
return {"status": response.status_code, "has_urlset": True, "has_urls": True}
def test_image_optimization():
"""测试图片优化"""
response = get_page(BASE_URL)
soup = parse_html(response.text)
images = soup.find_all('img')
results = []
for img in images:
src = img.get('src', '')
alt = img.get('alt', '')
width = img.get('width')
height = img.get('height')
result = {"src": src, "has_alt": bool(alt), "has_dimensions": bool(width or height)}
if src:
result["is_webp"] = src.endswith('.webp')
result["is_lazy"] = img.get('loading') == 'lazy'
results.append(result)
# 检查是否有图片缺少 alt
missing_alt = [r for r in results if not r.get('has_alt')]
return {"total_images": len(results), "missing_alt": len(missing_alt), "images": results[:5]}
def test_links():
"""测试链接有效性"""
response = get_page(BASE_URL)
soup = parse_html(response.text)
links = soup.find_all('a', href=True)
broken_links = []
for link in links:
href = link['href']
if href.startswith('#') or href.startswith('mailto:'):
continue
full_url = urljoin(BASE_URL, href)
try:
link_response = requests.get(full_url, timeout=5, allow_redirects=True)
if link_response.status_code >= 400:
broken_links.append({"href": href, "status": link_response.status_code})
except:
broken_links.append({"href": href, "status": "timeout"})
return {"total_links": len(links), "broken_links": broken_links}
def test_semantic_html():
"""测试语义化 HTML"""
response = get_page(BASE_URL)
soup = parse_html(response.text)
# 检查语义化标签
semantic_tags = ['header', 'nav', 'main', 'section', 'article', 'aside', 'footer']
found_tags = []
for tag in semantic_tags:
if soup.find(tag):
found_tags.append(tag)
# 检查 heading 层级
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
h1_count = len(soup.find_all('h1'))
return {
"semantic_tags_found": found_tags,
"heading_count": len(headings),
"h1_count": h1_count,
"has_main": 'main' in found_tags
}
def test_mobile_meta():
"""测试移动端 meta 标签"""
response = get_page(BASE_URL + "/app/")
soup = parse_html(response.text)
viewport_tag = soup.find('meta', attrs={'name': 'viewport'})
viewport = viewport_tag.get('content', '') if viewport_tag else ""
# 检查移动端相关 meta
mobile_meta = {
"viewport": viewport,
"apple_capable": soup.find('meta', attrs={'name': 'apple-mobile-web-app-capable'}) is not None,
"apple_status_bar": soup.find('meta', attrs={'name': 'apple-mobile-web-app-status-bar-style'}) is not None,
"mobile_capable": soup.find('meta', attrs={'name': 'mobile-web-app-capable'}) is not None,
"theme_color": soup.find('meta', attrs={'name': 'theme-color'}) is not None
}
return mobile_meta
def main():
"""主测试函数"""
print("=" * 60)
print("TradeMate 前端 SEO 与浏览器自动化测试")
print("=" * 60)
if not HAS_REQUESTS:
print("\n❌ requests/bs4 未安装,无法运行测试")
print(" 请运行: pip install requests beautifulsoup4")
return 1
# 首页测试
print("\n📄 首页测试")
run_test("首页 SEO 优化", test_homepage_seo)
run_test("首页性能", test_homepage_performance)
# 子页面测试
print("\n📊 子页面测试")
run_test("工作台 SEO", test_workspace_seo)
run_test("管理后台 SEO", test_admin_seo)
run_test("移动端 App SEO", test_app_seo)
# SEO 文件测试
print("\n🔍 SEO 文件测试")
run_test("robots.txt", test_robots_txt)
run_test("sitemap.xml", test_sitemap_xml)
# 其他测试
print("\n🖼️ 其他测试")
run_test("图片优化", test_image_optimization)
run_test("链接有效性", test_links)
run_test("语义化 HTML", test_semantic_html)
run_test("移动端 meta", test_mobile_meta)
# 输出结果
print("\n" + "=" * 60)
print("测试结果汇总")
print("=" * 60)
passed = sum(1 for r in TEST_RESULTS if r["status"] == "PASS")
failed = sum(1 for r in TEST_RESULTS if r["status"] == "FAIL")
for result in TEST_RESULTS:
status_icon = "" if result["status"] == "PASS" else ""
detail = result['detail']
if isinstance(detail, dict):
detail = json.dumps(detail, ensure_ascii=False, indent=2)
print(f"{status_icon} {result['name']}")
if result['status'] == 'FAIL':
print(f"{detail}")
print(f"\n总计: {len(TEST_RESULTS)} 个测试, {passed} 通过, {failed} 失败")
# 保存结果到 JSON
output_path = Path(__file__).parent / "test_results.json"
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(TEST_RESULTS, f, ensure_ascii=False, indent=2)
print(f"\n结果已保存到: {output_path}")
return 0 if failed == 0 else 1
if __name__ == "__main__":
exit_code = main()
sys.exit(exit_code)