Django生态下CMS平台:wagtail、Django-CMS

概述

在Python生态语境下,虽然FastAPI大有一统天下的态势,如FastAPI生态:最佳实践、FastAPI-MCP、FastApiAdmin、Aegis Stack;但传统Web框架,如Django、Flask还远远没有颓败之势。基于Django的衍生生态,不要太多,GitHub上随随便便都能找出来几十个。

本文聚焦于CMS领域;提到CMS,都不需要谈及上下文,几乎等价于内容管理系统,Content Management System。在开发者领域,往CMS里塞入文件管理内容,显得很不专业。

wagtail

官网,由英国数字机构Torchbox开发并维护、基于Django框架的开源(GitHub,20.3K Star,4.5K Fork)CMS,提供直观的内容编辑界面,保持Django的灵活性和可扩展性,让开发者能够构建高度定制化的网站。

特性

  • 流式字段(StreamField):灵活的内容编辑方式,支持多种内容块组合
  • 强大的图片管理:内置图片库,支持裁剪、滤镜和焦点选择
  • 直观的编辑界面:现代化管理后台,用户体验优秀
  • 多站点支持:单个Wagtail实例可管理多个网站
  • 版本控制:内容修订历史和草稿功能
  • 搜索功能:集成Elasticsearch,提供强大的搜索能力
  • 国际化支持:内置多语言内容管理
  • API支持:提供RESTful API和GraphQL接口

实战

本地部署

bash 复制代码
# 虚拟环境
python -m venv wagtail_env
source wagtail_env/bin/activate
# Windows
wagtail_env\Scripts\activate
# 安装Wagtail
pip install wagtail
# 创建新项目
wagtail start mysite
cd mysite
# 安装依赖并创建数据库
pip install -r requirements.txt
python manage.py migrate
# 创建超级用户
python manage.py createsuperuser
# 启动开发服务器
python manage.py runserver

浏览器打开http://127.0.0.1:8000/admin

页面是核心概念,每个页面都是一个Django模型。通过继承Page类,可创建自定义的页面类型,定义页面的字段和行为。

py 复制代码
from django.db import models
from wagtail.models import Page
from wagtail.fields import RichTextField
from wagtail.admin.panels import FieldPanel

class BlogPage(Page):
	date = models.DateField("发布日期")
	intro = models.CharField(max_length=250, verbose_name="简介")
	body = RichTextField(blank=True, verbose_name="正文")
	
	content_panels = Page.content_panels + [
	    FieldPanel('date'),
	    FieldPanel('intro'),
	    FieldPanel('body'),
	]
	
	class Meta:
	    verbose_name = "博客文章"

StreamField允许编辑者自由组合不同类型的内容块,如文本、图片、视频、引用等。特别适合构建营销页面、产品介绍等需要丰富内容展示的场景,无需编写代码就能创建精美的页面。

py 复制代码
from wagtail.fields import StreamField
from wagtail import blocks
from wagtail.images.blocks import ImageChooserBlock

class FlexiblePage(Page):
	body = StreamField([
		('heading', blocks.CharBlock(classname="title")),
		('paragraph', blocks.RichTextBlock()),
		('image', ImageChooserBlock()),
		('quote', blocks.BlockQuoteBlock()),
	], use_json_field=True)
	
	content_panels = Page.content_panels + [
		FieldPanel('body'),
	]

图片管理系统,支持图片上传、裁剪、滤镜应用等功能。在模板中,可使用图片标签生成不同尺寸的图片,自动优化加载性能。图片处理是响应式的,能根据不同设备自动调整图片大小,提升用户体验。

py 复制代码
# 在模型中使用图片字段
from wagtail.images.models import Image

class ArticlePage(Page):
	header_image = models.ForeignKey(
	'wagtailimages.Image',
	null=True,
	blank=True,
	on_delete=models.SET_NULL,
	related_name='+'
	)
	
	content_panels = Page.content_panels + [
		FieldPanel('header_image'),
	]

在模板中使用:

复制代码
{% load wagtailimages_tags %}
{% image page.header_image fill-800x400 %}

开发者创建自定义的StreamField块,实现特定的业务需求。通过继承StructBlock,可以组合多个字段创建复杂的内容结构,自定义块让内容管理更加结构化,并保持编辑界面的友好性。

py 复制代码
from wagtail import blocks

class PersonBlock(blocks.StructBlock):
	name = blocks.CharBlock(label="姓名")
	photo = ImageChooserBlock(label="照片")
	bio = blocks.TextBlock(label="简介")
	
	class Meta:
		template = 'blocks/person_block.html'
		icon = 'user'
		label = '团队成员'

class TeamPage(Page):
	team_members = StreamField([
		('person', PersonBlock()),
	], use_json_field=True)
	
	content_panels = Page.content_panels + [
		FieldPanel('team_members'),
	]

内置搜索功能,支持全文搜索和字段过滤。通过配置search_fields,可指定哪些字段参与搜索。支持多种搜索后端,包括数据库搜索和ES。

py 复制代码
from wagtail.search import index

class BlogPage(Page):
	date = models.DateField("发布日期")
	intro = models.CharField(max_length=250)
	body = RichTextField(blank=True)
	
	search_fields = Page.search_fields + [
		index.SearchField('intro'),
		index.SearchField('body'),
		index.FilterField('date'),
	]
# 在视图中使用搜索
def search(request):
	search_query = request.GET.get('query', None)
	if search_query:
		search_results = Page.objects.live().search(search_query)
	else:
		search_results = Page.objects.none()
	
	return render(request, 'search_results.html', {
		'search_results': search_results,
	})

支持多语言网站的构建,可为同一页面创建不同语言版本。通过wagtail-localize插件,可实现内容的翻译管理和同步,为不同地区的用户提供本地化体验,包括页面内容、菜单、表单等。

py 复制代码
# settings.py中配置
WAGTAIL_I18N_ENABLED = True
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
	('zh-hans', '简体中文'),
	('en', 'English'),
]

# 在模型中使用
from wagtail.models import TranslatableMixin

class MultilingualPage(TranslatableMixin, Page):
	body = RichTextField()

class Meta:
	unique_together = [
		('translation_key', 'locale'),
	]

Django-CMS

官网,基于Django的开源(GitHub,10.6K Star,3.2K Fork)CMS,把页面树、placeholder、plugin、前台编辑、多语言、版本和工作流这些能力拆成清晰层次,适合长期迭代、多人协作、和现有Django业务系统一起演进的内容平台。官方文档

三个核心环节:页面树、内容块、编辑入口。

特点

  • 适合内容复杂度高的站点,核心卖点:多语言、多站点、页面树、SEO友好URL、前台编辑、插件化内容编排。
  • 不是单体框架,能和现有Django项目渐进式融合。不用把业务系统整个推倒重来,而是可以先把页面管理、内容区块、编辑入口接进来,再按需要叠加apphook、版本管理、工作流、headless等扩展。
  • 它把编辑体验和开发扩展拆成了两层,团队协作更顺:编辑侧要的是拖得动、改得快、预览顺;开发侧要的是结构稳定、扩展清楚、不会被一堆魔法模板反噬。placeholder/plugin机制正好卡在这个交界面上。

适用场景

  • 已有Django业务系统,希望把官网、专题页、活动页、品牌页面也纳入同一套工程体系。
  • 内容结构复杂、栏目层级明显、需要运营同学频繁改版的站点。
  • 需要多语言、多站点、编辑预览、版本管理这类能力,且未来还会继续扩展。
  • 想要开发和编辑分层协作,由开发定义组件边界,内容团队负责组装页面。

不适用场景

  • 只是做一个非常轻的静态官网,内容几乎不更新,用更轻的静态方案往往更省。
  • 团队并不打算维护Django工程,只想要一个开箱即用的SaaS版CMS。
  • 对依赖数量非常敏感,不愿接受CMS、插件、filer、versioning这一整套生态。
  • 确定要走纯headless,且前端团队并不需要Django模板、placeholder、前台编辑这套能力。

实战

安装:pip install django-cms

官方提供脚手架:

create_page()会同时把页面节点、SEO路径、导航信息这些基础骨架建起来,适合先验证栏目层级和URL设计。

py 复制代码
from cms.api import create_page
from cms.models import Page
from django.contrib.auth import get_user_model
from pathlib import Path
import django
import os, sys

PROJECT_ROOT = Path.cwd() / "demo_project"
sys.path.insert(0, str(PROJECT_ROOT))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cmsdemo.settings")

django.setup()
User = get_user_model()
user, _ = User.objects.get_or_create(
    username="mumu",
    defaults={"email": "mumu@example.com"},
)
user.is_staff = True
user.is_superuser = True
user.save()

Page.objects.filter(
    urls__path__in=["release-hub", "release-hub/weekly-digest"]
).delete()

root = create_page(
	"Release Hub",
	"base.html",
	"en",
	created_by=user,
	in_navigation=True,
	slug="release-hub",
	reverse_id="release-hub-demo",
)
child = create_page(
	"Weekly Digest",
	"base.html",
	"en",
	created_by=user,
	parent=root,
	in_navigation=False,
	slug="weekly-digest",
	reverse_id="weekly-digest-demo",
)

for page in [root, child]:
	content = page.get_admin_content("en")
	print(f"title       : {content.title}")
	print(f"path        : /en/{page.get_path('en')}/")
	print(f"reverse id  : {page.reverse_id}")
	print(f"in nav      : {content.in_navigation}")
	print(f"children    : {page.get_child_pages().count()}")
	print("-" * 36)

placeholder/plugin体系组装可编辑内容区

py 复制代码
from pathlib import Path
import django
import os, sys
from django.contrib.auth import get_user_model
from django.template import Context
from django.test import RequestFactory
from cms.api import add_plugin, create_page
from cms.models import Page
from cms.plugin_rendering import ContentRenderer
from cms.toolbar.toolbar import CMSToolbar

User = get_user_model()
user = User.objects.get(username="mumu")

page = Page.objects.filter(reverse_id="release-hub-demo").first()
if page is None:
	page = create_page(
		"Release Hub",
		"base.html",
		"en",
		created_by=user,
		in_navigation=True,
		slug="release-hub",
		reverse_id="release-hub-demo",
	)

content = page.get_admin_content("en")
placeholder = content.placeholders.first()
placeholder.cmsplugin_set.all().delete()

# 添加插件
add_plugin(placeholder, "TextPlugin", "en", body="<p>Hello editors</p>")
add_plugin(
	placeholder,
	"LinkPlugin",
	"en",
	name="Stable docs",
	link="https://docs.django-cms.org/en/stable/",
)
add_plugin(
	placeholder,
	"TextPlugin",
	"en",
	body="<p>Ship pages without leaving Django.</p>",
)

request = RequestFactory().get("/en/release-hub/")
request.user = user
request.session = {}
request.toolbar = CMSToolbar(request)
renderer = ContentRenderer(request)
html = renderer.render_placeholder(
	placeholder,
	Context({"request": request}),
	language="en",
	page=page,
)

print("slot:", placeholder.slot)
print("plugin count:", placeholder.get_plugins("en").count())
for plugin in placeholder.get_plugins("en"):
	instance, _ = plugin.get_plugin_instance()
	extra = getattr(instance, "body", "") or getattr(instance, "name", "")
	print(f"{plugin.position:>2} {plugin.plugin_type:<10} {extra}")
print("html size:", len(html))
print("html head:", html[:140].replace("\\n", " "))

多语言、编辑入口、版本状态等运营链路里的关键节点都留接口。

同一个页面补德语内容版本,并直接取出对应draft状态和edit/preview URL。

py 复制代码
import os, sys
from pathlib import Path
import django
from django.contrib.auth import get_user_model
from django.test import override_settings
from cms.api import create_page, create_page_content
from cms.models import Page
from cms.models.contentmodels import EmptyPageContent
from cms.toolbar.utils import get_object_edit_url, get_object_preview_url
from djangocms_versioning.models import Version

User = get_user_model()
user, _ = User.objects.get_or_create(
    username="wong",
    defaults={"email": "wong@johnny.com"},
)
user.is_staff = True
user.is_superuser = True
user.save()

page = Page.objects.filter(reverse_id="release-hub-demo").first()
if page is None:
    page = create_page(
        "Release Hub",
        "base.html",
        "en",
        created_by=user,
        in_navigation=True,
        slug="release-hub",
        reverse_id="release-hub-demo",
    )

langs = [("en", "English"), ("de", "German")]
cms_langs = {
    1: [
        {
            "code": "en",
            "name": "English",
            "public": True,
            "redirect_on_fallback": True,
            "hide_untranslated": False,
            "fallbacks": ["de"],
        },
        {
            "code": "de",
            "name": "German",
            "public": True,
            "redirect_on_fallback": True,
            "hide_untranslated": False,
            "fallbacks": ["en"],
        },
    ],
    "default": {
        "public": True,
        "redirect_on_fallback": True,
        "hide_untranslated": False,
    },
}

with override_settings(LANGUAGES=langs, CMS_LANGUAGES=cms_langs):
    content_de = page.get_admin_content("de")
    if isinstance(content_de, EmptyPageContent):
        create_page_content(
            "de",
            "Release Center DE",
            page,
            slug="release-zentrum",
            created_by=user,
            menu_title="Freigaben",
        )
        page = Page.objects.get(pk=page.pk)
        page._clear_internal_cache()

    for lang in ["en", "de"]:
        content = page.get_admin_content(lang)
        version = Version.objects.get_for_content(content)
        print(f"{lang:<11} title={content.title}")
        print(f"version: v{version.number} {version.state}")
        print(f"path: /{lang}/{page.get_path(lang)}/")
        print(f"edit url: {get_object_edit_url(content, language=lang)}")
        print(f"preview url: {get_object_preview_url(content, language=lang)}")

最佳实践

  1. 先明确要不要引入djangocms-versioningdjangocms-aliasdjangocms-frontend这些官方扩展,不要把核心CMS与编辑工作流混在一起评估。
  2. 把页面模板、placeholder命名、可用plugin列表先设计清楚,避免后面运营把内容拼装能力用成隐形模板系统。
  3. 提前验证多语言、权限、预览链路和URL策略,尤其是老项目增量接入时的路径冲突与导航边界。
相关推荐
kybs199117 小时前
springboot租车系统--附源码68701
java·hadoop·spring boot·python·django·asp.net·php
wxin_VXbishe18 小时前
springboot新能源车充电站管理系统小程序-计算机毕业设计源码29213
java·c++·spring boot·python·spring·django·php
万事大吉CC18 小时前
【1】Django 基础:MTV 架构与核心组件
数据库·架构·django
万事大吉CC21 小时前
【3】深入剖析 Django 之 MTV:路径引用与资源加载机制
数据库·django·sqlite
YJlio1 天前
2026年5月5日60秒读懂世界:五一档票房、油价调整、汤姆斯杯夺冠与全球风险观察
数据分析·django·飞书·仪表盘·多维表格·图表联动
FYKJ_20101 天前
springboot校园兼职平台--附源码02041
java·javascript·spring boot·python·eclipse·django·php
心静财富之门2 天前
Django 超详细初级教程(零基础可学)
python·django
ZC跨境爬虫2 天前
Python Django开发者转向微信小程序:从架构理解到第一行代码的完整准备指南
开发语言·python·ui·微信小程序·django
绘梨衣5472 天前
django-elasticsearch-dsl-drf 搜索服务搭建教学文档
python·elasticsearch·django