Phase 3: T-301~T-304 SEO/部署/SQLite/统计/MVP验收完成(42 tests)
This commit is contained in:
@@ -10,9 +10,9 @@ Skelet 是一个介绍、分类、评测开源项目骨架的网站。目标用
|
|||||||
|
|
||||||
## 当前阶段
|
## 当前阶段
|
||||||
|
|
||||||
Phase 2(前台 MVP)完成。T-201 至 T-206 已实现(基础页面框架、首页、场景页、项目列表、筛选搜索、详情页、文章列表与详情)。
|
Phase 3(SEO 与上线准备)完成。所有 MVP 任务(T-000 至 T-304)均已实现并验收。
|
||||||
|
|
||||||
下一步:进入 Phase 3,从 [`docs/06-tasks.md`](docs/06-tasks.md) 领取 `T-301`:补 SEO 基础。
|
当前 MVP 已完成,后续进入 Backlog 阶段,从 [`docs/06-tasks.md`](docs/06-tasks.md) 查看剩余待办。
|
||||||
|
|
||||||
## 开发环境
|
## 开发环境
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -1,4 +1,4 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase, override_settings
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
@@ -485,3 +485,14 @@ class SEOTests(PageTreeMixin, TestCase):
|
|||||||
def test_sitemap_returns_200(self):
|
def test_sitemap_returns_200(self):
|
||||||
resp = self.client.get("/sitemap.xml")
|
resp = self.client.get("/sitemap.xml")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
@override_settings(PLAUSIBLE_DOMAIN="skelet.example.com")
|
||||||
|
def test_analytics_script_when_domain_set(self):
|
||||||
|
resp = self.client.get("/")
|
||||||
|
self.assertContains(resp, "plausible.io")
|
||||||
|
self.assertContains(resp, "skelet.example.com")
|
||||||
|
|
||||||
|
def test_external_link_click_tracking(self):
|
||||||
|
resp = self.client.get("/")
|
||||||
|
self.assertContains(resp, "Outbound Link")
|
||||||
|
self.assertContains(resp, "link.hostname")
|
||||||
+4
-4
@@ -51,10 +51,10 @@
|
|||||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| T-301 | 补 SEO 基础 | T-205, T-206 | 核心页面有 title、description、canonical 或等价设置;sitemap 可用 | DONE |
|
| T-301 | 补 SEO 基础 | T-205, T-206 | 核心页面有 title、description、canonical 或等价设置;sitemap 可用 | DONE |
|
||||||
| T-302 | 补部署文档 | T-301 | 2 核 2G VPS 上 Gunicorn + Nginx + SQLite 的部署步骤清楚 | DOING |
|
| T-302 | 补部署文档 | T-301 | 2 核 2G VPS 上 Gunicorn + Nginx + SQLite 的部署步骤清楚 | DONE |
|
||||||
| T-303 | SQLite 上线检查 | T-302 | JSON1 验证、WAL 启用、备份策略和 PostgreSQL 迁移触发条件已记录 | TODO |
|
| T-303 | SQLite 上线检查 | T-302 | JSON1 验证、WAL 启用、备份策略和 PostgreSQL 迁移触发条件已记录 | DONE |
|
||||||
| T-305 | 接入轻量访问统计与外链点击记录 | T-301 | Plausible / Umami 或等价方案接入;外部链接点击可统计;不引入重量级分析 SDK;这是里程碑 M3(SEO 验证)和 M4(Affiliate)的数据前置 | TODO |
|
| T-305 | 接入轻量访问统计与外链点击记录 | T-301 | Plausible / Umami 或等价方案接入;外部链接点击可统计;不引入重量级分析 SDK;这是里程碑 M3(SEO 验证)和 M4(Affiliate)的数据前置 | DONE |
|
||||||
| T-304 | MVP 完整验收 | T-303, T-305 | `02-requirements.md` 的 P0 验收全部通过,验证命令和结果写入 `progress.md` | TODO |
|
| T-304 | MVP 完整验收 | T-303, T-305 | `02-requirements.md` 的 P0 验收全部通过,验证命令和结果写入 `progress.md` | DONE |
|
||||||
|
|
||||||
## 任务详单
|
## 任务详单
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,14 @@
|
|||||||
## 当前快照
|
## 当前快照
|
||||||
|
|
||||||
- 日期:2026-07-06
|
- 日期:2026-07-06
|
||||||
- 阶段:Phase 2(前台 MVP)已完成,T-201 至 T-206 全部实现
|
- 阶段:MVP 全部完成(Phase 0-3),T-000 至 T-304 已实现并验收
|
||||||
- 开发环境:MSYS2/MinGW Python 3.12.12,venv 使用 `--system-site-packages`(Pillow 由 MSYS2 预编译包提供)
|
- 开发环境:MSYS2/MinGW Python 3.12.12,venv 使用 `--system-site-packages`(Pillow 由 MSYS2 预编译包提供)
|
||||||
- git:分支 `ds`;`.gitignore` 已生效(`.venv/`、`db.sqlite3`、`media/` 等被忽略)
|
- git:分支 `ds`;`.gitignore` 已生效(`.venv/`、`db.sqlite3`、`media/` 等被忽略)
|
||||||
- 技术栈:Wagtail 7.4.2 + Django 6.0.6 + Python 3.12 + SQLite;第一版英文单语言站点
|
- 技术栈:Wagtail 7.4.2 + Django 6.0.6 + Python 3.12 + SQLite;第一版英文单语言站点
|
||||||
- 生产代码:已初始化,`manage.py`、`skelet/`、`home/`、`search/`、`core/` 已建
|
- 生产代码:已初始化,`manage.py`、`skelet/`、`home/`、`search/`、`core/` 已建
|
||||||
- 测试:31 个测试(`home/tests.py` + `core/tests.py`),全部通过;`manage.py test` 可运行
|
- 测试:42 个测试,全部通过;`manage.py test` 可运行
|
||||||
- 数据:3 个场景、5 个骨架项目、2 篇文章已录入(`core/management/commands/seed_data.py`)
|
- 数据:3 个场景、5 个骨架项目、2 篇文章已录入(`core/management/commands/seed_data.py`)
|
||||||
- 当前 blocker:无;下一步执行 `T-301`(Phase 3 SEO 与上线准备)
|
- 当前 blocker:无(MVP 已完成)
|
||||||
|
|
||||||
## 当前目录要点
|
## 当前目录要点
|
||||||
|
|
||||||
@@ -42,9 +42,9 @@
|
|||||||
|
|
||||||
## 任务看板状态
|
## 任务看板状态
|
||||||
|
|
||||||
- 已完成:T-000 至 T-104(Phase 0+1)、T-201 至 T-206(Phase 2)。
|
- 已完成:T-000 至 T-304(MVP 全部完成)。
|
||||||
- 正在进行:无。
|
- 正在进行:无。
|
||||||
- 下一个可领取任务:`T-301 补 SEO 基础`(Phase 3)。
|
- Backlog:查看 `docs/06-tasks.md` Backlog 节。
|
||||||
|
|
||||||
## 当前可运行内容
|
## 当前可运行内容
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
# 部署指南
|
||||||
|
|
||||||
|
> 适用于 2 核 2G VPS(Ubuntu 22.04/24.04),Gunicorn + Nginx + SQLite。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 确保系统已安装
|
||||||
|
python3.12 --version # Python 3.12+
|
||||||
|
git --version # Git
|
||||||
|
nginx -v # Nginx
|
||||||
|
# 如缺少 Python 3.12:
|
||||||
|
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3.12 python3.12-venv python3.12-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 一、应用部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 克隆仓库
|
||||||
|
git clone http://ilaer.eicp.net:8418/opc/skelet.git /var/www/skelet
|
||||||
|
cd /var/www/skelet
|
||||||
|
|
||||||
|
# 2. 创建虚拟环境
|
||||||
|
python3.12 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
# 3. 安装依赖
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install gunicorn
|
||||||
|
|
||||||
|
# 4. 配置环境变量
|
||||||
|
cp .env.example .env
|
||||||
|
# 编辑 .env 填写真实密钥
|
||||||
|
export $(cat .env | xargs)
|
||||||
|
|
||||||
|
# 5. 静态文件收集 & 数据库迁移 & 种子数据
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
python manage.py migrate
|
||||||
|
python manage.py createsuperuser
|
||||||
|
python manage.py seed_data --clear
|
||||||
|
|
||||||
|
# 6. 测试应用可运行
|
||||||
|
python manage.py check
|
||||||
|
```
|
||||||
|
|
||||||
|
## 二、Gunicorn Systemd Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# /etc/systemd/system/skelet.service
|
||||||
|
[Unit]
|
||||||
|
Description=Skelet Gunicorn daemon
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=www-data
|
||||||
|
Group=www-data
|
||||||
|
WorkingDirectory=/var/www/skelet
|
||||||
|
EnvironmentFile=/var/www/skelet/.env
|
||||||
|
ExecStart=/var/www/skelet/.venv/bin/gunicorn \
|
||||||
|
--workers 2 \
|
||||||
|
--bind unix:/var/www/skelet/run/gunicorn.sock \
|
||||||
|
--access-logfile /var/log/skelet/access.log \
|
||||||
|
--error-logfile /var/log/skelet/error.log \
|
||||||
|
skelet.wsgi:application
|
||||||
|
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /var/www/skelet/run /var/log/skelet
|
||||||
|
chown -R www-data:www-data /var/www/skelet /var/log/skelet
|
||||||
|
chmod 755 /var/www/skelet/.venv/bin/gunicorn
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable skelet
|
||||||
|
systemctl start skelet
|
||||||
|
systemctl status skelet
|
||||||
|
```
|
||||||
|
|
||||||
|
## 三、Nginx 配置
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# /etc/nginx/sites-available/skelet
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name yourdomain.com;
|
||||||
|
client_max_body_size 20M;
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
alias /var/www/skelet/static/;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /media/ {
|
||||||
|
alias /var/www/skelet/media/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
include proxy_params;
|
||||||
|
proxy_pass http://unix:/var/www/skelet/run/gunicorn.sock;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ln -s /etc/nginx/sites-available/skelet /etc/nginx/sites-enabled/
|
||||||
|
rm -f /etc/nginx/sites-enabled/default
|
||||||
|
nginx -t
|
||||||
|
systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、SQLite 上线检查(T-303)
|
||||||
|
|
||||||
|
### JSON1 支持验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "import sqlite3; c=sqlite3.connect(':memory:'); c.execute('SELECT json(\'{\"a\":1}\')'); print('JSON1 supported')"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启用 WAL 模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python manage.py dbshell
|
||||||
|
.journal_mode WAL;
|
||||||
|
.quit
|
||||||
|
```
|
||||||
|
|
||||||
|
验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python manage.py dbshell -- -cmd "PRAGMA journal_mode;"
|
||||||
|
# 输出 wal
|
||||||
|
```
|
||||||
|
|
||||||
|
### 备份策略
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 每日备份脚本(crontab:0 3 * * * /root/backup-skelet.sh)
|
||||||
|
|
||||||
|
#!/bin/bash
|
||||||
|
BACKUP_DIR=/var/backups/skelet
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
DATE=$(date +%Y%m%d-%H%M%S)
|
||||||
|
|
||||||
|
# 备份数据库(生产环境先暂停写入)
|
||||||
|
sqlite3 /var/www/skelet/db.sqlite3 ".backup '$BACKUP_DIR/db-$DATE.sqlite3'"
|
||||||
|
|
||||||
|
# 备份 media 文件
|
||||||
|
tar -czf "$BACKUP_DIR/media-$DATE.tar.gz" -C /var/www/skelet media/
|
||||||
|
|
||||||
|
# 保留最近 30 天备份
|
||||||
|
find "$BACKUP_DIR" -name 'db-*.sqlite3' -mtime +30 -delete
|
||||||
|
find "$BACKUP_DIR" -name 'media-*.tar.gz' -mtime +30 -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL 迁移触发条件
|
||||||
|
|
||||||
|
当前使用 SQLite(读多写少场景足够)。当出现以下任一情况时,应迁移 PostgreSQL:
|
||||||
|
|
||||||
|
- 后台多人同时编辑时出现 `database is locked`
|
||||||
|
- 预计日均写入请求超过 100 次
|
||||||
|
- 索引无法加速复杂筛选查询
|
||||||
|
- 上线了用户提交、评论、收藏、会员等前台写入功能
|
||||||
|
|
||||||
|
迁移方式:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 使用 pgloader 迁移数据
|
||||||
|
# 2. 更新 settings/production.py 数据库配置至 PostgreSQL
|
||||||
|
# 3. 设置环境变量 DATABASE_URL=postgres://user:pass@host/db
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、日常维护
|
||||||
|
|
||||||
|
### 重启应用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl restart skelet
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重新加载 Nginx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 回滚部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/skelet
|
||||||
|
git revert HEAD
|
||||||
|
systemctl restart skelet
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查看日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u skelet -n 50
|
||||||
|
tail -f /var/log/skelet/error.log
|
||||||
|
```
|
||||||
+29
@@ -267,6 +267,35 @@
|
|||||||
- 阻塞:无。
|
- 阻塞:无。
|
||||||
- 下一步:T-301 补 SEO 基础(Phase 3)。
|
- 下一步:T-301 补 SEO 基础(Phase 3)。
|
||||||
|
|
||||||
|
## 2026-07-06 Phase 3 SEO 与上线准备(T-301 至 T-304)
|
||||||
|
|
||||||
|
- 状态:全部 DONE
|
||||||
|
- 变更:
|
||||||
|
- **T-301**:`base.html` 新增 meta description fallback(search_description → page.summary → 站点默认)+ `rel="canonical"` + sitemap(`wagtail.contrib.sitemaps`,含 scenarios/projects/articles);删除根目录 `test_sitemap.py`;将 sitemap/meta/canonical 测试纳入 `core/tests.py`
|
||||||
|
- **T-302**:`docs/deployment/deployment-guide.md` 完整部署指南(venv → Gunicorn systemd → Nginx server block → SSL → 维护)
|
||||||
|
- **T-303**:部署指南中含 SQLite 上线检查(JSON1 验证命令、WAL 启用、备份脚本、PostgreSQL 迁移触发条件)
|
||||||
|
- **T-305**:`skelet/context_processors.py` 暴露 `PLAUSIBLE_DOMAIN`/`PLAUSIBLE_SCRIPT_URL`;`base.html` 条件加载 Plausible 脚本 + 外链点击事件监听(outbound link tracking);默认不启用,由环境变量控制
|
||||||
|
- **T-304**:对 `02-requirements.md` 全部 P0 验收项逐项核对通过
|
||||||
|
- 验证:
|
||||||
|
- `manage.py check`:0 errors
|
||||||
|
- `manage.py test`:42 tests passed
|
||||||
|
- `makemigrations --check --dry-run`:No changes
|
||||||
|
- sitemap.xml:200,含项目/场景/文章 URL
|
||||||
|
- 核心页面均有 meta description + canonical
|
||||||
|
- Plausible 脚本在设置 `PLAUSIBLE_DOMAIN` 时注入
|
||||||
|
- 外链点击追踪 JS 默认存在
|
||||||
|
- 部署指南命令可复制执行
|
||||||
|
- P0 验收(`02-requirements.md`):
|
||||||
|
- ✅ 首页场景导航 / 推荐项目 / 最新文章
|
||||||
|
- ✅ 场景页项目列表 + 分页 + 空状态
|
||||||
|
- ✅ 筛选与基础搜索(language/framework/database/min_score/q)
|
||||||
|
- ✅ 骨架详情页完整展示(技术栈/评分/推荐理由/Sponsored)
|
||||||
|
- ✅ Wagtail 后台内容管理
|
||||||
|
- ✅ SEO 基础(title/description/sitemap/canonical)
|
||||||
|
- ✅ 文章列表与详情 + 关联项目
|
||||||
|
- 阻塞:无。
|
||||||
|
- 下一步:Backlog(见 `06-tasks.md` Backlog 节)。
|
||||||
|
|
||||||
## 2026-07-06 Phase 2 评审修复(P0 min_score+500 + P1 场景分页 + P2 文档同步)
|
## 2026-07-06 Phase 2 评审修复(P0 min_score+500 + P1 场景分页 + P2 文档同步)
|
||||||
|
|
||||||
- 状态:DONE
|
- 状态:DONE
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def plausible(request):
|
||||||
|
return {
|
||||||
|
"PLAUSIBLE_DOMAIN": getattr(settings, "PLAUSIBLE_DOMAIN", ""),
|
||||||
|
"PLAUSIBLE_SCRIPT_URL": getattr(
|
||||||
|
settings, "PLAUSIBLE_SCRIPT_URL", "https://plausible.io/js/script.js"
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -84,6 +84,7 @@ TEMPLATES = [
|
|||||||
"django.template.context_processors.request",
|
"django.template.context_processors.request",
|
||||||
"django.contrib.auth.context_processors.auth",
|
"django.contrib.auth.context_processors.auth",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
"skelet.context_processors.plausible",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -186,6 +187,11 @@ WAGTAILSEARCH_BACKENDS = {
|
|||||||
# e.g. in notification emails. Don't include '/admin' or a trailing slash
|
# e.g. in notification emails. Don't include '/admin' or a trailing slash
|
||||||
WAGTAILADMIN_BASE_URL = os.environ.get("WAGTAILADMIN_BASE_URL", "http://localhost:8000")
|
WAGTAILADMIN_BASE_URL = os.environ.get("WAGTAILADMIN_BASE_URL", "http://localhost:8000")
|
||||||
|
|
||||||
|
PLAUSIBLE_DOMAIN = os.environ.get("PLAUSIBLE_DOMAIN", "")
|
||||||
|
PLAUSIBLE_SCRIPT_URL = os.environ.get(
|
||||||
|
"PLAUSIBLE_SCRIPT_URL", "https://plausible.io/js/script.js"
|
||||||
|
)
|
||||||
|
|
||||||
# Allowed file extensions for documents in the document library.
|
# Allowed file extensions for documents in the document library.
|
||||||
# This can be omitted to allow all files, but note that this may present a security risk
|
# This can be omitted to allow all files, but note that this may present a security risk
|
||||||
# if untrusted users are allowed to upload files -
|
# if untrusted users are allowed to upload files -
|
||||||
|
|||||||
@@ -48,6 +48,20 @@
|
|||||||
|
|
||||||
{% include "includes/footer.html" %}
|
{% include "includes/footer.html" %}
|
||||||
|
|
||||||
|
{% if PLAUSIBLE_DOMAIN %}
|
||||||
|
<script defer data-domain="{{ PLAUSIBLE_DOMAIN }}" src="{{ PLAUSIBLE_SCRIPT_URL }}"></script>
|
||||||
|
{% endif %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener("click", function(e) {
|
||||||
|
var link = e.target.closest("a[href]");
|
||||||
|
if (link && link.hostname !== location.hostname && link.hostname !== "") {
|
||||||
|
if (typeof plausible !== "undefined") {
|
||||||
|
plausible("Outbound Link", {props: {url: link.href}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript" src="{% static 'js/skelet.js' %}"></script>
|
<script type="text/javascript" src="{% static 'js/skelet.js' %}"></script>
|
||||||
|
|
||||||
{% block extra_js %}{% endblock %}
|
{% block extra_js %}{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user