この記事は、Claude Codeに、書かせました。
はじめに
単一サイトを管理者一人で運用するだけなのに、WordPressだと「テーマ」「プラグイン」「管理画面のアップデート」「不要な汎用機能」まで背負い込むことになりがちです。
今回、デザイナーが納品した静的HTML/CSS/JSをそのままJinjaテンプレートとして組み込む前提で、必要最小限の機能だけを持つ小型CMSをPython(Flask)でスクラッチ実装しました。MITライセンスでGitHubに公開したので、作った経緯と中身を紹介します。
- リポジトリ: https://github.com/mesaka/small-cms-flask
- ライセンス: MIT
念のため補足すると、この記事はWordPressを否定する趣旨ではありません。WordPressは巨大なエコシステムと実績があり、多くの現場で最適な選択です。ただ「単一サイトを管理者一人で運用する」という限定用途では、WordPressの汎用性がそのままオーバースペックになる場面もあります。Small CMSは、そうした場面での選択肢の一つとして作りました。
Small CMSとは
「単一サイト・管理者中心」で運用することに割り切った小型CMSです。v0.1でできることは以下の通りです。
- 記事・固定ページの下書き/公開/編集/削除
- 管理者ログインと、管理画面への認可
- 画像アップロード(JPEG、PNG、GIF、WebP)
- 問い合わせフォーム、サンクスページ、自動返信・管理者通知メール
- 問い合わせの管理画面閲覧、対応状態の変更、CSV出力
- CSRF対策、XSS対策、入力検証、送信・ログインの回数制限、監査ログ
逆に、v0.1では意図的に持たせていない機能もあります。
- 複数管理者・外部ユーザー登録
- 自由にフォームを組み立てるフォームビルダー
- 決済機能
- プラグイン機構
「小さく作って、必要になったら足す」という方針です。
なぜWordPressではなくスクラッチにしたか
- テーマ・プラグインの脆弱性やアップデート追従のコストを背負いたくなかった
- デザイナーからの納品物は「完成済みの静的サイト」であり、WordPressテーマ化という変換コストが不要
- 管理画面は「記事・ページ」「問い合わせ」の2機能だけで足りるため、WP管理画面のフル機能は過剰
デザイナーには、PHPやWordPressテーマではなく、次の構成のレスポンシブ静的サイト一式を納品してもらう想定です。
design-delivery/
index.html # トップ/記事一覧
post-detail.html # 記事詳細
page.html # 固定ページ
contact.html # 問い合わせ
contact-thanks.html # サンクスページ
404.html # 404ページ
css/
js/
images/
fonts/
これをapp/templates/のJinjaテンプレートに変換して組み込みます。
デザイナーとのやり取り
デザイナーには仮コンテンツ入りの静的HTMLを6画面(トップ/一覧・記事詳細・固定ページ・問い合わせ・サンクス・404)納品してもらいます。ポイントは、どこがCMSで差し替わるかをHTMLコメントで示してもらうことです。
<!-- CMS: post_title -->
<h1>記事タイトル</h1>
<!-- CMS: post_published_at -->
<time datetime="2026-08-31">2026年8月31日</time>
<!-- CMS: post_body -->
<div class="article-body">
<p>記事本文です。</p>
</div>
このコメントを目印に、開発側でJinjaテンプレートへ変換します。デザイナー側にJinjaやPythonの知識は一切不要です。
いくつか、デザイナーに事前に伝えている制約もあります。
- お問い合わせフォームは見た目自由。ただし
name属性・method・actionはCMS側のバリデーション/DB/メール本文と直結しているため固定(項目の増減は開発側への事前相談が必要) - CMS初期設定のContent Security Policyにより、インラインJavaScript・インラインCSS・外部スクリプトはそのままでは動作しない。JavaScriptは
js/内の外部ファイルとして納品してもらう - 管理画面はCMS側の機能なので、デザイン納品の対象外
認識齟齬を防ぐため、上記をまとめた納品前チェックリストも用意しています(詳しくは次項)。
同梱ドキュメント
コードだけでなく、運用に必要なドキュメントもリポジトリのoutputs/配下に同梱しています。
-
outputs/CMS_USER_MANUAL.md— 管理者向け操作マニュアル。ログイン、記事・固定ページの作成、画像アップロード、問い合わせフォームの確認・CSV出力、フォーム項目を変更したい場合に開発側で直す3箇所(contact.html/routes.pyのcontact_submit/models.pyのInquiry)などをまとめています -
outputs/DESIGNER_INTEGRATION_SPEC.md— 前述のデザイナー向け納品・連携仕様書。納品物の構成、CMS差し替え箇所の指定方法(HTMLコメント)、フォームのHTML仕様、CSP起因の実装上の制約、納品前チェックリストを含みます
どちらも、実際にデザイナーとやり取りしたり管理者に運用してもらったりする中で溜まった「言った言わない」を防ぐためのドキュメントです。
技術構成
- Python 3.11+
- Flask
- SQLAlchemy(ORM、生SQL文字列は組み立てない)
- MariaDB 10.6+(
mysql-connector-python経由) - 本番はGunicorn + Nginx + Docker Compose
アプリ本体はapp/配下の7ファイル・約650行に収まっています。
| ファイル | 役割 | 行数 |
|---|---|---|
routes.py |
ルーティング(公開ページ・管理画面) | 289 |
models.py |
SQLAlchemyモデル | 103 |
security.py |
CSRF・レート制限・監査ログ | 54 |
services.py |
画像保存・メール送信・CSV生成 | 66 |
config.py / database.py / __init__.py
|
設定・DB・アプリファクトリ | 130 |
お問い合わせフォームも実装済み
「今後」の項目にフォームビルダー化(項目を自由に組み替えられるようにする話)を挙げていますが、それとは別に、固定項目のお問い合わせフォーム自体は既にv0.1で実装済みです。
- 名前・メールアドレス・電話番号・件名・本文の入力フォーム、送信後のサンクスページ
- ハニーポット(画面上には見えない
websiteという隠しフィールド)でボットの自動送信を弾く
@bp.post("/contact/submit")
def contact_submit():
validate_csrf()
if request.form.get("website"):
# 人間には見えない隠しフィールドが埋まっていたら、ボットとみなして無言でスルーする
return redirect(url_for("cms.contact_thanks"))
if contact_rate_limited():
abort(429, "送信回数が上限に達しました。しばらくしてから再度お試しください。")
- 同一IPからの送信を10分間で5件までに制限
- 送信者への自動返信メール、管理者への通知メールを自動送信
- 管理画面から問い合わせ一覧を確認し、対応状態(未対応/対応中/完了)を変更、CSVで書き出し
つまり「今すぐ使える固定フォーム」はすでにあり、「今後」で目指しているのは、その項目構成を管理画面から自由に増減できるようにする発展版、という位置づけです。
セキュリティ面で気をつけたこと
小さいアプリでも、公開する以上は最低限のセキュリティ対策は外せません。
-
CSRF対策: セッションに保存したトークンを
hmac.compare_digestで比較(フォームごとにhiddenフィールドで送信)
def validate_csrf() -> None:
submitted = request.form.get("csrf_token", "")
expected = session.get("csrf_token", "")
if not expected or not submitted or not hmac.compare_digest(submitted, expected):
abort(400, "Invalid form token")
-
XSS対策: 記事本文は
bleachで許可タグ・属性をホワイトリスト化してから保存。それ以外の箇所で外部入力に|safeを使わない - ログイン回数制限: 同一IP+メールアドレスの組み合わせで15分以内に5回失敗するとロック(監査ログのタイムスタンプを使って判定、専用のカウンタテーブルを持たない設計)
attempts = database.query(AuditLog).filter(
AuditLog.event == "login_failed",
AuditLog.detail == login_key,
AuditLog.created_at >= utcnow() - timedelta(minutes=15),
).count()
if attempts >= 5 or user is None or not user.is_active or not check_password_hash(user.password_hash, password):
audit("login_failed", login_key)
flash("メールアドレスまたはパスワードが正しくありません。", "error")
return redirect(url_for("cms.login"))
- 問い合わせフォームのレート制限: 同一IPからの送信を10分間で5件までに制限
- 監査ログ: ログイン成否・記事の作成/更新/削除・画像アップロード・問い合わせCSV出力を記録(問い合わせ本文やCSRFトークン自体はログに出さない)
-
セキュリティヘッダー:
X-Content-Type-Options、X-Frame-Options、CSP等を全レスポンスに付与
OSS公開に向けてやったこと
コード自体は動いていましたが、「公開してよい状態」にするために以下を追加しました。
- テスト: pytestで18件(公開ページ・お問い合わせ送信・ハニーポット・管理ログインの成功/失敗/ロックアウト・CSRF検証・XSSサニタイズ)。SQLiteの一時ファイルで完結するため、MariaDBやSMTPサーバーなしでCIが回せます
- Lint: ruffでコードスタイルを統一
- CI: GitHub ActionsでPython 3.11/3.12のマトリクスでlint+testを自動実行
- CONTRIBUTING.md / CODE_OF_CONDUCT.md: コントリビュートの手順と行動規範を明文化
- SECURITY.md: 脆弱性報告はGitHub Security Advisories経由に統一(公開Issueに書かない)
- 英語版README: 日本語READMEと相互リンク
セットアップ
py -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
Copy-Item .env.example .env
# .envのSECRET_KEY・DATABASE_URL・メール設定を実値に置き換える
flask --app app init-db
flask --app app create-admin
flask --app app run
開発用URLはhttp://127.0.0.1:5000/、管理画面は/admin/loginです。
Docker Compose(MariaDB + Gunicorn + Nginx)構成にも対応しています。こちらはdocker compose up -d --buildで起動し、Nginxがホストのhttp://127.0.0.1:8080/を公開します(venv構成の5000番とは別ポートです)。
本番ホスティング:EC2よりLightsailを推奨
このアプリはMariaDB・Flask(Gunicorn)・Nginxを1台に同居させる構成です。EC2の素の従量課金より、コンピュート・ストレージ・データ転送・固定IPを月額固定でまとめたLightsailの方が同スペックで2〜3割安く、運用もシンプルでした。
| スペック帯 | EC2インスタンスタイプ | Lightsailプラン | 月額目安(東京、EC2はEBS30GB込み概算) |
|---|---|---|---|
| 2 vCPU / 2GB | t3.small |
Small | EC2 約$22 / Lightsail $12 |
| 2 vCPU / 4GB | t3.medium |
Medium | EC2 約$42 / Lightsail $24 |
初期構成にはLightsail Small(2 vCPU・2GBメモリ・60GB SSD・データ転送3TB込み、$12/月)を推奨しています。アクセスが増えて手狭になったら、コンソールから数クリックでMediumへプラン変更できます。特別な理由(既存VPC構成への統合、将来の水平スケール等)がない限り、最初からEC2を選ぶ必要はありませんでした。詳しい手順はリポジトリのEC2_DEPLOY.mdにまとめています。
今後
WordPressでは、新規サイトなら大抵導入される定番プラグインというものがあります(SEO、バックアップ、画像最適化、キャッシュ、フォームビルダー等)。Small CMSでも、この用途に合う範囲でロードマップに取り込んでいく予定です。
- SEO基本対応(title/meta description個別設定、OGP、sitemap.xml)
- お問い合わせフォームビルダー化(項目を自由に組み替えられるように)
- 画像最適化(アップロード時の自動リサイズ・WebP変換)
- バックアップ自動化(定期実行+クラウド保管)
- キャッシュ(ページキャッシュによる高速化)
なお、WordPressのAdvanced Custom Fieldsに相当する「記事に自由な項目を追加できるカスタムフィールド機構」も検討しましたが、記事のデータモデル自体を汎用化することになり、「あくまでシンプルなCMS」という方針から逸脱するため、今後の対象からは外しています。
要望・バグ報告・プルリクエストはGitHubで歓迎です。
リンク
- GitHub: https://github.com/mesaka/small-cms-flask
- ライセンス: MIT