新着検知→X自動投稿:GAS+スプレッドシート(UTM&3連投)

新着検知→X自動投稿:GAS+スプレッドシート(UTM&3連投)

新着検知→X自動投稿:GAS+スプレッドシート

公開しても見られない問題の多くは「告知の頻度・タイミング不足」。本記事ではBloggerの新着を自動検知し、 スプレッドシートで文面を管理してX(Twitter)へ3回(当日/翌朝/3日後)自動投稿する仕組みを、 Google Apps Script(GAS)で実装します。UTM付きで計測もOK。

TL;DR(150字)

Atomフィードで新着を検知→シートに3件のポストを自動生成→トリガーで所定時刻に投稿。 X APIの書き込み権限を用意し、GASでOAuth1.0a署名→/2/tweetsにPOST。

全体像

  1. 新着検知:/atom.xml?redirect=false&start-index=1&max-results=10 をポーリング
  2. シートにキュー化:当日/翌朝/3日後の3件を作成(UTM付き)
  3. 投稿実行:時間主導トリガー(例:15分ごと)で期限到来分を投稿
  4. 失敗時はリトライ&エラーログ

スプレッドシート設計(1枚でOK)

sheet: posts
columns:
  status        // Draft/Queued/Posted/Error
  scheduled_at  // 2025-09-19T12:10:00+09:00(ISO8601)
  text          // 投稿文(120字程度+URL)
  url           // 記事URL(UTM付与前の素URL)
  utm_campaign  // auto_day0 など
  final_url     // UTM付与後のURL(自動生成)
  tweet_id      // 投稿後に格納
  error_message // 失敗時のメモ

GASコード(そのまま貼付)

Apps Scriptエディタを開き、以下をコピペ→スクリプトプロパティにAPIキー等を保存します。

1) 設定:スクリプトプロパティ

TW_CONSUMER_KEY=【API Key】
TW_CONSUMER_SECRET=【API Key Secret】
TW_ACCESS_TOKEN=【Access Token】
TW_ACCESS_TOKEN_SECRET=【Access Token Secret】
BLOG_FEED=https://smartlifebit.blogspot.com/atom.xml?redirect=false&start-index=1&max-results=10
SITE_URL_BASE=https://smartlifebit.blogspot.com

2) main.gs


// ---- Utilities
const PROPS = PropertiesService.getScriptProperties();
const SHEET = SpreadsheetApp.getActive().getSheetByName('posts');

function encRFC3986(s) {
  return encodeURIComponent(s).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase());
}
function nonce() { return Utilities.getUuid().replace(/-/g,''); }
function timestamp() { return Math.floor(Date.now()/1000); }

function oauthHeader(method, url, extraParams={}) {
  const params = {
    oauth_consumer_key: PROPS.getProperty('TW_CONSUMER_KEY'),
    oauth_nonce: nonce(),
    oauth_signature_method: 'HMAC-SHA1',
    oauth_timestamp: timestamp(),
    oauth_token: PROPS.getProperty('TW_ACCESS_TOKEN'),
    oauth_version: '1.0',
    ...extraParams
  };
  const baseParams = Object.keys(params).sort().map(k => `${encRFC3986(k)}=${encRFC3986(params[k])}`).join('&');
  const baseString = [method.toUpperCase(), encRFC3986(url), encRFC3986(baseParams)].join('&');
  const signingKey = `${encRFC3986(PROPS.getProperty('TW_CONSUMER_SECRET'))}&${encRFC3986(PROPS.getProperty('TW_ACCESS_TOKEN_SECRET'))}`;
  const sigBytes = Utilities.computeHmacSha1Signature(baseString, signingKey);
  const signature = Utilities.base64Encode(sigBytes);

  const header = 'OAuth ' + Object.keys(params).sort().map(k => `${encRFC3986(k)}="${encRFC3986(params[k])}"`).join(', ')
                    + `, oauth_signature="${encRFC3986(signature)}"`;
  return header;
}

// ---- X API
function postTweet_(text) {
  const url = 'https://api.twitter.com/2/tweets';
  const method = 'POST';
  const headers = {
    Authorization: oauthHeader(method, url),
    'Content-Type': 'application/json'
  };
  const payload = JSON.stringify({ text });
  const res = UrlFetchApp.fetch(url, { method, headers, payload, muteHttpExceptions: true });
  const code = res.getResponseCode();
  if (code >= 200 && code < 300) {
    const data = JSON.parse(res.getContentText());
    return { ok: true, id: data.data && data.data.id, raw: data };
  }
  return { ok: false, code, body: res.getContentText() };
}

// ---- URL/UTM
function addUtm_(url, campaign) {
  const u = new URL(url);
  u.searchParams.set('utm_source', 'twitter');
  u.searchParams.set('utm_medium', 'social');
  u.searchParams.set('utm_campaign', campaign || 'auto');
  return u.toString();
}

// ---- Feed → Queue(新着分だけ)
function enqueueFromFeed() {
  const feedUrl = PROPS.getProperty('BLOG_FEED');
  const xml = UrlFetchApp.fetch(feedUrl, { muteHttpExceptions: true }).getContentText('UTF-8');
  const doc = XmlService.parse(xml);
  const ns = XmlService.getNamespace('http://www.w3.org/2005/Atom');
  const entries = doc.getRootElement().getChildren('entry', ns);
  if (!entries.length) return;

  const store = PropertiesService.getScriptProperties();
  const lastSeen = store.getProperty('LAST_ENTRY_ID');

  // 最新→古い順に処理
  entries.forEach(e => {
    const id = e.getChildText('id', ns);
    const title = e.getChildText('title', ns);
    const linkEl = e.getChildren('link', ns).find(l => l.getAttribute('rel')?.getValue() === 'alternate');
    const url = linkEl ? linkEl.getAttribute('href').getValue() : '';
    const published = e.getChildText('published', ns);

    if (id === lastSeen) return;
    // 先頭エントリのみを新着とみなす(必要なら複数検知に拡張)
    if (!store.getProperty('NEW_ADDED')) {
      createThreePosts_(title, url);
      store.setProperty('NEW_ADDED', '1');
    }
    // 一番新しいIDを保存
    store.setProperty('LAST_ENTRY_ID', id);
  });
  store.deleteProperty('NEW_ADDED');
}

// 3件のポストを生成(当日 / 翌朝 / 3日後)
function createThreePosts_(title, url) {
  const now = new Date();
  const day1 = new Date(now.getTime() + 24*60*60*1000);
  const day3 = new Date(now.getTime() + 3*24*60*60*1000);

  const rows = [
    { when: now,     text: `記事公開:「${title}」\n${url}`, campaign: 'auto_day0' },
    { when: setClock_(day1, 8, 10),  text: `30分で要点チェック:${title}\n${url}`, campaign: 'auto_day1' },
    { when: setClock_(day3, 20, 30), text: `失敗しやすい点を再確認:${title}\n${url}`, campaign: 'auto_day3' },
  ];
  rows.forEach(r => {
    const final = addUtm_(url, r.campaign);
    SHEET.appendRow(['Queued', r.when.toISOString(), r.text, url, r.campaign, final, '', '']);
  });
}

function setClock_(dateObj, hh, mm) {
  const d = new Date(dateObj);
  d.setHours(hh, mm, 0, 0);
  return d;
}

// ---- 実行:期限到来分を投稿
function runScheduler() {
  const now = new Date();
  const values = SHEET.getDataRange().getValues();
  const header = values.shift();
  const idx = Object.fromEntries(header.map((h,i)=>[h,i]));

  values.forEach((row, rIdx) => {
    const status = row[idx.status];
    if (status !== 'Queued') return;
    const when = new Date(row[idx.scheduled_at]);
    if (when > now) return;

    const text = `${row[idx.text]}\n${row[idx.final_url]}`;
    const res = postTweet_(text);
    if (res.ok) {
      SHEET.getRange(rIdx+2, idx.status+1).setValue('Posted');
      SHEET.getRange(rIdx+2, idx.tweet_id+1).setValue(res.id);
      SHEET.getRange(rIdx+2, idx.error_message+1).setValue('');
    } else {
      const msg = `HTTP ${res.code} ${res.body}`.slice(0, 500);
      SHEET.getRange(rIdx+2, idx.status+1).setValue('Error');
      SHEET.getRange(rIdx+2, idx.error_message+1).setValue(msg);
    }
  });
}
    

3) 初期化&トリガー

  1. シートに posts を作成し、上記カラムを1行目にコピー
  2. メニュー:実行enqueueFromFeed を1回実行(権限承認)
  3. トリガーrunScheduler を「15分ごと」実行で作成

以後は新着時に自動で3件キュー化、時刻到来で順次ポストします。

X API前提(重要)

  • X開発者アカウントでアプリを作成し、書き込み権限(Read and write)を有効化
  • API Key/Secret と Access Token/Secret を発行し、スクリプトプロパティへ保存
  • 現在のX APIはプランにより制限があります。失敗時はerror_message列のレスポンスで確認

運用ヒント

  • 文面はシートで自由に編集OK(絵文字や改行も可)。URLはfinal_urlがUTM自動付与
  • 朝/夜の時間帯はシート側で調整。A/Bテストは同記事に複数行を追加
  • 画像添付まで自動化する場合は、メディアアップロードAPIの実装を追加

チェックリスト

  • [ ] Xアプリの書き込み権限ON/トークン4種を設定
  • [ ] postsシートのヘッダー行を作成
  • [ ] enqueueFromFeedを初回実行→キュー生成
  • [ ] runSchedulerをトリガー「15分ごと」に設定
  • [ ] Search ConsoleのクエリでUTM流入を確認

コメント

このブログの人気の投稿

Bloggerインデックス完全ガイド(Search Console編)

2025年無料家計簿アプリ3選!マネーフォワード ME、シンプル家計簿、Zaimを比較。節約&ガジェット好きに最適。