設計の方針

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 / Pagecontent.Post / content.Page
Category / Tagtaxonomy.Term または Category / Tag
Post Metacontent.PostMeta
Mediamedia.MediaAsset
Commentcomments.Comment
Menunavigation.Menu / MenuItem
Revisioncontent.Revision
Site Optionscore.SiteSetting

分類は、CategoryとTagを別モデルにする案より、Termにkindを持たせる案を優先します。シリーズや地域などの分類を追加しやすくするためです。投稿と固定ページは、最初の管理画面とクエリを単純にするため、別モデルにします。

モデルの項目と制約

モデル定義

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

共通項目は、次の4つの抽象モデルにまとめます。

  • TimeStampedModel: created_at, updated_at
  • SoftDeleteModel: deleted_at, is_deleted
  • PublishableModel: status, published_at, visibility, is_public
  • SluggedModel: 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で表します。返信の深さ制限は、モデルではなく表示側で制御します。

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を使います。タグ、カテゴリ、著者、期間でも絞り込めるようにします。

実装順序

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

投稿と固定ページの公開を先に作り、その後に分類、メディア、コメント、メニュー、履歴、SEOを追加します。

後から追加する機能

拡張候補は、監査ログ、リダイレクト、フィード、サイトマップ、多言語対応です。Siteのlocale、slugの一意性の範囲、公開イベントの監査、Redirectを追加できる構成を、設計時に考慮しています。

実装に進むとき

まずcore, content, taxonomyの3層で、モデル、制約、マネージャ、公開クエリを固めます。その後、Djangoモデル定義、migrationの依存順、adminクラス、公開QuerySet、URLconf、基本テンプレートに落とし込みます。

PostとPageの分離、追加項目用のPostMeta、独立したSEOEntryは、初期実装を抑えつつ拡張に備えるための設計判断です。実装時には、これらの責務と制約をコードに反映します。