結論

この仕様で定義されているのは、WordPress 互換 CMS を作るための巨大な願望リストではなく、Django でちゃんと動き、あとから広げられる現実的なベースラインだ。抽象モデルで公開条件と論理削除を共通化し、Site を先に置き、taxonomy を Term に寄せ、admin 運用を主軸にすることで、最初の実装範囲を抑えながら CMS として必要な骨格はほぼ揃う。

さらに、ソースの後半ではこの内容を英語で再定義しており、仕様の意図を日本語だけに閉じず、実装者向けの国際的な共有資料としても使える形になっていた。設計方針、モデル構成、URL 設計、公開条件、実装順序が二言語で揃っているので、チーム内共有の起点としても扱いやすい。

前提

WordPress の使い勝手は便利だが、そのまま互換を追いかけると最初の実装コストも運用の複雑さも一気に跳ねる。そこで今回は、WordPress の主要概念だけを抽出して、Django で長く育てられるブログ/CMS 基盤として再設計する前提を整理した。狙いは、投稿、固定ページ、カテゴリ、タグ、コメント、メディア、メニュー、下書き、公開予約、改訂履歴を最低限カバーしつつ、あとから API や検索や多言語対応を足していける土台を先に固めることだった。

この仕様は、完成品のコードではなく、何をどの単位で分け、どこに制約を置き、どういう順序で実装すると破綻しにくいかを決めるための設計メモである。Django admin を最初の運用UIに据えることで、フロントや API を急いで作らなくてもコンテンツ運用を先に成立させられる構成にしている。

スコープ

最初に明確にしたかったのは、WordPress の全部を再現する話ではないという点だった。必要なのは、日常的なブログ運用で本当に触る部分をカバーすることだ。つまり、投稿と固定ページがあり、カテゴリとタグで整理でき、コメントを承認フローで扱え、メディアを管理でき、ナビゲーションメニューを持てて、下書きや公開予約や履歴管理ができること。このラインまでを、Django らしい設計で組み立てる。

逆に、初期段階ではやらないことも切り分けた。マルチサイト完全互換、プラグイン互換、テーマ互換、Gutenberg の完全再現、WordPress と同じ WYSIWYG 体験は最初から背負わない。ここを曖昧にすると、仕様が膨らむ割に基盤がなかなか固まらない。だから最初は、Django で堅く動く CMS のコアだけに集中する。

設計判断

全体アーキテクチャ

方針

全体方針としては、関心ごとを Django app ごとに素直に分離する。コンテンツ、タクソノミー、メディア、コメント、ナビ、SEO、共通基盤を分けておけば、後から機能を拡張しても責務が崩れにくい。公開URLは slug と公開状態を中心に据え、数値IDを表に出さない。公開判定は statuspublished_atvisibility に寄せて、どの画面でも同じ判断基準を使えるようにする。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 やメディアの事情を最初から混ぜたくなかったからだ。WordPress 的にはひとつの管理画面から全部触れる体験が重要だが、実装側では単一巨大 app にしない方が明らかに保守しやすい。

命名・設計ルール

URL は原則 slug ベースにする。公開対象は共通で statuspublished_at を持ち、必要なら visibility も見る。削除は deleted_at を使った論理削除を基本とし、事故を避ける。代表画像は featured_media の FK で固定し、本文から推測しない。こういうルールを先に置いておくと、管理画面実装でも公開ビュー実装でも判断がぶれにくい。

ドメインモデル概要(WordPress対応表)

WordPress の概念と Django モデルの対応関係は次の通りに整理した。

WordPress 概念Django 側の対応
Post / Pagecontent.Post / content.Page
Category / Tagtaxonomy.Term または Category / Tag
Post Metacontent.PostMeta
Mediamedia.MediaAsset
Commentcomments.Comment
Menunavigation.Menu / MenuItem
Revisioncontent.Revision
Site Optionscore.SiteSetting

ここで重要なのは、WordPress の概念をそのまま真似るのではなく、Django で扱いやすいまとまりに置き直していることだ。たとえば taxonomy は CategoryTag を完全分離するより、Termkind を持たせる方が後で custom taxonomy を足しやすい。逆に Post と Page は、最初の実装容易性を優先して分けた方が管理画面と公開側の振る舞いを整理しやすい。

実装詳細

モデル定義

共通抽象モデル(apps/core/models/abstract.py)

共通抽象モデルはこの仕様の芯になる。

  • TimeStampedModel: created_at, updated_at
  • SoftDeleteModel: deleted_at, is_deleted
  • PublishableModel: status, published_at, visibility, is_public
  • SluggedModel: slug, slug_lock, get_absolute_url

この4つを抽象化しておくと、投稿、固定ページ、将来の追加コンテンツでも同じ公開条件と同じ監査項目を使い回せる。特に PublishableModel を共通にしておく意味は大きい。公開判定がモデルごとに散ると、一覧、詳細、予約公開、管理画面表示の整合が崩れやすいからだ。

core(サイト・設定)

Site は単一サイト前提でも用意しておく。name, domain, locale, timezone, default_og_image, is_active を持たせ、unique(domain) を置く。いまは1サイトしかなくても、site があるだけでコンテンツ・設定・メディア・taxonomy を横断して束ねられる。

SiteSetting は WordPress の option 的な位置づけで、site, key, value, is_public を持つ。valueJSONField にしておくと、文字列に限定せず設定の形を少し柔らかく保てる。unique(site, key) を前提にして、設定キーの衝突を避ける。

accounts(ユーザー)

ユーザーは Django 標準の User で足りるならそれで進める。必要になったときだけ display_nameavatar_media を足す。権限も最初はシンプルでよく、Editor と Admin くらいの粒度で十分だ。ここを凝りすぎるより、投稿と公開の責務をはっきりさせる方が先だと判断している。

taxonomy(カテゴリ/タグ)

taxonomy は WordPress ライクな体験を支えるが、内部実装はできるだけ単純にしたい。そこで本命は Term モデルで、site, name, slug, description, kind, parent を持たせる。カテゴリ階層は parent で表現し、タグはフラットに扱う。制約は unique(site, kind, slug) にする。

もし専用モデルとして CategoryTag を分けるなら実装はわかりやすいが、シリーズや著者や地域のような別分類を入れたくなった時に拡張コストが上がる。仕様でも「Term のみ」に寄せる方が後で楽、と明示されているのはそのためだ。

media(メディア)

MediaAsset には file, original_filename, mime_type, size, width, height, alt_text, caption, credit, sha256, created_by を持たせる。画像の派生物を作るなら MediaRendition のような別モデルに逃がす前提にしている。ここで sha256 を入れているのは、単なるアップロード保存ではなく、重複検出や将来のストレージ移行を見据えているからだ。

alt と caption を最初からモデルで持っておく判断も重要だ。後から SEO やアクセシビリティをやる段になって別管理になると、編集者にとっても開発者にとっても扱いが悪くなる。

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 と同じ構造だが、通常はカテゴリやタグを持たせない。WordPress 的には post_type 一本にまとめる発想もあるが、最初の実装では分離の方が管理画面もクエリもわかりやすい。

PostMeta は柔軟拡張の逃げ道として残している。post, key, value を持ち、valueJSONField にする。WordPress 互換なら非 unique にもできるが、事故を避けるなら unique(post, key) の方が扱いやすい。

Revisioncontent_type, post/page, title, slug, excerpt, body, body_format, created_by, created_at を持つ。保存時に差分かスナップショットを取って、管理画面から復元できるようにする。WordPress 的な「履歴を戻せる安心感」を出すうえで、これは意外と重要な要素だ。

comments(コメント)

Commentsite, 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 で表現するが、返信の深さ制限は表示ロジック側で制御する。モデルに無理に持ち込まないのがよい。

Menusite, name, slug, location を持つ。MenuItemmenu, parent, label, url, post, page, sort_order, is_enabled を持つ。重要なのは urlpost/page のどちらか一方だけを許すことと、post/page がある場合は get_absolute_url を優先することだ。こうしておくと、URL変更時でも内部リンクの追従性を保ちやすい。

seo(SEO/OGP)

SEOEntrypost または page に紐づき、meta_title, meta_description, canonical_url, og_title, og_description, og_image, robots を持つ。SEO を Post テーブルへ直接混ぜる方法もあるが、責務分離と将来拡張を考えると独立モデルの方が整理しやすい。

URL設計(公開側)

公開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 を未来日時とセットで扱う点も、UI 実装時の迷いを減らす。

管理画面(Django Admin)要件

管理画面では、Post/Page 一覧に status, published_at, author, updated_at を出し、slug の自動生成と slug_lock を運用し、taxonomy はオートコンプリートにし、MediaAsset はプレビューと alt 編集を行い、Comment は承認フローを持ち、Revision は閲覧と復元ができるようにする。

Django admin を第一の運用UIに置く以上、この仕様はかなり重要だ。モデルが整っていても、編集導線が悪いと WordPress 的な使い勝手にはならない。逆にここが整っていれば、公開側テンプレートや API を後から載せても運用は始められる。

検索

検索は最小なら title/excerpt/bodyicontains で始められるが、推奨は PostgreSQL の全文検索で、SearchVector と GIN index を使う。タグ、カテゴリ、著者、期間で絞り込めるようにすると、単なるブログではなく CMS としての使い勝手がかなり上がる。

実装順序

実装順序を先に切っているのも実務的だった。

  1. core.Site / accounts / content.Post / content.Page
  2. taxonomy.Term と Post への M2M
  3. media
  4. comments
  5. navigation
  6. revision
  7. seo

この順序なら、早い段階で投稿と固定ページの公開体験を作り、その後に整理機能、メディア、承認フロー、ナビ、履歴、SEO を積める。実際、CMS を最短で触れる状態にするならかなり妥当な順番だと思う。

注意事項

仕様の最後には、監査ログ、リダイレクト、フィード、サイトマップ、多言語対応といった拡張候補も整理されている。ここで重要なのは、まだ実装しない機能でも、今のモデルで将来の入り口を潰さないようにしている点だ。Sitelocale、slug の一意性スコープ、公開イベントの監査、Redirect の差し込み余地などは、まさにそのための設計になっている。

今後の作業

この仕様をそのまま実装に落とすなら、最初に着手すべきなのは core, content, taxonomy の3層だと思う。ここでモデル・制約・マネージャ・公開クエリを固めれば、その後の admin 調整やテンプレート作成がかなり安定する。逆に、ここを曖昧なままメディアやコメントやSEOへ広げると、あとで責務分離をやり直すことになる。

また、PostPage を最初は分ける判断、PostMeta を逃げ道として残す判断、SEOEntry を独立させる判断は、どれも「最初の実装速度」と「後の拡張性」の間でバランスを取っている。派手さはないが、運用を見据えた設計としては筋が良い。

次にやるなら、この仕様からそのまま Django モデル定義、migration の依存順、admin クラス、公開QuerySet、URLconf、基本テンプレートへ落としていく。仕様書としてはすでに十分具体的なので、実装フェーズへ進むときに必要なのは方向転換ではなく、どの粒度でコード化するかの決定だけだ。