[控场AI]
教程攻略· 10 分钟阅读· 5,400 字

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

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默认欢迎页面

理解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,实现漂亮的代码展示效果。

集成Prism语法高亮

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
#Wagtail#Django CMS#Wagtail教程#Python博客系统#StreamField#开源CMS#Django博客搭建
分享:

相关推荐