Designing a WordPress-like Django blog
A Django design for custom blog and CMS development, covering posts, pages, taxonomy, media, comments, model constraints, publication rules, and Django admin.
Design approach
This design covers a Django blog/CMS with posts, pages, categories, and tags. It also includes comments, media, menus, revisions, drafts, scheduled publishing, and visibility control.
Shared abstract models hold publication and soft-deletion rules. A Term model handles taxonomy. Django admin is the first editing interface; a custom frontend and API can follow.
What the design covers
This is a design proposal, not finished code or a runtime test. It defines model responsibilities, constraints, URLs, and implementation order. The source specification is available in Japanese and English.
Initial scope
The first version covers everyday blog operations: posts, pages, taxonomy, comment moderation, media, navigation, drafts, scheduling, and revisions.
Full multisite parity, plugin and theme compatibility, Gutenberg parity, and a WordPress-equivalent WYSIWYG editor are outside the initial scope. APIs, richer editors, search, redirects, audit logs, and multilingual support remain possible extensions.
App and model boundaries
Overall architecture
Common rules
Separate content, taxonomy, media, comments, navigation, SEO, and shared infrastructure into Django apps. Use slugs in public URLs rather than numeric IDs.
Centralize publication rules in common fields. PostgreSQL is the target database, with JSONField, full-text search, and indexes included in the design.
App structure
project/
├── config/
│ ├── settings/
│ │ ├── base.py
│ │ ├── dev.py
│ │ └── prod.py
│ ├── urls.py
│ ├── asgi.py
│ └── wsgi.py
├── apps/
│ ├── accounts/
│ ├── core/
│ ├── content/
│ ├── taxonomy/
│ ├── media/
│ ├── comments/
│ ├── navigation/
│ └── seo/
├── templates/
├── static/
└── locale/
Content, menus, media, and OGP have separate apps. They can share one editing interface without sharing one implementation responsibility.
Naming and design rules
Public models use publication state and time. Deletion is soft through deleted_at. Set the primary image explicitly through featured_media rather than extracting it from the body.
WordPress concepts and Django models
| WordPress concept | Django-side mapping |
|---|---|
| Post / Page | content.Post / content.Page |
| Category / Tag | taxonomy.Term or Category / Tag |
| Post Meta | content.PostMeta |
| Media | media.MediaAsset |
| Comment | comments.Comment |
| Menu | navigation.Menu / MenuItem |
| Revision | content.Revision |
| Site Options | core.SiteSetting |
The preferred taxonomy model is Term, with kind distinguishing categories, tags, and later taxonomies. Keep Post and Page separate initially to simplify admin behavior and queries.
Model fields and constraints
Model design
Shared abstract models
Use four abstract models:
TimeStampedModel:created_at,updated_atSoftDeleteModel:deleted_at,is_deletedPublishableModel:status,published_at,visibility,is_publicSluggedModel:slug,slug_lock,get_absolute_url
Posts, pages, and later content types reuse these fields. A shared publication rule keeps lists, detail pages, scheduled publishing, and admin views consistent.
core
Define Site even for one site. Its fields are name, domain, locale, timezone, default_og_image, and is_active, with unique(domain). A site relation groups content, settings, media, and taxonomy.
SiteSetting stores site, key, value, and is_public. Use JSONField for value so settings need not be plain strings. Its constraint is unique(site, key).
accounts
Use Django’s standard User when it is sufficient. Add display_name or avatar_media only if needed. The initial roles are Editor and Admin.
taxonomy
Term has site, name, slug, description, kind, and parent. Categories use parent for hierarchy; tags remain flat. Its constraint is unique(site, kind, slug).
Separate Category and Tag models are an alternative, but each later taxonomy would need another model. A shared Term can also support series, author groupings, and regions.
media
MediaAsset has file, original_filename, mime_type, size, width, height, alt_text, caption, credit, sha256, and created_by. The sha256 hash is intended for deduplication and later storage migration.
Keep alt text and captions with the asset. Put thumbnails or other derived images in a separate rendition model, such as MediaRendition, if needed.
content
The content layer has Post, Page, PostMeta, and Revision.
Post has site, author, title, slug, excerpt, body, body_format, featured_media, status, published_at, visibility, password, allow_comments, comment_count_cache, and seo. Connect taxonomy through M2M relations to Term. Add unique(site, slug) and an index for public lists.
Page has a similar structure but normally has no categories or tags. Keeping Post and Page separate initially simplifies admin screens, queries, and templates.
PostMeta stores extra fields through post, key, and value, using JSONField for the value. Multiple values per key are possible for WordPress compatibility, but this design favors unique(post, key).
Revision has content_type, post/page, title, slug, excerpt, body, body_format, created_by, and created_at. Record a diff or snapshot on save and allow restoration through admin.
comments
Comment has site, post, parent, author_user, author_name, author_email, author_url, body, status, ip_hash, user_agent, created_at, updated_at, and deleted_at. Only approved comments are public; the other moderation states are pending, spam, and trash. Store ip_hash rather than the IP address itself.
Use parent for replies. Limit thread depth in display logic rather than the model.
navigation
Menu has site, name, slug, and location. MenuItem has menu, parent, label, url, post, page, sort_order, and is_enabled.
An item links to either a raw URL or a Post/Page, never both. For linked content, use get_absolute_url() so the menu can follow URL changes.
SEO
Keep SEOEntry separate from content and link it to post or page. Its fields are meta_title, meta_description, canonical_url, og_title, og_description, og_image, and robots. This leaves room to extend SEO metadata without enlarging the content model.
Public URLs
- Blog index:
/blog/ - Post detail:
/blog/<slug>/ - Monthly archive:
/blog/YYYY/MM/ - Page:
/<slug>/or/pages/<slug>/ - Category:
/category/<slug>/ - Tag:
/tag/<slug>/
Root-level pages are intended for corporate information or landing pages.
Publication states and rules
The five workflow states are:
draftpendingprivatescheduledpublished
The public visibility condition is:
status == published
AND published_at <= now
AND visibility == public
AND deleted_at IS NULL
Use the same rule in admin, public lists, querysets, managers, templates, feeds, search, and APIs. Treat scheduled together with a future publication time.
Django admin requirements
Show status, published_at, author, and updated_at in Post/Page lists. The admin requirements also include:
- list filters for status, author, and publication time
- automatic slug generation plus
slug_lock - autocomplete for taxonomy relations
- media preview and alt-text editing
- comment moderation
- revision browsing and restore
These editing functions come before custom public templates or APIs because Django admin is the first operational interface.
Search
A minimal search can use icontains on title/excerpt/body. The recommended option is PostgreSQL full-text search with SearchVector and GIN indexes. Add filters for tags, categories, author, and date range.
Implementation order
Site,User,Post,Page- Taxonomy (
Term) and M2M relations MediaAsset- Comments
- Navigation
- Revisions
- SEO
Build post and page publication first, then add organization, media, moderation, menus, history, and SEO.
Later extensions
The extension candidates are audit logs, redirects, RSS/Atom feeds, sitemap.xml, PostgreSQL full-text search, and multilingual support. Consider the Site locale, slug uniqueness scope, publication auditing, and room for a Redirect model when defining the initial models.
Moving to implementation
Start with core, content, and taxonomy. Define their models, constraints, managers, and public queries before extending the other apps. Then turn the specification into Django models, migration dependencies, admin classes, public querysets, URL patterns, and starter templates.
Keep Post and Page separate initially, use PostMeta for additional fields, and keep SEO metadata independent. These choices limit the first implementation while leaving room for extension.
