WEB制作を学んで「畳んだ」会社員が、Claude Codeに丸投げして「同人サイトの裏ページ」をWordPressで実装した話

はじめに

私は超文系の事務職です。

数年前、WEB制作のオンラインスクールを1年以上受講し、卒業課題のWordPress実装までやり切ったうえで、「これを一生やるのは無理」と判断してWEB制作の道を畳みました。
挫折ではなく、完走後の適性判断です。
当時ぎりぎり書けた静的コーディングの知識は、現在カスミの向こうにあります。PHPは書けません。

それでも、令和に個人創作サイトを建てたくなりました。
古のオタクなので、個人サイトの時代──日記とイラスト置き場と、そして「裏ページ」があった文化圏──の出身だからです。

実装はClaude Codeにほぼ丸投げしました。
セッション20回以上の結果を、この記事にまとめます。

完成したサイト:https://kotodutefolio.com

この記事のスライド画像はすべてAI生成です。細部の表記が間違っている可能性があります。雰囲気でお願いします。あと、スライドでツッコミを入れている二人は、このサイトに住んでいる自作小説『理玄異聞録』の登場人物です(呼び水です)。

何を作ったか

WordPress(Arkhe親テーマ+子テーマ)で、以下の構成のサイトです。

  • カスタム投稿タイプ3種:writings(小説・エッセイ)/visuals(絵・映像)/journal(Threads風短文)
  • タクソノミー:series(作品シリーズ)× kind(本編・外伝・短編など)× post_tag
  • テンプレート約15本、functions.php 約2,000行
  • 独自プラグイン4本
  • 非公開セクション(後述の裏ページ)

設計の軸は「投稿の種類(CPT)」と「見せ方(kind)」の分離です。小説も絵も日記も、WordPress標準の投稿に押し込まず、最初から構造で分けました。後の拡張が全部この骨格に乗っています。

本題:「同人サイトの裏ページ」を真面目に実装する

昔の個人サイトには裏ページがありました。「キリ番を踏んだ方にURLをお教えします」のあれです。

※セキュリティ上の理由から、非公開領域の名称・識別子・URLなどは、実際の実装とは異なる仮名や簡略化した表現に置き換えています。

これを令和のWordPressでやるとどうなるか。要件はこうです。

  • 会員制にはしない(アカウント登録・認証なし。入口を知る人だけが入れる「ソフトゲート」)
  • 公開側の一覧・検索・RSS・アーカイブには出さない
  • うっかり設定を一つ間違えただけでは、本文が露出しない構造にする

実装は、kindタクソノミーに非公開フラグ(restricted)を立てて論理分離し、漏洩経路を三重で塞ぐ方式になりました。

防御①:pre_get_posts で自動除外

フロントエンドの全WP_Queryに kind NOT IN ('restricted') を自動付与します。裏ページ専用テンプレートだけ、クエリ引数 include_hidden_kind => true で除外をスキップします。

これで公開側の一覧・アーカイブ・検索・RSSからは構造的に消えます。

防御②:直接SQLは手動除外

サイドバーの月別アーカイブなど、$wpdb->get_results() を使う集計クエリには pre_get_posts が効きません。ここが落とし穴で、除外サブクエリのヘルパーを必ず噛ませる運用にしました。

AND p.ID NOT IN (
  SELECT tr.object_id FROM wp_term_relationships tr
  INNER JOIN wp_term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
  INNER JOIN wp_terms t ON tt.term_id = t.term_id
  WHERE tt.taxonomy = 'kind' AND t.slug = 'restricted'
)

防御③:REST APIの単体取得を塞ぐ

一番盲点だったのがここです。REST APIの一覧取得(/wp/v2/{type})は①で除外されますが、単体取得(/wp/v2/{type}/{id})はtax_queryを通らないので、IDを直接叩かれると本文が取れてしまいます。

対策は rest_pre_dispatch フィルタで該当ルートを検出し、kind=restrictedなら404のWP_Errorを返す方式。
ログイン編集者(current_user_can('edit_post'))だけ通します(Gutenbergで自分が編集できなくなるので)。

ハマりどころ:rest_prepare_{type} フィルタでWP_Errorを返すと link_header() でFatal(500)になります。必ず rest_pre_dispatch でやる必要がありました。

あとはOGP/meta出力プラグイン側で、裏ページ文脈では noindex,nofollow を付与し、OGPタグ自体を出さないようにしています。SNSにURLを貼られてもカードが展開されません。

設計思想を一行でいうと「見せたい人には見せる、事故では見せない」です。

なお、入口の場所はこの記事でも言いません。

その他、丸投げで生えてきたもの

  • 独自プラグイン4本:OGP管理/画像の自動WebP変換/動画サムネイル自動生成/そして一番大きいのが「ローカル→本番の差分同期デプロイ」。ローカルのWordPressで書き、ハッシュ比較で差分判定(new/update/skip)して、REST API+アプリケーションパスワード認証で本番へ反映します。画像も転送してURLを書き換えます(media_map)。
  • Transientキャッシュ:サイドバー系の重い集計を12時間キャッシュ、投稿保存時に自動フラッシュ。
  • セキュリティの基本:save_post系の権限チェック、$wpdb->prepare() の徹底。
  • 読ませ方・探し方の調整:シリーズ・種別・タグ・年月によるアーカイブ絞り込み、一覧をその場で追加表示する「続きを見る」、絵図を大きく閲覧するライトボックスなど、創作物の種類に合わせて表示と移動方法を作り分けました。

文系がハマった落とし穴メモ

  • URLパラメータに year / month は使えない(WordPress予約変数と衝突)。fy / fm に逃がしました
  • 固定ページIDをコードにハードコードすると、本番移行で全部ズレる。定数に集約して一箇所で直せるようにしました
  • レンタルサーバーのWAFがREST APIへのDELETEを403で弾く。デプロイはPOSTのみで設計

「丸投げ」の実際

正確に言うと、丸投げ=何もしない、ではありませんでした。分担はこうです。

私がやったこと

  • 「古の同人サイトの裏ページがほしい」のような要件を、文化ごと伝える
  • 動作確認して「ここが違う」と具体的に言い続ける
  • セッションをまたぐので、仕様書をAI自身に書かせて引き継ぐ(最終的に「ゼロから再実装できる粒度」の仕様書が2本残りました)
  • TOPページの組み立て(ヒーロー画像+作品紹介セクション)とInstagram連携プラグインの導入──サイトの「顔」は自分でやりました

AIがやったこと

  • 上記以外の全部(functions.php 2,000行、テンプレート15本、プラグイン4本)

スクール時代の知識は、コードを書くにはまったく足りませんでしたが、AIの説明を「言っていることは分かる」程度に追うのには役立ちました。
丸投げに必要なのはコーディング力ではなく、要件を言い続ける根気だと思います。

最後のセッションで、AIから「これは“ちょっと作った”ではない」という所見をもらいました。日記帳がほしかっただけなのですが。

おわりに

好きなものを好きなように作ったら、個人サイトが小規模CMSになっていました。

1年学んで畳んだ道の先に、まさかこういう形で戻ってくるとは思いませんでした。

この記事自体も、AnthropicのAI(Claude Fable)に書いてもらいました。技術的なことは私には分からないので、体験と素材を渡して整理してもらった形です。

作るのは楽しかった。
守るのは、これからが本番です。

Codexよ、あとは頼んだ。

そしてこのサイト、まだまだ発展途上です。
唐突な「NOT FOUND 404」にも、どうぞご容赦を。

サイト:https://kotodutefolio.com
住んでいる物語:『理玄異聞録』──標本医と書記官が、名づけられないものを観測し記録する近代幻想です。よければ文庫の棚からどうぞ。