Content Management System
Create a flexible CMS with Django featuring content types, user roles, workflow management, and a publishing system. Learn how platforms like Wagtail and Django CMS are architected under the hood.
75 min•By Priygop Team•Last updated: Feb 2026
CMS Architecture
A CMS in Django separates content structure (models) from presentation (templates). Key components include: a Page model hierarchy using django-mptt or django-treebeard for nested pages, a rich text editor integration (TinyMCE or CKEditor), versioning/drafts system, and role-based access control for authors, editors, and publishers.
CMS Design Decisions
- Use abstract base models for shared fields (title, slug, status, created_at, updated_at)
- Implement draft/published workflow with a status field and publish_date
- Use django-guardian for object-level permissions (author can only edit own posts)
- Add revision history with django-reversion for content rollback
- Implement SEO fields (meta_title, meta_description, og_image) on every page model
- Use Django signals to auto-generate slugs and invalidate cache on publish
CMS Models & Workflow
Example
# cms/models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils.text import slugify
class Page(models.Model):
STATUS_CHOICES = [
("draft", "Draft"),
("review", "In Review"),
("published", "Published"),
("archived", "Archived"),
]
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True, blank=True)
content = models.TextField()
excerpt = models.TextField(max_length=500, blank=True)
featured_image = models.ImageField(upload_to="pages/", blank=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="draft")
publish_date = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# SEO fields
meta_title = models.CharField(max_length=60, blank=True)
meta_description = models.CharField(max_length=160, blank=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
if not self.meta_title:
self.meta_title = self.title[:60]
super().save(*args, **kwargs)
def publish(self):
from django.utils import timezone
self.status = "published"
self.publish_date = timezone.now()
self.save()
def __str__(self):
return self.title
class Meta:
ordering = ["-publish_date"]
# cms/views.py
from django.views.generic import ListView, DetailView
class PageListView(ListView):
model = Page
queryset = Page.objects.filter(status="published")
template_name = "cms/page_list.html"
context_object_name = "pages"
paginate_by = 10
class PageDetailView(DetailView):
model = Page
template_name = "cms/page_detail.html"
context_object_name = "page"
def get_queryset(self):
if self.request.user.is_staff:
return Page.objects.all() # Staff can preview drafts
return Page.objects.filter(status="published")