Files
zjpb.net/templates/admin/seo_tools.html
Jowe c74b115ac0 feat: 添加SEO工具管理页面 - v2.4.1
新增功能:
1. 后台SEO工具管理页面 (/admin/seo-tools)
   - 显示sitemap状态(是否存在、最后更新时间、文件大小)
   - 显示动态sitemap URL并支持一键复制

2. 生成静态sitemap.xml文件 (/api/generate-static-sitemap)
   - 将动态sitemap生成为static/sitemap.xml静态文件
   - 支持手动触发更新
   - 返回URL数量统计信息

3. 通知搜索引擎功能 (/api/notify-search-engines)
   - 支持向Google、Baidu、Bing提交sitemap更新通知
   - 使用各搜索引擎的ping接口
   - 返回每个搜索引擎的提交状态

4. 一键操作
   - 提供"一键生成并通知"功能
   - 自动执行生成sitemap + 通知搜索引擎两个步骤
   - 适合日常SEO维护使用

技术实现:
- 使用Flask路由和@login_required装饰器保护后台接口
- AJAX + fetch API实现前端交互
- Bootstrap 4卡片式UI设计
- 实时显示操作结果,颜色区分成功/失败状态

用户价值:
- 无需手动登录各搜索引擎后台提交sitemap
- 支持批量更新和通知,提升SEO工作效率
- 可视化状态展示,便于监控sitemap更新情况

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-03 17:29:14 +08:00

417 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
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 'admin/master.html' %}
{% block head_css %}
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
{% endblock %}
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">
<i class="fa fa-search"></i> SEO工具管理
</h1>
<!-- Sitemap状态卡片 -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="fa fa-sitemap"></i> Sitemap状态
</h5>
</div>
<div class="card-body">
{% if sitemap_info.exists %}
<div class="alert alert-success">
<i class="fa fa-check-circle"></i> 静态sitemap.xml已生成
</div>
<table class="table table-sm">
<tr>
<th width="40%">最后更新时间:</th>
<td>{{ sitemap_info.last_updated }}</td>
</tr>
<tr>
<th>文件大小:</th>
<td>{{ (sitemap_info.size / 1024) | round(2) }} KB</td>
</tr>
<tr>
<th>文件路径:</th>
<td><code>static/sitemap.xml</code></td>
</tr>
</table>
{% else %}
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i> 静态sitemap.xml尚未生成
</div>
<p class="text-muted mb-0">点击下方按钮生成静态sitemap文件</p>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="fa fa-globe"></i> 动态Sitemap
</h5>
</div>
<div class="card-body">
<p>动态sitemap路由始终可用</p>
<div class="input-group mb-3">
<input type="text" class="form-control" value="{{ request.url_root }}sitemap.xml" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('{{ request.url_root }}sitemap.xml')">
<i class="fa fa-copy"></i> 复制
</button>
</div>
<p class="text-muted small mb-0">
<i class="fa fa-info-circle"></i> 动态sitemap实时反映数据库内容无需手动生成
</p>
</div>
</div>
</div>
</div>
<!-- 操作按钮区 -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header bg-dark text-white">
<h5 class="mb-0">
<i class="fa fa-tools"></i> 快速操作
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<h6><i class="fa fa-file-code"></i> 生成静态Sitemap</h6>
<p class="text-muted small">生成static/sitemap.xml文件可用于静态服务或下载</p>
<button id="generateSitemapBtn" class="btn btn-success btn-lg w-100">
<i class="fa fa-cog"></i> 生成静态sitemap.xml
</button>
</div>
<div class="col-md-6 mb-3">
<h6><i class="fa fa-bell"></i> 通知搜索引擎</h6>
<p class="text-muted small">向Google、Baidu、Bing提交sitemap更新通知</p>
<button id="notifyEnginesBtn" class="btn btn-primary btn-lg w-100">
<i class="fa fa-paper-plane"></i> 通知搜索引擎更新
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<button id="doAllBtn" class="btn btn-warning btn-lg w-100">
<i class="fa fa-rocket"></i> 一键生成并通知(推荐)
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 操作结果显示区 -->
<div class="row">
<div class="col-md-12">
<div id="resultArea" class="card" style="display: none;">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">
<i class="fa fa-history"></i> 操作结果
</h5>
</div>
<div class="card-body" id="resultContent">
<!-- 动态填充内容 -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.card {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.btn-lg {
padding: 12px 24px;
font-size: 16px;
}
#resultArea {
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.result-item {
padding: 12px;
border-left: 4px solid #ddd;
margin-bottom: 10px;
background: #f8f9fa;
}
.result-item.success {
border-left-color: #28a745;
background: #d4edda;
}
.result-item.error {
border-left-color: #dc3545;
background: #f8d7da;
}
.result-item.warning {
border-left-color: #ffc107;
background: #fff3cd;
}
</style>
<script>
// 复制到剪贴板
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
alert('已复制到剪贴板!');
});
}
// 禁用按钮
function disableButtons() {
document.querySelectorAll('#generateSitemapBtn, #notifyEnginesBtn, #doAllBtn').forEach(btn => {
btn.disabled = true;
});
}
// 启用按钮
function enableButtons() {
document.querySelectorAll('#generateSitemapBtn, #notifyEnginesBtn, #doAllBtn').forEach(btn => {
btn.disabled = false;
});
}
// 显示结果
function showResult(html) {
const resultArea = document.getElementById('resultArea');
const resultContent = document.getElementById('resultContent');
resultContent.innerHTML = html;
resultArea.style.display = 'block';
resultArea.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// 生成静态sitemap
document.getElementById('generateSitemapBtn').addEventListener('click', async function() {
disableButtons();
const originalText = this.innerHTML;
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i> 生成中...';
try {
const response = await fetch('/api/generate-static-sitemap', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showResult(`
<div class="result-item success">
<h5><i class="fa fa-check-circle"></i> 生成成功</h5>
<p>${data.message}</p>
<table class="table table-sm mt-2">
<tr><th>URL数量</th><td>${data.total_urls}</td></tr>
<tr><th>文件路径:</th><td><code>${data.file_path}</code></td></tr>
<tr><th>生成时间:</th><td>${data.timestamp}</td></tr>
</table>
</div>
`);
// 刷新页面以更新sitemap状态
setTimeout(() => location.reload(), 2000);
} else {
showResult(`
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 生成失败</h5>
<p>${data.message}</p>
</div>
`);
}
} catch (error) {
showResult(`
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 请求失败</h5>
<p>网络错误: ${error.message}</p>
</div>
`);
} finally {
this.innerHTML = originalText;
enableButtons();
}
});
// 通知搜索引擎
document.getElementById('notifyEnginesBtn').addEventListener('click', async function() {
disableButtons();
const originalText = this.innerHTML;
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i> 通知中...';
try {
const response = await fetch('/api/notify-search-engines', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
let resultsHtml = `
<div class="result-item success">
<h5><i class="fa fa-check-circle"></i> ${data.message}</h5>
<p>Sitemap URL: <code>${data.sitemap_url}</code></p>
</div>
`;
data.results.forEach(result => {
const statusClass = result.status === 'success' ? 'success' : 'error';
const icon = result.status === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
resultsHtml += `
<div class="result-item ${statusClass}">
<h6><i class="fa ${icon}"></i> ${result.engine}</h6>
<p>${result.message}</p>
</div>
`;
});
showResult(resultsHtml);
} else {
showResult(`
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 通知失败</h5>
<p>${data.message}</p>
</div>
`);
}
} catch (error) {
showResult(`
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 请求失败</h5>
<p>网络错误: ${error.message}</p>
</div>
`);
} finally {
this.innerHTML = originalText;
enableButtons();
}
});
// 一键生成并通知
document.getElementById('doAllBtn').addEventListener('click', async function() {
disableButtons();
const originalText = this.innerHTML;
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i> 处理中...';
let allResultsHtml = '';
try {
// Step 1: 生成sitemap
const sitemapResponse = await fetch('/api/generate-static-sitemap', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const sitemapData = await sitemapResponse.json();
if (sitemapData.success) {
allResultsHtml += `
<div class="result-item success">
<h5><i class="fa fa-check-circle"></i> 步骤1生成sitemap成功</h5>
<p>${sitemapData.message}</p>
</div>
`;
// Step 2: 通知搜索引擎
const notifyResponse = await fetch('/api/notify-search-engines', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const notifyData = await notifyResponse.json();
if (notifyData.success) {
allResultsHtml += `
<div class="result-item success">
<h5><i class="fa fa-check-circle"></i> 步骤2${notifyData.message}</h5>
</div>
`;
notifyData.results.forEach(result => {
const statusClass = result.status === 'success' ? 'success' : 'warning';
const icon = result.status === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
allResultsHtml += `
<div class="result-item ${statusClass}">
<h6><i class="fa ${icon}"></i> ${result.engine}</h6>
<p>${result.message}</p>
</div>
`;
});
} else {
allResultsHtml += `
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 步骤2通知搜索引擎失败</h5>
<p>${notifyData.message}</p>
</div>
`;
}
} else {
allResultsHtml += `
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 步骤1生成sitemap失败</h5>
<p>${sitemapData.message}</p>
</div>
`;
}
showResult(allResultsHtml);
// 如果全部成功2秒后刷新页面
if (sitemapData.success) {
setTimeout(() => location.reload(), 3000);
}
} catch (error) {
showResult(`
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 操作失败</h5>
<p>网络错误: ${error.message}</p>
</div>
`);
} finally {
this.innerHTML = originalText;
enableButtons();
}
});
</script>
{% endblock %}