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策略,尤其是老项目增量接入时的路径冲突与导航边界。
相关推荐
2601_962065258 小时前
从零创建一个 Django 项目
后端·python·django
千里码aicood9 小时前
Django 基于Django的电脑推荐与分析可视化系统设计与实现
python·django·电脑
汤姆yu10 小时前
基于Django的学习资料共享与智能推荐系统
后端·python·django·毕业设计·学习资料共享
Mr数据杨20 小时前
【Codex】用学生入学评估模块建立新生能力画像
android·java·javascript·django·codex·项目开发
Mr数据杨20 小时前
【Codex】用部门管理模块维护组织架构与管理层级
架构·django·codex·项目开发
Mr数据杨20 小时前
【Codex】用附件管理模块统一维护系统文件资源
django·codex·项目开发
IT毕设实战小研21 小时前
基于安卓的空气质量查询系统
android·大数据·vue.js·爬虫·python·django·课程设计
Mr数据杨1 天前
【Codex】用座位划分模块优化班级空间与课堂管理
django·codex·项目开发
Mr数据杨1 天前
【Codex】用系统配置工作台集中维护平台运行参数
django·codex·项目开发
Mr数据杨1 天前
【Codex】用学生生活日常模块记录校园生活过程
django·生活·codex·项目开发