n8nでGSC/GA4からリライト候補を自動抽出するフローを作った
n8nでAI記事生成フローを作り、記事を自動投稿できるようになったとき、次の問題に気づきました。「記事を作る仕組みはできたが、どの記事を直すべきか、次に何を書くべきかが分からない」という問題です。
Search ConsoleやGA4の管理画面を見れば数字は分かります。ただ、膨大なクエリや指標を眺めるだけでは、具体的なアクションに変換しにくい。そこでフロー0として、週次でGSC/GA4データを取得し、既存記事台帳と照合してリライト候補と新規キーワード候補を自動で出す仕組みを作りました。
この記事でわかること:
- n8nでGSC/GA4データを取得する構成
- Pythonスクリプトとn8nを組み合わせる設計判断
- summary_cache.jsonと照合して既存記事か新規候補かを分ける方法
- リライト候補・新規キーワード候補のスコアリング設計
- Slack通知の分け方とCloudflare対策の詰まり
GSC/GA4を見ても「次に何をすべきか」が決まらなかった
Search ConsoleとGA4にはSEO改善に使えるデータが揃っています。どのクエリで表示されているか、クリック率はどうか、ページに来た後の滞在時間はどうか。こうしたデータは管理画面から確認できます。
ただ、週次で確認しようとすると問題が出てきました。記事が増えるほど、確認すべき記事数も増えます。10本なら手動でも回せますが、30本・50本になると管理画面を見ているだけでは「どれを直すか」の判断がついてきません。
AI記事生成パイプラインでは、記事を作るだけでなく、作った記事を改善する仕組みが必要です。フロー0は、その改善ループの起点として設計しました。
今回作ったフロー0の全体像
フロー0は記事生成(フロー1・2)とは独立した、週次の分析フローです。
GSC/GA4データ取得(Pythonスクリプト)
↓
performance_cache.jsonとしてVPSに保存
↓
n8nでperformance_cacheを読み込み
↓
summary_cache.json(既存記事台帳)を読み込み
↓
スコアリング(既存記事 × GSC/GA4指標)
↓
リライト候補 / 新規キーワード候補に分岐
↓
Slack通知(別々のメッセージで送信)
↓
必要に応じてkeyword_queue.csvへ候補を追加フロー1・2の記事生成フローとの関係については、n8nでWebhook後にデータが消える問題をsession.jsonで解決した記事で書いたフロー構成を参照してください。フロー0はそれらの前段にあたる分析フローで、何を書くかを決めるための入力を作ります。
実際のn8nノード構成はこのようになっています。
Schedule Trigger(週次)
↓
Execute Command(fetch_metrics_json.py実行)
↓
performance_cache.json読み込み
↓
Extract from File(JSONパース)
↓
Google Driveからsummary_cache.json取得
↓
Codeノード(URLと照合・スコアリング)
↓
IFノード:リライト候補あり?
→ Slack通知(リライト候補)
IFノード:新規候補あり?
→ Slack通知(新規キーワード候補)
→ keyword_queue.csvへ追加(承認後)Execute CommandノードでPythonを実行してからJSONを読み込む流れは、n8nのMergeノードでデータが取れない原因と直接参照で解決した記事で触れたExtract from Fileノードの扱いと同じ注意点があります。バイナリとして読み込まれる場合があるため、JSONパースのステップを忘れずに挟みます。
データ取得はPythonスクリプトに分離した
n8nだけでGSC/GA4の認証・取得を組むことも技術的には可能です。ただし、Google APIの認証フロー・スコープ設定・レート制限への対応を全部n8nのHTTPノードで組むと、フロー自体が複雑になりすぎます。
今回は fetch_metrics_json.py を作成してVPSの /root/metrics/ に配置し、n8nのExecute Commandノードから実行する構成にしました。
cd /root/metrics && python3 fetch_metrics_json.pyPythonスクリプトが実行されると、取得結果が /root/metrics/performance_cache.json として保存されます。n8nはその後でperformance_cache.jsonを読み込んで分析に使います。
performance_cache.jsonの構造はこのようになっています。
{
"generated_at": "2026-05-10T00:00:00+09:00",
"pages": [
{
"url": "https://bizinets.life/n8n-webhook-session-json/",
"gsc": {
"impressions": 1200,
"clicks": 18,
"ctr": 0.015,
"position": 14.8,
"queries": [
{
"query": "n8n session.json 状態保持",
"impressions": 300,
"clicks": 4,
"position": 16.2
}
]
},
"ga4": {
"users": 85,
"avg_duration": 42,
"engagement_rate": 0.51,
"scroll_50": 0.38,
"scroll_75": 0.21,
"scroll_90": 0.08
}
}
]
}この構造にすることで、ページ単位でGSCとGA4の指標を一元管理できます。n8n側で読み込んだときに、urlをキーとしてsummary_cache.jsonと照合する設計にしています。
この設計の狙いは役割分担です。n8nはオーケストレーター(処理の順序管理・通知・ファイル更新)、Pythonはデータ取得担当として分離しました。Pythonスクリプト側はn8nを意識せず、取得→保存だけに集中できます。
取得したGSCデータ:表示回数・クリック・順位・クエリ
Search Console APIから取得したデータは以下のとおりです。
取得期間:過去30日
取得件数:上位100件の検索クエリ
取得項目:
impressions:表示回数clicks:クリック数ctr:クリック率position:平均掲載順位query:検索クエリpage:対応するページURL
GSCデータで特に注目したのは「表示はあるがクリックされていない」「掲載順位が11〜30位で伸びしろがある」という組み合わせです。表示回数が多く順位が20位前後の記事は、タイトルや構成を改善することでクリック率が上がる可能性があります。
また、想定していなかったクエリで表示されている記事も候補になります。そのクエリに合わせた内容を追記・修正することで、既存記事の射程を広げられます。
取得したGA4データ:滞在・エンゲージメント・スクロール
GA4からは、記事に流入した後の読まれ方を取得しました。
取得項目:
users:ユーザー数avg_duration:平均エンゲージメント時間engagement_rate:エンゲージメント率scroll_50:50%スクロール率scroll_75:75%スクロール率scroll_90:90%スクロール率(GTMでカスタムイベントとして計測)
GSCが「検索からページへの入口データ」だとすれば、GA4は「ページに来た後の読まれ方データ」です。
流入はあるが avg_duration が極端に短い記事、scroll_50 が低い記事は、期待した読者に届いていないか、内容が読者の意図に合っていない可能性があります。これらはリライト候補として優先度が上がります。
summary_cache.jsonと照合して「既存記事か新規候補か」を分ける
フロー0の中心設計がここです。
GSCには検索クエリが出てきますが、そのクエリに対応する既存記事があるかどうかはGSCだけでは判断できません。そこで summary_cache.json と照合します。
summary_cache.json は公開済み記事の台帳で、以下の情報が入っています。
{
"slug": "n8n-webhook-session-json",
"url": "https://bizinets.life/n8n-webhook-session-json/",
"wp_title": "n8nでWebhook後にデータが消える問題をsession.jsonで解決した",
"target_keyword": "n8n session.json Webhook 状態保持",
"summary": "記事の要約テキスト",
"primary_topic": "practice-log",
"related_topics": ["n8n", "Webhook", "session管理"]
}照合の流れ:
- GSCのページURLと
summary_cache.urlを照合 - 一致する記事がある → 既存記事のリライト候補 として扱う
- GSCで表示されているクエリのうち、
summary_cache.target_keyword/title/related_topics/summaryに十分対応できる記事がないもの → 新規キーワード候補 として扱う
「記事台帳(summary_cache)と成果データ(performance_cache)をJOINする」という発想が、このフローの核心です。どちらか一方だけでは「どの記事を直すか」「何を新しく書くか」の判断ができません。
リライト候補のスコアリング設計
既存記事の中からリライト優先度を決めるスコアリングを設計しました。
優先度が上がる条件(ANDではなく、複数該当するほど優先度が上がる):
- 表示回数が一定以上ある(検索需要が存在する)
- 平均掲載順位が11〜30位(上位に届いていないが表示されている)
- CTRが低い(検索者のクリックを獲得できていない)
avg_durationが短い(読まれていない)engagement_rateが低い(すぐ離脱している)scroll_50/scroll_75が低い(途中で読むのをやめている)
スコアリングの出力イメージ:
{
"type": "rewrite_candidate",
"url": "https://bizinets.life/n8n-webhook-session-json/",
"title": "n8nでWebhook後にデータが消える問題をsession.jsonで解決した",
"reason": "表示回数はあるがCTRが低く、平均順位が15位前後のため改善余地あり",
"metrics": {
"impressions": 1200,
"clicks": 18,
"position": 14.8,
"ctr": 0.015,
"avg_duration": 42,
"scroll_50": 0.38
}
}スコアリングはあくまで「判断材料」です。スコアが高いからといって自動的にリライトを実行するわけではありません。Slack通知で人間確認を挟んでから、実際にリライトするかどうかを決めます。
新規キーワード候補のスコアリング設計
GSC検索クエリの中から、既存記事で十分に対応できていないテーマを新規キーワード候補として抽出します。
抽出の流れ:
- GSCで表示されているクエリを取得
summary_cache.target_keywordとrelated_topicsに含まれないクエリを抽出- 表示回数が一定以上あるクエリを優先
- 既存記事のタイトル・要約と意味的に近すぎるものはカニバリの可能性があるため除外
出力イメージ:
{
"type": "new_keyword_candidate",
"query": "n8n ga4 連携 python",
"reason": "GSCで月間300表示があるが、summary_cache内に直接対応する記事がない",
"suggested_angle": "n8nとPythonを組み合わせてGA4データを取得する方法"
}新規候補はそのままkeyword_queue.csvに追加するわけではありません。Slack通知で確認してから、必要なものだけ追記します。「全部を記事化」ではなく、人間が取捨選択する前提の設計です。
Slack通知をリライト候補と新規候補で分けた理由
リライト候補と新規キーワード候補は、判断の性質が異なります。
- リライト候補:既存記事の内容・構成・タイトルを直すかどうかの判断
- 新規候補:新しい記事として作るかどうか、keyword_queue.csvに入れるかどうかの判断
同じSlackメッセージに混ぜると、どちらについて判断すべきかが分かりにくくなります。そのため、別々のメッセージとして送信します。
Slack通知の内容には「候補名・判断理由・主要指標・推奨アクション」を含めます。通知を見た時点でアクションを決められる情報量にするのがポイントです。
実際の通知イメージ:
【リライト候補】
記事:n8nでWebhook後にデータが消える問題をsession.jsonで解決した
理由:表示回数はあるがCTRが低く、平均順位が15位前後
推奨:タイトル・冠頭・内部リンクを確認
主要指標: impressions:1200 / ctr:1.5% / position:14.8 / scroll_50:38%
【新規キーワード候補】
クエリ:n8n ga4 連携 python
理由:GSCで300表示があるが、summary_cache内に直接対応する記事がない
推奨:新規記事候補としてkeyword_queue.csv追加を検討Slack承認フローの詳細はn8nでSlack返信を使った承認フローを実装した記事で書いています。
詰まり:Cloudflare対策でUser-Agentが必要だった
fetch_metrics_json.py を使った処理の中で、ページURLの確認やHTML取得を伴う处理を行ったときに、Cloudflare側でリクエストが弾かれることがありました。GSC/GA4 APIの取得そのものではなく、サイト側へアクセスする処理で発生した問題です。
PythonのデフォルトのUser-Agentはbot的なアクセスとして認識されやすく、Cloudflareのセキュリティチェックに引っかかる場合があります。対策として、ブラウザに近いUser-Agentを明示的に設定しました。
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}これはサイト構成や取得方法によって対応が変わりますが、Pythonスクリプトでデータ取得するときに詰まったら、まずUser-Agentを確認することをおすすめします。
また、Pythonスクリプトの実行が失敗したときにn8n側でSlack通知を飛ばす処理も入れています。データ取得が静かに失敗していても気づけるようにするためです。
注意点:データだけで自動判断しない
スコアリングで候補を出せるようになりましたが、数値だけで自動的にリライト・記事化の判断をしないように設計しています。
理由はいくつかあります。
表示回数が少ない記事でも重要な場合がある: 特定の読者層を意識した記事や、内部リンクの起点になっている記事は、流入数が少なくても価値があります。
CTRが低い理由はタイトルだけではない: 検索意図とページ内容のズレ、スニペットの出方、競合記事のクオリティなど、複数の要因があります。
滞在時間が短くても問題ない記事がある: 即答型の記事(「〜するコマンドは何か」など)は短時間で解決するため、滞在時間が短くても読者満足度は高い場合があります。
新規クエリが既存記事とカニバリを起こす可能性がある: 似たテーマで複数記事を作ると、検索結果で自分の記事同士が競合することがあります。
だから、スコアリングはSlack通知までで止め、最終判断は人間が行う設計にしています。「スコアリングは判断材料、決定は人間」という原則です。
まとめ
GSC/GA4を見るだけでは次のアクションに変換しにくい。そこでn8nでデータを取得し、既存記事台帳と照合して候補として出すフロー0を作りました。
フロー0のポイントは:
- データ取得をPythonスクリプトに分離(n8nはオーケストレーター)
- GSC(入口データ)とGA4(読まれ方データ)を組み合わせて見る
- summary_cache.jsonと照合して「既存記事か新規候補か」を分ける
- リライト候補と新規候補を別々のSlack通知で出す
- 数値は判断材料であり、最終決定は人間が行う
今回のフロー0で重要だったのは、GSC/GA4のデータを取得することそのものではなく、取得したデータを「次に直す記事」「次に作る記事」という判断可能な形に変換することでした。このような自動化の判断軸はbizinets.biz側で整理しています。
bizinets.biz
記事を読んで「もっと知りたい」と思ったあなたへ
bizinets.biz では、ビジネスの仕組みづくりを体系的に学べるコンテンツを用意しています。まずは一度、覗いてみてください。
bizinets.biz へ行く →