Xの投稿を毎日続けたいと思っていても、手動で投稿し続けるのは意外と大変です。投稿する内容は決まっているのに、忙しくて投稿を忘れてしまうこともあります。
そこで便利なのが、GoogleスプレッドシートとGoogle Apps Scriptを使ったX自動投稿です。スプレッドシートに投稿文を一覧で用意しておき、Google Apps Scriptで「予約済み」の投稿を1件ずつXへ投稿する仕組みを作ります。
この方法なら、投稿文の管理、投稿状況の確認、エラー確認までスプレッドシート上で行えます。
この記事では、初心者の方でも順番に設定できるように、X APIの準備からGASコードの貼り付け、定期実行の設定まで詳しく解説します。
- X自動投稿の完成イメージ
- 事前に用意するもの
- スプレッドシートの列を作る
- 投稿ステータスの意味
- スプレッドシートの入力例
- X DeveloperでAPIキーを取得する
- APIキーの取り扱いに注意する
- Apps Scriptを開く
- GASコード全文
- APIキーを設定する
- setXApiKeysOnceを実行する
- テスト投稿を用意する
- postNextReservedPostToXを実行する
- 投稿URLを記録する方法
- 定期実行トリガーを設定する
- 毎日1件ずつ投稿される仕組み
- エラーが出たときの確認方法
- 投稿されないときの確認ポイント
- 投稿文の文字数を確認する
- 投稿文を作るときのコツ
- スプレッドシート管理のコツ
- 自動投稿を安全に運用するポイント
- 初心者におすすめの始め方
- まとめ
X自動投稿の完成イメージ
今回作る仕組みは、とてもシンプルです。
Googleスプレッドシートに投稿文を用意します。
投稿したい文章のステータスを「予約済み」にしておくと、Google Apps Scriptがその行を読み取り、Xへ投稿します。
投稿が成功すると、ステータスが「投稿済み」に変わります。
エラーが出た場合は、ステータスが「エラー」になり、エラー内容も記録できます。
全体の流れは次の通りです。
| 手順 | 内容 |
|---|---|
| 1 | スプレッドシートに投稿文を用意する |
| 2 | X DeveloperでAPIキーを取得する |
| 3 | Apps Scriptにコードを貼り付ける |
| 4 | APIキーを設定する |
| 5 | テスト投稿する |
| 6 | 時間主導型トリガーを設定する |
| 7 | 毎日自動でXへ投稿する |
この仕組みを一度作っておけば、あとはスプレッドシートに投稿文を追加していくだけで運用できます。
事前に用意するもの
X自動投稿を作るには、次のものを用意します。
| 必要なもの | 用途 |
|---|---|
| Googleアカウント | スプレッドシートとApps Scriptを使うため |
| Googleスプレッドシート | 投稿文を管理するため |
| X Developerアカウント | X APIを使うため |
| X APIキー | GASからXへ投稿するため |
すでにスプレッドシートを作成している場合は、そのシートをそのまま使えます。
スプレッドシートの列を作る
まず、作成済みのGoogleスプレッドシートを開きます。
1行目に、次の見出しを作成してください。
カテゴリー
元ネタ
X投稿文
投稿ステータス
投稿日時
投稿ID
投稿URL
エラー内容
横に並べると、次のような形です。
| カテゴリー | 元ネタ | X投稿文 | 投稿ステータス | 投稿日時 | 投稿ID | 投稿URL | エラー内容 |
|---|
最低限必要なのは、「X投稿文」と「投稿ステータス」です。
ただし、運用しやすくするために、「投稿日時」「投稿ID」「投稿URL」「エラー内容」も作っておくことをおすすめします。
投稿ステータスの意味
投稿ステータスは、自動投稿を管理するための重要な列です。
| ステータス | 意味 |
|---|---|
| 予約済み | これから投稿する文章 |
| 投稿中 | 処理中の文章 |
| 投稿済み | 投稿が完了した文章 |
| エラー | 投稿に失敗した文章 |
最初は、投稿したい行の「投稿ステータス」に「予約済み」と入力します。
GASが実行されると、予約済みの行を上から順番に探し、最初に見つかった1件だけを投稿します。
投稿が成功すると、その行は「投稿済み」に変わります。
そのため、次回実行時には次の「予約済み」の行が投稿されます。
スプレッドシートの入力例
たとえば、次のように入力します。
| カテゴリー | 元ネタ | X投稿文 | 投稿ステータス |
|---|---|---|---|
| WordPress | ログインURLの変更 | WordPressのログインURLを初期状態のままにしていませんか。セキュリティ対策として、ログインURLの変更は基本です。 | 予約済み |
| SEO | 画像のAlt属性 | 画像の代替テキストを空欄にしていませんか。Alt属性を設定すると、検索エンジンにも画像の意味が伝わりやすくなります。 | 予約済み |
| 高速化 | WebP画像 | サイト表示を速くしたいなら、画像の軽量化が重要です。WebP形式を使うと、画質を保ちながら容量を減らせます。 | 予約済み |
まずはテスト用として、1件だけ「予約済み」にしておくと安心です。
X DeveloperでAPIキーを取得する
次に、X DeveloperでAPIキーを取得します。
X APIを使って投稿するには、開発者用のアプリを作成し、認証情報を取得する必要があります。
取得する情報は次の4つです。
API Key
API Key Secret
Access Token
Access Token Secret
手順は次の通りです。
- X Developerにログインする
- Developer Portalを開く
- Projectを作成する
- Appを作成する
- App PermissionsをRead and Writeに変更する
- API KeyとAPI Key Secretを取得する
- Access TokenとAccess Token Secretを取得する
ここで重要なのは、App PermissionsをRead and Writeにすることです。
Readだけでは投稿できません。
また、権限を変更した後は、Access TokenとAccess Token Secretを再生成してください。
古いトークンのままだと、投稿権限が反映されない場合があります。
APIキーの取り扱いに注意する
APIキーやアクセストークンは、Xアカウントを操作するための重要な情報です。
外部に公開してはいけません。
次のような場所に書かないように注意してください。
| 避ける場所 | 理由 |
|---|---|
| ブログ記事内 | 誰でも見られるため |
| SNS投稿 | 不正利用される可能性があるため |
| 共有ドキュメント | 閲覧権限の管理が難しいため |
| 公開リポジトリ | 検索で見つかる可能性があるため |
今回は、Apps Scriptのスクリプトプロパティに保存する形で管理します。
Apps Scriptを開く
作成済みのスプレッドシートを開いた状態で、上部メニューから次の順番で進みます。
拡張機能
↓
Apps Script
Apps Scriptの画面が開いたら、最初から入っているコードを削除します。
その後、次のコードを貼り付けます。
GASコード全文
const SHEET_NAME = '';
const STATUS_RESERVED = '予約済み';
const STATUS_PROCESSING = '投稿中';
const STATUS_POSTED = '投稿済み';
const STATUS_ERROR = 'エラー';
function setXApiKeysOnce() {
PropertiesService.getScriptProperties().setProperties({
X_API_KEY: 'ここにAPI Keyを入力',
X_API_KEY_SECRET: 'ここにAPI Key Secretを入力',
X_ACCESS_TOKEN: 'ここにAccess Tokenを入力',
X_ACCESS_TOKEN_SECRET: 'ここにAccess Token Secretを入力',
X_USERNAME: 'ここにXのユーザー名を入力'
}, true);
}
function postNextReservedPostToX() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(30000)) {
console.log('別の処理が実行中のため終了しました。');
return;
}
try {
const sheet = getTargetSheet_();
const values = sheet.getDataRange().getValues();
if (values.length < 2) {
console.log('投稿データがありません。');
return;
}
const headers = values[0].map(header => String(header).trim());
const col = getColumnIndexes_(headers);
for (let i = 1; i < values.length; i++) {
const row = values[i];
const status = String(row[col.status] || '').trim();
if (status !== STATUS_RESERVED) {
continue;
}
const rowNumber = i + 1;
const text = String(row[col.text] || '').trim();
if (!text) {
updateIfColumnExists_(sheet, rowNumber, col.status, STATUS_ERROR);
updateIfColumnExists_(sheet, rowNumber, col.error, 'X投稿文が空です。');
return;
}
updateIfColumnExists_(sheet, rowNumber, col.status, STATUS_PROCESSING);
SpreadsheetApp.flush();
try {
const result = createXPost_(text);
const postId = result.data && result.data.id ? result.data.id : '';
updateIfColumnExists_(sheet, rowNumber, col.status, STATUS_POSTED);
updateIfColumnExists_(sheet, rowNumber, col.postedAt, new Date());
updateIfColumnExists_(sheet, rowNumber, col.postId, postId);
updateIfColumnExists_(sheet, rowNumber, col.error, '');
const username = PropertiesService.getScriptProperties().getProperty('X_USERNAME');
if (username && postId && col.postUrl !== -1) {
const cleanUsername = username.replace(/^@/, '');
const postUrl = 'https://x.com/' + cleanUsername + '/status/' + postId;
updateIfColumnExists_(sheet, rowNumber, col.postUrl, postUrl);
}
console.log('投稿成功: ' + text);
return;
} catch (error) {
updateIfColumnExists_(sheet, rowNumber, col.status, STATUS_ERROR);
updateIfColumnExists_(sheet, rowNumber, col.error, String(error.message || error));
console.error(error);
return;
}
}
console.log('予約済みの投稿はありません。');
} finally {
lock.releaseLock();
}
}
function createXPost_(text) {
const url = 'https://api.x.com/2/tweets';
const payload = JSON.stringify({
text: text
});
const authorizationHeader = createOAuth1Header_('POST', url);
const response = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
payload: payload,
headers: {
Authorization: authorizationHeader
},
muteHttpExceptions: true
});
const statusCode = response.getResponseCode();
const responseText = response.getContentText();
if (statusCode < 200 || statusCode >= 300) {
throw new Error('X APIエラー: HTTP ' + statusCode + ' / ' + responseText);
}
return JSON.parse(responseText);
}
function createOAuth1Header_(method, url) {
const props = PropertiesService.getScriptProperties();
const apiKey = props.getProperty('X_API_KEY');
const apiKeySecret = props.getProperty('X_API_KEY_SECRET');
const accessToken = props.getProperty('X_ACCESS_TOKEN');
const accessTokenSecret = props.getProperty('X_ACCESS_TOKEN_SECRET');
if (!apiKey || !apiKeySecret || !accessToken || !accessTokenSecret) {
throw new Error('X APIキーが設定されていません。setXApiKeysOnceを実行してください。');
}
const oauthParams = {
oauth_consumer_key: apiKey,
oauth_nonce: createNonce_(),
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp: Math.floor(Date.now() / 1000).toString(),
oauth_token: accessToken,
oauth_version: '1.0'
};
const parameterString = Object.keys(oauthParams)
.sort()
.map(key => encodeRfc3986_(key) + '=' + encodeRfc3986_(oauthParams[key]))
.join('&');
const signatureBaseString = [
method.toUpperCase(),
encodeRfc3986_(url),
encodeRfc3986_(parameterString)
].join('&');
const signingKey = encodeRfc3986_(apiKeySecret) + '&' + encodeRfc3986_(accessTokenSecret);
const signatureBytes = Utilities.computeHmacSignature(
Utilities.MacAlgorithm.HMAC_SHA_1,
signatureBaseString,
signingKey
);
const signature = Utilities.base64Encode(signatureBytes);
oauthParams.oauth_signature = signature;
const authorizationHeader = 'OAuth ' + Object.keys(oauthParams)
.sort()
.map(key => encodeRfc3986_(key) + '="' + encodeRfc3986_(oauthParams[key]) + '"')
.join(', ');
return authorizationHeader;
}
function getTargetSheet_() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
if (SHEET_NAME) {
const sheet = spreadsheet.getSheetByName(SHEET_NAME);
if (!sheet) {
throw new Error('指定したシートが見つかりません: ' + SHEET_NAME);
}
return sheet;
}
return spreadsheet.getSheets()[0];
}
function getColumnIndexes_(headers) {
const text = headers.indexOf('X投稿文');
const status = headers.indexOf('投稿ステータス');
if (text === -1) {
throw new Error('1行目に「X投稿文」という見出しが必要です。');
}
if (status === -1) {
throw new Error('1行目に「投稿ステータス」という見出しが必要です。');
}
return {
text: text,
status: status,
postedAt: headers.indexOf('投稿日時'),
postId: headers.indexOf('投稿ID'),
postUrl: headers.indexOf('投稿URL'),
error: headers.indexOf('エラー内容')
};
}
function updateIfColumnExists_(sheet, rowNumber, columnIndex, value) {
if (columnIndex === -1 || columnIndex === undefined || columnIndex === null) {
return;
}
sheet.getRange(rowNumber, columnIndex + 1).setValue(value);
}
function createNonce_() {
return Utilities.getUuid().replace(/-/g, '');
}
function encodeRfc3986_(value) {
return encodeURIComponent(value)
.replace(/[!'()]/g, function(character) {
return '%' + character.charCodeAt(0).toString(16).toUpperCase();
});
}
APIキーを設定する
コードを貼り付けたら、まず次の部分を自分の情報に変更します。
function setXApiKeysOnce() {
PropertiesService.getScriptProperties().setProperties({
X_API_KEY: 'ここにAPI Keyを入力',
X_API_KEY_SECRET: 'ここにAPI Key Secretを入力',
X_ACCESS_TOKEN: 'ここにAccess Tokenを入力',
X_ACCESS_TOKEN_SECRET: 'ここにAccess Token Secretを入力',
X_USERNAME: 'ここにXのユーザー名を入力'
}, true);
}
入力する内容は次の通りです。
| 項目 | 入力する内容 |
|---|---|
| X_API_KEY | X Developerで取得したAPI Key |
| X_API_KEY_SECRET | X Developerで取得したAPI Key Secret |
| X_ACCESS_TOKEN | X Developerで取得したAccess Token |
| X_ACCESS_TOKEN_SECRET | X Developerで取得したAccess Token Secret |
| X_USERNAME | 自分のXユーザー名 |
X_USERNAMEは、投稿URLをスプレッドシートに記録するために使います。
たとえば、Xのユーザー名がexampleの場合は、次のように入力します。
X_USERNAME: 'example'
@は付けなくて大丈夫です。
setXApiKeysOnceを実行する
APIキーを入力したら、Apps Script画面上部の関数選択から次を選びます。
setXApiKeysOnce
その状態で「実行」をクリックします。
初回実行時は、Googleアカウントの承認画面が表示されます。
内容を確認して、許可してください。
実行が完了すると、APIキーがスクリプトプロパティに保存されます。
この作業は基本的に最初の1回だけで大丈夫です。
テスト投稿を用意する
次に、スプレッドシートにテスト投稿を1件用意します。
例として、次のように入力します。
| カテゴリー | 元ネタ | X投稿文 | 投稿ステータス |
|---|---|---|---|
| テスト | GAS投稿テスト | Google Apps ScriptからXへ自動投稿するテストです。 | 予約済み |
最初から本番用の長い文章で試すのではなく、短いテスト文で確認するのがおすすめです。
postNextReservedPostToXを実行する
Apps Script画面上部の関数選択から、次を選びます。
postNextReservedPostToX
その状態で「実行」をクリックします。
処理が成功すると、Xに投稿されます。
スプレッドシート側では、投稿ステータスが次のように変わります。
予約済み
↓
投稿中
↓
投稿済み
投稿日時、投稿ID、投稿URLの列を作っている場合は、それらも自動で記録されます。
投稿URLを記録する方法
投稿URLを記録したい場合は、スプレッドシートの1行目に次の見出しを作ってください。
投稿URL
さらに、APIキー設定時にX_USERNAMEを入力しておきます。
X_USERNAME: '自分のユーザー名'
投稿が成功すると、次のようなURLが自動で入ります。
https://x.com/ユーザー名/status/投稿ID
投稿URLがあると、後から投稿内容を確認しやすくなります。
定期実行トリガーを設定する
テスト投稿が成功したら、毎日自動で実行されるようにトリガーを設定します。
手順は次の通りです。
- Apps Script画面の左側にある「トリガー」をクリックする
- 右下の「トリガーを追加」をクリックする
- 実行する関数を「postNextReservedPostToX」にする
- イベントのソースを「時間主導型」にする
- 時間ベースのトリガーのタイプを「日付ベースのタイマー」にする
- 時刻を選択する
- 保存する
たとえば、毎朝投稿したい場合は、次のように設定します。
| 項目 | 設定内容 |
|---|---|
| 実行する関数 | postNextReservedPostToX |
| イベントのソース | 時間主導型 |
| タイマーの種類 | 日付ベースのタイマー |
| 時刻 | 午前8時から9時 |
これで、毎日指定した時間帯にスクリプトが実行されます。
スプレッドシートを閉じていても、自動で動きます。
毎日1件ずつ投稿される仕組み
今回のコードでは、「予約済み」の投稿を上から順番に探します。
最初に見つかった1件だけを投稿します。
投稿が成功した行は「投稿済み」になるため、次回はその次の「予約済み」が投稿されます。
たとえば、30件の投稿文を用意しておけば、30日分の自動投稿リストとして使えます。
投稿文を追加したい場合は、スプレッドシートに新しい行を追加して、投稿ステータスを「予約済み」にするだけです。
エラーが出たときの確認方法
投稿に失敗した場合は、投稿ステータスが「エラー」になります。
エラー内容の列を作っている場合は、そこに原因が記録されます。
よくあるエラーは次の通りです。
| エラー | 主な原因 |
|---|---|
| 401 Unauthorized | APIキーやトークンが間違っている |
| 403 Forbidden | 書き込み権限がない |
| 429 Too Many Requests | APIの利用制限に達している |
| 400 Bad Request | 投稿本文やリクエスト内容に問題がある |
| 予約済みがない | 投稿対象の行がない |
特に多いのは、書き込み権限の不足です。
App PermissionsをRead and Writeに変更し、Access TokenとAccess Token Secretを再生成してください。
投稿されないときの確認ポイント
投稿されない場合は、次の点を確認してください。
| 確認項目 | 内容 |
|---|---|
| 投稿ステータス | 「予約済み」と完全一致しているか |
| 見出し | 「X投稿文」と「投稿ステータス」があるか |
| APIキー | 4つのキーが正しく入力されているか |
| 権限 | App PermissionsがRead and Writeになっているか |
| トリガー | postNextReservedPostToXが設定されているか |
| 投稿文 | 空欄になっていないか |
特に、投稿ステータスの表記ゆれに注意してください。
次のような表記は正しく判定されません。
予約
予約済
予約済み
予約済み
正しくは次の通りです。
予約済み
余計なスペースが入らないようにしてください。
投稿文の文字数を確認する
自動投稿では、投稿文が長すぎるとエラーになることがあります。
そのため、スプレッドシートに文字数確認用の列を作っておくと便利です。
X投稿文がC列にある場合、隣の列に次の関数を入れます。
=LEN(C2)
これで投稿文の文字数を確認できます。
投稿文は、短すぎても内容が薄くなり、長すぎても読みにくくなります。
目安としては、180文字から260文字程度にすると扱いやすいです。
投稿文を作るときのコツ
自動投稿用の文章は、ただ情報を並べるだけでは反応が取りにくくなります。
おすすめの構成は次の通りです。
| 順番 | 内容 |
|---|---|
| 1 | 読者の悩みを書く |
| 2 | 解決策を伝える |
| 3 | 具体例を入れる |
| 4 | 最後に一言でまとめる |
たとえば、WordPressのTipsなら次のように書けます。
WordPressのログインURLを初期状態のままにしていませんか。/wp-adminのままだと、不正アクセスの標的になりやすくなります。SiteGuard WP PluginなどでログインURLを変更するだけでも、基本的なセキュリティ対策になります。
このように、「何に困るのか」「何をすればいいのか」が分かる投稿にすると、初心者にも伝わりやすくなります。
スプレッドシート管理のコツ
投稿リストは、カテゴリーごとに整理しておくと運用しやすくなります。
たとえば、次のように曜日ごとにテーマを決める方法があります。
| 曜日 | 投稿テーマ |
|---|---|
| 月曜日 | 初期設定 |
| 火曜日 | セキュリティ |
| 水曜日 | SEO |
| 木曜日 | 高速化 |
| 金曜日 | functions.php |
| 土曜日 | プラグイン |
| 日曜日 | 運用改善 |
このようにテーマを分けると、発信内容に一貫性が出ます。
読者にも「このアカウントは何を発信しているのか」が伝わりやすくなります。
自動投稿を安全に運用するポイント
自動投稿は便利ですが、完全に放置するのはおすすめしません。
週に1回はスプレッドシートを確認し、次の点をチェックしましょう。
| 確認項目 | 内容 |
|---|---|
| 投稿済み | 正常に投稿されているか |
| エラー | 失敗している行がないか |
| 投稿文 | 古い情報が含まれていないか |
| 反応 | どの投稿が読まれているか |
| 予約数 | 残りの投稿文が足りているか |
自動化の目的は、手抜きではなく、継続しやすい仕組みを作ることです。
投稿後の反応を見ながら、文章を改善していくことが大切です。
初心者におすすめの始め方
最初は、いきなり大量の投稿を用意する必要はありません。
おすすめの始め方は次の通りです。
- テスト投稿を1件作る
- 手動実行で投稿できるか確認する
- 予約投稿を5件作る
- 毎日1件のトリガーを設定する
- 1週間運用して確認する
- 問題なければ30件まで増やす
まずは小さく始めることが大切です。
最初から複雑にしすぎると、どこでエラーが出ているのか分かりにくくなります。
まとめ
GoogleスプレッドシートとGoogle Apps Scriptを使えば、Xの自動投稿を作ることができます。
必要な流れは次の通りです。
- スプレッドシートに投稿文を用意する
- 投稿ステータスを「予約済み」にする
- X DeveloperでAPIキーを取得する
- Apps Scriptにコードを貼り付ける
- APIキーを設定する
- テスト投稿する
- 時間主導型トリガーで毎日実行する
この仕組みを作れば、スプレッドシートに投稿文を追加するだけで、毎日1件ずつXへ自動投稿できます。
投稿文、投稿日時、投稿URL、エラー内容をスプレッドシートで管理できるため、初心者でも運用しやすい方法です。
まずはテスト投稿を1件成功させるところから始めて、少しずつ投稿リストを増やしていきましょう。

