Wagtail入门教程:用Django从零构建可定制CMS博客系统

Wagtail是基于Django的高度可定制开源CMS,适合从零构建博客系统。
Wagtail是一个基于Django的开源CMS,采用「代码即配置」的设计理念,要求开发者用Python代码定义页面模型和内容结构,提供极致的可定制性。文章介绍了从项目初始化、定义页面模型与模板、构建博客系统,到StreamField灵活内容编排、Snippets管理非页面数据实体等核心功能,展示了Wagtail构建完整博客的全流程。
什么是Wagtail?
Wagtail是一个基于Django的开源内容管理系统(CMS),专为博客和内容发布场景设计。如果你熟悉WordPress,可以把Wagtail理解为它的「反面」——WordPress开箱即用、插件丰富,而Wagtail更像是「Arch Linux式」的CMS:你需要从零定义一切,包括博客文章的数据结构、页面模板、甚至首页长什么样。
这种设计哲学的好处显而易见:极致的可定制性。你不会被框架的预设限制,同时又能享受Django「自带电池」的便利——ORM、Admin面板、迁移系统等一应俱全。对于想要完全掌控自己博客系统的开发者来说,Wagtail是一个非常值得关注的选择。
Django生态背景:Django是Python生态中最成熟的Web框架之一,诞生于2005年,其设计哲学正是「自带电池」(batteries included)——ORM、Admin、表单系统、认证框架等核心功能开箱即用。Wagtail构建在Django之上,天然继承了Django的所有优势:强大的ORM支持多种数据库后端(PostgreSQL、MySQL、SQLite),内置的迁移系统让数据库结构变更有据可查,而Django Admin则被Wagtail改造成了专为内容编辑设计的现代化界面。这意味着你在使用Wagtail时,实际上同时在使用一个经过十余年生产环境验证的Web框架,其稳定性和社区支持远超大多数小众CMS。
项目初始化与基本结构
安装Wagtail并创建项目
Wagtail的安装非常简单,可以通过pip直接安装,也可以使用更现代的包管理工具uv:
# 传统方式
pip install wagtail
wagtail start mysite mysite
# 使用uv(推荐)
uv init
uv add wagtail
uv run wagtail start mysite mysite
创建完成后,项目结构与标准Django项目非常相似:有manage.py、settings配置目录、以及一个默认的home应用。熟悉Django的开发者会感到非常亲切。
初始化数据库和创建管理员用户的流程也完全一致:
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver

理解Wagtail「从零开始」的设计理念
启动项目后访问Admin面板,你会发现一个有趣的现象:虽然有一个Home页面,但编辑时除了标题之外什么都不能修改。这正是Wagtail「从零开始」理念的体现——你还没有定义页面应该包含哪些内容字段。
这与WordPress的思路截然不同。WordPress预置了文章(Post)、页面(Page)等内容类型,并提供了古腾堡编辑器作为通用内容编辑界面。Wagtail则要求开发者先用Python代码声明「这个页面有哪些字段」,再由框架自动生成对应的编辑界面。这种「代码即配置」的方式让内容结构完全版本化,可以通过Git追踪每一次字段变更。
定义页面模型与模板
为首页添加内容字段
Wagtail的核心工作流是:先在models.py中定义数据模型,再在模板中渲染内容。以首页为例,添加一个富文本字段:
from wagtail.fields import RichTextField
from wagtail.models import Page
class HomePage(Page):
body = RichTextField(blank=True)
content_panels = Page.content_panels + [
FieldPanel('body'),
]
对应的模板中使用{{ page.body|richtext }}来渲染富文本内容,同时需要在模板顶部加载{% load wagtailcore_tags %}。每次修改模型后,都需要执行makemigrations和migrate来同步数据库。
richtext模板过滤器不仅仅是输出HTML字符串,它还会处理Wagtail内部链接的解析——当编辑在富文本中插入指向另一个Wagtail页面的链接时,存储的是页面ID而非URL,richtext过滤器会在渲染时将其转换为实际URL,这样即使页面路径发生变化,内部链接也不会失效。
用Wagtail构建博客系统
博客系统需要两个核心模型:博客索引页(BlogIndexPage)和博客文章页(BlogPage)。它们之间是父子关系——索引页列出所有文章,文章页展示具体内容。
class BlogPage(Page):
intro = models.CharField(max_length=250)
date = models.DateField("Post date")
body = RichTextField(blank=True)
content_panels = Page.content_panels + [
FieldPanel('date'),
FieldPanel('intro'),
FieldPanel('body'),
]

在索引页模板中,通过page.get_children遍历子页面来列出所有文章。一个实用技巧是使用{% with post.specific as post %}语句,避免每次都写post.specific.intro这样冗长的访问方式。
specific属性的技术原理:Wagtail的页面树在数据库中统一存储为Page基类记录,get_children()返回的是通用Page对象,不包含子类的自定义字段。specific属性会触发一次额外的数据库查询,获取该页面对应子类(如BlogPage)的完整实例。对于列表页面这类需要批量获取子页面的场景,可以使用specific_deferred或select_related等方式优化查询性能,避免N+1查询问题。
进阶功能:过滤、代码块与Snippets
过滤已发布文章
默认情况下,索引页会显示所有子页面,包括草稿。更合理的做法是重写get_context方法,只返回已发布的文章:
class BlogIndexPage(Page):
def get_context(self, request):
context = super().get_context(request)
blogpages = self.get_children().live().order_by('-first_published_at')
context['blog_pages'] = blogpages
return context
这样草稿状态的文章不会出现在前台,且文章按发布时间倒序排列。
.live()是Wagtail QuerySet的扩展方法,它过滤出live=True的页面记录。Wagtail的发布系统维护了一套独立于Django模型的状态机:页面可以处于草稿(Draft)、待审核(In Review)、已发布(Live)或已下线(Expired)等状态,并保留完整的修订历史。这意味着你可以随时回滚到任意历史版本,这是WordPress等传统CMS难以原生支持的特性。

用StreamField实现灵活的内容编排
Wagtail最强大的特性之一是StreamField,它允许你定义灵活的内容块组合。比如,你想在博客中交替使用富文本和代码块:
from wagtail.fields import StreamField
from wagtail import blocks
class CodeBlock(blocks.StructBlock):
language = blocks.ChoiceBlock(choices=[
('python', 'Python'), ('javascript', 'JavaScript'),
('bash', 'Bash'), ('html', 'HTML'), ('css', 'CSS'),
], default='python')
code = blocks.TextBlock()
class BlogPage(Page):
body = StreamField([
('paragraph', blocks.RichTextBlock()),
('code', CodeBlock()),
], use_json_field=True, blank=True)
StreamField的技术原理:StreamField的底层实现依赖JSON字段存储结构化内容,每个内容块被序列化为带类型标识的JSON对象数组,例如
[{"type": "paragraph", "value": "<p>...</p>"}, {"type": "code", "value": {"language": "python", "code": "print('hello')"}}]。这种设计解决了传统CMS「一个大富文本框」的痛点——内容与展示逻辑解耦,同时保留了编辑时的所见即所得体验。use_json_field=True参数是Wagtail 3.0之后的推荐做法,使用数据库原生JSON字段替代旧版的文本序列化方案,在PostgreSQL上可以利用GIN索引获得更好的查询性能,并支持JSONPath查询语法。
配合Prism.js等语法高亮库,可以在全局模板中引入相关CSS和JavaScript,实现漂亮的代码展示效果。

Snippets:管理无页面的数据实体
Snippets用于定义那些需要存在于数据库中、但不需要独立页面的实体,比如作者信息。通过@register_snippet装饰器注册,然后用ParentalManyToManyField与博客文章建立多对多关系:
@register_snippet
class Author(models.Model):
name = models.CharField(max_length=255)
author_image = models.ForeignKey('wagtailimages.Image', ...)
Snippets与ParentalManyToManyField的技术细节:Snippets本质上是普通的Django模型,通过
@register_snippet装饰器告知Wagtail将其纳入Admin管理界面。与Page模型不同,Snippets不参与Wagtail的页面树结构,没有URL路由,也不具备发布/草稿状态管理(尽管Wagtail 4.x开始为Snippets引入了可选的修订和发布功能)。ParentalManyToManyField是Wagtail对Django标准ManyToManyField的扩展,核心区别在于它支持Wagtail的修订(Revision)系统——当你保存页面草稿时,关联的多对多关系也会被正确记录在修订历史中,而不是立即写入数据库的中间表,从而保证草稿内容的完整性和可回滚性。
在Admin面板中,Snippets会出现在独立的管理区域,你可以创建作者并在编辑文章时通过复选框选择关联的作者。
官方演示模板:学习完整项目结构
Wagtail提供了一个官方演示模板,可以帮助你快速了解一个完整的Wagtail项目是什么样的:
wagtail
相关推荐
教程攻略Cursor+Codex双IDE协同:开源项目二开实战方法论
基于实战经验总结的开源项目二次开发完整方法论,详解Cursor+Codex双IDE协同工作流,涵盖二开七环节、MVP验证、AI读源码技巧,帮助开发者三天跑通项目、两周完成业务集成。
教程攻略Cursor多Agent实战:50分钟搭建Next.js全栈博客
使用Cursor IDE多Agent协作模式,50分钟内从零搭建全栈博客。涵盖Next.js、Clerk认证、Supabase数据库集成,详解4个AI Agent分阶段开发流程与关键避坑经验。
教程攻略从零搭建AI软件工厂:Cursor工程师的多Agent协作实战经验
Cursor工程师Eric分享AI软件工厂构建实战:从自动化六层级、护栏设计、并行Agent管理到规模化扩展,详解如何用多Agent协作实现7×24小时高效软件开发。