n8nでSEOリライトを自動化した方法|GSC・GA4活用とフロー0統合の実践ログ
# n8nでSEOリライトを自動化した方法|GSC・GA4活用とフロー0統合の実践ログ
GSC/GA4のデータを毎週取得しているのに、リライトだけ手作業になっていませんか。
私もJogtimeの運用で、データ取得と改善フローが分離されている状態がしばらく続いていました。
この記事では、n8nのフロー0(キーワード抽出)にリライト処理を統合し、GSC/GA4データを再取得せずに改善フローまで自動化した実装構成を公開します。rewrite_type(intro/body/cta)の判定設計・Drive上の既存記事取得・AIプロンプト設計・Slack通知まで、実際に動いた構成をそのまま残しています。
リライトフローをフロー0に統合した理由
最初はリライトを独立したフローとして設計しようとしていました。ただ、よく考えると独立フローにすると1つの問題が出ます。GSC/GA4データを二度取得しなければならないということです。
フロー0では毎週GSC/GA4データをバッチ取得してperformance_cache_jogtime.jsonに保存しています。このデータには、新規キーワード候補の抽出に使う検索クエリデータだけでなく、既存記事のGA4指標(直帰率・スクロール到達率・CTA率)も含まれています。
リライト候補の判定にはこのGA4指標がそのまま使えます。わざわざ別フローを立ち上げてデータを再取得するのは無駄です。フロー0のキーワードスコアリングノードでrewrite_candidates配列を生成し、新規キーワード処理の後にそのままリライトフローへ流す設計が最もシンプルでした。
フロー全体のつながりはこうなっています。
フロー0(Cron週次)
→ GSC/GA4バッチ取得
→ キーワードスコアリング
→ new_keywords(新規候補)
→ rewrite_candidates(リライト候補)
→ 新規キーワード処理(AI精査→Sheets追記)
→ Slack通知(新規キーワード)
→ リライト候補あり?(IF)
→ false: フロー0完了通知(Slack)
→ true: リライト候補展開
→ リライトループ(1記事ずつ)
→ ループ完了後: フロー0完了通知新規キーワードがゼロでもリライト候補があれば処理が走り、どちらも完了した段階でフロー0完了通知を送る構成です。
GSC/GA4データでrewrite_type(intro/body/cta)を判定する設計
リライトで一番重要なのは「どこを直すか」の判定です。全部まとめて直しても読者の行動は変わりません。GA4の指標から「どこで読者が離脱しているか」を逆算してrewrite_typeを決める設計にしました。
判定ロジックはキーワードスコアリングのCodeノードに含まれています。
const rewrite_candidates = (perfData.articles ?? [])
.filter(a => a.ga4_sessions !== null && a.ga4_sessions >= 10)
.map(article => {
const b = article.ga4_bounce_rate ?? 999;
const s50 = article.ga4_scroll_50 ?? 0;
const s90 = article.ga4_scroll_90 ?? 0;
const cta = article.ga4_cta_click_rate ?? 0;
let priority = 5;
let rewrite_type = 'none';
let rewrite_axis = '対応不要';
if (b > 70 && s50 < 40) {
priority = 1;
rewrite_type = 'intro';
rewrite_axis = `直帰率${b}%・scroll_50が${s50}%。冒頭で記事の価値が伝わっていない。`;
} else if (s90 > 20 && cta < 5) {
priority = 2;
rewrite_type = 'cta';
rewrite_axis = `scroll_90が${s90}%まで読まれているがCTA率${cta}%。`;
} else if (s50 > 60 && s90 < 20) {
priority = 3;
rewrite_type = 'body';
rewrite_axis = `scroll_50は${s50}%だがscroll_90が${s90}%。本文中盤で離脱している。`;
}
return { slug, rewrite_type, rewrite_axis, priority, ...metrics };
})
.filter(a => a.rewrite_type !== 'none')
.sort((a, b) => a.priority - b.priority);3つのrewrite_typeの判定基準はこうなっています。
intro(冒頭離脱): 直帰率70%超かつscroll_50到達率40%未満。記事を開いたけれど読まずに離脱しているパターンです。H1直下の導入文が読者の期待と合っていないケースが多いです。
cta(読了後CTA未踏): scroll_90到達率20%超かつCTAクリック率5%未満。最後まで読んでいるのにCTAを踏んでいないパターンです。CTA直前のベネフィット訴求が弱いケースです。
body(中盤離脱): scroll_50到達率60%超かつscroll_90到達率20%未満。前半は読まれているのに中盤で離脱しているパターンです。H2構成や具体例の薄さが原因になりやすいです。
セッション数10以上のフィルターを入れているのは、データが少なすぎる記事の誤判定を防ぐためです。
判断基準:
- 直帰率が高くてscroll_50が低い → intro(冒頭を見直す)
- scroll_90まで読まれているのにCTAが踏まれていない → cta(CTA直前を見直す)
- scroll_50は高いがscroll_90が低い → body(中盤を見直す)
- セッション数10未満 → 対象外(データ不足で誤判定リスクがある)
リライトループの構成とDriveからの記事取得
rewrite_candidatesはpriority順(1→2→3)にソートされた配列です。この配列をLoop Over Items(Batch Size: 1)で1記事ずつ処理します。ループ内のノード構成は以下です。
リライトループ(Loop Over Items)
├─ loop: リライト記事検索(Google Drive Search)
│ → article.md検索・ダウンロード
│ → meta.json検索・ダウンロード
│ → リライトデータ統合(Code)
│ → Slack着手通知
│ → リライト構成案生成(OpenAI)
│ → リライト構成案パース(Code)
│ → リライト本文生成(Claude)
│ → リライト本文パース(Code)
│ → リライトmeta.jsonエンコード(Code)
│ → article.md書き込み(VPS SSH)
│ → meta.json書き込み(VPS SSH)
│ → publish.mjs実行(VPS SSH)
│ → article.mdアップロード(Drive PATCH)
│ → meta.jsonアップロード(Drive PATCH)
│ → 記事フォルダ削除(VPS SSH)
│ → Slack完了通知(リライト)
└─ done: フロー0完了通知(Slack)Drive上の記事取得で詰まったポイントが1つあります。記事検索ノードのSearch Queryです。
={{ 'name contains \'' + $json.slug + '\'' }}フォルダIDには引き継ぎプロンプトに記載したJogtime記事フォルダ(1zbUE4wQe_d11ZXv_mBbSwdMRkdIGPFZ3)を指定しています。最初はbizinets.biz側のフォルダIDが入ったままになっていて、記事が見つからない状態が続きました。移植時のフォルダID差し替え漏れが原因でした。
リライトデータ統合ノードでは、ループの出力・article.mdのダウンロード内容・meta.jsonの解析結果を1つにまとめています。
const loopData = $('リライトループ').first().json;
const articleBinary = $('article.mdダウンロード').first().binary?.data;
const article_md = articleBinary
? Buffer.from(articleBinary.data, 'base64').toString('utf-8')
: '';
const metaBinary = $('meta.jsonダウンロード').first().binary?.data;
const meta = metaBinary
? JSON.parse(Buffer.from(metaBinary.data, 'base64').toString('utf-8'))
: {};rewrite_typeごとのAIプロンプト設計
リライトは2段階のAI処理で行っています。まずOpenAIで「どこを・なぜ・どう直すか」の構成案を生成し、次にClaudeで実際のリライト後本文を生成します。
第1段階:構成案生成(OpenAI)
OpenAIへのシステムプロンプトにはrewrite_typeごとの修正方針を明示しています。
rewrite_typeがintroの場合:
- H1直下に「この記事でわかること」を箇条書き3点で追加する
- 導入3行を「読者の状況への共感→記事で得られる価値→読み続ける理由」に再設計する
- タイトルは原則変更しない
rewrite_typeがctaの場合:
- 記事末尾のCTA直前セクションの末尾2〜3文のみを書き直す
- PDFで得られる具体的なベネフィットを1文で明示してからCTAへ誘導する
rewrite_typeがbodyの場合:
- 中盤(全体の40〜70%付近)のH2を1本追加または再構成する
- 具体例・数値・判断基準を中盤に厚く入れる出力はJSON形式(rewrite_sections・rewrite_reasons・rewrite_policy・title_change・priority)で受け取り、Codeノードでパースします。
第2段階:本文生成(Claude)
Claudeへのプロンプトには構成案の内容と元記事のarticle.md全文を渡しています。重要な制約は「指定されたセクションのみ書き直す・他のセクションは原文のまま残す」です。
これを明示しないと、AIが記事全体を書き直してしまいます。Jogtimeの文体ルール(ですます調・問いかけ・一人称「私」・恐怖訴求禁止)も合わせてシステムプロンプトに入れています。
判断基準:
- 構成案生成でJSONパースエラーが出る → システムプロンプトに「出力はJSONのみ。前後に説明文・コードブロックを含めないこと」を明示する
- Claudeが記事全体を書き直してしまう → 「指定されたセクションのみ書き直す・他は原文のまま」をシステムプロンプトに追加する
- title_changeが毎回変わってしまう → 「順位20位以下かつキーワード詰め込みタイトルの場合のみ変更」という条件をプロンプトに入れる
Drive上書きからSlack通知までの最終構成
リライト後の記事は以下の順序でVPSとDriveの両方に反映します。実際に動かすまではDrive API側の上書き処理やVPS更新順序の調整が必要でしたが、最終的に以下の構成に落ち着きました。
VPS書き込みとWordPress更新
# article.md書き込み
mkdir -p /root/wp-auto-jogtime/articles/{slug} && \
printf '%s' "{article_md}" > /root/wp-auto-jogtime/articles/{slug}/article.md
# meta.json書き込み(base64デコード)
echo {encoded_meta} | base64 -d > /root/wp-auto-jogtime/articles/{slug}/meta.json
# publish.mjs実行
cd /root/wp-auto-jogtime && node publish.mjs articles/{slug} --no-imagepublish.mjsの実行結果に「OK:」が含まれていることを確認してからフォルダを削除しています。
echo "{stdout}" | grep -q "OK:" && \
rm -rf /root/wp-auto-jogtime/articles/{slug} || \
echo "publish失敗のため削除スキップ"Drive上書き(PATCH)
DriveへのアップロードはPATCHメソッドを使って既存ファイルを上書きします。最初にSlugで対象フォルダを検索してファイルIDを取得し、そのIDに対してPATCH APIを呼び出す設計です。
https://www.googleapis.com/upload/drive/v3/files/{file_id}?uploadType=media新規アップロード(POST)と上書き(PATCH)でURLが異なる点がポイントです。最初に新規投稿ノードをそのまま使って「ファイルが増え続ける」問題が発生しました。
Slack通知の設計
リライト1記事ごとに完了通知を送ります。
✅ リライト完了:{title}
URL:{url}
🛠 実施した修正方針
{rewrite_policy}
📊 対応した問題
{rewrite_axis}全記事のループが完了した後、フロー0完了通知でまとめを送ります。
✅ フロー0完了
新規キーワード追加:{stats.filtered_keywords}件
リライト実行:{stats.rewrite_count}件
更新日時:{now}ただし、このままだと自分のサイトに合ったrewrite_type判定基準や改善設計の判断軸は決められません。
なぜなら、「GA4の数値をどう解釈して改善判断に結びつけるか」という軸がまだ整理されていないからです。
→ 判断軸の整理はこちら
よくある質問
Q. rewrite_candidatesが常に空配列になります。
performance_cache_jogtime.jsonのarticles配列にga4_sessionsが含まれているか確認してください。GA4データが取れていない場合、セッション数フィルター(ga4_sessions >= 10)でゼロ件になります。fetch_metrics_jogtime.pyの出力をSSHで直接確認するのが早いです。
Q. Drive検索でarticle.mdが見つかりません。
フォルダIDがJogtime記事フォルダ(1zbUE4wQe_d11ZXv_mBbSwdMRkdIGPFZ3)になっているか確認してください。bizinets.biz側のフォルダIDが残っていると、slugが一致してもファイルが見つかりません。
Q. PATCHで上書きしてもDrive上のファイルが更新されません。
PATCHのURLにuploadType=mediaパラメータが含まれているか確認してください。また、Bodyにファイルの内容が正しく渡されているか確認が必要です。Content-Typeヘッダーをtext/markdownに設定しないとバイナリとして扱われることがあります。
まとめ
n8nでリライト自動化を実装するとき、最初に決めるべきはフロー0との統合設計です。GSC/GA4データをリライト判定にも使い回すことで、データの二重取得をなくせます。
今日できる一歩として、performance_cache.jsonのarticles配列にga4_bounce_rateとga4_scroll_50とga4_scroll_90が含まれているか確認することをおすすめします。この3指標が揃っていれば、rewrite_type判定のコードをそのまま流用できます。
フローを分離する前に、「取得したデータを別用途に再利用できないか」を確認すると、構成がかなり整理しやすくなります。
ただし、このままだと自分のサイトに合ったrewrite_type判定基準や改善設計の判断軸は決められません。
なぜなら、「GA4の数値をどう解釈して改善判断に結びつけるか」という軸が、まだ整理されていないからです。
→ 判断軸の整理はこちら
bizinets.biz
記事を読んで「もっと知りたい」と思ったあなたへ
bizinets.biz では、ビジネスの仕組みづくりを体系的に学べるコンテンツを用意しています。まずは一度、覗いてみてください。
bizinets.biz へ行く →