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.

  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 conceptDjango-side mapping
Post / Pagecontent.Post / content.Page
Category / Tagtaxonomy.Term or Category / Tag
Post Metacontent.PostMeta
Mediamedia.MediaAsset
Commentcomments.Comment
Menunavigation.Menu / MenuItem
Revisioncontent.Revision
Site Optionscore.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_at
  • SoftDeleteModel: deleted_at, is_deleted
  • PublishableModel: status, published_at, visibility, is_public
  • SluggedModel: 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.

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:

  • draft
  • pending
  • private
  • scheduled
  • published

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.

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

  1. Site, User, Post, Page
  2. Taxonomy (Term) and M2M relations
  3. MediaAsset
  4. Comments
  5. Navigation
  6. Revisions
  7. 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.