【完全無料】SEO検索順位チェックツールを自作する方法 Search Console API × GASで順位自動取得+毎日メール通知
INDEX

SEO検索順位チェックを毎日手動で行うのは大変
SEOを続けていると、必ず気になってくるのが 検索順位 と 検索データの変化 です。
たとえば、こんなことはないでしょうか。
- SEOチェキでキーワードの順位を手動チェック
- 昨日まで上がっていたキーワードが、今日は落ちている
- どのページが検索に強いのか、なんとなくしか把握できていない
- Search Consoleは見ているけれど、データを蓄積していない
- 毎日確認したいが、手作業で見るのは正直しんどい
- クライアントごとにSEOの状況を見たいが、毎回同じ作業を繰り返している
- 有料のSEO順位チェックツールを使うか悩んでいる
私自身、Search Consoleを見ながら「この作業、もう少しどうにかならないか」と何度も思ってきました。
しかし、
- 手動チェックは時間がかかる
- 有料ツールは月額費用が高い
- Search Consoleではキーワード単位の順位履歴が追いにくい
という問題があります。
手動チェックだと、検索順位や表示回数、CTRの変化はSEO改善のヒントそのものなのに、記録していないだけで見逃してしまうことが本当に多いからです。
そこで作ったのが、今回紹介する
Google Search Consoleの検索データを、Google Apps Script(GAS)とスプレッドシートで毎日自動取得し、SEO分析までできる仕組み
です。
この記事では、単なる「Search Consoleのデータ取得」では終わりません。
- 毎日自動取得
- スプレッドシートに蓄積
- キーワード分析
- ページ分析
- 改善キーワード抽出
- 地域分析
- 順位変動チェック
- 要約メール通知
までを、1つの無料ツールとして自作する方法をまとめて解説します。
しかも、今回紹介するコードは他サイトでも使い回せる構成 にしています。
つまり、
- 自社サイト用
- クライアントサイト用
- 地域SEOの分析用
- SEO運用の社内テンプレート用
として、そのまま応用できます。
この記事は少し長いですが、そのぶん
この記事1本で、Search Consoleの自動取得とSEO分析の土台まで全部作れるようにしています。
この記事でできるようになること
この記事を読み終えると、次のことができるようになります。
Search Consoleの検索データを毎日自動取得できる
手動でSearch Consoleを開かなくても、GASが毎日自動でデータを取得してくれます。
スプレッドシートにSEOデータを蓄積できる
検索クエリ、ページ、クリック数、表示回数、CTR、平均掲載順位を日々の履歴として残せます。
SEO分析ができる
取得したデータをもとに
- どんな検索キーワードで流入しているか
- どのページが検索に強いか
- 改善すべきキーワードは何か
- どの地域で見られているか
が分かります。
毎日メールで状況を把握できる
毎日、要点だけをまとめたSEOレポートがメールで届くので、Search Consoleを毎回開かなくても全体の変化を追えます。
このツールが他の記事より実用的な理由
Search Console × GAS の記事はネット上にもあります。
ただ、多くは次のどちらかです。
- データを取るだけで終わる
- コードはあるが、実運用まで考えられていない
つまり、“動くこと”がゴールになっている記事が多いです。
でも、SEOの現場で本当に必要なのはそこではありません。
必要なのは、
- 毎日自動で動くこと
- データが重複しないこと
- 分析できる形で整理されること
- 他サイトにも使い回せること
- メール通知まで含めて手間が減ること
です。
今回のツールは、そのために
- 重複防止
- 9シート構造
- 変化チェック
- 要約メール
- 設定シートによる使い回し対応
まで入れています。
この点が、一般的な「GASで取得してみた」記事との大きな違いです。
完成イメージ
このツールを作ると、スプレッドシートには次の9シートができます。
作成される9シート
- 設定
- 検索データ
- 検索クエリ分析
- ページ分析
- 改善キーワード
- 地域分析
- 月次サマリー
- 変化チェック
- ログ
各シートの役割
設定
取得対象サイトや通知先メール、分類用の辞書を管理します。
他サイトに使い回すときの起点になるシートです。
検索データ
Search Consoleから取得した生データを蓄積します。
ここが元データになります。
検索クエリ分析
検索クエリ単位で集計し、
表示回数・CTR・平均順位などを分析します。
ページ分析
どのページがどの程度検索に強いかを見ます。
改善キーワード
「表示はされているが、もっと伸ばせる」キーワードを抽出します。
地域分析
地域名を含むクエリをまとめ、
ローカルSEOの状況を把握します。
月次サマリー
月単位でSEO全体の推移を見ます。
変化チェック
前回と今回を比較し、
新規出現・改善・悪化・消失を確認します。
ログ
処理結果やエラーの記録です。
不具合時の切り分けに役立ちます。
なぜSearch Consoleをスプレッドシートに保存するべきなのか
Search Consoleは非常に便利なツールですが、日々のSEO分析を続けるうえでは、いくつか不便な点もあります。
たとえば、
- 過去データを自分の見たい形で比較しづらい
- キーワードの変化を一覧で追いにくい
- 地域別や業種別に整理しづらい
- クライアントごとにレポート化するのが大変
といった点です。
そのため、Search Consoleのデータをスプレッドシートに保存しておくと、日々のSEO分析がかなりやりやすくなります。
理由1 – 過去データを蓄積できる
Search Consoleでも一定期間のデータは見られますが、長期的な比較や、独自の切り口での分析には向いていません。
スプレッドシートに保存しておけば、
- 先月との比較
- 去年との比較
- タイトル変更前後の変化
- 記事公開後の推移
などを、自分の見たい形で整理できます。
理由2 – SEO改善のヒントを見つけやすい
SEOでは、順位だけを見ても十分ではありません。
- 表示回数が多いのにクリックされていないキーワード
- 順位が少しずつ上がってきているページ
- 新しく検索に出始めたクエリ
などを見つけることが重要です。
スプレッドシートに保存して分析すると、こうした改善ポイントを見つけやすくなります。
理由3 – クライアントワークや社内共有がしやすい
SEOデータをそのまま共有しようとすると、毎回Search Consoleを開いて説明する必要があります。
一方で、スプレッドシートに整理しておけば、
- クライアント向けレポート
- 社内共有
- 月次振り返り
がしやすくなります。
このツールが向いている人
このツールは、次のような人に向いています。
制作会社の担当者
複数のクライアントサイトを見ていて、SEO状況をまとめて把握したい人。
自社サイトを育てたい人
ブログやサービスページの検索評価を、ちゃんと数字で追いたい人。
地域SEOを強化したい人
「香川県」「高松市」「東かがわ市」など、地域キーワードでの見え方を整理したい人。
無料でSEO分析の仕組みを作りたい人
高額なSEOツールを使わず、まずは無料でしっかり分析環境を整えたい人。
準備するもの
作業前に必要なものを確認しておきます。
1. Googleアカウント
Gmailが使えるGoogleアカウントが必要です。
2. Search Consoleに登録済みのサイト
対象サイトがSearch Consoleに登録されている必要があります。
3. Googleスプレッドシート
データ保存と分析に使います。
4. Google Apps Script
スプレッドシートから使えます。
追加料金は不要です。
STEP1|スプレッドシートを作成する
まずはGoogleスプレッドシートを新規作成します。
名前は何でも構いませんが、たとえば次のようにしておくと分かりやすいです。
- Search Console SEO分析ツール
- SEO順位・検索データ管理
- Search Console自動取得ツール
STEP2|9つのシートを作成する
次に、以下の9シートを作成してください。
作成するシート名
- 設定
- 検索データ
- 検索クエリ分析
- ページ分析
- 改善キーワード
- 地域分析
- 月次サマリー
- 変化チェック
- ログ

STEP3|設定シートを入力する
このツールの使い回しやすさは、設定シート にあります。
コードを書き換えなくても、設定シートの内容を変えるだけで別サイトに流用できます。
A列・B列に入れる項目
| A列 | B列 |
|---|---|
| サイトURL | https://example.com/ |
| 通知メール | yourmail@example.com |
| 取得日数 | 28 |
| データ遅延日数 | 2 |
| 改善候補最小表示回数 | 20 |
| 改善候補順位下限 | 5 |
| 改善候補順位上限 | 20 |
D列〜H列に入れる項目
| D列 | E列 | F列 | G列 | H列 |
|---|---|---|---|---|
| 監視キーワード | 指名語句 | 地域語句 | 業種語句 | 採用語句 |
たとえば、地域のWEB制作会社であれば次のように設定できます。
※監視キーワードは、実際に検索上位を狙っているキーワードを入れるのがおすすめです。
| 監視キーワード | 指名語句 | 地域語句 | 業種語句 | 採用語句 |
|---|---|---|---|---|
| WEB制作 香川県 | 香川 | WEB制作 | 採用 | |
| ホームページ制作 さぬき市 | 香川県 | ホームページ制作 | 求人 | |
| ウェブ制作 東かがわ市 | 高松 | Web制作会社 | 募集 | |
| WordPress制作 香川県 | 高松市 | WordPress制作 | エンジニア | |
| ホームページ制作 香川県 | 東かがわ | SEO | デザイナー | |
| WEB制作 高松市 | 東かがわ市 | WEB制作会社 | スタッフ |
指名語句には、会社名やサービス名を入れます。
例えば
- 株式会社フック
- フック WEB制作
- フック ホームページ制作
などです。
これは「ブランド検索」と呼ばれ、ユーザーが会社名を知っている状態で検索したキーワードになります。
ただし、今回のツールは地域SEO分析が主目的なので、必ず設定する必要はありません。
なぜ設定シートで辞書を持つのか
ここがこのツールの独自性です。
多くのGAS記事は、会社名や地域名がコードに直接書かれています。
そのため、別サイトに流用しようとするとコードを書き換える必要があります。
でもこのツールは、
会社名・地域名・業種語句・採用語句を設定シートで管理するので、対象サイトが変わっても使い回しやすいです。

STEP4|Apps Scriptを開く
スプレッドシートの上部メニューから、以下を開きます。
操作手順
拡張機能 → Apps Script
これでGASの編集画面が開きます。

STEP5|appsscript.json を表示する
このツールでは、Search Console APIを直接呼び出すため、appsscript.json の設定も必要です。
表示方法
Apps Script画面左の 歯車アイコン(プロジェクトの設定) を開きます。
その中にある「appsscript.json マニフェスト ファイルをエディタで表示」をオンにします。
オンにすると、エディタに「appsscript.json」が追加されます。

appsscript.json に入れる内容
appsscript.jsonの内容を下記のコードに書き換えます。
{
"timeZone": "Asia/Tokyo",
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/script.send_mail",
"https://www.googleapis.com/auth/webmasters.readonly"
]
}この設定が必要な理由
このツールでは
- スプレッドシートの読み書き
- 外部APIへのリクエスト
- メール送信
- Search Consoleデータ読み取り
を行うため、必要な権限を明示しておく必要があります。

STEP6|Google Cloud プロジェクトと Search Console API を有効化する
ここは、多くの人がつまずくポイントです。
でも避けて通れないので、丁寧に説明します。
なぜ必要なのか
Apps Script から Search Console API を使うには、
紐づいている Google Cloud Platform(GCP)プロジェクト で
Search Console API を有効化しておく必要があります。
つまり、
- APIを有効化したプロジェクト
- Apps Scriptが使っているプロジェクト
この2つが一致していないとエラーになります。
手順

- Apps Script の「プロジェクトの設定⚙️」を開く
- 「Google Cloud Platform プロジェクト」の欄を確認する
- デフォルトの場合、「プロジェクトを変更」をクリックする
こちらのリンクから Google Cloud に移動する- 対象プロジェクトを選択する
- 左上の「三本線メニュー」→ 「APIとサービス」→「ライブラリ」をクリック。
検索欄に「Search Console API」と入力。
すると「Google Search Console API」が出ます。
そのページに入ると「有効にする」ボタンがあるのでクリックして、「Search Console API」を有効にする - 対象プロジェクトで表示されている プロジェクト番号 を Apps Script 側でGoogle Cloud Platform(GCP)プロジェクトに設定する


よくある落とし穴
落とし穴① APIを有効化したのに403が出る
原因は、プロジェクトが違うことが多いです。
落とし穴② 権限が足りない
デフォルトプロジェクトだと操作できないことがあります。
その場合は、自分が操作できるGCPプロジェクトを設定し直す必要があります。
プロジェクトを変更するために、OAuth 同意の詳細から認証情報ページに移動して設定してください。
認証情報ページの左メニューから「OAuth 同意画面」 をクリック。
左メニュー「概要」が選択されている状態で、画像中央の「開始」をクリック。

アプリ情報を入力します。
アプリ名:「SEO順位チェックツール (任意)」
ユーザーサポートメール:「自分のGoogleアドレス」
対象:「外部」
連絡先情報:「自分のGoogleアドレス」
Google API サービス: ユーザーデータに関するポリシー :同意する
最後に作成ボタンをクリックします。

STEP7|コードを貼り付ける
ここで、記事の中心になるコードを「コード.gs」に貼ります。貼り付け後は、保存してください。
※デフォルトのコードは削除してください。
このコードには、検索データ取得、重複防止、検索クエリ分析、ページ分析、改善キーワード抽出、地域分析、月次サマリー、変化チェック、要約メール送信、さらに設定シートによる使い回し対応が含まれています。
コード全文
/*************************************************
* 地方SEO分析ツール 最終版
* 他サイト使い回し対応 / 日本語シート名対応
* - Search Console API 直接取得
* - 重複防止
* - 検索クエリ分析
* - ページ分析
* - 改善キーワード抽出
* - 地域分析
* - 月次サマリー
* - 変化チェック
* - 要約メール送信
* - 会社名 / 地域名 / 業種語句 / 採用語句を設定シートで変更可能
*************************************************/
const シート設定 = "設定";
const シート検索データ = "検索データ";
const シート検索クエリ分析 = "検索クエリ分析";
const シートページ分析 = "ページ分析";
const シート改善キーワード = "改善キーワード";
const シート地域分析 = "地域分析";
const シート月次サマリー = "月次サマリー";
const シート変化チェック = "変化チェック";
const シートログ = "ログ";
/* =========================
* 公開用実行関数
* ========================= */
function 初期設定() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const 必要シート = [
シート設定,
シート検索データ,
シート検索クエリ分析,
シートページ分析,
シート改善キーワード,
シート地域分析,
シート月次サマリー,
シート変化チェック,
シートログ
];
必要シート.forEach(name => {
if (!ss.getSheetByName(name)) ss.insertSheet(name);
});
ヘッダー初期化_();
ログを書く_("INFO", "初期設定を完了しました");
}
function SEO分析を実行() {
ヘッダー初期化_();
検索データを取得_();
検索クエリ分析を作成_();
ページ分析を作成_();
改善キーワードを作成_();
地域分析を作成_();
月次サマリーを作成_();
変化チェックを作成_();
要約レポートを送信_();
}
/* =========================
* 1. Search Console から取得
* ========================= */
function 検索データを取得_() {
const 設定 = 設定を読む_();
const siteUrl = String(設定["サイトURL"] || "").trim();
const 取得日数 = Number(設定["取得日数"] || 28);
const 遅延日数 = Number(設定["データ遅延日数"] || 2);
if (!siteUrl) throw new Error("設定シートの『サイトURL』が未設定です");
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(シート検索データ);
const tz = "Asia/Tokyo";
const now = new Date();
const end = new Date(now.getTime() - 遅延日数 * 24 * 60 * 60 * 1000);
const start = new Date(end.getTime() - (取得日数 - 1) * 24 * 60 * 60 * 1000);
const 取得日時 = Utilities.formatDate(now, tz, "yyyy-MM-dd HH:mm:ss");
const 集計開始日 = Utilities.formatDate(start, tz, "yyyy-MM-dd");
const 集計終了日 = Utilities.formatDate(end, tz, "yyyy-MM-dd");
const 辞書 = 辞書を読む_();
const 既存キー集合 = 検索データキー集合を作る_(sheet);
let startRow = 0;
let hasMore = true;
const 追加行 = [];
while (hasMore) {
const payload = {
startDate: 集計開始日,
endDate: 集計終了日,
dimensions: ["query", "page"],
rowLimit: 25000,
startRow: startRow
};
const json = SearchConsoleAPIを呼ぶ_(siteUrl, payload);
if (!json.rows || json.rows.length === 0) {
hasMore = false;
break;
}
json.rows.forEach(row => {
const query = String((row.keys && row.keys[0]) || "").trim();
const page = String((row.keys && row.keys[1]) || "").trim();
const key = [集計開始日, 集計終了日, query, page].join("||");
if (既存キー集合.has(key)) return;
追加行.push([
取得日時,
集計開始日,
集計終了日,
query,
page,
Number(row.clicks || 0),
Number(row.impressions || 0),
Number(row.ctr || 0),
Number(row.position || 0),
検索タイプを分類_(query, 辞書),
地域を分類_(query, 辞書),
サービスを分類_(query, 辞書),
指名検索か_(query, 辞書) ? "はい" : "",
監視キーワード一致_(query, 辞書)
]);
既存キー集合.add(key);
});
if (json.rows.length < 25000) {
hasMore = false;
} else {
startRow += 25000;
Utilities.sleep(300);
}
}
if (追加行.length === 0) {
ログを書く_("INFO", "新規追加データはありませんでした");
return;
}
sheet.getRange(sheet.getLastRow() + 1, 1, 追加行.length, 追加行[0].length).setValues(追加行);
ログを書く_("INFO", "検索データに " + 追加行.length + " 行追加しました");
}
function 検索データキー集合を作る_(sheet) {
const set = new Set();
const values = sheet.getDataRange().getValues();
if (values.length < 2) return set;
for (let i = 1; i < values.length; i++) {
const row = values[i];
const 開始日 = String(row[1] || "").trim();
const 終了日 = String(row[2] || "").trim();
const 検索キーワード = String(row[3] || "").trim();
const ページURL = String(row[4] || "").trim();
if (!開始日 || !終了日 || !検索キーワード) continue;
set.add([開始日, 終了日, 検索キーワード, ページURL].join("||"));
}
return set;
}
/* =========================
* 2. 最新期間の分析
* ========================= */
function 検索クエリ分析を作成_() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const raw = ss.getSheetByName(シート検索データ);
const out = ss.getSheetByName(シート検索クエリ分析);
const values = raw.getDataRange().getValues();
if (values.length < 2) return;
const 最新期間 = 最新期間を取得_();
if (!最新期間) return;
const map = {};
for (let i = 1; i < values.length; i++) {
const row = values[i];
const 開始日 = String(row[1] || "");
const 終了日 = String(row[2] || "");
if (開始日 !== 最新期間.開始日 || 終了日 !== 最新期間.終了日) continue;
const 検索キーワード = String(row[3] || "").trim();
const ページURL = String(row[4] || "").trim();
const クリック数 = Number(row[5] || 0);
const 表示回数 = Number(row[6] || 0);
const CTR = Number(row[7] || 0);
const 平均掲載順位 = Number(row[8] || 0);
const 検索タイプ = String(row[9] || "");
const 地域 = String(row[10] || "");
const サービス分類 = String(row[11] || "");
const 指名検索 = String(row[12] || "");
if (!検索キーワード) continue;
if (!map[検索キーワード]) {
map[検索キーワード] = {
検索キーワード,
検索タイプ,
地域,
サービス分類,
指名検索,
クリック数合計: 0,
表示回数合計: 0,
ctr加重合計: 0,
順位加重合計: 0,
ページ集合: {}
};
}
map[検索キーワード].クリック数合計 += クリック数;
map[検索キーワード].表示回数合計 += 表示回数;
map[検索キーワード].ctr加重合計 += CTR * 表示回数;
map[検索キーワード].順位加重合計 += 平均掲載順位 * 表示回数;
if (ページURL) map[検索キーワード].ページ集合[ページURL] = true;
}
out.clearContents();
ヘッダー初期化_();
const rows = [];
Object.keys(map).sort().forEach(key => {
const item = map[key];
const imp = item.表示回数合計 || 0;
const 平均CTR = imp ? item.ctr加重合計 / imp : 0;
const 平均順位 = imp ? item.順位加重合計 / imp : 0;
rows.push([
item.検索キーワード,
item.検索タイプ,
item.地域,
item.サービス分類,
item.指名検索,
item.クリック数合計,
item.表示回数合計,
平均CTR,
平均順位,
Object.keys(item.ページ集合).length,
最新期間.開始日,
最新期間.終了日
]);
});
if (rows.length > 0) {
out.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
}
ログを書く_("INFO", "検索クエリ分析を更新しました");
}
function ページ分析を作成_() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const raw = ss.getSheetByName(シート検索データ);
const out = ss.getSheetByName(シートページ分析);
const values = raw.getDataRange().getValues();
if (values.length < 2) return;
const 最新期間 = 最新期間を取得_();
if (!最新期間) return;
const map = {};
for (let i = 1; i < values.length; i++) {
const row = values[i];
const 開始日 = String(row[1] || "");
const 終了日 = String(row[2] || "");
if (開始日 !== 最新期間.開始日 || 終了日 !== 最新期間.終了日) continue;
const 検索キーワード = String(row[3] || "").trim();
const ページURL = String(row[4] || "").trim();
const クリック数 = Number(row[5] || 0);
const 表示回数 = Number(row[6] || 0);
const CTR = Number(row[7] || 0);
const 平均掲載順位 = Number(row[8] || 0);
if (!ページURL) continue;
if (!map[ページURL]) {
map[ページURL] = {
ページURL,
クリック数合計: 0,
表示回数合計: 0,
ctr加重合計: 0,
順位加重合計: 0,
検索キーワード集合: {}
};
}
map[ページURL].クリック数合計 += クリック数;
map[ページURL].表示回数合計 += 表示回数;
map[ページURL].ctr加重合計 += CTR * 表示回数;
map[ページURL].順位加重合計 += 平均掲載順位 * 表示回数;
if (検索キーワード) map[ページURL].検索キーワード集合[検索キーワード] = true;
}
out.clearContents();
ヘッダー初期化_();
const rows = [];
Object.keys(map).sort().forEach(key => {
const item = map[key];
const imp = item.表示回数合計 || 0;
rows.push([
item.ページURL,
item.クリック数合計,
item.表示回数合計,
imp ? item.ctr加重合計 / imp : 0,
imp ? item.順位加重合計 / imp : 0,
Object.keys(item.検索キーワード集合).length,
最新期間.開始日,
最新期間.終了日
]);
});
if (rows.length > 0) {
out.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
}
ログを書く_("INFO", "ページ分析を更新しました");
}
function 改善キーワードを作成_() {
const 設定 = 設定を読む_();
const 最小表示回数 = Number(設定["改善候補最小表示回数"] || 20);
const 順位下限 = Number(設定["改善候補順位下限"] || 5);
const 順位上限 = Number(設定["改善候補順位上限"] || 20);
const ss = SpreadsheetApp.getActiveSpreadsheet();
const src = ss.getSheetByName(シート検索クエリ分析);
const out = ss.getSheetByName(シート改善キーワード);
const values = src.getDataRange().getValues();
if (values.length < 2) return;
out.clearContents();
ヘッダー初期化_();
const rows = [];
for (let i = 1; i < values.length; i++) {
const row = values[i];
const 検索キーワード = String(row[0] || "");
const 検索タイプ = String(row[1] || "");
const 地域 = String(row[2] || "");
const サービス分類 = String(row[3] || "");
const クリック数 = Number(row[5] || 0);
const 表示回数 = Number(row[6] || 0);
const CTR = Number(row[7] || 0);
const 平均順位 = Number(row[8] || 0);
if (表示回数 >= 最小表示回数 && 平均順位 >= 順位下限 && 平均順位 <= 順位上限) {
let 改善タイプ = "順位改善";
let 改善メモ = "本文補強・内部リンク・見出し最適化を検討";
if (CTR < 0.03) {
改善タイプ = "CTR改善";
改善メモ = "title と description の改善余地あり";
}
rows.push([
検索キーワード,
検索タイプ,
地域,
サービス分類,
クリック数,
表示回数,
CTR,
平均順位,
改善タイプ,
改善メモ
]);
}
}
if (rows.length > 0) {
out.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
}
ログを書く_("INFO", "改善キーワードを更新しました");
}
function 地域分析を作成_() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const src = ss.getSheetByName(シート検索クエリ分析);
const out = ss.getSheetByName(シート地域分析);
const values = src.getDataRange().getValues();
if (values.length < 2) return;
out.clearContents();
out.appendRow([
"地域",
"クリック数合計",
"表示回数合計",
"平均CTR",
"平均掲載順位",
"検索キーワード数",
"指名検索数",
"地域×業種数",
"採用検索数"
]);
const map = {};
for (let i = 1; i < values.length; i++) {
const row = values[i];
const 検索タイプ = String(row[1] || "");
const 地域 = String(row[2] || "未分類");
const 検索キーワード = String(row[0] || "");
const クリック数 = Number(row[5] || 0);
const 表示回数 = Number(row[6] || 0);
const CTR = Number(row[7] || 0);
const 平均順位 = Number(row[8] || 0);
if (!map[地域]) {
map[地域] = {
クリック数合計: 0,
表示回数合計: 0,
ctr加重合計: 0,
順位加重合計: 0,
キーワード集合: {},
指名検索数: 0,
地域業種数: 0,
採用検索数: 0
};
}
map[地域].クリック数合計 += クリック数;
map[地域].表示回数合計 += 表示回数;
map[地域].ctr加重合計 += CTR * 表示回数;
map[地域].順位加重合計 += 平均順位 * 表示回数;
map[地域].キーワード集合[検索キーワード] = true;
if (検索タイプ === "指名" || 検索タイプ === "指名×地域") map[地域].指名検索数++;
if (検索タイプ === "地域×業種") map[地域].地域業種数++;
if (検索タイプ === "採用") map[地域].採用検索数++;
}
const rows = [];
Object.keys(map).sort().forEach(area => {
const item = map[area];
const imp = item.表示回数合計 || 0;
rows.push([
area,
item.クリック数合計,
item.表示回数合計,
imp ? item.ctr加重合計 / imp : 0,
imp ? item.順位加重合計 / imp : 0,
Object.keys(item.キーワード集合).length,
item.指名検索数,
item.地域業種数,
item.採用検索数
]);
});
if (rows.length > 0) {
out.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
}
ログを書く_("INFO", "地域分析を更新しました");
}
function 月次サマリーを作成_() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const src = ss.getSheetByName(シート検索クエリ分析);
const out = ss.getSheetByName(シート月次サマリー);
const values = src.getDataRange().getValues();
if (values.length < 2) return;
let 総クリック数 = 0;
let 総表示回数 = 0;
let ctr加重合計 = 0;
let 順位加重合計 = 0;
let 指名検索表示回数 = 0;
let 地域検索表示回数 = 0;
let 地域業種表示回数 = 0;
let 採用検索表示回数 = 0;
let 改善候補数 = 0;
const 月 = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy-MM");
for (let i = 1; i < values.length; i++) {
const row = values[i];
const 検索タイプ = String(row[1] || "");
const 表示回数 = Number(row[6] || 0);
const CTR = Number(row[7] || 0);
const 平均順位 = Number(row[8] || 0);
総表示回数 += 表示回数;
総クリック数 += Number(row[5] || 0);
ctr加重合計 += CTR * 表示回数;
順位加重合計 += 平均順位 * 表示回数;
if (検索タイプ === "指名" || 検索タイプ === "指名×地域") 指名検索表示回数 += 表示回数;
if (検索タイプ === "地域") 地域検索表示回数 += 表示回数;
if (検索タイプ === "地域×業種") 地域業種表示回数 += 表示回数;
if (検索タイプ === "採用") 採用検索表示回数 += 表示回数;
if (表示回数 >= 20 && 平均順位 >= 5 && 平均順位 <= 20) 改善候補数++;
}
out.clearContents();
out.appendRow([
"月",
"総クリック数",
"総表示回数",
"平均CTR",
"平均掲載順位",
"指名検索表示回数",
"地域検索表示回数",
"地域×業種表示回数",
"採用検索表示回数",
"改善候補数"
]);
out.appendRow([
月,
総クリック数,
総表示回数,
総表示回数 ? ctr加重合計 / 総表示回数 : 0,
総表示回数 ? 順位加重合計 / 総表示回数 : 0,
指名検索表示回数,
地域検索表示回数,
地域業種表示回数,
採用検索表示回数,
改善候補数
]);
ログを書く_("INFO", "月次サマリーを更新しました");
}
/* =========================
* 3. 変化チェック
* ========================= */
function 変化チェックを作成_() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const raw = ss.getSheetByName(シート検索データ);
const out = ss.getSheetByName(シート変化チェック);
const values = raw.getDataRange().getValues();
if (values.length < 2) return;
const periods = 期間一覧を取得_();
if (periods.length < 2) {
out.clearContents();
out.appendRow([
"検索キーワード",
"前回表示回数",
"今回表示回数",
"前回平均掲載順位",
"今回平均掲載順位",
"表示回数差分",
"順位差分",
"判定"
]);
return;
}
const prev = periods[periods.length - 2];
const latest = periods[periods.length - 1];
const prevMap = 期間別クエリ集計を作る_(values, prev.開始日, prev.終了日);
const latestMap = 期間別クエリ集計を作る_(values, latest.開始日, latest.終了日);
out.clearContents();
out.appendRow([
"検索キーワード",
"前回表示回数",
"今回表示回数",
"前回平均掲載順位",
"今回平均掲載順位",
"表示回数差分",
"順位差分",
"判定"
]);
const allKeys = new Set([...Object.keys(prevMap), ...Object.keys(latestMap)]);
const rows = [];
Array.from(allKeys).sort().forEach(query => {
const p = prevMap[query] || { impressions: 0, position: "", exists: false };
const l = latestMap[query] || { impressions: 0, position: "", exists: false };
let 判定 = "";
if (!p.exists && l.exists) {
判定 = "新規出現";
} else if (p.exists && !l.exists) {
判定 = "消失";
} else {
const diffPos = Number(p.position || 0) - Number(l.position || 0);
if (Math.abs(diffPos) >= 1) {
判定 = diffPos > 0 ? "改善" : "悪化";
} else {
判定 = "横ばい";
}
}
rows.push([
query,
p.impressions || 0,
l.impressions || 0,
p.position === "" ? "" : p.position,
l.position === "" ? "" : l.position,
(l.impressions || 0) - (p.impressions || 0),
(p.position === "" || l.position === "") ? "" : Number(p.position || 0) - Number(l.position || 0),
判定
]);
});
if (rows.length > 0) {
out.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
}
ログを書く_("INFO", "変化チェックを更新しました");
}
function 期間別クエリ集計を作る_(values, 開始日, 終了日) {
const map = {};
for (let i = 1; i < values.length; i++) {
const row = values[i];
if (String(row[1] || "") !== 開始日 || String(row[2] || "") !== 終了日) continue;
const query = String(row[3] || "").trim();
const impressions = Number(row[6] || 0);
const position = Number(row[8] || 0);
if (!query) continue;
if (!map[query]) {
map[query] = {
impressions: 0,
posWeighted: 0,
exists: true
};
}
map[query].impressions += impressions;
map[query].posWeighted += position * impressions;
}
Object.keys(map).forEach(query => {
const item = map[query];
item.position = item.impressions ? item.posWeighted / item.impressions : "";
});
return map;
}
/* =========================
* 4. メール
* ========================= */
function 要約レポートを送信_() {
const 設定 = 設定を読む_();
const 通知メール = String(設定["通知メール"] || "").trim();
if (!通知メール) return;
const ss = SpreadsheetApp.getActiveSpreadsheet();
const qSheet = ss.getSheetByName(シート検索クエリ分析);
const oSheet = ss.getSheetByName(シート改善キーワード);
const cSheet = ss.getSheetByName(シート変化チェック);
const qValues = qSheet.getDataRange().getValues();
const oValues = oSheet.getDataRange().getValues();
const cValues = cSheet.getDataRange().getValues();
let 指名表示回数 = 0;
let 地域表示回数 = 0;
let 採用表示回数 = 0;
for (let i = 1; i < qValues.length; i++) {
const row = qValues[i];
const タイプ = String(row[1] || "");
const imp = Number(row[6] || 0);
if (タイプ === "指名" || タイプ === "指名×地域") 指名表示回数 += imp;
if (タイプ === "地域" || タイプ === "地域×業種") 地域表示回数 += imp;
if (タイプ === "採用") 採用表示回数 += imp;
}
const 改善行 = [];
for (let i = 1; i < oValues.length; i++) {
改善行.push(oValues[i]);
}
改善行.sort((a, b) => Number(b[5] || 0) - Number(a[5] || 0));
const 新規出現 = [];
const 改善 = [];
const 悪化 = [];
const 消失 = [];
for (let i = 1; i < cValues.length; i++) {
const row = cValues[i];
const 判定 = String(row[7] || "");
if (判定 === "新規出現") 新規出現.push(row);
if (判定 === "改善") 改善.push(row);
if (判定 === "悪化") 悪化.push(row);
if (判定 === "消失") 消失.push(row);
}
let body = "";
body += "地域SEO分析レポート\n\n";
body += "【要約】\n";
body += "指名検索表示回数: " + 指名表示回数 + "\n";
body += "地域系検索表示回数: " + 地域表示回数 + "\n";
body += "採用系検索表示回数: " + 採用表示回数 + "\n\n";
body += "【改善キーワード 上位5件】\n";
if (改善行.length === 0) {
body += "なし\n";
} else {
改善行.slice(0, 5).forEach(r => {
body += "- " + r[0] + " / 表示:" + r[5] + " / 順位:" + Number(r[7]).toFixed(1) + " / " + r[8] + "\n";
});
}
body += "\n【新規出現キーワード 上位5件】\n";
if (新規出現.length === 0) {
body += "なし\n";
} else {
新規出現.slice(0, 5).forEach(r => {
body += "- " + r[0] + " / 今回表示:" + r[2] + "\n";
});
}
body += "\n【改善したキーワード 上位5件】\n";
if (改善.length === 0) {
body += "なし\n";
} else {
改善
.sort((a, b) => Number(b[6] || 0) - Number(a[6] || 0))
.slice(0, 5)
.forEach(r => {
body += "- " + r[0] + " / 前回:" + Number(r[3] || 0).toFixed(1) + " → 今回:" + Number(r[4] || 0).toFixed(1) + "\n";
});
}
body += "\n【悪化したキーワード 上位5件】\n";
if (悪化.length === 0) {
body += "なし\n";
} else {
悪化
.sort((a, b) => Number(a[6] || 0) - Number(b[6] || 0))
.slice(0, 5)
.forEach(r => {
body += "- " + r[0] + " / 前回:" + Number(r[3] || 0).toFixed(1) + " → 今回:" + Number(r[4] || 0).toFixed(1) + "\n";
});
}
body += "\n【消失キーワード 上位5件】\n";
if (消失.length === 0) {
body += "なし\n";
} else {
消失.slice(0, 5).forEach(r => {
body += "- " + r[0] + " / 前回表示:" + r[1] + "\n";
});
}
MailApp.sendEmail({
to: 通知メール,
subject: "地域SEO分析レポート",
body: body
});
ログを書く_("INFO", "要約レポートを送信しました");
}
/* =========================
* Search Console API
* ========================= */
function SearchConsoleAPIを呼ぶ_(siteUrl, payload) {
const url = "https://searchconsole.googleapis.com/webmasters/v3/sites/" +
encodeURIComponent(siteUrl) + "/searchAnalytics/query";
const res = UrlFetchApp.fetch(url, {
method: "post",
contentType: "application/json",
headers: {
Authorization: "Bearer " + ScriptApp.getOAuthToken()
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
const code = res.getResponseCode();
const text = res.getContentText();
if (code !== 200) {
throw new Error("Search Console API エラー (" + code + "): " + text);
}
return JSON.parse(text);
}
/* =========================
* ヘッダー
* ========================= */
function ヘッダー初期化_() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const 検索データ = ss.getSheetByName(シート検索データ);
if (検索データ.getLastRow() === 0) {
検索データ.appendRow([
"取得日時",
"集計開始日",
"集計終了日",
"検索キーワード",
"ページURL",
"クリック数",
"表示回数",
"CTR",
"平均掲載順位",
"検索タイプ",
"地域",
"サービス分類",
"指名検索",
"監視キーワード一致"
]);
}
const 検索クエリ分析 = ss.getSheetByName(シート検索クエリ分析);
if (検索クエリ分析.getLastRow() === 0) {
検索クエリ分析.appendRow([
"検索キーワード",
"検索タイプ",
"地域",
"サービス分類",
"指名検索",
"クリック数合計",
"表示回数合計",
"平均CTR",
"平均掲載順位",
"表示されたページ数",
"集計開始日",
"集計終了日"
]);
}
const ページ分析 = ss.getSheetByName(シートページ分析);
if (ページ分析.getLastRow() === 0) {
ページ分析.appendRow([
"ページURL",
"クリック数合計",
"表示回数合計",
"平均CTR",
"平均掲載順位",
"検索キーワード数",
"集計開始日",
"集計終了日"
]);
}
const 改善キーワード = ss.getSheetByName(シート改善キーワード);
if (改善キーワード.getLastRow() === 0) {
改善キーワード.appendRow([
"検索キーワード",
"検索タイプ",
"地域",
"サービス分類",
"クリック数",
"表示回数",
"CTR",
"平均掲載順位",
"改善タイプ",
"改善メモ"
]);
}
const 地域分析 = ss.getSheetByName(シート地域分析);
if (地域分析.getLastRow() === 0) {
地域分析.appendRow([
"地域",
"クリック数合計",
"表示回数合計",
"平均CTR",
"平均掲載順位",
"検索キーワード数",
"指名検索数",
"地域×業種数",
"採用検索数"
]);
}
const 月次サマリー = ss.getSheetByName(シート月次サマリー);
if (月次サマリー.getLastRow() === 0) {
月次サマリー.appendRow([
"月",
"総クリック数",
"総表示回数",
"平均CTR",
"平均掲載順位",
"指名検索表示回数",
"地域検索表示回数",
"地域×業種表示回数",
"採用検索表示回数",
"改善候補数"
]);
}
const 変化チェック = ss.getSheetByName(シート変化チェック);
if (変化チェック.getLastRow() === 0) {
変化チェック.appendRow([
"検索キーワード",
"前回表示回数",
"今回表示回数",
"前回平均掲載順位",
"今回平均掲載順位",
"表示回数差分",
"順位差分",
"判定"
]);
}
const ログ = ss.getSheetByName(シートログ);
if (ログ.getLastRow() === 0) {
ログ.appendRow([
"記録日時",
"レベル",
"内容"
]);
}
}
/* =========================
* 期間系
* ========================= */
function 最新期間を取得_() {
const periods = 期間一覧を取得_();
if (periods.length === 0) return null;
return periods[periods.length - 1];
}
function 期間一覧を取得_() {
const values = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName(シート検索データ)
.getDataRange()
.getValues();
const map = {};
for (let i = 1; i < values.length; i++) {
const start = String(values[i][1] || "").trim();
const end = String(values[i][2] || "").trim();
if (!start || !end) continue;
map[start + "||" + end] = { 開始日: start, 終了日: end };
}
return Object.values(map).sort((a, b) => a.終了日.localeCompare(b.終了日));
}
/* =========================
* 設定読み込み
* ========================= */
function 設定を読む_() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(シート設定);
const values = sheet.getRange(1, 1, Math.max(sheet.getLastRow(), 1), 2).getValues();
const obj = {};
values.forEach(row => {
const key = String(row[0] || "").trim();
const value = row[1];
if (key) obj[key] = value;
});
return obj;
}
function 辞書を読む_() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(シート設定);
const lastRow = sheet.getLastRow();
const 読み取り = function(col) {
if (lastRow < 2) return [];
return sheet
.getRange(2, col, lastRow - 1, 1)
.getValues()
.flat()
.map(v => String(v || "").trim())
.filter(v => v !== "");
};
return {
監視キーワード: 読み取り(4), // D列
指名語句: 読み取り(5), // E列
地域語句: 読み取り(6), // F列
業種語句: 読み取り(7), // G列
採用語句: 読み取り(8) // H列
};
}
/* =========================
* 分類ロジック
* ========================= */
function 検索タイプを分類_(query, 辞書) {
const q = 正規化_(query);
辞書 = 辞書 || 辞書を読む_();
const 指名あり = 辞書.指名語句.some(word => q.includes(正規化_(word)));
const 地域あり = 辞書.地域語句.some(word => q.includes(正規化_(word)));
const 業種あり = 辞書.業種語句.some(word => q.includes(正規化_(word)));
const 採用あり = 辞書.採用語句.some(word => q.includes(正規化_(word)));
if (指名あり && 地域あり) return "指名×地域";
if (地域あり && 業種あり) return "地域×業種";
if (指名あり) return "指名";
if (採用あり) return "採用";
if (地域あり) return "地域";
if (業種あり) return "業種";
return "その他";
}
function 地域を分類_(query, 辞書) {
const q = 正規化_(query);
辞書 = 辞書 || 辞書を読む_();
for (let i = 0; i < 辞書.地域語句.length; i++) {
const word = 辞書.地域語句[i];
if (q.includes(正規化_(word))) return word;
}
return "未分類";
}
function サービスを分類_(query, 辞書) {
const q = 正規化_(query);
辞書 = 辞書 || 辞書を読む_();
for (let i = 0; i < 辞書.業種語句.length; i++) {
const word = 辞書.業種語句[i];
if (q.includes(正規化_(word))) return word;
}
for (let i = 0; i < 辞書.採用語句.length; i++) {
const word = 辞書.採用語句[i];
if (q.includes(正規化_(word))) return "採用";
}
return "未分類";
}
function 指名検索か_(query, 辞書) {
const q = 正規化_(query);
辞書 = 辞書 || 辞書を読む_();
return 辞書.指名語句.some(word => q.includes(正規化_(word)));
}
function 監視キーワード一致_(query, 辞書) {
const q = 正規化_(query);
辞書 = 辞書 || 辞書を読む_();
for (let i = 0; i < 辞書.監視キーワード.length; i++) {
const kw = 正規化_(辞書.監視キーワード[i]);
if (kw && q.includes(kw)) return 辞書.監視キーワード[i];
}
return "";
}
/* =========================
* 共通
* ========================= */
function 正規化_(text) {
return String(text || "")
.trim()
.replace(/\s+/g, " ")
.toLowerCase();
}
function ログを書く_(level, message) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(シートログ);
const now = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy-MM-dd HH:mm:ss");
sheet.appendRow([now, level, message]);
}コードについての理解
このコードは、単にSearch Consoleデータを取るだけではなく、
検索データを取得_()で生データ取得検索データキー集合を作る_()で重複防止検索クエリ分析を作成_()でクエリ分析ページ分析を作成_()でページ評価改善キーワードを作成_()でSEO改善候補抽出地域分析を作成_()でローカルSEO分析月次サマリーを作成_()で月次把握変化チェックを作成_()で順位変動比較要約レポートを送信_()でメール通知
まで一連で行います。
STEP8|初回実行を行う
コードを貼ったら、まずは手動で一度動かします。
最初に実行する関数
まずは初期設定を実行します。これはヘッダーなどを整える関数です。
実行後、各シートの ヘッダーが作成されます。

初回認証について
初回はGoogleの認証画面が出ます。
必要な権限にチェックを入れて許可してください。
ここで権限を許可しないと、API呼び出しやメール送信が動きません。


認証に失敗した場合
① Google Cloud Console に戻る
左メニュー:Google Auth Platform
② 「対象」をクリック
左メニュー:対象
③ 「テストユーザー」 を追加
テストユーザー → 「+ Add users」をクリック。
メールアドレスをGメールアドレスを入力。入力後、保存してください。

⑤ もう一度 Apps Script を実行
すると「このアプリは Google に確認されていません」という画面が出ます。
そこで、「詳細」→「SEO順位チェックツールに移動 (安全ではないページ)」を押せばOK。
「SEO分析を実行」を押すと起きること
初期設定が完了したら、次は 「SEO分析を実行」 をクリックします。
このボタンを押すと、Google Search Console のデータを取得し、スプレッドシートにSEOの分析結果を記録する処理が自動で行われます。
処理の流れは、大きく分けて次の4つです。
1. Search Consoleから検索データを取得する
まず、Google Apps Script が Search Console API を通じて、サイトの検索データを取得します。
このとき取得される主なデータは次の4つです。
| データ | 内容 |
|---|---|
| クリック数 | 検索結果から実際にクリックされた回数 |
| 表示回数 | 検索結果に表示された回数 |
| CTR | クリック率 |
| 平均掲載順位 | 検索結果での平均順位 |
これらのデータは、Google検索におけるサイトのパフォーマンスを把握するための基本的な指標です。
2. 登録したキーワードごとに順位を分析する
次に、シートに登録されているキーワードを1つずつ確認し、それぞれの検索データを取得します。
例えば、次のようなキーワードを登録していた場合です。
香川県 ホームページ制作
香川県 WEB制作
東かがわ市 ホームページ制作スクリプトはこれらのキーワードについて、
- 検索順位
- クリック数
- 表示回数
などのデータを取得します。
3. 分析結果をスプレッドシートに蓄積する
取得したデータは、検索データシートに自動で追加されていきます。
例えば、次のような形でデータが蓄積されます。
| 日付 | キーワード | ページ | 順位 | クリック | 表示 |
|---|---|---|---|---|---|
| 2026/3/15 | 香川県 WEB制作 | / | 9.2 | 3 | 120 |
| 2026/3/15 | 東かがわ市 ホームページ制作 | / | 2.3 | 8 | 40 |
このように、毎日データが追加されていくことで、順位の推移を追跡できるようになります。
4. 順位変動のレポートをメールで受け取る(設定している場合)
スクリプトにメール通知機能を設定している場合、順位の変動を自動でメールで受け取ることもできます。
例えば次のようなレポートです。
SEO順位レポート
香川県 WEB制作
昨日:9位
今日:6位
↑ +3
東かがわ市 ホームページ制作
昨日:3位
今日:5位
↓ -2このように、順位の上昇・下降を毎日チェックできるため、
SEO施策の効果を素早く把握することができます。
STEP9|毎日自動実行するトリガーを設定する
ここが、このツールを “本当に使えるもの”にする最重要ポイント です。
コードを貼って手動で実行するだけでは、毎日自動で動きません。
トリガーとは何か
トリガーは、GASを指定した時間に自動実行する仕組みです。
これを設定することで
- 毎日Search Consoleデータ取得
- 分析シート更新
- 変化チェック
- メール送信
が全部自動で動くようになります。
トリガー設定手順
- Apps Script左の 時計アイコン(トリガー) を開く
- トリガーを追加 をクリック
- 以下のように設定する

トリガー設定内容
- 実行する関数:
SEO分析を実行 - デプロイ:
Head - イベントのソース:
時間主導型 - 時間ベースのトリガー:
日付ベースのタイマー - 時刻:朝5時〜6時 など

なぜ朝の時間帯が良いのか
Search Consoleのデータはリアルタイムではなく、一般的に 1〜2日遅れ で反映されます。
そのため、朝に取得することで比較的安定した確定データを取りやすくなります。
トリガー設定後に起こること
トリガーを設定すると、毎日自動で次の処理が走ります。
- Search Consoleデータ取得
- 検索データシート更新
- 検索クエリ分析更新
- ページ分析更新
- 改善キーワード更新
- 地域分析更新
- 月次サマリー更新
- 変化チェック更新
- メール通知送信
つまり、何もしなくても毎日SEOレポートが更新される状態 になります。
毎日届くメール通知では何が分かるのか
このツールの強みの1つが 要約レポートのメール送信 です。
メールで届く内容
- 指名検索表示回数
- 地域系検索表示回数
- 採用系検索表示回数
- 改善キーワード 上位5件
- 新規出現キーワード 上位5件
- 改善したキーワード 上位5件
- 悪化したキーワード 上位5件
- 消失キーワード 上位5件
これが便利な理由
Search Consoleを毎回開かなくても、その日のSEO状況の要点だけ が分かります。
たとえば
- 新しく出てきた検索クエリがある
- 重要キーワードが悪化している
- 地域系の露出が増えている
といった変化を、メール1通で把握できます。
GASコード全文の重要ポイントを解説する
ここからは、掲載しているコードの重要な処理について解説します。
コードをそのまま使うこともできますが、仕組みを理解しておくと、他サイトへの応用やカスタマイズもしやすくなります。
1. 重複防止が入っている理由
このコードでは 検索データキー集合を作る_() という処理が入っています。
これは、
- 集計開始日
- 集計終了日
- 検索キーワード
- ページURL
の組み合わせをキーにして、同じデータを二重で保存しないようにする仕組みです。
なぜ重複防止が必要なのか
手動テストやトリガー再実行をすると、同じ期間のデータを何度も取りに行くことがあります。
この処理がないと
- 表示回数が二重に見える
- クリック数が膨らむ
- 分析結果が壊れる
という問題が起きます。
つまり、地味ですが実運用ではかなり大事な処理です。
2. 検索クエリ分析で何を見ているのか
検索クエリ分析を作成_() は、最新期間の検索クエリを集計して
- クリック数合計
- 表示回数合計
- 平均CTR
- 平均掲載順位
- 表示されたページ数
をまとめています。
これで何が分かるか
たとえば
- どのキーワードが表示されているか
- どのキーワードが実際にクリックされているか
- 1つのキーワードが複数ページで出ているか
が見えます。
特に、1つのキーワードが複数ページにまたがっている場合は、評価が分散している可能性もあるので注意ポイントになります。
3. 改善キーワードはなぜ重要なのか
改善キーワードを作成_() では、
- 表示回数が一定以上ある
- 平均順位が5〜20位に入っている
ものを抽出しています。
5〜20位が“伸ばしやすい”理由
SEOでは、1位を目指す前に
まず “今あと一歩で伸びそうなキーワード” を見つけるのが効率的です。
たとえば
- 18位 → 少しの改善で10位台前半に入る可能性
- 9位 → タイトル改善や内部リンク追加で上位を狙える可能性
があります。
このゾーンは、SEO改善の費用対効果が高いことが多いです。
4. 地域分析がローカルSEOで効く理由
地域分析を作成_() では、設定シートの地域語句をもとにクエリを分類しています。
地域分析の具体例
たとえば、香川県の制作会社なら
- 香川
- 香川県
- 高松
- 高松市
- 東かがわ
- 東かがわ市
などを地域語句として登録しておくことで、
- どの地域から見られているか
- どの地域語句で露出しているか
- 地域×業種の掛け合わせが取れているか
を確認できます。
地域SEOは「香川県」だけ見ていても不十分で、市区町村単位の検索意図 まで見た方が改善しやすいです。
5. 変化チェックで見えること
変化チェックを作成_() は、前回期間と今回期間を比較して
- 新規出現
- 改善
- 悪化
- 横ばい
- 消失
を判定します。
これが便利な場面
たとえば
- ブログ公開後に新しいクエリが出てきた
- タイトル変更後に順位が改善した
- なぜか特定のキーワードだけ消えた
といった変化を、一覧で把握できます。
SEO分析方法 どこを見ると改善につながるのか
このツールを作っても、「どこを見ればいいか」が分からないと活かしきれません。
ここでは、実務で見るポイントを具体的に解説します。
1. 検索クエリ分析で“狙うべきキーワード”を見つける
まず見るべきは 検索クエリ分析 です。
見るポイント
- 表示回数が多いキーワード
- CTRが低いキーワード
- 平均順位が5〜20位のキーワード
具体例
たとえば
- 「香川県 ホームページ制作」
表示回数 800、CTR 1.2%、平均順位 8.7
なら、
- タイトル改善
- ディスクリプション見直し
- 関連ページから内部リンク追加
で伸ばせる可能性があります。
2. ページ分析で“どのページがSEOを支えているか”を見る
ページ分析 では、どのURLが表示され、クリックされているかを確認します。
見るポイント
- トップページばかりに依存していないか
- ブログ記事が検索流入を取れているか
- サービスページや制作実績ページが評価されているか
改善例
もしトップページしか取れていないなら、サービスページや実績ページの内容が弱い可能性があります。
逆に、ブログ記事ばかり伸びているなら、その記事からサービスページへ導線を作ることで問い合わせにつなげやすくなります。
3. 改善キーワードで“次に直すべきポイント”を決める
改善キーワード は、優先順位を決めるためのシートです。
改善の考え方
- CTRが低い → タイトル改善を優先
- 順位が中位 → 本文補強・内部リンク強化
- 地域語句が多い → ローカルSEOページの強化
何から手をつけるべきか迷ったときは、
まずここを見ると判断しやすいです。
4. 地域分析でローカルSEOの方向性を確認する
地域分析 は、地域密着型のサイトで特に重要です。
具体例
たとえば
- 高松市では見られているが、香川県全体では弱い
- 東かがわ市のクエリが増えてきた
- さぬき市の露出はまだ少ない
などが見えます。
そうすると、
- 地域ページを作る
- 制作実績に地域名を入れる
- タイトルに市区町村名を含める
といった改善が考えられます。
SEO改善事例 このデータをどう活かすか
ここでは、実際に起こりやすいケースを例に、改善の考え方を示します。
事例1. 表示回数は多いのにクリックが少ない
状況
- キーワード:香川県 ホームページ制作
- 表示回数:1200
- CTR:1.1%
- 平均順位:8.2
考えられる原因
- タイトルが弱い
- ディスクリプションが魅力的でない
- 競合より訴求が弱い
改善例
- タイトルに「香川県」「高松市対応」などの具体性を加える
- “制作会社”と“ホームページ制作”のどちらが検索意図に近いか再確認する
- meta description に強みを入れる
事例2. 新規出現キーワードが増えた
状況
ブログ公開後、変化チェック で「新規出現」が増えた。
考えられる原因
- 記事のテーマが検索意図に合っていた
- 内部リンクが効いて関連クエリが広がった
- ページ評価が立ち上がってきた
改善例
- 新規出現キーワードを
検索クエリ分析で確認 - その語句を見出しや本文に自然に補強
- 関連記事を追加してテーマの深掘りをする
事例3. 地域×業種キーワードが弱い
状況
- 地域名だけのクエリはある
- 業種だけのクエリもある
- でも「地域×業種」の組み合わせが少ない
考えられる原因
- 地域と業種を掛け合わせたページが不足している
- タイトルや見出しに地域名が入っていない
- 実績ページに地域情報が足りない
改善例
- 「香川県のホームページ制作」などの地域特化ページを作る
- 制作実績に地域名を入れる
- FAQや導入文で対応エリアを明記する
よくあるエラーと対処法
このセクションは検索意図にかなり効きます。
実際、設定時に詰まりやすい人が多いからです。
エラー1. Search Console API エラー 403
原因
- APIが有効化されていない
- Apps Script が使っているGCPプロジェクトと違う
- 権限不足
対処法
- GCPプロジェクトで Search Console API を有効化
- Apps Script 側のプロジェクト番号を合わせる
- 必要なら再認証する
エラー2. appsscript.json が表示されない
原因
マニフェストファイルの表示設定がオフ
対処法
Apps Script のプロジェクト設定で
「appsscript.json マニフェスト ファイルをエディタで表示」をオンにする
エラー3. データが入らない
原因
- サイトURLが Search Console のプロパティ表記と違う
- 対象期間にデータが少ない
- 認証は通ったが rows が返っていない
対処法
- URL表記を確認する
- まず query だけで広く取得できるか試す
- Search Console 管理画面でも対象期間を確認する
エラー4. メールが届かない
原因
通知メールが未設定- トリガー未設定
- 初回認証でメール権限を許可していない
対処法
- 設定シートを見直す
- トリガー設定を確認する
- 手動実行して認証を通し直す
よくある質問
Q1. このツールは無料で使えますか?
はい。
Googleアカウントがあれば、基本的に無料で使えます。
Q2. 他のサイトにも使い回せますか?
使えます。設定 シートの内容を変えるだけで流用できます。
この点が、このツールの大きな強みです。
Q3. 会社名や地域名はコードで直す必要がありますか?
基本的にはありません。
設定シートの辞書を書き換えれば対応できます。
Q4. 毎日必ずメール通知されますか?
SEO分析を実行 に対してトリガーを設定していれば、毎日自動で動きます。
通知メールが設定されていれば、要約レポートが届きます。
Q5. Search Consoleの順位は正確ですか?
Search Consoleの「平均掲載順位」は、
実際の検索結果をもとに集計された平均値です。
手動検索より、分析用途ではかなり信頼しやすいです。
Q6. 制作会社がクライアント用に使っても大丈夫ですか?
かなり向いています。
スプレッドシートを複製して設定シートを書き換えるだけで運用しやすいです。
この記事の使い方まとめ
ここまでの内容を、実際の作業順にまとめると次の通りです。
作業手順まとめ
- スプレッドシートを作る
- 9シートを作成する
- 設定シートを入力する
- Apps Script を開く
- appsscript.json を表示して設定する
- GCPプロジェクトと Search Console API を設定する
- 最終コード全文を貼る
初期設定を実行するSEO分析を実行を実行する- トリガーを設定する
- 毎日メール通知を確認する
まとめ
この記事では、Google Search Consoleの検索データを
- 毎日自動取得
- スプレッドシートに蓄積
- 9シートでSEO分析
- 変化チェック
- メール通知
までできる 無料のSEO分析ツール の作り方を解説しました。
このツールの価値は、単にデータを取ることではありません。
- どのキーワードが伸びているか
- どのページが検索を支えているか
- どの地域で露出しているか
- 次に何を改善すべきか
を、毎日の運用の中で判断しやすくすること にあります。
しかも、設定シートを書き換えるだけで他サイトにも流用できるので、
自社サイトだけでなく、クライアント運用にも向いています。
「Search Consoleは見ているけれど、活かしきれていない」と感じているなら、
この仕組みはかなり役に立つはずです。