diff --git a/core/tests.py b/core/tests.py
index 69b703a..3cfe024 100644
--- a/core/tests.py
+++ b/core/tests.py
@@ -444,4 +444,44 @@ class ArticlePageTests(PageTreeMixin, TestCase):
article.related_projects.add(project)
self.assertEqual(article.related_projects.count(), 1)
- self.assertEqual(article.related_projects.first().title, "Related Project")
\ No newline at end of file
+ self.assertEqual(article.related_projects.first().title, "Related Project")
+
+
+class SEOTests(PageTreeMixin, TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.setUpPageTree()
+ cls.lang = Language.objects.create(name="Rust", slug="rust")
+
+ def test_homepage_meta_description(self):
+ resp = self.client.get("/")
+ self.assertContains(resp, '`,但当前核心页面的 `search_description` 均为空。
+- 复测页面:
+ - `/`
+ - `/projects/`
+ - `/scenarios/crm/`
+ - `/projects/djangocrm/`
+ - `/articles/`
+
+复测结果:上述页面均返回 200,但均未输出 ``,也没有 `rel="canonical"`。
+
+任务要求是“核心页面有 title、description、canonical 或等价设置;sitemap 可用”。当前只有 title 和 sitemap 可用,description/canonical 未达标。
+
+建议修复方向:
+
+- 为种子页面写入 `search_description`,或在模板层提供可解释的 fallback description。
+- 明确 canonical 的处理方式:输出 ``,或在文档中说明采用的 Wagtail 等价机制并用测试覆盖。
+- 增加测试断言首页、项目详情页、场景页、文章页含 meta description,并断言 sitemap 含项目 URL。
+
+### P1:根目录 `test_sitemap.py` 是非规范测试文件,导入即写数据库并发请求
+
+- 影响任务:T-301 / 验证基线
+- 位置:`test_sitemap.py:1` 至 `test_sitemap.py:17`
+- 现象:
+ - 文件位于仓库根目录,命名匹配 Django test discovery。
+ - 模块顶层直接执行 `django.setup()`、`Site.objects.get_or_create(...)` 和 `Client().get("/sitemap.xml")`。
+ - `manage.py test` 运行时会打印 `Site created/updated`、`sitemap.xml: 200` 等输出。
+
+这不是稳定的测试写法。测试发现阶段导入模块就修改数据库,会污染开发数据库,也会让测试基线依赖导入副作用。
+
+建议修复方向:
+
+- 删除根目录 `test_sitemap.py`。
+- 把 sitemap 验证迁移到 `core/tests.py` 或新的 app 测试文件中,使用 `TestCase` 和断言。
+
+### P1:T-302 部署文档未完成
+
+- 影响任务:T-302
+- 当前事实:仓库只有早前的 `docs/deployment/environment-assessment.md`,它是 Lightsail 环境评估,不是可复制执行的部署手册。
+- 缺口:
+ - 没有完整 Gunicorn systemd service 示例。
+ - 没有 Nginx server block 示例。
+ - 没有静态文件、media、环境变量、迁移、superuser、备份、重启/回滚步骤。
+
+T-302 要求“2 核 2G VPS 上 Gunicorn + Nginx + SQLite 的部署步骤清楚”,当前未满足。
+
+### P1:T-303 SQLite 上线检查未完成
+
+- 影响任务:T-303
+- 缺口:
+ - 未记录 JSON1 验证命令和结果。
+ - 未记录 WAL 启用命令和 `PRAGMA journal_mode;` 证据。
+ - 未记录备份策略。
+ - 未记录迁移 PostgreSQL 的触发条件。
+
+任务要求这些内容写入部署文档;当前没有。
+
+### P1:T-305 访问统计与外链点击未接入
+
+- 影响任务:T-305
+- 复查方式:搜索 `Plausible`、`Umami`、`analytics`、`onclick`、`data-analytics`、`click`。
+- 结果:代码没有轻量统计脚本,没有外链点击事件标记或处理逻辑。
+
+T-305 未实现。
+
+### P1:T-304 MVP 完整验收未完成
+
+- 影响任务:T-304
+- 原因:T-303 和 T-305 仍未完成,T-304 的依赖不满足;也没有看到 `02-requirements.md` P0 完整验收记录。
+
+T-304 未达标。
+
+## 已完成部分
+
+### Sitemap 基础可用
+
+- 位置:
+ - `skelet/settings/base.py:56` 至 `skelet/settings/base.py:58`
+ - `skelet/urls.py:18` 至 `skelet/urls.py:23`
+ - `skelet/sitemaps.py:1` 至 `skelet/sitemaps.py:22`
+- 复测:`/sitemap.xml` 返回 200,并包含项目、文章、场景 URL。
+
+这一部分满足 T-301 的 sitemap 子项。
+
+## 逐任务验收
+
+| 任务 | 结论 | 说明 |
+| --- | --- | --- |
+| T-301 补 SEO 基础 | 未达标 | sitemap 可用;核心页面缺少 meta description 和 canonical;测试方式不规范。 |
+| T-302 补部署文档 | 未达标 | 没有 Gunicorn + Nginx + SQLite 的可执行部署手册。 |
+| T-303 SQLite 上线检查 | 未达标 | 没有 JSON1、WAL、备份、PostgreSQL 迁移触发条件记录。 |
+| T-305 接入轻量访问统计与外链点击记录 | 未达标 | 未接入 Plausible/Umami 或等价方案,也没有外链点击统计。 |
+| T-304 MVP 完整验收 | 未达标 | 依赖任务未完成;没有 P0 完整验收记录。 |
+
+## 验证记录
+
+已执行:
+
+```bash
+.venv/bin/python3.12.exe manage.py check
+.venv/bin/python3.12.exe manage.py makemigrations --check --dry-run
+.venv/bin/python3.12.exe manage.py test
+.venv/bin/python3.12.exe manage.py showmigrations sites
+.venv/bin/python3.12.exe manage.py shell -c "from wagtail.models import Page; qs=Page.objects.live().public().specific(); print([(p.url, bool(getattr(p, 'seo_title', '')), bool(getattr(p, 'search_description', ''))) for p in qs if p.url in ['/', '/projects/', '/scenarios/crm/', '/projects/djangocrm/', '/articles/']])"
+.venv/bin/python3.12.exe manage.py shell -c "from django.test import Client; c=Client(); paths=['/','/projects/','/scenarios/crm/','/projects/djangocrm/','/articles/','/sitemap.xml']; [print(p, c.get(p).status_code, ('
{% if page.search_description %}
+ {% elif page.specific.summary %}
+
+ {% else %}
+
{% endif %}
{% if request.in_preview_panel %}
+ {% else %}
+ {% if page %}
+
+ {% endif %}
{% endif %}
diff --git a/skelet/urls.py b/skelet/urls.py
index c2e8a0c..452900e 100644
--- a/skelet/urls.py
+++ b/skelet/urls.py
@@ -1,35 +1,35 @@
from django.conf import settings
from django.urls import include, path
from django.contrib import admin
+from django.contrib.sitemaps.views import sitemap
from wagtail.admin import urls as wagtailadmin_urls
from wagtail import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
from search import views as search_views
+from skelet.sitemaps import sitemaps
urlpatterns = [
path("django-admin/", admin.site.urls),
path("admin/", include(wagtailadmin_urls)),
path("documents/", include(wagtaildocs_urls)),
path("search/", search_views.search, name="search"),
+ path(
+ "sitemap.xml",
+ sitemap,
+ {"sitemaps": sitemaps},
+ name="wagtail_sitemap",
+ ),
]
-
if settings.DEBUG:
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
- # Serve static and media files from development server
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns = urlpatterns + [
- # For anything not caught by a more specific rule above, hand over to
- # Wagtail's page serving mechanism. This should be the last pattern in
- # the list:
path("", include(wagtail_urls)),
- # Alternatively, if you want Wagtail pages to be served from a subpath
- # of your site, rather than the site root:
- # path("pages/", include(wagtail_urls)),
]