WordPress記事公開でThreadsへ自動投稿する仕組みをGASで自作した話【コード編】

まめすけあの、ちょっと気づいちゃったんだけど、
ここ2ヶ月、Threadsに全然投稿されてないんですけど…



え?・・・
以前使っていた自動投稿プラグインが壊れていたことに、恥ずかしながら2ヶ月も気づいていなかったんです。今回はその顛末と、GAS(Google Apps Script)+WordPressで自力で自動投稿の仕組みを作り直した話です。
あなたはどっち?
コードを直接編集するのに抵抗がない
→ このまま読み進めてください
管理画面から設定するだけで済ませたい
以下は、子テーマのfunctions.phpにコードを追加する「コード版」の作り方です。
きっかけは「気づいたら2ヶ月投稿ゼロ」
もともとウェブlogでは、Uncanny Automatorというプラグインを使って「記事を公開したら自動でThreadsに投稿する」仕組みを運用していました。ところが6月頃、Uncanny AutomatorとThreads側の連携でエラーが発生。プラグインを削除してそのまま放置してしまい、気づいた時にはThreadsへの投稿が完全に止まって2ヶ月が経っていました。
「動いているはず」という思い込みが、一番危ないんだなと痛感した出来事です。
導入手順4ステップ
Threads APIを使うには、Meta for Developersでアプリを登録し、Threads APIの利用申請を行って長期アクセストークン(60日有効)を取得する必要があります。この手順はボリュームがあるので、[Meta申請編の記事](Meta申請編記事URL)にまとめています。
Threadsのアクセストークンは60日で失効します。放置すると投稿が止まってしまうため、GASで自動更新の仕組みを作りました。
// threads_auto_post.gs
function refreshAccessToken() {
const props = PropertiesService.getScriptProperties();
const token = props.getProperty('THREADS_ACCESS_TOKEN');
const url = 'https://graph.threads.net/refresh_access_token'
+ '?grant_type=th_refresh_token'
+ '&access_token=' + token;
const res = UrlFetchApp.fetch(url);
const newToken = JSON.parse(res.getContentText()).access_token;
props.setProperty('THREADS_ACCESS_TOKEN', newToken);
}
// WordPress側からの問い合わせに応答するWebアプリ
function doGet(e) {
const props = PropertiesService.getScriptProperties();
const secret = props.getProperty('SECRET');
if (e.parameter.secret !== secret) {
return ContentService
.createTextOutput(JSON.stringify({ error: 'unauthorized' }))
.setMimeType(ContentService.MimeType.JSON);
}
const token = props.getProperty('THREADS_ACCESS_TOKEN');
return ContentService
.createTextOutput(JSON.stringify({ token }))
.setMimeType(ContentService.MimeType.JSON);
}
// 初回セットアップ時、自分のThreadsユーザーIDを調べるためのヘルパー
function getMyThreadsUserId() {
const token = PropertiesService.getScriptProperties().getProperty('THREADS_ACCESS_TOKEN');
const res = UrlFetchApp.fetch(
'https://graph.threads.net/v1.0/me?fields=id,username&access_token=' + token
);
Logger.log(res.getContentText());
}- refreshAccessToken(): 月次のトリガーで実行し、トークンを更新
- doGet(e): WordPress側からの問い合わせに応答するWebアプリとして公開。`secret`パラメータで認証
- getMyThreadsUserId(): 初回セットアップ時に、自分のThreadsユーザーIDを調べるためのヘルパー関数
ここで1つ注意点です。GASの「デプロイ」と「トリガー」は別物なので、コードを修正したら必ずデプロイし直さないと変更が反映されません。これに気づかず、しばらく古いコードのまま動かしてしまったことがありました。
最初の設計はRSS監視方式でしたが、うまくいきませんでした
XmlService.parseで厳密にXMLパースしようとしたところ、SAXParseExceptionが発生。本文中の<meta>タグの閉じ忘れが原因でした。正規表現ベースの緩い抽出方式(<item>ブロックをマッチさせてtitle/link/pubDate/categoryを個別抽出)に変更して、ここは解決しました。
ところが実行すると今度は501 Not Implementedエラー。調べてみると、契約しているシンレンタルサーバーの「海外からのアクセス制限」に該当していたことが判明しました。
記事公開のタイミングをフックして、Threads APIを2ステップ(コンテナ作成→公開)で呼び出す処理を追加します。
<?php
// functions.php に追記
// 記事公開時にThreadsへの自動投稿をスケジュールする
add_action( 'transition_post_status', function( $new_status, $old_status, $post ) {
if ( $new_status !== 'publish' || $old_status === 'publish' ) {
return;
}
if ( get_post_meta( $post->ID, '_threads_posted', true ) ) {
return; // 二重投稿防止
}
// 管理画面の「公開」ボタンをブロックしないよう、5秒後に非同期実行
wp_schedule_single_event( time() + 5, 'weblog_threads_post_event', array( $post->ID ) );
}, 10, 3 );
add_action( 'weblog_threads_post_event', 'weblog_post_to_threads' );
function weblog_post_to_threads( $post_id ) {
if ( get_post_meta( $post_id, '_threads_posted', true ) ) {
return;
}
$post = get_post( $post_id );
$access_token = get_option( 'weblog_threads_access_token' );
$threads_user = get_option( 'weblog_threads_user_id' );
$text = weblog_build_threads_text( $post );
// STEP1: コンテナ作成
$container = wp_remote_post( "https://graph.threads.net/v1.0/{$threads_user}/threads", array(
'body' => array(
'media_type' => 'TEXT',
'text' => $text,
'access_token' => $access_token,
),
) );
$body = json_decode( wp_remote_retrieve_body( $container ) );
$container_id = $body->id ?? null;
if ( ! $container_id ) {
return;
}
sleep( 3 ); // Threads側の処理待ち
// STEP2: 公開
wp_remote_post( "https://graph.threads.net/v1.0/{$threads_user}/threads_publish", array(
'body' => array(
'creation_id' => $container_id,
'access_token' => $access_token,
),
) );
update_post_meta( $post_id, '_threads_posted', current_time( 'mysql' ) );
}
// 投稿文を組み立てる(ハッシュタグは1つだけ)
function weblog_build_threads_text( $post ) {
$categories = get_the_category( $post->ID );
$tag = $categories ? '#' . $categories[0]->name : '#WordPress';
return $post->post_title . "\n" . get_permalink( $post ) . "\n\n" . $tag;
}- transition_post_status: 記事のステータスが「公開」に変わった瞬間をフック
- wp_schedule_single_event: 5秒後に非同期実行。公開ボタンの動作を止めないための工夫
- _threads_posted: 二重投稿防止用のpost meta
- コンテナ作成→3秒待機→公開、という2ステップのAPI呼び出し
ハッシュタグは「1つだけ」が正解でした
最初、投稿文に`#WordPress`や`#ブログ`など複数のハッシュタグを入れていたのですが、実際の投稿を確認すると1つ目の`#WordPress`だけ`#`記号が欠落して表示されていました。
調べてみると、Threadsは1投稿につき1つの「トピックタグ」しか認識せず、1つ目のハッシュタグの`#`は自動的に除去され、2つ目以降はただの文字列として残るだけで機能しないという仕様でした。上のコードでハッシュタグをカテゴリ名優先のシングルタグ(なければ`#WordPress`固定)にしているのはこのためです。
ここでもう1つハマりました。`add_query_arg()`は渡した値を自動でエンコードしてくれないため、合言葉に含めていた「+」記号が半角スペースに変換されてしまい、認証エラー(`{“error”:”unauthorized”}`)になっていたんです。`rawurlencode()`で明示的にエンコードすることで解決しました。
<?php
// WP-Cronで1日1回、GASにトークンを問い合わせる
add_action( 'weblog_threads_token_sync', function() {
$gas_url = get_option( 'weblog_threads_gas_url' );
$secret = get_option( 'weblog_threads_secret' );
$url = add_query_arg( 'secret', rawurlencode( $secret ), $gas_url );
$res = wp_remote_get( $url );
$body = json_decode( wp_remote_retrieve_body( $res ), true );
if ( ! empty( $body['token'] ) ) {
update_option( 'weblog_threads_access_token', $body['token'] );
}
} );
if ( ! wp_next_scheduled( 'weblog_threads_token_sync' ) ) {
wp_schedule_event( time(), 'daily', 'weblog_threads_token_sync' );
}GASで更新したトークンをWordPress側に反映する仕組みも必要です。当初はGAS→WordPressへ直接POSTするWebhook方式で設計していましたが、これも`501`エラーに。海外アクセス制限が`/wp-json/…`のREST APIにも適用されていたためでした。
最終的に、WordPress側から1日1回GASへ問い合わせる「PULL方式」に変更しました。
応用・発展情報: 「受信方向」だけ制限されていた海外アクセス制限
今回、海外アクセス制限にはRSS取得とWebhook送信の2回引っかかりましたが、原因を切り分けていく中で分かったのは、この制限は「受信」方向にだけ効く仕組みだということでした。
GASからWordPressへアクセスする経路は制限の対象になりますが、WordPress側からGASへ問い合わせに行く分には制限を受けません。方向を変えるだけでセキュリティを緩めずに解決できたのは、今回の収穫の一つでした。
まとめ
- Threads自動投稿の仕組みは、GASでのトークン自動更新+WordPressの`transition_post_status`フックで実現できる
- RSS監視・Webhook方式は「受信方向」の海外アクセス制限に引っかかりやすい。WordPress側から能動的に問い合わせるPULL方式が安定する
- ハッシュタグはThreads側の仕様上、1投稿につき1つだけが有効
- add_query_arg()は値を自動エンコードしないため、記号を含む値を渡す場合はrawurlencode()が必須
- GASは「デプロイ」と「トリガー」が別物。コード修正後は再デプロイを忘れずに



地味な設定ミス一つで全部止まっちゃうんだね…
実際、今回の一番の教訓は管理画面のステータス表示や失敗時のメール通知など、「止まったらすぐ気づける仕組み」まで含めて自動化することだったと思っています。
関連記事











コメント