新着検知→X自動投稿:GAS+スプレッドシート(UTM&3連投)
新着検知→X自動投稿:GAS+スプレッドシート(UTM&3連投)
公開しても見られない問題の多くは「告知の頻度・タイミング不足」。本記事ではBloggerの新着を自動検知し、 スプレッドシートで文面を管理してX(Twitter)へ3回(当日/翌朝/3日後)自動投稿する仕組みを、 Google Apps Script(GAS)で実装します。UTM付きで計測もOK。
TL;DR(150字)
Atomフィードで新着を検知→シートに3件のポストを自動生成→トリガーで所定時刻に投稿。
X APIの書き込み権限を用意し、GASでOAuth1.0a署名→/2/tweetsにPOST。
全体像
- 新着検知:
/atom.xml?redirect=false&start-index=1&max-results=10をポーリング - シートにキュー化:当日/翌朝/3日後の3件を作成(UTM付き)
- 投稿実行:時間主導トリガー(例:15分ごと)で期限到来分を投稿
- 失敗時はリトライ&エラーログ
スプレッドシート設計(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) 初期化&トリガー
- シートに
postsを作成し、上記カラムを1行目にコピー - メニュー:実行→
enqueueFromFeedを1回実行(権限承認) - トリガー:
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流入を確認
コメント
コメントを投稿