← 阅读目录:个人展示网站

PROJECT ARTICLE / 项目文章

从本地写作出发:我的个人网站后端设计

从本地 Markdown 写作出发,介绍文章同步、元数据与目录编排、阅读导航,以及通过发布快照和候选版本完成整站部署的后端设计。

我习惯在自己的电脑上写东西。文章放在本地文件夹里,想改哪一段就直接打开文件,写完以后再整理到网站中。我对网站维护的期待也差不多:先在电脑上把内容和页面改好,确认没有问题,再把这一版整体同步到服务器。

这个习惯不一定适合管理网站,尤其是以后有多人参与的时候。但对目前只有我一个维护者的个人网站来说,它很方便。我不太想为了更新几篇文章,就把写作过程搬进一个在线后台,再围绕这个后台重新建立自己的工作方式。

所以,这个网站的后端设计要先回答一个很实际的问题:怎样保留本地文件编辑、前后端代码修改的便利,同时让服务器上的文章、目录、排序和链接保持一致?

先说明一下分工。这个网站现在的后端设计几乎全部由 Astra 完成,包括数据结构、内容处理、导航规则和发布流程。我有一些 Django 基础,能够读懂模型、视图、模板之间的关系,也能对照代码理解它为什么这样处理,但这个基础没有在实际设计中帮上多大的忙。我主要做的是说明自己怎么写东西、希望页面怎么用,再根据实际效果提出修改。下面介绍的是 Astra 围绕这些需求做出的设计,以及我对这套设计的理解。

先让正文继续留在电脑里

保留本地写作习惯,最直接的办法就是使用 Markdown。文章仍然是普通文件,可以用自己顺手的编辑器打开。写作文件夹里分成“写作中”“已完成”和“历史归档”三个部分,真正进入网站同步范围的只有“已完成”。

平时我在“写作中”写文章,确认完成后把文件挪到“已完成”。如果还要修改已经交付的文章,也直接打开“已完成”里的文件,不再维护一份原稿、一份交稿副本和一份网站正文。网站内容目录里的文件由脚本生成,我不用再到那里改第二遍。

同步时,程序以整个“已完成”文件夹为准。新出现的文件自动登记成文章,同名文件覆盖网站上的正文,源目录里已经不存在的文件则从网站中删除。因此,把文章移回“写作中”,也意味着下次同步时从网站内容中撤下它。

删除不只是删掉一个 Markdown 文件。文章可能还在首页精选、某个板块或项目目录里,这些收录关系必须一起清除,其他文章的顺序则继续保留。如果别的正文里仍有指向被删文章的链接,校验会指出具体位置,等我处理好链接再完成同步,避免留下点不开的入口。

文件名在这里承担识别文章的作用。同名就继续更新原来的文章;改名则按删除旧文章、新增另一篇处理,不继承旧网址和收录关系。我接受这个规则,但也不想每改一次标题就换一次文件名。

所以,文章首行使用 # 标题。新写时可以和文件名一致,以后想改网页上的标题,只修改这一行。程序在同步时把它提取到标题字段,剩下的部分作为正文处理,页面上不会重复出现两个标题。没有填写首行标题时,就默认使用文件名。新增文章自动分配 article-数字 这样的网址标识,我也不用再给每篇文章想一个英文名字。

除了标题和正文,一篇文章还有摘要、发布日期、标签、创作方式和公开状态,也可能同时被多个项目收录。这些信息由本地 Django 编辑后台维护,保存在 SQLite 数据库里。目录顺序和首页精选也各自编排,不写进文章文件。

移入“已完成”只表示这篇文章可以同步到后台,不意味着立即公开。新文章登记后保持不公开,摘要为空、创作方式待填写,等我补全信息、决定放在哪里以后再打开公开状态。已经登记过的文章,同名同步只更新正文和标题,保留这些后台设置。

这部分通常由我手动操作。如果我懒得逐项填写,也可以在对话里让 AI 根据正文整理摘要、设置标签,或者收录到指定项目。它修改的仍是同一套后台数据,不需要另外生成一份交稿记录。创作分类和实际参与的模型要有依据,不能因为这次是 AI 帮忙填写,就把本人创作的文章改成 AI 创作。

本地数据库里的内容,怎样跟着文件一起发布

正文留在文件里,文章信息留在本地数据库里,接下来就有一个问题:只同步文件,服务器怎么知道我刚刚调整了目录,或者把某篇文章设成了公开?

直接把本地 SQLite 数据库上传覆盖,当然也是一种办法。但数据库里除了文章信息,还有编辑账号等运行数据。把整个数据库当作发布文件,会让内容更新和运行环境混在一起。

现在的做法,是从本地数据库导出一份 catalog.json。它记录文章和项目的信息、正文路径、公开状态、收录关系以及排序,作为这一版内容的发布快照。这个文件由程序生成,不需要我手工编辑。

这样,准备发布的内容就有了明确的组成:提取标题后的 Markdown 正文、需要公开的图片,以及描述内容组织方式的快照。它们和网站代码一起进入 Git 版本。服务器根据快照和正文恢复内容数据,保留自己的运行环境,不接收我电脑上的整份数据库。

整个过程可以理解为:

写作中 → 已完成目录中的文章
                ↓ 整批同步、提取标题、处理新增与删除
       网站正文副本 + 本地后台数据与预览
                ↓ 补全资料、编排目录、确认公开状态
       正文、公开资源 + catalog.json 发布快照
                ↓ 检查、Git 提交、服务器候选版本验证
          正式版本切换 → 读者页面

日常使用时,运行 sync-writing.sh 就会把“已完成”里的文章同步到本地后台,更新预览并生成快照。如果我随后又调整了摘要、标签或目录顺序,再运行一次同步,或者用 export_content 单独导出后台数据,就能把这些设置带进准备发布的版本。

文件保存后并不会立即上传,脚本也不是一直在后台监听文件夹。我可以先在电脑上修改、同步和查看效果,确认这一版没有问题,再提交 Git 并部署。网站中现有文章的删除,也是在这次正式部署后才对读者生效。

服务器恢复内容时,使用 sync_content --from-snapshot。这个命令会按快照替换内容信息,不能拿它当作普通的本地刷新,否则尚未导出的后台修改可能被旧快照覆盖。普通的 sync_content 则负责检查网站正文副本并重新编译,不承担从写作文件夹交稿的工作。

这样的流程确实比“保存即发布”多几步。但它符合我希望先在电脑上整理完整、再一起同步的习惯,也能让我在本地查看尚未公开的文章。

网站打开时,实际读取的是数据库

正文使用 Markdown,不意味着每来一个读者,网站都要重新打开全部文件,解析一遍目录和文章。

内容同步时,程序先检查正文和后台登记是否对应、图片是否存在、站内链接是否有效,再把 Markdown 转成页面需要的 HTML,保存到数据库。公开文章如果链接到了隐藏内容,也会在这个阶段被检查出来。校验失败时,不把半套结果写进数据库。

读者访问页面时,Django 根据网址查询文章、项目或栏目,判断是否允许访问,再把查到的数据交给模板。正文已经提前处理好,请求过程中不需要重新扫描 Markdown 文件。

所以数据库在两端承担的作用稍有区别。本地数据库还保存我正在编辑的文章资料和编排;服务器数据库提供已经发布的内容查询与渲染结果,可以用这一版正文和快照恢复。只保存 Markdown 并不能恢复整个网站的内容组织,快照也必须一起保存。

技术上,网站使用 Django、SQLite、服务端模板、普通 CSS 和少量原生 JavaScript。以目前的页面功能和维护方式,没有额外拆出一套前后端分离的 API。列表切换排序时,浏览器可以取得服务器生成的页面并替换目录区域;关闭 JavaScript,或者请求失败时,仍然可以通过普通链接完成页面跳转。

代码也围绕这些工作分开:模型保存内容与收录关系,查询模块决定展示哪些内容和如何排序,导航模块计算返回位置与下一篇,内容处理模块负责 Markdown 编译和校验,发布模块负责快照读写及数据库更新。模板则使用这些结果生成页面。

页面层级跟着内容走

这个网站有实践、观察、技术和创作四个首页入口。目前实践和技术采用“栏目—项目—文章”的结构,观察直接展示文章。创作入口暂时承担作品介绍,还没有接入一套已经开放的小说章节阅读系统。

实践和技术需要项目这一层,是因为单篇文章很难说清一件持续推进的事情。比如这个网站,项目页可以说明整体情况、当前状态、涉及的技术和各自的分工,下面再收录前端设计、后端架构和自动优化等文章。读者可以先了解项目,也可以直接进入某篇具体记录。

观察文章通常可以独立阅读,因此从栏目进入文章就够了,没有必要再套一个项目。

项目页上的介绍和补充信息可以单独维护,文章页则集中展示标题、日期、创作方式、正文以及阅读导航。项目“进行中”或“已结束”的业务状态,与页面是否公开分开保存:一个项目可以还在进行,但已经公开;一篇已经写完的文章,也可以暂时不公开。

文章本身只有一份,可以被多个项目或栏目收录。收录关系另外保存顺序,因此同一篇文章在不同目录里可以处于不同位置,也不会因为进入首页精选就多出一份正文。

目录提供三种顺序:人工编排的默认顺序、按时间倒序、按时间顺序。文章使用发布日期,项目使用单独的排序日期。项目卡片上显示的“2026.9 至今”之类文字只用于介绍,不拿来直接比较大小。同一天的文章在普通目录中再按稳定的网址标识确定先后,保证列表顺序明确。

首页精选和最近发布,各自回答一个问题

首页需要照顾两种访问方式。第一次来的读者,可能想知道这里有什么值得看;已经来过的人,更关心最近更新了什么。

精选由我手动选择和排序,既可以放文章,也可以放整个项目。它的顺序独立于项目目录,不受发布日期影响。一篇较早发表、但更适合介绍我的文章,可以一直留在这里;一个值得完整介绍的项目,也不必被压缩成某一篇文章才能推荐。

最近发布则自动生成。目前的规则是按上海时间,取包含当天在内的最近七个自然日中的公开文章,按发布日期从新到旧排列。同一天有多篇时,再按后台登记顺序从后往前排。这个登记顺序会进入快照,服务器重建数据库后也能保持一致。

因此,最近发布并不是固定取全站最新的几篇。超过这个时间范围的旧文会退出列表,未来日期的文章不会提前进入。如果这一周没有符合条件的文章,这个板块就不显示。

两个板块默认都先展示前三条,更多内容可以展开。某篇文章也可以同时出现在精选和最近发布中:前者说明我希望推荐它,后者说明它刚刚发布。两个入口虽然打开同一篇正文,接下来的阅读顺序却可能不同。

“下一篇”要看读者是从哪里进来的

这一点是我觉得后端里比较值得解释的地方。文章下面一个很普通的“下一篇”按钮,实际依赖的是读者正在浏览哪一份列表。

假设项目目录里有甲、乙、丙三篇文章,默认顺序是甲、乙、丙。从这个目录进入乙,下一篇自然是丙。但如果按时间倒序后,列表变成丙、乙、甲,那么从乙继续阅读时,下一篇应该是甲。

同一篇乙如果还在首页精选里,它后面推荐的可能又是丁。不能因为乙属于某个项目,就一律把读者带回项目顺序;也不能把“下一篇”简单理解为发布日期更晚的文章。

当前实现会在进入文章的链接中携带来源、排序方式和返回地址。后端据此重新取得对应的有序列表,找到当前文章,再取紧接着的下一项。目录显示和导航计算使用同一套查询规则,不另外维护一套“猜测下一篇”的逻辑。

具体来说,几种入口的行为是:

进入文章的入口 继续阅读的顺序
项目目录 该项目当前选定的文章顺序
直接展示文章的栏目 该栏目当前选定的文章顺序
首页精选 精选中的文章顺序,跳过项目卡片
最近发布 最近七个自然日内公开文章的排列顺序

从精选中的项目卡片进入项目,再打开文章,就已经进入项目的文章列表,后续按照项目顺序阅读。直接从精选文章进入,则继续读精选中的下一篇文章。到达当前列表最后一篇时,就不再提供下一篇链接,不会自动跳去另一份目录。

返回按钮也保留来时的目录和排序。比如从按时间倒序的项目页进入文章,读完返回时仍然是那个项目的倒序目录;如果项目本身来自首页,相关返回信息也可以继续保留。

但网址参数不能直接拿来当作可信结果。程序会检查来源是否成立、项目是否真的收录了这篇文章、返回地址是否对应有效且可访问的站内页面。无效信息会退回可用的默认入口。

如果读者直接打开别人分享的文章地址,没有携带来源,系统就无法知道他原来在读哪份列表。当前规则会优先寻找有效的所属项目,其次寻找直接收录文章的栏目,使用默认编排;多个候选按稳定标识选择。都找不到时,文章仍可独立展示,返回首页,也不强行推荐下一篇。

这里还有一个边界:最近发布会随日期变化,目录和精选也可能重新编排。链接保留的是入口和排序规则,并没有永久冻结读者当时看到的列表。这是目前个人网站采用的处理方式。

整体同步,也要保证是完整的一版

我希望整体同步到服务器,不代表把电脑上的网站目录不加区分地覆盖过去。正式发布只以应用目录为源,写作中的草稿、历史归档、私人记录和运维资料都不跟着上传;本地数据库和环境配置也不会覆盖服务器上的运行数据。

发布前,先确认正文已经同步、后台设置已经导出,再检查配置、数据库迁移和快照是否一致,运行应用测试与失败恢复测试,查看本地页面。之后把当前工作区的修改完整提交到 Git,让代码、正文和内容编排属于同一个可以追溯的版本。

得到部署指令后,程序先只读检查服务器状态和同步差异,再把完整应用同步到服务器上的候选目录。正式网站此时仍然使用原来的版本,候选版本利用现有数据库的副本完成测试、迁移、静态资源生成和内容恢复。

静态资源使用带内容哈希的文件名,正文里的图片地址也要对应这一版资源。因此,程序先生成静态资源清单,再编译导入正文,随后检查公开内容能否访问、隐藏内容和编辑后台是否仍然不可访问。候选版本没有通过这些检查,就不进入正式切换。

预检通过后,才短暂停止服务,再复制一次最新运行数据,完成候选版本的迁移和内容准备。旧应用目录完整保留为备份,候选目录成为当前版本,随后启动服务,检查应用响应、正式域名和后台访问限制。

如果切换或健康检查失败,就恢复旧的完整目录,让代码、数据库和静态资源一起回到原来的版本。只换回代码却留下已经改变的数据库,不能算完整的恢复;这也是整版发布需要同时考虑运行数据的原因。

编辑后台只在本地开发环境开放,正式网站不提供编辑入口。内容是否公开由本地确认后的发布数据决定,部署程序不会因为文章文件存在,就自动把它公开。网站上线后,我也会从实际栏目和项目入口查看文章,确认正文、资源与阅读导航都符合预期。

我想保留的,归根结底还是在电脑上写文章、整理文件、查看效果,再把确认好的版本同步出去的工作方式。为了让这个习惯能够稳定地运转,背后需要正文、文章信息、目录关系和发布快照各自承担一部分工作。Astra 完成了其中绝大部分设计和实现;我目前能做的是理解这些结构,确认它们符合自己的使用需求,再继续把网站用起来。

关于 AI 创作标识

完全由我创作

文章的观点和正文均由我完成。写作过程中,我可能使用 AI 查阅资料;完成后,也可能请它提出修改建议,检查不宜公开的信息或过度情绪化的表达。

AI 不直接撰写正文,所有修改都由我本人完成。

AI 辅助创作

文章的观点、判断和主要内容由我完成。部分段落会由 AI 根据我提供的材料和要求撰写,用于辅助论证,并由我审阅、核实后采用。

我不会让 AI 整理公开资料、提出观点,再围绕这些观点写成文章。

完全由 AI 整理

仅用于技术类文章。AI 根据我的实际项目记录整理过程、撰写正文,再由我核对事实、检查贡献归属,并作必要修改。

这类文章用于保存和分享实践过程,让我能把更多时间投入技术学习。正文由 AI 整理,不代表项目中的工作全部由 AI 完成;具体分工会在文章中说明。