WordPressに近いDjangoブログ基盤の設計
ブログ・CMSの受託開発を想定したDjangoの設計案です。投稿、固定ページ、分類、メディア、コメントのモデルと制約、公開条件、Django adminの要件を整理しました。
設計の方針
Djangoで、投稿、固定ページ、カテゴリ、タグを扱うブログ/CMS基盤を設計しました。コメント、メディア、メニュー、改訂履歴、下書き、公開予約も対象です。
公開条件と論理削除は共通の抽象モデルにまとめ、分類にはTermを使います。最初の運用画面はDjango adminとし、独自のフロントやAPIは後から追加する想定です。
この設計で扱うこと
この記事は設計案です。完成したコードや動作確認の結果ではありません。モデルの責務、制約、URL、実装順序を整理しています。元の仕様には、日本語と英語の両方があります。
初期実装の範囲
日常のブログ運用に必要な、投稿、固定ページ、分類、コメント承認、メディア管理、メニュー、下書き、公開予約、履歴管理を対象にします。
マルチサイトの完全互換、プラグイン互換、テーマ互換、Gutenbergの完全再現、WordPressと同じWYSIWYG編集は初期範囲に含めません。API、検索、多言語対応は後の拡張として扱います。
アプリとモデルの分け方
全体構成
共通の方針
コンテンツ、分類、メディア、コメント、ナビゲーション、SEO、共通基盤をDjango appごとに分けます。公開URLにはslugを使い、数値IDを出しません。
公開判定はstatus、published_at、visibilityにまとめます。DBはPostgreSQLを前提に、JSONField、全文検索、インデックスを使う設計です。
アプリ構成
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/
本文、メニュー、メディア、OGPの処理を別のappに置きます。管理画面からまとめて操作できても、実装の責務は分けておきます。
共通のルール
公開対象はstatusとpublished_atを持ち、必要に応じてvisibilityも確認します。削除にはdeleted_atを使う論理削除を採用します。代表画像はfeatured_mediaのFKで指定し、本文から推測しません。
WordPressの概念とモデルの対応
| WordPress 概念 | Django 側の対応 |
|---|---|
| Post / Page | content.Post / content.Page |
| Category / Tag | taxonomy.Term または Category / Tag |
| Post Meta | content.PostMeta |
| Media | media.MediaAsset |
| Comment | comments.Comment |
| Menu | navigation.Menu / MenuItem |
| Revision | content.Revision |
| Site Options | core.SiteSetting |
分類は、CategoryとTagを別モデルにする案より、Termにkindを持たせる案を優先します。シリーズや地域などの分類を追加しやすくするためです。投稿と固定ページは、最初の管理画面とクエリを単純にするため、別モデルにします。
モデルの項目と制約
モデル定義
共通抽象モデル(apps/core/models/abstract.py)
共通項目は、次の4つの抽象モデルにまとめます。
TimeStampedModel:created_at,updated_atSoftDeleteModel:deleted_at,is_deletedPublishableModel:status,published_at,visibility,is_publicSluggedModel:slug,slug_lock,get_absolute_url
投稿、固定ページ、将来のコンテンツで同じ項目を使います。特に公開判定を共通化し、一覧、詳細、予約公開、管理画面で条件がずれないようにします。
core(サイト・設定)
単一サイトでもSiteを用意します。項目はname, domain, locale, timezone, default_og_image, is_activeで、制約はunique(domain)です。コンテンツ、設定、メディア、分類をsiteでまとめます。
SiteSettingにはsite, key, value, is_publicを持たせます。valueはJSONFieldとし、文字列以外の設定も保存できるようにします。制約はunique(site, key)です。
accounts(ユーザー)
Django標準のUserで足りる間は、そのまま使います。必要になった場合にdisplay_nameやavatar_mediaを追加します。初期の権限はEditorとAdminを想定しています。
taxonomy(カテゴリ/タグ)
Termの項目はsite, name, slug, description, kind, parentです。カテゴリの階層はparentで表し、タグは階層を持たせません。制約はunique(site, kind, slug)です。
CategoryとTagを分ける案は、初期実装が分かりやすい一方、分類の追加ごとにモデルが増えます。今回は、著者や地域などの拡張にも使えるTermを選ぶ方針です。
media(メディア)
MediaAssetの項目はfile, original_filename, mime_type, size, width, height, alt_text, caption, credit, sha256, created_byです。sha256は重複検出と将来のストレージ移行に使う想定です。
altとcaptionはメディアと一緒に管理します。サムネイルなどの派生画像が必要になったら、MediaRenditionのような別モデルを用意します。
content(投稿・固定ページ・本文・改訂)
Postの項目はsite, author, title, slug, excerpt, body, body_format, featured_media, status, published_at, visibility, password, allow_comments, comment_count_cache, seoです。カテゴリとタグはM2MでTermに結びます。unique(site, slug)と公開一覧用のインデックスを置きます。
PageはほぼPostと同じ構造ですが、通常はカテゴリやタグを持ちません。post_typeでまとめる案もありますが、初期実装では管理画面とクエリを分けやすい構造を選びます。
PostMetaは追加項目を保存するためのモデルです。post, key, valueを持ち、valueにはJSONFieldを使います。WordPress互換の複数値を許す案もありますが、ここではunique(post, key)を優先します。
Revisionの項目はcontent_type, post/page, title, slug, excerpt, body, body_format, created_by, created_atです。保存時に差分かスナップショットを記録し、管理画面から復元できるようにします。
comments(コメント)
Commentの項目はsite, post, parent, author_user, author_name, author_email, author_url, body, status, ip_hash, user_agent, created_at, updated_at, deleted_atです。公開側にはapprovedだけを表示し、pending, spam, trashを管理状態として使います。IPは直接保存せず、ip_hashにします。
返信の関係はparentで表します。返信の深さ制限は、モデルではなく表示側で制御します。
navigation(メニュー)
Menuにはsite, name, slug, locationを持たせます。MenuItemの項目はmenu, parent, label, url, post, page, sort_order, is_enabledです。
リンク先はurlかpost/pageのどちらか一方にします。post/pageがある場合はget_absolute_urlを使い、URLが変わっても内部リンクが追従できるようにします。
seo(SEO/OGP)
SEOEntryをpostまたはpageに結びます。項目はmeta_title, meta_description, canonical_url, og_title, og_description, og_image, robotsです。投稿本文とは別モデルに置き、SEO項目を追加しやすくします。
公開URL
- 新着ノート一覧:
/blog/ - 記事詳細:
/blog/<slug>/ - 月別アーカイブ:
/blog/YYYY/MM/ - 固定ページ:
/<slug>/または/pages/<slug>/ - カテゴリ:
/category/<slug>/ - タグ:
/tag/<slug>/
Pageをトップ直下に置く案は、企業情報やLPなどの固定ページを想定しています。
公開状態と条件
公開状態はdraft, pending, private, scheduled, publishedの5段階です。公開条件は次のとおりです。
status == published
AND published_at <= now
AND visibility == public
AND deleted_at IS NULL
管理画面、公開一覧、RSS、検索、APIで同じ条件を使います。scheduledは未来の公開日時と組み合わせて扱います。
Django adminの要件
Post/Page一覧にはstatus, published_at, author, updated_atを表示します。slugの自動生成とslug_lock、分類のオートコンプリート、MediaAssetのプレビューとalt編集、Commentの承認、Revisionの閲覧と復元を用意します。
Django adminを最初の運用画面にするため、これらを公開側のテンプレートやAPIより先に整えます。
検索
最小構成ならtitle/excerpt/bodyのicontainsで検索します。推奨案はPostgreSQLの全文検索で、SearchVectorとGIN indexを使います。タグ、カテゴリ、著者、期間でも絞り込めるようにします。
実装順序
core.Site/accounts/content.Post/content.Pagetaxonomy.TermとPostへのM2Mmediacommentsnavigationrevisionseo
投稿と固定ページの公開を先に作り、その後に分類、メディア、コメント、メニュー、履歴、SEOを追加します。
後から追加する機能
拡張候補は、監査ログ、リダイレクト、フィード、サイトマップ、多言語対応です。Siteのlocale、slugの一意性の範囲、公開イベントの監査、Redirectを追加できる構成を、設計時に考慮しています。
実装に進むとき
まずcore, content, taxonomyの3層で、モデル、制約、マネージャ、公開クエリを固めます。その後、Djangoモデル定義、migrationの依存順、adminクラス、公開QuerySet、URLconf、基本テンプレートに落とし込みます。
PostとPageの分離、追加項目用のPostMeta、独立したSEOEntryは、初期実装を抑えつつ拡張に備えるための設計判断です。実装時には、これらの責務と制約をコードに反映します。
