自作の制作ツールです。掲載しているLP3枚は動作確認用に作ったもので、事業者名・電話番号・住所・料金はすべて架空です。

TOOL — LPツクレール

LPを量産するための、データベースを
使わないCMS「LPツクレール」

ランディングページは1枚ずつ作って、1枚ずつ捨てるものです。そのたびにデータベースを用意して、 移設のたびにダンプを取るのは重すぎます。専門的な知識がなくても、 ブロックを並べて作る形にしました。エックスサーバーにアップロードするだけで動きます (本アプリはエックスサーバーでの検証しか行っていないため、他のサーバーは対象外とさせていただきます)。

631自動テスト(失敗 0)
0外部ライブラリ
9ブロックの種類
11テンプレート
11,442PHP の行数
パーソナルジムのLP
3列カードと番号リストで畳みかける型
自家焙煎珈琲店のLP
引用と 2:1 の非対称カラムで読ませる型
歯科医院のLP
表を先頭に置き、1:1 で情報を並べる型
01

何を解決するために作ったのか

LPは 「速く作って、速く公開して、効果がなければ捨てる」 ものです。 ところが既存のCMSは、1サイトを長く運用する前提で作られています。 LPを10枚作ると、データベースが10個増えます。

よくあるCMS 1サイトを長く運用する前提 LP 1 DB LP 2 DB LP 3 DB … 10枚つくると、DB も 10個 LPツクレール 1枚ずつ作って、1枚ずつ捨てる LP 1 LP 2 LP 3 … data/ … JSON ファイルだけ DB は 0個
増えるのはファイルだけです。設置・移設・バックアップの手間が、枚数に比例して増えません。

必要だったのは、次の3つを同時に満たすものでした。

  • 設置が軽いこと — データベースの作成も、移設時のダンプも要らない
  • 1枚ごとに違ってよいこと — LPは宛先も電話番号もページごとに違う
  • LPページが壊れないこと — 編集する人がHTMLを知らなくても、レイアウトが崩れない

既製品を組み合わせるより、作ったほうが速いと判断しました。 外部ライブラリは1つも使っていません(composer も npm も使いません)。 依存が増えるほど、納品後に「動かない」が増えるためです。

02

LPツクレールとは

このツールで作った3枚です。骨格から作り分けています。 色を変えただけの使い回しではありません。枠の中はスクロールできます。

パーソナルジム STUDIO ARC のLP全体
パーソナルジム。写真→帯見出し→3列カード→料金表→番号リスト→ボタン→フォーム。 申し込みまで一直線に運ぶ構成です。見出しは背景帯、差し色は緑。
自家焙煎珈琲 まめの木 のLP全体
自家焙煎の喫茶店。店主の言葉を引用ブロックで主役にし、本文と「今月の豆」を 2:1 の 非対称カラムで組みました。見出しは下線だけ。急がせない誌面にしています。
みどり台デンタルクリニック のLP全体
歯科医院。来訪者が最初に知りたいのは診療時間なので、表を最上部に置きました。 診療内容とアクセスは 1:1 で並列。見出しは左に縦棒、色は紺。
この3枚は、同じテンプレートから複製したものではありません。

テンプレートは「複製して増やす」ための機能で、04 で扱います。 ここで見せたいのは、同じツールでまったく違う型を作れることです。

03

LPツクレールの管理画面

ブロックを上から積むだけです。HTMLもCSSも書きません。 1枚の画面に、ページの中身・ヘッダー・構造化データ・公開設定がすべて収まっています。

ブロックエディタ。画像ブロックと見出しブロックが並んでいる
ページ編集画面の上部。折り畳まれた「ヘッダー」「会社情報」の見出しには、 いま何が入っているかの要約が出ます(開かなくても分かるように)。

ブロックそのものも折り畳めます。畳むと 「入力項目4件(お名前 / メールアドレス / …)」のような要約が出るので、 開かなくても中身が分かります。並べ替えと削除は畳んだままでも使えるため、 長いページの構成を組み替えるのが速くなります。「すべて閉じる/すべて開く」も付けました。

編集者が壊せる余地を、意図的に狭くしています。 余白は「なし・小・中・大」から選ぶだけで、数値は入力させません。負の値を1つ入れられるだけで レイアウトは破綻します。色も5色のプリセットからで、カラーピッカーは付けていません(装飾の一部は、許可した範囲でCSSを直接指定できます)。

ヘッダーの設定欄。SNSのURLを入れるとアイコンが出る
ヘッダーはページごと。URLを入れるとSNSのアイコンが出ます(9サービスを判定)。
会社情報の設定欄。構造化データ用
検索エンジンとAI向けの構造化データ(schema.org)。これもページごとに持ちます。
ヘッダーをページごとにしたのは、作り直した結果です。

最初はサイト共通にしていました。実際に使ってみると、LPは1枚ごとに宛先も電話番号も違います。 「サイト」ではなく「1枚」が単位でした。ここで設計を入れ替えています。

04

量産の入口

新規作成すると、まずテンプレートを選びます。白紙を含めて11種類(白紙・シンプルLP・ サービス紹介・商品販売・セミナー/イベント告知・資料請求・採用・お問い合わせ・会社概要・ キャンペーン・よくある質問)。 既存のページをそのままテンプレートとして登録することもできます。

テンプレート選択画面
選んだ時点ではまだ保存されません。構成が入った編集画面が開くだけです。
ページ一覧
一覧から複製・ゴミ箱へ移動。トップページは削除できません—— 消すとサイトの / が404になり、しかも気づきにくいためです。理由を画面に書いています。
05

装飾は迷わせない、最低限を実装

装飾の選択肢は、意図的に絞っています。選べる項目が多いほど、作る人は迷います。 そのうえで、実際にLPを組んで「これが無いと成立しない」と分かった2つだけを足しました。 飾りを増やすためではなく、枚数を作ることに集中できるようにするためです。

画像の上に文字を重ねる

画像の上に重ねる文字の入力欄
位置(左・中央・右)と文字の明暗を選べます。

写真編集ソフトで文字を焼き込む方法は採りませんでした。 焼き込むと、検索エンジンにも読み上げにも社名が伝わらず、スマートフォンで縮んだときに読めません。 ここで入れた文字は本物のテキストとして出力されます。

位置と明暗を両方持たせたのは、3枚とも条件が違ったからです。 ジムは左が明るく、喫茶店は右の壁が暗く、歯科は中央に余白がありました。 固定値にすると3枚のうち2枚が読めなくなります。読みやすさのため、文字の側にだけ薄い幕をかけています。

スクロールでふわっと出す

余白の設定とスクロールでふわっと出すのチェックボックス
余白の隣にチェックが1つ。ブロックごとに選べます。
スクロールに合わせて要素をフェードインさせています。

この種の演出は、アニメーションが動かなかったときに要素が見えるか消えるかを先に決めておく必要があります。 要素を隠す処理は、JavaScriptではなくCSSに持たせました。 CSSは @media (scripting: enabled) の中で要素を隠します。 JavaScriptが動かない環境ではこの条件が偽になるため、ブラウザは隠す指定を適用せず、要素をそのまま表示します。 CSSはページの読み込み時点で効くので、要素が一度見えてから消えるちらつきも起きません。

※ OSで「動きを減らす」を選んでいる利用者には、ブラウザがアニメーションを実行しません。要素は最初から表示されたままになります。

06

なぜデータベースを使わないのか

設置ファイルをアップロードするだけ。データベースの作成も接続情報の設定も要りません
移設フォルダごとコピーすれば動きます。ダンプもインポートも不要
バックアップフォルダを丸ごと取れば完了。取り忘れる部分がありません
費用データベースの無いプランでも動きます

代わりに、データベースが面倒を見てくれていた部分を自分で作る必要がありました。

  • 書き込みの競合 — flock() で排他し、一時ファイルへ書いてから差し替える(書きかけのファイルが残らない)
  • 同時編集 — ページごとに版番号を持ち、古い版での上書きを拒否する。3人で同時に編集する検査を入れています
  • 履歴 — 保存のたびに世代バックアップを取る
/home/○○○/example.com/ app/ プログラム本体 data/ ページ・ユーザー・問い合わせ tools/ 運用のコマンド public_html/ index.php / admin/ / assets/ URL で届くのは、ここから下だけ URL では辿り着けない 設定ミスで中身が読まれることがない
サブドメインでは公開領域が親ドメインの public_html の中に入るため、 入口が自分の位置を辿って app/ を探す作りにしています。
07

HTMLを保存しない

本文を「自由なHTML」として持っていません。種類と値だけの構造化データとして保存し、 表示するときに組み立てます。

{ "type": "heading", "level": 2, "text": "料金", "deco": "band", "color": "green" }
{ "type": "image",   "src": "hero.jpg", "over": "STUDIO ARC", "over_pos": "left" }

保存されるのは値だけです。編集者が <script> と入力しても、 そう表示されるだけでスクリプトにはなりません。

HTML を保存する方式 入力 サニタイザ HTML として保存 出力 書き方の抜け道が見つかるたびに更新が要る LPツクレール 入力 値だけ保存 表示時に組み立て 出力 抜け道を探す相手がいない
サニタイザは「悪いものを消す」仕組みなので、消し漏れが起きます。 値しか保存しなければ、消す対象がそもそも生まれません。
「サニタイザで消す」のではなく、「そもそも通り道が無い」形にしています。

HTMLサニタイザは、書き方の抜け道が見つかるたびに更新が要ります。外部ライブラリへの依存も増えます。 値しか保存しない設計なら、抜け道を探す相手がいません。

見た目の調整は3つに限定しました。表の列ごとの配置のような意味のある属性、 設定ファイルで許可したクラス名だけを選ぶバリエーション、 比率がプリセットで入れ子が1段までのカラム。自由なCSSは受け取りません。

色と内側の余白だけは、ブロックごとに直接指定できます。ここも position・margin・@media・url()・ !important・負の値は拒否します。13項目の検査で確かめています。

スクリプトの持ち込み口も閉じています。

CSPは script-src 'self'。unsafe-inline は開けません。 構造化データやプレビューのインラインCSSは、内容のSHA-256をCSPに載せて通しています。

08

ログインと権限

ログイン画面
連続して失敗すると一定時間ロックします。
ユーザー管理画面
権限は管理者と編集者の2つだけ。増やすと運用で間違えます。

ユーザーの追加はメール招待です。保存するのはトークンのハッシュだけで、 招待リンクを再発行できる状態にはしていません。

アカウント画面。パスキーと二要素認証
パスキー(WebAuthn)と二要素認証(TOTP)。赤い注意書きはローカル環境で撮影したためで、 パスキーがHTTPSを要求する仕様によるものです。本番では出ません。
QRコードの生成も自作しています。

認証アプリに読ませるQRコードを、外部ライブラリなしで作っています。 リード・ソロモン符号・マスク8種・型番の自動選択まで実装し、 規格の値および参照実装との一致を36項目の検査で確かめています。 出力するSVGにはインラインの style も外部参照も入れません(CSPを緩めないため)。

2人が同じページを同時に開いても、片方の作業が消えません。

ファイルへの書き込みは flock() で排他し、一時ファイルへ書いてから差し替えます。 途中で処理が止まっても、書きかけのファイルが残りません。

さらにページごとに版番号を持ち、古い版での上書きは拒否します。 あとから保存した人には「他の人が更新しています」と伝わるので、 気づかないうちに消えることがありません。 3人が同時に編集する場面を検査に入れています。

09

お問い合わせ

問い合わせの一覧
どのLPから届いたかが分かります。
問い合わせの詳細
削除は世代バックアップにも残しません(個人情報のため)。

管理者へ通知し、送信者へ控えを返します。迷惑送信への対策は、 送信の挙動と本文の両面を、複数の条件で見て判定しています。 判定の一部は、設置先の事情に合わせて管理画面から調整できます。

弾いた理由を、送信者には教えません。

例外は1項目だけ——正当な送信者が自分で気づいて直せるものに限り、理由を伝えます。 誤って弾いた人を、直しようのないまま帰さないためです。 記録には生のIPを残さず、ハッシュ化して保存します。

通知先のメールアドレスだけは、管理画面から変更できません。

変更できると、大きな問題に発展しかねません。 ここは設定ファイルを直接編集する形にし、その手順を取扱説明書に書いています。

10

特商法・規約のページ

LPで物やサービスを売るなら、特定商取引法に基づく表記・ 利用規約・プライバシーポリシーが要ります。 ところが「何をどう書けばいいか」で止まりがちです。 入力欄そのものを書式にして、白紙から書かせないようにしました。

特定商取引法に基づく表記 販売業者・所在地・支払方法・引渡時期・返品の扱いなど、 記載が必要な11項目があらかじめ並びます。内容を埋めるだけです
利用規約 適用・禁止事項・提供の停止・免責・準拠法など7条の下書き
プライバシーポリシー 取得する情報・利用目的・第三者提供・開示請求など8条の下書き

規約は別ページとして作らせません。LPに入力すると、 /<LPのURL>/terms/ のような専用URLが自動でできて、 フッターのリンクから新しいタブでその規約だけが開きます。 LPを複製すれば規約もついてきますし、LPを消せば規約も一緒に消えます。 ページ一覧が規約で埋まることもありません。

特定商取引法のページには、所在地の地図も出せます。 住所は会社情報から取るので、入力し直す必要はありません。 地図を出すページだけ、外部の枠内表示を許可する指定 (frame-src)を足します。出さないページでは閉じたままで、 スクリプトの許可には一切触れていません。

用意するのは書式です。

入力されるひな形は書式の見本であって、そのまま使える完成品ではありません。 記載内容が要件を満たしているかは、依頼元またはお近くの専門家の確認が要ります。 管理画面にもそう明記しています。

11

納品後について

FTPを触れない人でも設定を変えられるようにしています。

サイト設定画面
サイト名・公開の切り替え・SNS共有画像・画像の上限・迷惑送信の判定基準まで、画面から変更できます。 書き換えてよい項目は許可リストで限定し、それ以外の設定には触れません。

問い合わせは保存日数を過ぎると自動で消えます(サーバーの定期実行)。 取扱説明書はHTML1枚で同梱し、更新時に data を上書きしてはいけないことを 赤枠で書いています。ここを間違えると、ページも問い合わせも消えます。

12

自動テスト 631項目

画面を目で見て確かめるのは、回数が増えると必ず抜けます。 コマンド1つで631項目を通すようにしました。実際に本番へ上げる前に、毎回これを通します。

壊れやすい所同時編集の競合、URLの重複、予約公開、ゴミ箱、トップページの入れ替え
安全側XSS、CSRF、権限、セッション、CSPのハッシュ、招待トークン、通知先が漏れていないこと
規格TOTP(RFC 6238のテストベクタ)、QRコード(生成多項式・シンドローム・参照実装との一致)
整合SNSの判定表がPHPとJavaScriptで一致すること、編集画面の入力欄と保存名が食い違わないこと
この検査は、実際に不具合を見つけています。

見出しの段数が抜けたときに <h0> という無効なタグが出ること、 表の見出しの空セルが捨てられて列が1つずれること。 どちらも通常の操作では起きませんが、直したうえで検査を足しました。

13

構成と規模

PHP49ファイル / 約11,442行
CSS・JavaScript約2,644行
クラス20(Auth Blocks Csrf Css Form Qr Totp Webauthn ほか)
外部ライブラリなし(composer / npm を使っていません)
動作環境PHP 8.3以上 / エックスサーバー
状態本番稼働中。取扱説明書つきで配布できる状態