← 返回个人展示网站

PROJECT ARTICLE / 项目文章

Django 后端重构

从一次推荐阅读功能的调整出发,记录网站如何把文章本体与展示关系彻底拆开。

这次后端重构,是从一个看起来很小的问题开始的:“推荐阅读”究竟是谁的数据?

如果把“是否推荐”写进文章自己的元数据,似乎最省事。但这样一来,一篇文章只能回答“我是不是推荐文章”,却无法回答“我在哪里被选中”“在这个项目中排第几”“换到另一个入口后是否仍然精选”。继续讨论后,推荐的含义也变得更明确:它不是具体目录对内容的重复强调,而是首页对全站内容的一次独立选择。

继续把这个问题往下推,就会发现旧结构里不只推荐关系放错了位置。所属板块、所属项目和文章编号也都描述文章出现在哪里,而不是文章本身是什么。只要这些字段仍然写在文章里,一篇文章就很难同时出现在多个板块;即使强行实现,也会产生重复文件、多个网址和互相冲突的排序。

所以这次没有继续给旧元数据增加例外,而是重新划分了整个网站的内容关系。

先区分内容本体与展示位置

当前网站实际管理八类数据:

数据 表示什么 典型字段
Section 一个板块 slug
Project 一个实践或技术项目 标题、摘要、状态、周期、职责与技术信息
Article 一篇全站唯一的文章 标题、摘要、发布时间、创作方式、标签与正文索引
SectionProject 项目在板块中的收录位置 目录顺序
SectionArticle 文章在板块中的收录位置 目录顺序
ProjectArticle 文章在项目中的收录位置 目录顺序
Homepage 唯一的首页编辑入口 首页精选容器
HomepageFeature 文章或项目在首页精选中的位置 内容类型、内容与顺序

Section、Project 和 Article 是内容本体,其余模型表达页面如何使用这些内容。项目不会因为出现在技术板块而改变自身内容,文章也不会因为被某个项目收录或被首页精选就变成另一篇文章。真正随展示位置变化的,是收录关系、排序和阅读上下文。

这也是为什么我最终没有把所有内容继续塞进一张通用记录表。通用表适合快速验证页面,但当“项目”和“文章”开始拥有不同字段、不同关系和不同查询方式后,它会把本来清晰的概念重新压扁。现在它们分别成为 Django 模型,数据库约束也可以直接表达:同一容器内不能重复收录同一篇文章,而显示顺序只属于这条收录关系。

Markdown 只保留正文

我的文章并不是直接在网站仓库里写,而是在独立的写作目录中形成原稿和审定副本。如果为了让网站识别文章,又要求我记住一组 Front Matter 字段,那么一个拼错的字段名就可能让整次同步失败;把收录列表换到板块或项目文件里,也只是把难编辑的问题挪了位置。

因此,网站中的 Markdown 最终只保留正文,文件名就是稳定的 slug:

content/articles/article-slug.md

标题、摘要、日期、公开状态、创作方式和标签不再写进文件。文章与板块、项目的关系也不通过目录层级表达。写作完成后,审定正文被复制到统一文章目录;同一篇文章不论出现在哪里,都只维护这一份正文。

元数据进入本地编辑后台

结构化信息更适合由表单约束。网站现在只在本地开发环境开放 Django 编辑后台:文章页维护文章自身事实;项目详情侧栏由一组可自由填写小标题和内容、并可拖动排序的“项目信息”构成,不再预设页面说明或技术字段;项目页同时维护项目文章编排;“板块内容”只提供三个固定系统板块的内容编排,不允许日常新增或删除;首页精选则有唯一的独立编辑入口。slug 在创建后变为只读,避免改名时意外改变文章网址。

日常编辑时,SQLite 是元数据和展示关系的工作数据源。Article 只保存文章自身事实;ProjectInfo 保存项目侧栏中的标题、内容与顺序;SectionProject、SectionArticle 和 ProjectArticle 分别保存各自目录中的收录位置;HomepageFeature 单独保存首页精选的文章、项目与混排顺序。同一篇文章要出现在新的目录,只需在相应板块或项目的编辑页新增一条关系;要进入精选,只需在首页精选中添加它,不复制正文,也不修改 Article。

这个后台不是公开 CMS。生产环境不注册编辑路由,Nginx 也继续阻断后台路径;它只解决我在本机维护结构化信息时容易拼错字段的问题。

发布快照负责重建与部署

如果元数据只存在本地 SQLite,换电脑、重建数据库或部署服务器时就无法恢复;但如果再维护一份手写配置,又会重新形成两个编辑入口。为此,网站增加了一份由命令自动生成的 catalog.json

它是发布快照,不是人工编辑源。确认正文、元数据、收录关系、首页精选和公开状态以后,导出命令会先检查全部正文、图片与站内链接,再按固定顺序写出板块、项目、文章、目录关系与首页精选。Git 保存正文、图片与这份快照;新数据库则从快照恢复元数据和关系,再把 Markdown 正文解析为安全 HTML。

这样,工作状态和发布状态有了明确边界:本地后台负责编辑,发布快照负责传递和恢复,Markdown 负责正文,数据库负责查询。重新建库不需要重新填写后台,也不需要手改 JSON。

同步、导出与部署彼此分开

保存或复制 Markdown 后,需要显式执行正文同步。同步只更新正文的渲染结果和校验值,不覆盖后台里的标题、目录排序或首页精选。确认后台数据后,再执行导出命令生成发布快照。

正式环境在迁移后显式从快照恢复内容,网页请求本身不会扫描文件或修改数据库。Git 提交、正文同步、快照导出和正式部署仍然是不同动作;visibility 决定正式环境能否显示内容,但在本地保存为公开也不等于已经上线。

一篇文章可以拥有多个阅读上下文

文章现在使用全站唯一的地址:/articles/<slug>/。它的身份不再绑在某个项目路径下,因此同一篇文章被多个位置收录时,不会复制正文,也不会产生多个彼此竞争的文章身份。

从栏目或项目点进文章时,链接会附带当前收录上下文和原始返回目标。Django 据此判断“下一篇”应该沿着哪一组文章继续,并把同一个返回目标传给下一篇,而不是把上一篇文章误当成上级页面;如果来源不可用,再退回当前栏目、项目或首页。

这使文章身份和阅读路径能够同时成立:文章本身只有一个,但读者从不同入口进入时,可以继续沿着不同项目阅读。上下文只影响导航,不会改变文章内容。

目录排序与首页精选各自独立

这次重构最初要解决的推荐阅读,也在实际使用后进一步收敛。板块或项目已经拥有可拖动的目录顺序,如果推荐只是为了让重要内容靠前,它会与目录排序重复;而在每个具体页面都重复一组相同链接,也会增加维护成本。

因此,SectionProject、SectionArticle 和 ProjectArticle 最终只负责收录与目录顺序。站点级的精选集中到首页,由 HomepageFeature 在文章和项目之间建立统一序列。精选为空时首页不输出空板块;存在内容时,只显示标题,并按拖动后的顺序逐行排列。

项目在板块中的位置属于 SectionProject,文章在板块或项目中的位置分别属于 SectionArticle 和 ProjectArticle。文章可以在甲项目排第二,在乙项目排第五,而 Article 本身无需保存一个无法兼顾所有场景的“全局编号”。首页精选又是另一条关系,因此调整首页展示不会改变任何目录,也不会影响项目内部的阅读顺序。

前台在此基础上允许读者用一个按钮循环切换“默认”“按时间倒序”和“按时间顺序”。默认顺序就是后台拖动得到的人工编排;时间顺序则临时按日期查询,不回写关系表,也不改变默认位置。文章直接使用发布日期,项目另设结构化的排序日期,因为“2026.9~”之类用于展示的周期文字并不适合可靠比较。切换时只替换当前目录并同步更新地址栏,排序方式仍会随返回链接和“下一篇”继续传递,既不会重载整页,也不会在阅读途中悄悄回到另一套顺序。

这层关系带来的不只是功能扩展。它让数据的含义更准确:目录负责收录与内部排序,首页负责全站精选,文章只负责保存自身事实。

Django 在这套结构中负责什么

Django 目前承担的是本地元数据编辑、关系管理、内容校验、Markdown 解析、数据库查询、路由、可见性过滤、模板渲染和错误处理。它暂时不承担公网写作、投稿、评论、用户注册或复杂媒体管理,因为这些能力并不是当前个人网站的必要条件。

这种取舍不是认为后台编辑永远没有价值,而是让架构与实际规模一致。现在只有一名维护者,完整重建的内容数量也很少,简单、可检查和可恢复比局部在线编辑更重要。如果以后出现多人协作、大量内容、站内搜索或频繁在线发布,再把同步改成增量更新、增加后台工作流,会比现在提前建设一套 CMS 更合适。

设计与实现如何分工

这个项目采用纯 Vibe Coding,我没有手写后端代码。Django 模型、迁移、同步程序、视图、模板连接和自动化测试都由 AI 完成。

但后端架构并不是由 AI 独立决定的。我负责提出网站要管理哪些对象、不断检查字段究竟属于谁,并在推荐阅读、返回路径和多板块收录等具体问题出现后,决定把文章本体与展示关系彻底分开。AI 负责把这些判断转成可以运行、迁移和测试的代码,再由我根据页面结果继续验收。

因此,这次重构对我来说最有价值的并不是多建了几张表,而是确认了一个以后仍可沿用的边界:Markdown 负责正文,本地编辑后台负责元数据,关系表负责内容如何出现,发布快照负责恢复与部署,模板负责页面如何呈现。 当一篇文章需要进入更多板块时,网站增加的是关系,不再制造另一篇文章。