PHP 8.3で静的ブログCMSを自作した話——設計・実装・ライセンスまとめ

はじめに

「ブログを書くためのツールを、自分で作ってしまおう」——そんな発想から、PHP 8.3とMySQLで動く静的ブログCMSを開発しました。

管理画面はPHPで動作し、公開側は静的HTMLを配信する構成です。WordPressのような動的CMSとは異なり、アクセスのたびにDBへ問い合わせることなく、事前に生成したHTMLファイルをそのまま返します。

この記事では、開発の経緯・技術スタック・設計判断・使用ライブラリのライセンスまでを一通りまとめます。

なぜ静的ブログCMSを自作したのか

既存のツールには以下の選択肢がありますが、それぞれ一長一短でした。

ツール 課題
WordPress 動的生成のためサーバー負荷が高い。プラグイン管理が煩雑
Hugo / Jekyll ローカルでビルドしてデプロイする運用が必要
microCMS等 外部サービス依存。月額コストが発生する場合も

今回は以下を満たすものが欲しかったのです。

  • 管理画面付き(ブラウザから記事を書ける)
  • 静的配信(表示が速く、サーバー負荷が極小)
  • ロリポップの共有サーバーで完結(SSHやローカルビルド不要)
  • 最小構成(余計な機能がない)

技術スタック

レイヤー 技術
言語 PHP 8.3
DB MySQL (PDO)
テンプレート Twig 3
Markdown変換 league/commonmark 2
検索 Fuse.js 7(クライアントサイド)
サーバー ロリポップ (LiteSpeed)
配信 静的HTML + Apache Rewrite

依存ライブラリはComposerで管理し、フロントエンドのビルドツール(webpack等)は一切使っていません。

アーキテクチャ

全体構成

管理画面(PHP) → DB(MySQL) → ビルド → 静的HTML → Apache → ブラウザ

管理画面で記事を書き、「公開」ボタンを押すと全量ビルドが走ります。全公開記事のHTMLを再生成し、古いファイルは一度削除してからクリーンに作り直す方式です。

ディレクトリ設計

blog-cms/           ← 非公開(app, themes, vendor)
public_html/
  admin/             ← 管理画面(PHP)
  blog/              ← 記事HTML(静的)
  assets/            ← CSS/JS(静的)
  uploads/           ← 画像
  blog.html          ← 一覧ページ
  search.html        ← 検索ページ
  search-index.json  ← 検索用インデックス
  sitemap.xml

重要なのは、blog-cms/(PHPコード・設定・vendor)がドキュメントルートの外に配置されている点です。公開ディレクトリには静的ファイルと管理画面のエントリポイントのみを置いています。

URL設計とApache Rewrite

公開URLは拡張子なしの /blog/slug 形式にこだわりました。実体は .html ファイルで、.htaccess のRewriteRuleで拡張子を隠しています。

/blog           → /blog.html
/blog/my-post   → /blog/my-post.html
/blog/page/2    → /blog/page/2.html

.html を直接アクセスした場合は、拡張子なしURLへ301リダイレクトすることで正規化しています。

主な機能

管理画面

  • 記事CRUD(Markdown入力、カテゴリ・タグ・SEO項目付き)
  • カテゴリ管理(作成・編集・削除)
  • 画像アップロード(MIME/拡張子/サイズ検証、ファイル名ランダム化)
  • 公開/非公開ボタン(押すと即座に全量ビルド)
  • 手動ビルド(ビルドログ表示付き)
  • 簡易プレビュー(編集画面でMarkdownをHTML変換して表示)

静的生成(全量ビルド)

ビルドで生成されるページ:

  • 記事詳細ページ
  • ブログ一覧(10件/ページ、ページネーション付き)
  • カテゴリ別一覧(ページネーション付き)
  • タグ別一覧(ページネーション付き)
  • 検索ページ(+ search-index.json)
  • sitemap.xml
  • 404.html

非公開にした記事は次回ビルドで静的ファイルが削除されます。全量ビルド方式なので、生成対象のディレクトリをクリーンしてから再生成することで、確実に不要ファイルを除去しています。

クライアントサイド検索

記事数が約1000本の規模であれば、サーバーサイド検索は不要です。ビルド時に search-index.json(タイトル・本文抜粋・カテゴリ・タグ等)を生成し、Fuse.jsでブラウザ上で検索を実行します。

セキュリティ対策

対策 実装
CSRF 全POSTリクエストにトークン検証
パスワード bcrypt (cost=12) でハッシュ化
XSS (Markdown) CommonMarkの html_input: strip で生HTMLを除去
XSS (テンプレート) Twigのautoescapeを有効化
アップロード 拡張子・MIMEタイプ・サイズの三重検証、保存名のランダム化
管理画面 noindex/nofollowメタタグ
ビルド ロックファイルで同時実行防止

使用ライブラリとライセンス

本プロジェクトで使用している全ライブラリのライセンスを確認しました。すべて商用利用可能なオープンソースライセンスです。

PHP(Composer)

パッケージ バージョン ライセンス
twig/twig v3.23.0 BSD-3-Clause
league/commonmark 2.8.0 BSD-3-Clause
league/config v1.2.0 BSD-3-Clause
dflydev/dot-access-data v3.0.3 MIT
nette/schema v1.3.4 BSD-3-Clause / GPL-2.0 / GPL-3.0
nette/utils v4.1.2 BSD-3-Clause / GPL-2.0 / GPL-3.0
psr/event-dispatcher 1.0.0 MIT
symfony/deprecation-contracts v3.6.0 MIT
symfony/polyfill-ctype v1.33.0 MIT
symfony/polyfill-mbstring v1.33.0 MIT
symfony/polyfill-php80 v1.33.0 MIT

JavaScript

パッケージ バージョン ライセンス
Fuse.js v7.0.0 Apache-2.0

MITBSD-3-ClauseApache-2.0はいずれも寛容なライセンスであり、著作権表示とライセンス文の同梱のみが条件です。netteパッケージはデュアルライセンス(BSD-3-Clause or GPL)ですが、BSD-3-Clause側で利用しています。

設計判断のポイント

なぜ全量ビルドなのか

差分ビルドのほうが効率的に見えますが、以下の理由で全量ビルドを選びました。

  • 1000記事程度なら数秒で完了する(実測値)
  • 差分管理の複雑さを排除できる
  • 非公開記事のファイル削除漏れを確実に防げる
  • ページネーションの再計算が自然にできる

なぜクエリビルダを使わないのか

記事規模が小さく、テーブル数も6個のみ。PDOの直書きで十分であり、ORMやクエリビルダの学習コスト・依存追加に見合わないと判断しました。

なぜフレームワークを使わないのか

LaravelやSlimを使えば認証やルーティングは楽になりますが、ロリポップの共有サーバーでの動作保証と、依存の最小化を優先しました。管理画面のルーティングは match(true) による簡易パターンマッチで十分です。

Claude Codeで開発した話

実はこのCMS、コードの大部分はClaude Code(Anthropic公式のCLIツール)を使って開発しました。

Claude Codeとは

Claude CodeはターミナルからClaudeを呼び出し、ファイルの読み書き・コマンド実行・Web検索などを対話的に行えるツールです。単にコードを生成するだけでなく、「SSH接続してファイルをアップロードし、composer installを実行してDBテーブルを作成する」といった一連のデプロイ作業まで任せることができます。

今回の開発フロー

今回の開発は以下のように進みました。

  1. 要件定義を自然言語で入力 — 機能一覧・DB設計・ディレクトリ構成・URL設計をまとめてプロンプトとして渡した
  2. Claude Codeが全ファイルを生成 — PHP、Twig、CSS、JS、.htaccess、README、CLIスクリプトまで約44ファイルを一気に生成
  3. コード整合性の自動検証 — Claude Codeの探索エージェントが、名前空間・コンストラクタ引数・テンプレート変数・RewriteRule等の整合性をチェック
  4. ロリポップへのデプロイもClaude Codeが実行 — SSH接続→ディレクトリ作成→scp転送→composer install→DBテーブル作成→初期ユーザー作成→.htaccess統合→動作確認まで全て対話的に実行
  5. 問題の即時対応 — PHP 7.4→8.3の切替が必要だった問題、utf8mb4のキー長制限、WordPressとの.htaccess共存など、デプロイ中に発生した問題をその場で解決
  6. ブログ記事の執筆・投稿も — この記事自体もClaude Codeが書き、管理画面のAPIを通じて投稿した

人間がやったのは「何を作りたいか」を伝えることと、ロリポップ管理画面でのPHPバージョン変更くらいです。

所感

Claude Codeの強みは「コード生成」と「環境操作」がシームレスにつながる点です。

従来のAIコード生成ツールは、生成されたコードをコピーして手動でファイルに貼り付け、手動でデプロイする必要がありました。Claude Codeでは「この要件で作って、ロリポップにデプロイして」と言えば、ファイル作成からサーバー操作までワンストップで進みます。

特に今回のように、PHPバージョンの違い既存WordPressとの共存共有サーバー特有の制約など、ドキュメントに書いていない環境固有の問題が次々と出てくるケースでは、その場でコードを修正→再アップロード→検証を繰り返せるClaude Codeの対話的なアプローチが非常に有効でした。

Claude Code × ロリポップ Tips

ロリポップの共有サーバーでClaude Codeを活用する際のTipsをまとめます。

1. SSH接続はexpect経由で

ロリポップのSSHはパスワード認証のみ対応(鍵認証不可の場合あり)。Claude Codeからは sshpass がない環境でも expect コマンドでパスワード入力を自動化できます。

expect -c '
set timeout 30
spawn ssh -o StrictHostKeyChecking=no -p 2222 user@ssh.lolipop.jp "コマンド"
expect "password:"
send "パスワード\r"
expect eof
'

ファイル転送も同様に scpexpect でラップします。

2. PHPバージョンの指定

ロリポップにはPHP 5.3〜8.4まで複数バージョンがインストールされていますが、Web経由とSSH経由でバージョンが異なる場合があります。

  • Web側: ロリポップ管理画面の「PHP設定」でドメインごとに指定
  • CLI側: /usr/local/php/8.3/bin/php のようにフルパスで指定

Claude Codeにビルドスクリプトを実行させる場合は、CLIのPHPパスを明示的に指定しましょう。

3. composer installはサーバー上で

ロリポップにはComposerがプリインストールされていませんが、composer.phar をその場でダウンロードして使えます。

/usr/local/php/8.3/bin/php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
/usr/local/php/8.3/bin/php composer-setup.php
/usr/local/php/8.3/bin/php composer.phar install --no-dev --optimize-autoloader

4. MySQLのutf8mb4キー長制限に注意

ロリポップの一部サーバーではInnoDBのデフォルトROW_FORMATが COMPACT で、utf8mb4VARCHAR(255) にUNIQUEキーを張ると767バイト制限に引っかかります。対策は2つ。

  • VARCHAR(191) に抑える(191 × 4 = 764 < 767)
  • ROW_FORMAT=DYNAMIC を明示指定する

5. 既存WordPressとの.htaccess共存

WordPressが動いているドメインに別のアプリを共存させる場合、.htaccess のルール順が重要です。

  • ブログCMSのルールをWordPressの # BEGIN WordPress より前に配置
  • DirectorySlash Off を指定し、/blog ディレクトリと blog.html の競合を防ぐ
  • /admin パスはWordPressの wp-admin と被らないように注意

元の .htaccess は必ずバックアップしてから変更しましょう。

6. WAFに注意

ロリポップにはWAF(Web Application Firewall)が有効になっており、大きなPOSTリクエストやSQLインジェクションに似た文字列を含むリクエストがブロックされることがあります。

記事内にSQLやコードブロックを含む場合、管理画面からの投稿がWAFにブロックされる場合があります。その場合は、CLIスクリプト経由でDBに直接投入する方法が確実です。

7. デプロイ全体をClaude Codeに任せるコツ

Claude Codeに効率よくデプロイを任せるには、以下の情報を最初に渡すのがポイントです。

  • SSHの接続情報(ホスト、ポート、ユーザー名、パスワード)
  • ドキュメントルートのパスweb/ドメイン名/ が一般的)
  • DBの接続情報(wp-config.phpから読めればなお良い)
  • 既存ファイルの有無(WordPress等が入っているか)

これらがあれば、Claude Codeはディレクトリ構造の把握→ファイル転送→依存インストール→DB構築→設定→動作確認まで一気通貫で進められます。

まとめ

「管理画面付きの静的ブログCMS」というニッチな需要に対して、PHP + MySQL + Composerという枯れた技術スタックで最小限の実装を行いました。

ポイントをまとめると:

  • 管理はPHP、配信は静的HTMLという分離構成
  • 全量ビルド方式で運用をシンプルに
  • クライアントサイド検索でサーバー負荷ゼロ
  • ロリポップの共有サーバーで完結
  • 使用ライブラリはすべてOSSライセンスで権利関係クリア
  • Claude Codeでコード生成からデプロイまでワンストップ

自分だけが使う小規模ブログには、このくらいの規模感がちょうど良いと感じています。