Files
zjpb.net/templates/detail_new.html
Jowe 7da0bb6e54 feat: v2.4.0 - SEO全面优化
新增功能:
1. 自动化SEO基础设施
   - Sitemap.xml 动态生成 (/sitemap.xml)
   - Robots.txt 动态配置 (/robots.txt)

2. Schema.org 结构化数据
   - 工具详情页添加 SoftwareApplication 结构化数据
   - 面包屑导航添加 BreadcrumbList 结构化数据
   - Open Graph 标签支持社交媒体分享

3. 智能内链系统
   - 自动识别工具名称并添加内部链接
   - auto_link 过滤器支持内容互联

4. 标签专题页SEO优化
   - Tag模型新增字段: seo_title, seo_description, seo_keywords
   - 支持自定义标签页SEO信息
   - 提供迁移脚本: migrate_tag_seo_fields.py

5. 面包屑导航
   - 可视化导航: 首页 > 标签 > 工具名
   - 支持Schema.org和视觉显示

6. 页面级SEO改进
   - 工具详情页: canonical链接, 动态meta标签
   - 标签页: 专属SEO信息支持
   - 首页: 完整meta标签配置

技术改进:
- 迁移脚本支持幂等性检查
- Windows控制台编码兼容性优化
- 数据库字段注释标注版本

部署文档: DEPLOY_v2.4.0.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-03 16:32:13 +08:00

956 lines
26 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'base_new.html' %}
{% block title %}{{ site.name }} - ZJPB AI工具导航{% endblock %}
{% block extra_head %}
<!-- SEO Meta Tags (v2.4新增) -->
<meta name="description" content="{{ site.short_desc or (site.description[:150] if site.description else '') }}">
<meta name="keywords" content="{{ site.name }},{% for tag in site.tags %}{{ tag.name }},{% endfor %}AI工具,自己品吧,ZJPB">
<link rel="canonical" href="{{ request.url }}">
<!-- Open Graph (v2.4新增) -->
<meta property="og:type" content="website">
<meta property="og:title" content="{{ site.name }}">
<meta property="og:description" content="{{ site.short_desc or (site.description[:150] if site.description else '') }}">
<meta property="og:url" content="{{ request.url }}">
{% if site.logo %}<meta property="og:image" content="{{ request.url_root.rstrip('/') }}{{ site.logo }}">{% endif %}
<!-- Schema.org 结构化数据 (v2.4新增) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "{{ site.name }}",
"url": "{{ site.url }}",
{% if site.logo %}"image": "{{ request.url_root.rstrip('/') }}{{ site.logo }}",{% endif %}
"description": "{{ site.short_desc or site.description or site.name }}",
{% if site.features %}"featureList": {{ site.features.split('\n') | tojson }},{% endif %}
"applicationCategory": "WebApplication",
"operatingSystem": "Any",
{% if site.tags %}"keywords": "{{ site.tags | map(attribute='name') | join(', ') }}",{% endif %}
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "CNY"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"ratingCount": "{{ site.view_count or 1 }}"
}
}
</script>
<!-- BreadcrumbList 结构化数据 (v2.4新增) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "首页",
"item": "{{ request.url_root.rstrip('/') }}"
}{% if site.tags and site.tags|length > 0 %},
{
"@type": "ListItem",
"position": 2,
"name": "{{ site.tags[0].name }}",
"item": "{{ request.url_root.rstrip('/') }}/?tag={{ site.tags[0].slug }}"
}{% endif %},
{
"@type": "ListItem",
"position": {% if site.tags and site.tags|length > 0 %}3{% else %}2{% endif %},
"name": "{{ site.name }}",
"item": "{{ request.url }}"
}
]
}
</script>
{% endblock %}
{% block extra_css %}
<style>
/* v2.4新增: 面包屑导航样式 */
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 0;
font-size: 14px;
color: var(--text-secondary);
flex-wrap: wrap;
}
.breadcrumb-item {
display: flex;
align-items: center;
gap: 8px;
}
.breadcrumb-item a {
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s;
}
.breadcrumb-item a:hover {
color: var(--primary-blue);
}
.breadcrumb-item.active {
color: var(--text-primary);
font-weight: 500;
}
.breadcrumb-separator {
color: var(--text-muted);
user-select: none;
}
/* 返回链接 */
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--text-secondary);
text-decoration: none;
font-size: 24px;
font-weight: 500;
transition: color 0.2s;
}
.back-link:hover {
color: var(--text-primary);
}
/* 产品头部区域 */
.product-header-wrapper {
display: flex;
gap: 32px;
margin-bottom: 32px;
align-items: flex-start;
position: relative;
}
.product-main-section {
flex: 1;
background: var(--bg-white);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 32px;
}
.product-header {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.product-logo-large {
width: 88px;
height: 88px;
border-radius: var(--radius-lg);
object-fit: cover;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.product-main-info {
flex: 1;
}
.product-main-info h1 {
font-size: 32px;
font-weight: 700;
margin-bottom: 8px;
line-height: 1.2;
}
.product-link {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--primary-blue);
text-decoration: none;
font-size: 14px;
margin-bottom: 16px;
}
.product-link:hover {
text-decoration: underline;
}
.product-meta {
display: flex;
gap: 20px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.meta-item {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-secondary);
font-size: 14px;
}
.product-tags-list {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.product-tag {
padding: 6px 12px;
background: #f1f5f9;
color: #64748b;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
text-decoration: none;
transition: all 0.2s;
}
.product-tag:hover {
background: rgba(14, 165, 233, 0.1);
color: var(--primary-blue);
}
.product-tag.free {
background: rgba(139, 92, 246, 0.1);
color: #8b5cf6;
}
/* Try It Now卡片 - 独立定位 */
.try-now-card {
position: sticky;
top: 88px;
width: 300px;
background: var(--bg-white);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 24px;
flex-shrink: 0;
}
.try-now-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.try-now-header h3 {
font-size: 16px;
font-weight: 600;
margin: 0;
}
.status-badge {
padding: 4px 10px;
background: rgba(34, 197, 94, 0.1);
color: #059669;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.visit-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 12px;
background: var(--primary-blue);
color: white;
border: none;
border-radius: var(--radius-md);
text-decoration: none;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.visit-btn:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
}
.visit-hint {
text-align: center;
margin-top: 12px;
font-size: 12px;
color: var(--text-muted);
}
/* 主内容布局 */
.content-layout {
display: grid;
grid-template-columns: 1fr 300px;
gap: 32px;
margin-bottom: 48px;
}
.main-column {
min-width: 0;
}
.sidebar-column {
position: sticky;
top: 88px;
align-self: flex-start;
}
/* 内容块 */
.content-block {
background: var(--bg-white);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 32px;
margin-bottom: 24px;
}
.content-block h2 {
display: flex;
align-items: center;
gap: 10px;
font-size: 20px;
font-weight: 700;
margin-bottom: 20px;
}
.content-block p {
color: var(--text-secondary);
line-height: 1.7;
margin-bottom: 16px;
}
.content-block ul,
.content-block ol {
margin-left: 20px;
margin-bottom: 16px;
}
.content-block li {
color: var(--text-secondary);
line-height: 1.7;
margin-bottom: 8px;
}
/* 新闻卡片 */
.news-item {
padding: 20px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
margin-bottom: 16px;
transition: all 0.2s;
}
.news-item:hover {
border-color: var(--primary-blue);
box-shadow: var(--shadow-md);
}
.news-item:last-child {
margin-bottom: 0;
}
.news-badge {
display: inline-block;
padding: 4px 10px;
background: rgba(139, 92, 246, 0.1);
color: #8b5cf6;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
.news-item h4 {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-primary);
}
.news-item p {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 8px;
line-height: 1.6;
}
.news-date {
font-size: 12px;
color: var(--text-muted);
}
/* 推荐卡片 */
.recommendations-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.recommendation-card {
padding: 20px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
text-decoration: none;
color: var(--text-primary);
transition: all 0.2s;
display: flex;
gap: 12px;
position: relative;
}
.recommendation-card:hover {
border-color: var(--primary-blue);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.recommendation-card .arrow-icon {
position: absolute;
top: 20px;
right: 20px;
color: #cbd5e1;
font-size: 20px;
transition: all 0.2s;
}
.recommendation-card:hover .arrow-icon {
color: var(--primary-blue);
transform: translate(2px, -2px);
}
.rec-logo {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
object-fit: cover;
flex-shrink: 0;
}
.rec-info {
flex: 1;
padding-right: 24px;
}
.rec-info h4 {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
}
.rec-info p {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 8px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.rec-tags {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.rec-tag {
padding: 2px 8px;
background: #f1f5f9;
color: #64748b;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
/* Markdown内容样式 */
.markdown-content {
color: var(--text-secondary);
line-height: 1.8;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3 {
color: var(--text-primary);
font-weight: 600;
margin-top: 24px;
margin-bottom: 16px;
line-height: 1.4;
}
.markdown-content h1 {
font-size: 24px;
}
.markdown-content h2 {
font-size: 20px;
}
.markdown-content h3 {
font-size: 18px;
}
.markdown-content p {
margin-bottom: 16px;
line-height: 1.8;
}
.markdown-content ul,
.markdown-content ol {
margin: 16px 0;
padding-left: 24px;
}
.markdown-content ul li {
list-style: none;
position: relative;
padding-left: 20px;
margin-bottom: 12px;
line-height: 1.8;
}
.markdown-content ul li:before {
content: "▸";
position: absolute;
left: 0;
color: var(--primary-blue);
font-weight: bold;
}
.markdown-content ol li {
margin-bottom: 12px;
line-height: 1.8;
padding-left: 8px;
}
.markdown-content code {
background: #f1f5f9;
color: #e11d48;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.9em;
}
.markdown-content pre {
background: #1e293b;
color: #e2e8f0;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin: 16px 0;
}
.markdown-content pre code {
background: transparent;
color: inherit;
padding: 0;
border-radius: 0;
}
.markdown-content strong {
font-weight: 600;
color: var(--text-primary);
}
.markdown-content em {
font-style: italic;
}
.markdown-content a {
color: var(--primary-blue);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
.markdown-content a:hover {
border-bottom-color: var(--primary-blue);
}
.markdown-content blockquote {
border-left: 4px solid var(--primary-blue);
padding-left: 16px;
margin: 16px 0;
color: var(--text-secondary);
font-style: italic;
}
.markdown-content hr {
border: none;
border-top: 1px solid var(--border-color);
margin: 24px 0;
}
/* 响应式 */
@media (max-width: 968px) {
.product-header-wrapper {
flex-direction: column;
}
.try-now-card {
width: 100%;
position: static;
}
.content-layout {
grid-template-columns: 1fr;
}
.sidebar-column {
position: static;
}
.recommendations-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block content %}
<div class="main-content">
<!-- 顶部空白 -->
<div style="height: 20px;"></div>
<!-- v2.4新增: 面包屑导航 -->
<nav class="breadcrumb" aria-label="breadcrumb">
<div class="breadcrumb-item">
<a href="/">首页</a>
</div>
<span class="breadcrumb-separator"></span>
{% if site.tags and site.tags|length > 0 %}
<div class="breadcrumb-item">
<a href="/?tag={{ site.tags[0].slug }}">{{ site.tags[0].name }}</a>
</div>
<span class="breadcrumb-separator"></span>
{% endif %}
<div class="breadcrumb-item active">
{{ site.name }}
</div>
</nav>
<div style="height: 10px;"></div>
<!-- 返回链接 -->
<a href="/" class="back-link">
<span></span>
返回首页
</a>
<!-- 底部空白 -->
<div style="height: 20px;"></div>
<!-- 产品头部区域 -->
<div class="product-header-wrapper">
<!-- 左侧主内容 -->
<div class="product-main-section">
<div class="product-header">
<!-- Logo -->
{% if site.logo %}
<img src="{{ site.logo }}" alt="{{ site.name }}" class="product-logo-large">
{% else %}
<div class="product-logo-large" style="background: linear-gradient(135deg, #0ea5e9 0%, #8b5cf6 100%);"></div>
{% endif %}
<!-- 产品信息 -->
<div class="product-main-info">
<h1>{{ site.name }}</h1>
<a href="{{ site.url }}" target="_blank" class="product-link">
{{ site.url }}
<span></span>
</a>
<div class="product-meta">
<div class="meta-item">
<span>👁</span>
<span>{{ site.view_count | default(0) }} 次浏览</span>
</div>
<div class="meta-item">
<span>📅</span>
<span>添加于 {{ site.created_at.strftime('%Y年%m月%d日') }}</span>
</div>
</div>
<div class="product-tags-list">
{% for tag in site.tags %}
<a href="/?tag={{ tag.slug }}" class="product-tag">{{ tag.name }}</a>
{% endfor %}
<span class="product-tag free">免费试用</span>
</div>
</div>
</div>
</div>
<!-- Try It Now卡片 -->
<div class="try-now-card">
<div class="try-now-header">
<h3>立即访问</h3>
<span class="status-badge">在线</span>
</div>
<a href="{{ site.url }}" target="_blank" class="visit-btn">
访问网站
<span></span>
</a>
<p class="visit-hint">在新标签页打开 • {{ site.url.split('/')[2] if site.url else '' }}</p>
</div>
</div>
<!-- 内容布局 -->
<div class="content-layout">
<!-- 主列 -->
<div class="main-column">
<!-- Product Overview -->
<div class="content-block">
<h2>
<span></span>
产品概述
</h2>
<div class="markdown-content">{{ site.description | auto_link(site.id) | markdown | safe }}</div>
</div>
<!-- Detailed Description -->
{% if site.features %}
<div class="content-block">
<h2>
<span>📋</span>
主要功能
</h2>
<div class="markdown-content">{{ site.features | auto_link(site.id) | markdown | safe }}</div>
</div>
{% endif %}
<!-- Related News -->
{% if news_list %}
<div class="content-block">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">
<span>📰</span>
相关新闻
</h2>
<button id="refreshNewsBtn" class="refresh-news-btn" onclick="refreshNews('{{ site.code }}')">
<span class="refresh-icon"></span> 获取最新资讯
</button>
</div>
<div id="newsContainer">
{% for news in news_list %}
<div class="news-item">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
<span class="news-badge">{{ news.news_type }}</span>
{% if news.source_name %}
<div style="display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted);">
{% if news.source_icon %}
<img src="{{ news.source_icon }}" alt="{{ news.source_name }}" style="width: 16px; height: 16px; border-radius: 2px;">
{% endif %}
<span>{{ news.source_name }}</span>
</div>
{% endif %}
</div>
<h4>
{% if news.url %}
<a href="{{ news.url }}" target="_blank" rel="noopener noreferrer" style="color: var(--text-primary); text-decoration: none;">
{{ news.title }}
</a>
{% else %}
{{ news.title }}
{% endif %}
</h4>
{% if news.content %}
<p>{{ news.content[:200] }}{% if news.content|length > 200 %}...{% endif %}</p>
{% endif %}
<div style="display: flex; justify-content: space-between; align-items: center;">
<div class="news-date">
{% if news.published_at %}
{{ news.published_at.strftime('%Y年%m月%d日') }}
{% else %}
未知日期
{% endif %}
</div>
{% if news.url %}
<a href="{{ news.url }}" target="_blank" rel="noopener noreferrer" style="font-size: 12px; color: var(--primary-blue); text-decoration: none;">
阅读全文 ↗
</a>
{% endif %}
</div>
</div>
{% endfor %}
</div><!-- End newsContainer -->
</div>
{% endif %}
</div>
<!-- 侧边栏 -->
<div class="sidebar-column">
<!-- Similar Recommendations -->
{% if recommended_sites %}
<div class="content-block">
<h2>
<span></span>
相似推荐
</h2>
{% for rec_site in recommended_sites %}
<a href="/site/{{ rec_site.code }}" class="recommendation-card" style="display: flex; gap: 12px; padding: 16px; border: 1px solid var(--border-color); border-radius: 12px; margin-bottom: 12px; text-decoration: none; transition: all 0.2s;">
{% if rec_site.logo %}
<img src="{{ rec_site.logo }}" alt="{{ rec_site.name }}" style="width: 48px; height: 48px; border-radius: 8px; flex-shrink: 0;">
{% else %}
<div style="width: 48px; height: 48px; border-radius: 8px; background: linear-gradient(135deg, #0ea5e9 0%, #8b5cf6 100%); flex-shrink: 0;"></div>
{% endif %}
<div style="flex: 1; min-width: 0;">
<h4 style="font-size: 14px; font-weight: 600; margin: 0 0 4px 0; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ rec_site.name }}</h4>
<p style="font-size: 12px; color: var(--text-secondary); margin: 0; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;">{{ rec_site.short_desc or rec_site.description }}</p>
</div>
</a>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<style>
.refresh-news-btn {
padding: 8px 16px;
background: linear-gradient(135deg, var(--primary-blue) 0%, var(--primary-dark) 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-news-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
}
.refresh-news-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.refresh-news-btn.loading .refresh-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.news-status {
padding: 12px;
margin: 16px 0;
border-radius: 8px;
font-size: 14px;
display: none;
}
.news-status.success {
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
color: #22c55e;
}
.news-status.error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #ef4444;
}
</style>
<script>
function refreshNews(siteCode) {
const btn = document.getElementById('refreshNewsBtn');
const newsContainer = document.getElementById('newsContainer');
if (!btn || !newsContainer) return;
// 禁用按钮,显示加载状态
btn.disabled = true;
btn.classList.add('loading');
btn.innerHTML = '<span class="refresh-icon">↻</span> 正在获取...';
// 显示加载提示
const loadingMsg = document.createElement('div');
loadingMsg.className = 'news-status';
loadingMsg.style.display = 'block';
loadingMsg.textContent = '正在获取最新资讯,请稍候...';
newsContainer.insertAdjacentElement('beforebegin', loadingMsg);
// 调用刷新API
fetch(`/api/refresh-site-news/${siteCode}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 刷新成功,重新加载页面
loadingMsg.className = 'news-status success';
loadingMsg.textContent = `✓ 成功获取 ${data.saved_count || 0} 条新资讯,页面即将刷新...`;
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
// 刷新失败
loadingMsg.className = 'news-status error';
loadingMsg.textContent = `${data.message || '获取失败,请稍后重试'}`;
btn.disabled = false;
btn.classList.remove('loading');
btn.innerHTML = '<span class="refresh-icon">↻</span> 获取最新资讯';
// 3秒后隐藏错误消息
setTimeout(() => {
loadingMsg.style.display = 'none';
}, 3000);
}
})
.catch(error => {
console.error('Error:', error);
loadingMsg.className = 'news-status error';
loadingMsg.textContent = '✗ 网络请求失败,请检查网络连接';
btn.disabled = false;
btn.classList.remove('loading');
btn.innerHTML = '<span class="refresh-icon">↻</span> 获取最新资讯';
// 3秒后隐藏错误消息
setTimeout(() => {
loadingMsg.style.display = 'none';
}, 3000);
});
}
</script>
{% endblock %}