2026年10月1日改訂:生成失敗の文言を投稿しないよう、APIエラー時は停止する実装に変更しました。
投稿はH列をREADYにした確認済みの行だけが対象です。
F列・G列に各投稿IDを文字列で保存し、途中失敗時は自動再送しません。
既存の時間トリガーは、列追加と認証・検証が済むまで停止してください。
本改訂の検証は外部送信のない模擬テストで、実API認証・投稿は未検証です。
WordPressで投稿した記事の要約をAIで処理、それを適切な形でXにポストしたいことがないでしょうか?
Web制作初心者向けの記事を書いていて、SNS(X/旧Twitter)にも拡散したい…でも、投稿文を毎回考えるのが面倒!
GASからOpenAI APIで投稿文を生成し、人が内容を確認した行をX APIへ送る構成です。

上の画像は旧版で要約と記事タイトル・URLをスレッド投稿した記録です。
2026年10月1日の改訂コードによる投稿成功を示すものではありません。
掲載画像は旧版の画面であり、現在の設定画面や改訂コードの実行結果とは異なる場合があります。
- ブログ記事の内容(スプレッドシートに入力)を元に
- OpenAI APIでWeb初心者向けの投稿文を生成
- 人が内容を確認してH列をREADYにした行をX(旧Twitter)へ投稿
- スレッド形式(2ツイート)にも対応
- 投稿IDと途中状態を記録し、応答不明時の自動再送を停止
WordPressで本文の内容を分析してXにポストする方法
WordPressからCSVを出力し、スプレッドシートに読み込んでからGASで要約と投稿を処理します。
OpenAI APIキーとX側のOAuth 2.0設定を別々に用意します。
WordPressの投稿記事のタイトル、URL、本文を出力する
まずはfunctions.phpを使ってWordPressの投稿記事のタイトル、URL、本文を出力してみます。
add_action('init', function () {
if (isset($_GET['download_csv']) && current_user_can('manage_options')) {
export_posts_to_csv();
}
});
function export_posts_to_csv() {
$args = [
'post_type' => 'post',
'posts_per_page' => -1,
'post_status' => 'publish',
'has_password' => false
];
$posts = get_posts($args);
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="posts_export.csv"');
header('Pragma: no-cache');
header('Expires: 0');
$output = fopen('php://output', 'w');
fwrite($output, chr(0xEF) . chr(0xBB) . chr(0xBF)); // BOM
fputcsv($output, ['タイトル', 'URL', '本文']);
foreach ($posts as $post) {
if (stripos($post->post_content, 'codoc') !== false) { continue; }
$title = $post->post_title;
$url = get_permalink($post->ID);
$content = strip_tags($post->post_content);
$content = preg_replace('/\s+/', ' ', $content); // 改行・空白除去
fputcsv($output, array_map(function ($cell) {
return preg_match('/^[=+@\-\t\r]/', $cell) ? "'" . $cell : $cell;
}, [$title, $url, $content]));
}
fclose($output);
exit;
}
コードを設置した自分のサイトで、管理者権限のあるユーザーとしてログインしてから、URLに?download_csv=1を付けて開きます。
次のURLは当サイトの場合の例です。
https://motoki-design.co.jp/wordpress/csv-export/?download_csv=1出力されるファイル名はposts_export.csvです。
スプレッドシートにcsvを読み込ませると以下の通りです。

OpenAI APIを使ったSNS投稿文の自動生成(GAS)
事前にOpenAI APIキーを用意します。
ChatGPTのログイン用トークンを使うものではありません。
スクリプトの編集権限を持つ人はプロパティにもアクセスできるため、共有範囲を確認します。
APIキーを使う認証方式はOpenAI公式の認証資料で確認できます(2026年10月4日確認)。
先程の記事の情報を読み込んだスプレッドシートを用意します。
スプレッドシート構成
| A列 | B列 | C列 | D列 | E列 | F列 | G列 | H列 |
|---|---|---|---|---|---|---|---|
| タイトル | 記事URL | 本文 | 投稿文 | 完了日 | 1件目ID | 2件目ID | 状態 |
GASのコード
スプレッドシートの上部メニュー > 拡張機能 > Apps Scriptからコードを記載します。
歯車アイコンのプロジェクト設定から、スクリプトプロパティにOpenAI APIキーを登録します。
掲載コードに合わせてプロパティ名はACCESS_TOKENとしますが、保存する値はChatGPTのログイン用トークンではありません。
| 名前 | 値:OpenAI APIキー |
|---|---|
| ACCESS_TOKEN | xxxxxxxxxxxxxxxx |

さらに下記のコードも記載
// カスタムメニュー追加
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu("AI要約")
.addItem("SNS向け投稿文を作成(D列)", "generateSocialPost")
.addToUi();
}
// D列:X向け投稿文
function generateSocialPost() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
const apiKey = PropertiesService.getScriptProperties().getProperty("ACCESS_TOKEN");
const lastRow = sheet.getLastRow();
for (let i = 2; i <= lastRow; i++) {
const body = sheet.getRange(i, 3).getValue();
const postText = sheet.getRange(i, 4).getValue();
if (body && !postText) {
const prompt = `
以下の文章をもとに、Web制作初心者でもわかるようにX(旧Twitter)向けの投稿文を作ってください。
【ルール】
- 難しい言葉は避ける
- 絵文字やハッシュタグはOK
- 140文字以内にこだわらなくていい
- 読んだ人が記事を見たくなるように!
文章:
${body}
`.trim();
const result = callChatGPT(apiKey, prompt);
sheet.getRange(i, 4).setValue(result);
Utilities.sleep(1500);
}
}
}
function callChatGPT(apiKey, prompt) {
const payload = {
model: "gpt-4o",
messages: [{ role: "user", content: prompt }],
temperature: 0.7
};
const options = {
method: "post",
contentType: "application/json",
headers: { Authorization: "Bearer " + apiKey },
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const res = UrlFetchApp.fetch("https://api.openai.com/v1/chat/completions", options);
const status = res.getResponseCode();
if (status < 200 || status >= 300) throw new Error("OpenAI HTTP " + status);
const json = JSON.parse(res.getContentText());
const choice = json.choices?.[0];
const text = choice?.message?.content;
if (choice?.finish_reason !== "stop" || typeof text !== "string" || !text.trim()) {
throw new Error("要約が正常に完了していません。D列には保存しません。");
}
return text.trim();
}generateSocialPost()を実行するとC列の本文を見てD列に投稿用の文章が生成されます。
今回はgpt-4oを使ってますが、他にもいろいろなモデルがあります。
この例はgpt-4oを指定するChat Completions用です。
音声・Realtime向けモデルなどを同じ形式で差し替えることはできません。
変更時はモデルの対応APIと入力形式を確認してください。
X(旧Twitter)のAPIとOAuth 2.0の設定
まずはOAuth 2.0 を使う準備をします。
- Xの開発者アカウントに登録(無料)
- プロジェクトとアプリを作成
- GASに設定(CLIENT_ID / CLIENT_SECRET)
- 自作関数でOAuth2の設定を記述
Xの開発者アカウントに登録(無料)
まずは下記のURLから開発者アカウントを作成しましょう。
途中、デベロッパーアカウント申請時に詳細を説明する(250文字以上)箇所があるのですが、例文を書いておきます。

私は、自社で運営しているWordPressブログの記事を、X(旧Twitter)アカウントに自動投稿する目的でTwitter APIを使用します。
具体的には、記事本文をスプレッドシートにまとめ、ChatGPT APIを活用してわかりやすい短文(要約)を生成し、それをXへ1日1件投稿します。
投稿内容はすべて自社オリジナルの記事に限定し、再配信・分析・再販売・DM送信・フォロー操作などは一切行いません。
純粋に情報発信を効率化することを目的とした非侵襲的な利用です。
この用途の範囲内でのみAPIを利用し、ポリシーや利用規約を厳守いたします。
こちらのサイトも参考になります。
プロジェクトとアプリを作成
スプレッドシートから拡張機能 > Apps Scriptを選択
先に後述のOAuth2ライブラリを追加してから、下記コードでCallback URLを取得します。
ここではCLIENT_ID/CLIENT_SECRETはまだ不要です。
function logRedirectUri() {
Logger.log("Redirect URI(Callback URL):");
Logger.log(OAuth2.getRedirectUri());
}
その後は開発者アカウントのサイトに戻り、Appの設定をします。


Callback URI のところに先程のCallback URLを記入します。


保存後にCLIENT_ID / CLIENT_SECRETが表示されるのでコピーしておきます。

GASに設定(CLIENT_ID / CLIENT_SECRET)
スクリプトプロパティに下記の2つを追加しておきます。

| 名前 | ID |
|---|---|
| CLIENT_ID | xxxxxxxxxxxxxxxx |
| CLIENT_SECRET | xxxxxxxxxxxxxxxx |
さらにGASに下記のコードを挿入
function main() {
const service = getService();
if (service.hasAccess()) {
Logger.log("Already authorized");
} else {
const authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s', authorizationUrl);
}
}後述のOAuth2ライブラリと認証関数を追加してからmain()を実行します。
掲載コードは未認証ならOpen the following URL and re-run the script:と認証用URLをログへ出し、認証済みならAlready authorizedと表示します。

ログに出た認証用URLを開き、対象アカウントと要求される権限を確認してから、アプリへのアクセスを許可します。

掲載コードのauthCallback()は、認証成功時に認証完了と画面を閉じる案内を返す実装です。
以下のSuccess!画面は旧版の記録で、改訂版の実認証は未検証です。

同じGASプロジェクトへ次の認証関数を追加します。
OAuth2ライブラリはgenerateCodeVerifierを備える版を使い、X側ではClient Secretを使うWebアプリとして登録します。
認証ユーザーと時間トリガーの作成者を同じにしてください。
function getService() {
const props = PropertiesService.getScriptProperties();
const id = props.getProperty("CLIENT_ID");
const secret = props.getProperty("CLIENT_SECRET");
if (!id || !secret) throw new Error("Xのクライアント設定がありません");
return OAuth2.createService("WazaX")
.setAuthorizationBaseUrl("https://x.com/i/oauth2/authorize")
.setTokenUrl("https://api.x.com/2/oauth2/token")
.setClientId(id)
.setClientSecret(secret)
.setCallbackFunction("authCallback")
.setPropertyStore(PropertiesService.getUserProperties())
.setCache(CacheService.getUserCache())
.setLock(LockService.getUserLock())
.setScope("tweet.read tweet.write users.read offline.access")
.setTokenHeaders({Authorization: "Basic " + Utilities.base64Encode(id + ":" + secret)})
.generateCodeVerifier();
}
function authCallback(request) {
const ok = getService().handleCallback(request);
return HtmlService.createHtmlOutput(ok ? "認証しました。この画面を閉じてください。" : "認証されませんでした。");
}
自作関数でOAuth2の設定を記述
認証用ライブラリ(OAuth2)をGASで使います。
Apps Script 左メニュー「ライブラリ」→ 「+ライブラリを追加」
以下のスクリプトIDを貼り付けて「検索」
- Apps Script 左メニュー「ライブラリ」→ 「+ライブラリを追加」
- 以下のスクリプトIDを貼り付けて「検索」
1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
ライブラリ名はそのまま OAuth2 でOK。
バージョンは最新を選び、「追加」。

あとは実際のポストのコードです。
function postXThread() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(1000)) return;
try {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
if (!sheet) throw new Error("シート1がありません");
const data = sheet.getDataRange().getValues();
const service = getService();
if (!service.hasAccess()) throw new Error("実行ユーザーのX認証が必要です");
for (let i = 1; i < data.length; i++) {
const [title, url, body, postText, postedDate, firstId, secondId, state] = data[i];
if (postedDate || state !== "READY") continue;
if (typeof postText !== "string" || !postText.trim() || !/^https:\/\//.test(String(url))) {
throw new Error("本文またはURLを確認してください");
}
const row = i + 1;
let parentId = String(firstId || "");
const ids = [String(firstId || ""), String(secondId || "")];
if (ids[1] && !ids[0]) throw new Error("投稿IDの組合せを確認してください");
const texts = [postText.trim(), `${title} ${url}`.trim()];
for (let j = 0; j < 2; j++) {
if (ids[j]) continue;
sheet.getRange(row, 8).setValue("SENDING_" + (j + 1));
SpreadsheetApp.flush();
// 送信後に応答・保存が失敗した場合も、自動では再送しない。
const payload = {text: texts[j]};
if (parentId) payload.reply = {in_reply_to_tweet_id: parentId};
const response = UrlFetchApp.fetch("https://api.x.com/2/tweets", {
method: "post", contentType: "application/json",
headers: {Authorization: "Bearer " + service.getAccessToken()},
payload: JSON.stringify(payload), muteHttpExceptions: true
});
const status = response.getResponseCode();
if (status < 200 || status >= 300) throw new Error("X HTTP " + status + ": 投稿状況を手動確認してください");
const result = JSON.parse(response.getContentText());
const id = result.data?.id;
if (typeof id !== "string" || !/^\d+$/.test(id)) throw new Error("投稿IDを確認できません");
sheet.getRange(row, 6 + j).setNumberFormat("@").setValue(id);
SpreadsheetApp.flush();
parentId = id;
}
sheet.getRange(row, 5).setValue(new Date()).setNumberFormat("yyyy/MM/dd HH:mm");
sheet.getRange(row, 8).setValue("DONE");
SpreadsheetApp.flush();
return;
}
} finally {
lock.releaseLock();
}
}
D列の投稿文とB列のURLを確認し、投稿する行だけH列をREADYにしてから、postXThread()を手動実行する手順です。
実行するとXへの外部送信が発生します。
以下の実行画面は旧版の記録です。

以下は旧版での投稿例です。
今回の改訂版で実投稿が成功した証拠ではありません。

認証と手動実行を自分の環境で確認した後に、postXThread()の時間主導型トリガーを設定します。
掲載コードは1回の実行で最初の対象行を1行処理し、H列がREADYでない行は投稿しません。
- GASメニュー「時計マーク」(トリガー)をクリック
- 「新しいトリガーを追加」
- 関数:
postXThread - イベントの種類:時間主導型
- 実行タイミング:日付ベースのタイマー → 午前◯時(任意)
認証と手動実行の検証が済み、READYにした行がある場合に処理します。
時間主導型トリガーの実行時刻には幅があり、分単位の定刻実行は保証されません。

以上です。
GitHubのMOTOKI-LLC/WAZA-codeは過去の保管版で、この記事の改訂コードとは同期していません。
E列は完了日、F列は1件目の投稿ID、G列は2件目の投稿ID、H列は状態として使います。
F・G列は最初からプレーンテキストに設定し、投稿中は行の並べ替えや本文編集をしないでください。
READYは本文・URL・Xの文字数制限を人が確認した行だけに設定します。
SENDING_1/2で止まったらX上の投稿を確認し、成功済みのIDをF/Gへ記録してから再開を判断します。
状態を確認せずREADYへ戻すと重複します。
複数のGASプロジェクトを同じシートへ接続しないでください。
公開本文でも、パスワード保護記事や非公開部分を含む元データは外部APIへ渡さないでください。
CSVはパスワード付き記事とcodocという文字列を含む記事を除外し、数式として解釈され得る先頭文字を無害化しています。
ただし他方式の会員限定記事などを網羅する判定ではないので、出力を確認して公開可能な行だけ使用してください。
大量記事の分割出力は未実装です。
旧GitHub例は同期していません。
認証仕様はGoogleのOAuth2ライブラリ、応答形式はOpenAI公式資料、投稿形式はX公式資料を参照してください。



