From dcc626544c53d4316c6923ba79df3f6986cd525d Mon Sep 17 00:00:00 2001 From: QiuSW Date: Mon, 6 Jul 2026 23:32:57 +0800 Subject: [PATCH] =?UTF-8?q?Phase=203:=20T-301~T-304=20SEO/=E9=83=A8?= =?UTF-8?q?=E7=BD=B2/SQLite/=E7=BB=9F=E8=AE=A1/MVP=E9=AA=8C=E6=94=B6?= =?UTF-8?q?=E5=AE=8C=E6=88=90(42=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 4 +- core/tests.py | 15 +- docs/06-tasks.md | 8 +- docs/current-state.md | 10 +- docs/deployment/deployment-guide.md | 210 ++++++++++++++++++++++++++++ progress.md | 29 ++++ skelet/context_processors.py | 10 ++ skelet/settings/base.py | 6 + skelet/templates/base.html | 14 ++ 9 files changed, 293 insertions(+), 13 deletions(-) create mode 100644 docs/deployment/deployment-guide.md create mode 100644 skelet/context_processors.py diff --git a/AGENTS.md b/AGENTS.md index 93a9e90..5af3e82 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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) 查看剩余待办。 ## 开发环境 diff --git a/core/tests.py b/core/tests.py index 3cfe024..a52b54a 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,4 +1,4 @@ -from django.test import TestCase +from django.test import TestCase, override_settings from django.db import IntegrityError from django.core.exceptions import ValidationError @@ -484,4 +484,15 @@ class SEOTests(PageTreeMixin, TestCase): def test_sitemap_returns_200(self): resp = self.client.get("/sitemap.xml") - self.assertEqual(resp.status_code, 200) \ No newline at end of file + 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") \ No newline at end of file diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 74ec294..87b8458 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -51,10 +51,10 @@ | ID | 任务 | 依赖 | 验收要点 | 状态 | | --- | --- | --- | --- | --- | | T-301 | 补 SEO 基础 | T-205, T-206 | 核心页面有 title、description、canonical 或等价设置;sitemap 可用 | DONE | -| T-302 | 补部署文档 | T-301 | 2 核 2G VPS 上 Gunicorn + Nginx + SQLite 的部署步骤清楚 | DOING | -| T-303 | SQLite 上线检查 | T-302 | JSON1 验证、WAL 启用、备份策略和 PostgreSQL 迁移触发条件已记录 | TODO | -| T-305 | 接入轻量访问统计与外链点击记录 | T-301 | Plausible / Umami 或等价方案接入;外部链接点击可统计;不引入重量级分析 SDK;这是里程碑 M3(SEO 验证)和 M4(Affiliate)的数据前置 | TODO | -| T-304 | MVP 完整验收 | T-303, T-305 | `02-requirements.md` 的 P0 验收全部通过,验证命令和结果写入 `progress.md` | TODO | +| T-302 | 补部署文档 | T-301 | 2 核 2G VPS 上 Gunicorn + Nginx + SQLite 的部署步骤清楚 | DONE | +| T-303 | SQLite 上线检查 | T-302 | JSON1 验证、WAL 启用、备份策略和 PostgreSQL 迁移触发条件已记录 | DONE | +| 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` | DONE | ## 任务详单 diff --git a/docs/current-state.md b/docs/current-state.md index 12abd63..d1d5d2a 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -11,14 +11,14 @@ ## 当前快照 - 日期: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 预编译包提供) - git:分支 `ds`;`.gitignore` 已生效(`.venv/`、`db.sqlite3`、`media/` 等被忽略) - 技术栈:Wagtail 7.4.2 + Django 6.0.6 + Python 3.12 + SQLite;第一版英文单语言站点 - 生产代码:已初始化,`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`) -- 当前 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 节。 ## 当前可运行内容 diff --git a/docs/deployment/deployment-guide.md b/docs/deployment/deployment-guide.md new file mode 100644 index 0000000..d9da735 --- /dev/null +++ b/docs/deployment/deployment-guide.md @@ -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 +``` diff --git a/progress.md b/progress.md index 0b5e2c8..02dc845 100644 --- a/progress.md +++ b/progress.md @@ -267,6 +267,35 @@ - 阻塞:无。 - 下一步: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 文档同步) - 状态:DONE diff --git a/skelet/context_processors.py b/skelet/context_processors.py new file mode 100644 index 0000000..6882628 --- /dev/null +++ b/skelet/context_processors.py @@ -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" + ), + } diff --git a/skelet/settings/base.py b/skelet/settings/base.py index 703cff8..187a34a 100644 --- a/skelet/settings/base.py +++ b/skelet/settings/base.py @@ -84,6 +84,7 @@ TEMPLATES = [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "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 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. # 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 - diff --git a/skelet/templates/base.html b/skelet/templates/base.html index 08e20b6..3a9db33 100644 --- a/skelet/templates/base.html +++ b/skelet/templates/base.html @@ -48,6 +48,20 @@ {% include "includes/footer.html" %} + {% if PLAUSIBLE_DOMAIN %} + + {% endif %} + + {% block extra_js %}{% endblock %}