記事の下のカード、文言はAとBのどちらがクリックされるのか。
WordPress時代は、テーマのPHPがこの比較を勝手にやってくれていました。
ところが2026年10月にブログを静的サイトへ移したら、サーバーで動く仕組みがまるごと消えて、ABテストも一緒に止まりました。
有料ツールを入れるほどのアクセスはありません。
そこで11月1日に、Astroの部品1つとGA4だけでABテストを自作しました。
振り分けのコードは39行です。
動かしてみて分かった落とし穴と、月数千PVのブログで結果が出るまでの日数の見積もりまで、実物のコードでまとめます。
この記事でわかること
- 静的ブログでABテストを自作する仕組み(ブラウザで振り分け→GA4で比べる)
- 無料ツール・Cloudflare Workers・自作の3つの選び方
- Astroで実際に動かしているコード一式と4つの手順
- 画面のガタつきとGA4の数字のずれを防ぐ3つの落とし穴
- 必要なサンプル数の計算と、結果が出るまでの日数の見積もり
PARTS LIST /目次
静的ブログのABテストは自作できる?結論と全体像
静的ブログのABテストは、ブラウザ側で振り分けてGA4で比べれば自作できます。
サーバーが無くても困らない理由は単純で、振り分けも計測もすべて読者のブラウザの中で終わるからです。
用意するものは次の3つだけでした。
- テストの一覧を書いた設定ファイル(どのページで・いつから・何を比べるか)
- ページを描く前に訪問者をA・Bに分ける短いスクリプト
- 振り分け結果を全イベントに付けて送るGA4の設定
全体の流れを図にすると、こうなります。
flowchart TD
A["訪問者がページを開く"] --> B{"localStorageに<br>振り分け済み?"}
B -->|"まだ"| C["乱数でAかBを決めて保存"]
B -->|"済み"| D["保存した側を使う"]
C --> E["html要素にab-キー-aかbのクラスを付ける"]
D --> E
E --> F["CSSで片方の要素だけ表示"]
E --> G["GA4の全イベントにab_キー=aかbを付ける"]
G --> H["GA4の探索でAとBを比べる"]
ポイントは、振り分けを「ページを描く前」に済ませることです。
ここを後回しにすると、Bの要素が一瞬遅れて出てきて画面がガタつきます。この話は後半の落とし穴でもう一度出てきます。
ABテストとは?1人に片方だけ見せて比べる方法
ABテストとは、訪問者をランダムに2組に分け、片方にだけ変更を見せて、行動の差を比べる方法です。
AとBを同じ期間に並べて走らせるのがミソで、曜日や季節の影響が両方に同じだけ乗ります。
「先月はA、今月はB」と時期で比べると、お盆や検索順位の上下まで混ざってしまいます。
実際、私のブログは2026年8月にサイト全体の順位が一時的に落ちて、9月に戻りました。時期比較だったら、文言の効果なのか順位の波なのか、まず区別できなかったはずです。
同じ人には次に来たときも同じ側を見せることも大事な条件です。来るたびにAとBが入れ替わると、比べている中身が崩れてしまいます。
WordPressのABテストが静的サイトに移せなかった
2026年10月23日、code-rebuildをWordPressからAstro+Cloudflare Pagesの静的サイトに切り替えました。
移行の計画を立てた段階で、引っかかったのがABテストです。
記事下の「NEXT READ」カードとサイドのカードは、WordPress時代にPHPで振り分けるABテストを回していました。
移行で失う機能を表に書き出していくと、ABテストだけ置き換え先がすぐに決まりませんでした。PHPで動くものは、静的サイトでは動きません。候補は「GA4のイベントとJavaScriptで振り分ける」か「CloudflareのWorkersで振り分ける」の2つ。結局、記事下とサイドのカードは、テスト中だった片方の文言のまま静的サイトへ移し、ABテストはいったん止めました。
止めたまま9日ほど過ごして、11月1日に自作版を動かし始めました。
移行そのものの話は、別の記事にまとめています。
無料のABテストツールとどちらを選ぶ?3つの方法
静的サイトでABテストをする方法は、大きく3つあります。
どれが正解というより、アクセスの量と「何を外に預けたくないか」で決まります。
月数千PVの個人ブログなら、ブラウザで振り分ける自作がいちばん軽い、というのが私の結論です。
ブラウザで振り分ける・Workersで振り分ける・ツールを入れる
3つの方法を、静的ブログで使う前提で並べました。
Workersは強力ですが、記事ページの配信の前に毎回コードが挟まります。壊したときにサイト全体が止まる場所へ、ABテストのためだけに手を入れたくありませんでした。
ツールは管理画面が楽なぶん、スクリプトを1本増やすことになります。表示速度を気にして静的サイトへ移った身としては、ここも避けたいところでした。
私がブラウザの振り分けを選んだ理由
ブラウザでの振り分けを選んだ理由は、アクセスの少なさと、壊れたときの被害の小ささです。
当時のcode-rebuildは14日で約4,200PVでした。
この量では、細かい文言の違いを比べても、差が出るまでに何か月もかかります。高機能なツールを入れても、使える場面がほとんどありません。
自作で済んだこと
振り分け・片方だけの表示・GA4への記録。設定ファイルとAstroの部品1つで足りた
自作で諦めたこと
有意差の自動判定・結果の画面。GA4の探索で数字を出し、判定は自分で計算する
もう1つの決め手は、失敗したときの姿です。
振り分けのスクリプトが動かなかった場合、ABテスト用の要素は両方とも隠れたままになり、読者には今までどおりのページが出ます。壊れても「テストが行われないだけ」で済む作りにできたのが大きかった。
AstroでABテストを自作する4ステップ
ここからは、code-rebuildで実際に動かしているコードを順に見せます。
部品は設定ファイル(36行)と振り分けの部品(39行)の2つで、記事側には目印の属性を1つ付けるだけです。
手順は次の4つに分かれます。
テストの一覧をabtests.tsに書く
step
1テストの一覧をsrc/data/abtests.tsに書く
最初に、どのテストを・いつから・どのページで走らせるかを1か所にまとめます。
テストを増やすたびに部品を書き換えずに済むよう、設定と仕組みを分けました。
export interface ABTest {
/** テストの名前(英小文字と _)。クラス名・GA4 のパラメータ名になる */
key: string;
/** 開始日時(ISO 8601。日本時間なら +09:00 を付ける) */
start: string;
/** 終了日時(この時刻から振り分けない) */
end?: string;
/** 対象ページのパス。省略すると全ページ */
paths?: string[];
/** 何を比べているか(メモ) */
note: string;
}
export const AB_TESTS: ABTest[] = [
{
key: "books_top",
start: "2026-11-01T00:00:00+09:00",
paths: ["/ai/claude-code-books-recommendation/"],
note: "B だけ「結論」の直後に5冊の購入ボタンを並べる",
},
];
開始日時はブラウザで判定するので、開始日に合わせてデプロイし直す必要はありません。先に書いておけば、その時刻から勝手に始まります。
終わったテストは消さずに end を入れて残しておくと、あとで「いつ何を比べたか」を追えます。
head の最初で訪問者をA・Bに振り分ける
step
2head の最初で訪問者をA・Bに振り分ける
振り分けのスクリプトは、<head> のいちばん最初、Googleタグより前に置きます。
本番のコードは1行に詰めているので、読みやすく開いたものを載せます。
(function () {
var tests = [/* abtests.ts からビルド時に埋め込む */];
var now = Date.now();
var path = location.pathname;
var params = {};
tests.forEach(function (t) {
// 期間外・対象外のページでは何もしない
if (now < t.start || (t.end && now >= t.end) || (t.paths && t.paths.indexOf(path) < 0)) return;
var key = "cr-ab-" + t.key;
var v;
try { v = localStorage.getItem(key); } catch (e) {}
// 初めての人だけ半々で決めて覚える
if (v !== "a" && v !== "b") {
v = Math.random() < 0.5 ? "a" : "b";
try { localStorage.setItem(key, v); } catch (e) {}
}
document.documentElement.classList.add("ab-" + t.key + "-" + v);
params["ab_" + t.key] = v;
});
window.crAB = params;
})();
localStorage に覚えるので、同じ人は次に来ても同じ側を見ます。プライベートブラウズなどで読み書きが失敗しても、try で囲んであるので止まりません。
Astroでは、このスクリプトを <script is:inline> で出力します。is:inline を付けないと、Astroがスクリプトを処理して type="module" の読み込みに変えるので、HTMLを読み終わるまで実行されません。
片方だけに出す要素にdata-abを付ける
step
3片方だけに出す要素にdata-abを付ける
Bにだけ出したい要素には、data-ab="books_top:b" のように「テスト名:側」の属性を付けます。
表示の切り替えはCSSだけで行います。振り分けの部品が、テストの一覧から次のようなCSSを作って <head> に入れます。
/* 既定では両方とも隠す */
[data-ab] { display: none }
/* 振り分けたクラスが付いたときだけ出す */
.ab-books_top-a [data-ab="books_top:a"] { display: block }
.ab-books_top-b [data-ab="books_top:b"] { display: block }
既定で隠しておくのがコツで、期間外のページやJavaScriptが動かない環境では、ABテスト用の要素はどちらも出ません。読者には元のページがそのまま見えます。
記事のMDXには、Bの要素をふつうに書いて属性を足すだけです。
<div class="cr-books-quick" data-ab="books_top:b">
<a href="https://www.amazon.co.jp/...">Amazon</a>
<a href="https://hb.afl.rakuten.co.jp/...">楽天</a>
</div>
GA4の全イベントに振り分け結果を付ける
step
4GA4の全イベントに振り分け結果を付ける
最後に、どちらの側を見た人の行動なのかをGA4に記録します。
Googleタグの初期化で、振り分けの結果 window.crAB を gtag("set") と gtag("config") の両方に渡しました。
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag("js", new Date());
gtag("set", window.crAB || {});
gtag("config", "G-XXXXXXXXXX", window.crAB || {});
こうすると、page_view を含む全イベントに ab_books_top=a のようなパラメータが付きます。
GA4側では、管理画面の「カスタム定義」で、イベントスコープのディメンション ab_books_top を登録しておきます。登録した日からしか集計されないので、テストを始める前に済ませるのが安全です。
GA4での比べ方
探索のレポートで、行にディメンション ab_books_top、値にイベント数やユーザー数を置くと、A・Bそれぞれの数字が並びます。
振り分けで画面がずれる・数字がずれる3つの落とし穴
自作ABテストの落とし穴は、画面の見た目と、GA4の数字の2か所に出ます。
どちらも動かしてすぐには気づきにくく、数週間たってから「なんか変だ」となるタイプです。
私が踏んだもの、踏みかけたものを3つ挙げます。
Bの要素が後から出て画面がガタつく
振り分けをページの読み込み後に行うと、Bの人には要素が一瞬遅れて現れ、本文がガクッと下にずれます。
読者にとって不快なうえ、Bの側だけ「ずれた画面」を見せることになるので、比べている条件そのものが変わってしまいます。
2本目のテスト related_mid では、記事下にある関連記事2本を、本文の真ん中の見出しの前へ移して見せます。要素を動かす処理を本文の描画後に回すと、ずれが目に見えてしまう。そこで要素の直後に is:inline のスクリプトを置き、描画の前に移動を済ませました。Bでない人や、見出しが4つ未満の短い記事では、その場で要素ごと消します。
対策は、振り分けも要素の移動も、描画より前に終わらせることに尽きます。
gtag("set") を "js" より前に積むと無視される
最初は、振り分けのスクリプトの中で gtag("set") まで済ませるつもりでした。
振り分けはGoogleタグより前に動くので、まだ gtag("js") が積まれる前に set を積むことになります。
ところが、この順番で積んだ set は効きませんでした。そこで振り分けのスクリプトは結果を window.crAB に置くだけにして、Googleタグの初期化の側で gtag("js") のあとに渡す形へ直しました。
確かめるときは、GA4のDebugViewで page_view に ab_books_top のパラメータが付いているかを見ます。付いていなければ、何週間回してもA・Bの比較ができません。
期間外・JSなしの人に何を見せるか
意外と決め忘れるのが、テストの対象にならなかった人に何を見せるかです。
開始前・終了後・対象外のページ・JavaScriptが動かない環境では、どちらの側も選ばれません。
私は「元のページ=A」としていて、Aの要素は基本的に何も足していません。この形だと、テストが壊れても読者の画面は今までと同じです。
逆に、Aにも専用の要素を出すテストを作るときは、両方とも隠れたページが元のページより貧弱にならないかを確かめます。
ABテストのサンプル数はどう決める?途中で止めない
ABテストは、始める前に「何人分集めたら結果を見るか」を決めておくのがいちばん大事な準備です。
途中で何度も数字を見て、差が開いた瞬間に止めると、たまたまの偏りを「勝ち」と読み違えやすくなります。
ここでは、私が実際に2本のテストで計算した数字を出します。
必要な数は有意水準5%・検出力80%で計算する
必要なサンプル数は、今の割合・見分けたい差・有意水準・検出力の4つで決まります。
有意水準5%・検出力80%は、ABテストでよく使われる慣例の値です。2つの割合を比べるときは、次の近似式で1群あたりの人数が出ます。
n = ( 1.96 × √(2p̄(1−p̄)) + 0.84 × √(p1(1−p1) + p2(1−p2)) )² ÷ (p2 − p1)²
p1 = 今の割合 / p2 = 見分けたい割合 / p̄ = (p1 + p2) ÷ 2
この式に、code-rebuildの実績を入れた結果が次の表です。
計算ツールを使っても同じ式で出ます。式を覚えるより、「今の割合」と「見分けたい差」を先に決めることのほうが効きます。
月数千PVのブログは差の大きい案だけ比べる
アクセスの少ないブログでは、差の小さい案を比べるとサンプルが集まりきらず、結論の出ないまま終わります。
見分けたい差が小さいほど、必要な人数は急に増えるからです。

同じ式で、クリック率13%を14%と見分けようとすると、A・B合計で約36,700PV。おすすめ本の記事は2週間で148PVなので、ざっと10年かかります。
そこで、比べる案は「差が大きく出そうなもの」だけに絞りました。
books_topは、おすすめ本の記事で購入ボタンを結論の直後に並べるかどうか。related_midは、関連記事を記事の途中に出すかどうか。どちらも、文言の言い換えではなく「あるか・ないか」の比較です。
実際に動かしている2つのテストと結果を見る日
2026年11月4日の時点で、code-rebuildでは2本のテストを用意しています。
結果はまだ出ていません。結果を見る日を先に決めておき、それまでは途中の数字で判断しないことにしています。
books_topの対象にしたのは、ブログ全体のアフィリエイトのクリックがほぼこの1記事に集まっていたからです。14日で23回のクリックのうち、ほぼすべてがこの記事でした。
related_midの開始を11月15日にずらしたのには理由があります。
10月17日に、記事の前半にリンクを足したり関連記事を上へ動かしたりする回遊の改善をまとめて入れていて、その効果を11月14日に測る予定でした。同じ時期に関連記事のテストを始めると、どちらの効果か分からなくなります。そこで設定ファイルの開始日時だけ11月15日にして、仕組みは先に入れておきました。
テストを終わらせるときは、設定に end を入れます。Bが勝てばBの要素から data-ab を外して常に出し、負ければ要素ごと消します。
よくある質問
静的ブログでABテストを自作するときに、引っかかりやすい点をまとめました。振り分けの仕組み・GA4の設定・サンプル数の3つに関する質問が中心です。細かいコードは各手順の見出しもあわせて確認してください。
Q1. 静的サイトでもABテストはできますか?
A1. できます。読者のブラウザでA・Bを振り分け、結果をGA4のイベントに付けて送れば、サーバーが無くても比べられます。Cloudflare Workersのように配信の手前で振り分ける方法もあります。
Q2. 無料のABテストツールではなく自作する利点は何ですか?
A2. スクリプトを増やさずに済み、壊れても元のページが出るだけで済む点です。そのかわり有意差の判定や結果の画面はないので、GA4の探索で数字を出して自分で判断します。
Q3. ABテストの振り分けはランダムでいいのですか?
A3. 初回だけ乱数で半々に決め、localStorageに保存して次回も同じ側を見せれば十分です。来るたびに振り分け直すと、同じ人がA・B両方を見てしまい、比べる条件が崩れます。
Q4. GA4でABテストの結果を見るには何が必要ですか?
A4. 振り分け結果をパラメータとして全イベントに付けることと、そのパラメータをイベントスコープのカスタムディメンションとして登録することの2つです。登録した日からしか集計されないので、テスト開始前に登録します。
Q5. ABテストのサンプル数はどれくらい必要ですか?
A5. 今の割合と見分けたい差で決まります。code-rebuildでは、クリック率13%を20%と見分けるのにA・B合計で約880PVが必要でした。3%を4.5%と見分けるほうは約5,030セッションです(どちらも有意水準5%・検出力80%)。
Q6. 途中で差がついたらテストを止めてもいいですか?
A6. 決めた数に届くまでは止めないのが原則です。何度も途中の数字を見て差が開いた時点で止めると、たまたまの偏りを勝ちと読み違えやすくなります。
まとめ:自作ABテストを始める前に決める3つのこと
静的ブログのABテストは、ブラウザで振り分けてGA4で比べる形なら、部品1つで自作できます。
コードより先に、次の3つを決めておくと迷いません。
- 何を比べるか(月数千PVなら「あるか・ないか」くらい差の大きい案)
- 何人分集めたら結果を見るか(今の割合と見分けたい差から計算する)
- 対象にならなかった人に何を見せるか(元のページをAにすると壊れても安全)
私のテストは、1本目の結果が2026年12月6日ごろ、2本目が2027年1月末ごろに出る見込みです。
購入ボタンを結論の直後に置いて、クリックは本当に増えるのか。今のところ、増えるに違いないと思っています。その思い込みが当たっているかどうかは、決めた数が集まるまで見ないことにしています。
GA4で今のクリック率を出し、見分けたい割合と一緒に式へ入れるだけです。必要な日数が1年を超えるなら、もっと差の大きい案に変えます。
続きを追う:X(@Tom_Java_Tech) / RSS
途中経過は、また書きます。



