WordPress REST API と X API を連携させた自動投稿を試した記録

WordPressで記事を公開するたびに、手でXに投稿リンクを貼り付けている。この作業、月に20〜30本の記事を運用するようになると、じわじわと時間を食う。

「APIで自動化できるはず」と考える人は多いが、いざ手を動かすと最初の認証まわりでつまずいて放置、というパターンをよく聞く。WordPress REST APIの認証方式とX API(旧Twitter API) v2の認証方式が別物であることが理解されていないまま実装を始めると、401エラーで止まる。

この記事では、WordPressの記事公開をトリガーに、X(旧Twitter)へ自動投稿するPythonスクリプトを実際に動かした手順を記録する。前提知識はPythonの基礎と、WordPressを自分で運用した経験があれば十分だ。

結論から書きます

WordPress REST APIで「公開済み記事の一覧」を取得し、Xに投稿していないものをPythonスクリプトが検知してツイートする、という構成が最もシンプルに動く。

認証は2種類に分けて管理する。WordPress側はApplication Passwordsで対応し、X側はOAuth 1.0a(Consumer Key + Access Token)で対応する。どちらも公式の推奨手順に従えば、コードそのものはシンプルに書ける。

検証環境と前提条件

項目 バージョン / 値
Python 3.11.9
WordPress 6.5.3
WordPress REST API v2 (コア同梱)
X API v2 (Free tier)
主なライブラリ requests 2.32、tweepy 4.14
検証日 2026-05-21
実行環境 Ubuntu 22.04 LTS (VPS)

X API のFree tierは、月間ツイート投稿数に上限がある(2024年時点では月500ツイートまで)。記事の自動投稿程度であれば問題ないが、大量投稿には有料プランが必要になる(出典: X Developer Platform 公式ドキュメント)。

WordPressのApplication Passwordsは、WordPress 5.6以降で標準搭載されている機能だ。ユーザーの「プロフィール」ページから発行でき、通常のログインパスワードとは別に管理できる。


認証情報の扱いについて補足しておく。すべての認証情報(APIキー、アクセストークン、WordPressのApplication Password)は、スクリプト本体には一切書かない。.envファイルに格納し、python-dotenvで読み込む方式を使う。.envはGitの管理対象から外す(.gitignoreに追記)。

WordPress REST APIで記事データを取得する

エンドポイントの基本構造

WordPress REST APIのエンドポイントは次の形式で構成される。

https://your-site.com/wp-json/wp/v2/posts

認証なしでもアクセスできるが、下書きや非公開記事を取得したい場合はApplication Passwordsによる認証が必要になる。今回は「公開済み記事の取得」が目的なので、認証は必須ではない。ただし後工程でX未投稿かどうかを管理するために、投稿済みのpost_idをローカルファイルに記録する仕組みを入れる。

実装コード

まず.envファイルを用意する。

WP_BASE_URL=https://your-site.com
X_CONSUMER_KEY=your_consumer_key
X_CONSUMER_SECRET=your_consumer_secret
X_ACCESS_TOKEN=your_access_token
X_ACCESS_TOKEN_SECRET=your_access_token_secret

次に、WordPressの記事一覧を取得するコードを書く。

import os
import json
import requests
from dotenv import load_dotenv

load_dotenv()

WP_BASE_URL = os.getenv("WP_BASE_URL")

def get_recent_posts(per_page: int = 10) -> list[dict]:
    """公開済み記事を新しい順に取得する"""
    url = f"{WP_BASE_URL}/wp-json/wp/v2/posts"
    params = {
        "status": "publish",
        "per_page": per_page,
        "orderby": "date",
        "order": "desc",
        "_fields": "id,title,link,date"  # 必要フィールドのみ取得
    }
    response = requests.get(url, params=params, timeout=10)
    response.raise_for_status()
    return response.json()

_fieldsパラメータで取得フィールドを絞ることで、レスポンスのサイズを小さくできる。全フィールドを取得するとcontentやexcerptが含まれ、記事本文まで流れてくる。不要なデータを取らない習慣は、特にAPI呼び出し頻度が上がったときに効いてくる。

未投稿記事の検出ロジック

投稿済みのpost_idをJSONファイルに保存しておき、差分を取る。

POSTED_IDS_FILE = "posted_ids.json"

def load_posted_ids() -> set[int]:
    """投稿済みIDをセットとして読み込む"""
    if not os.path.exists(POSTED_IDS_FILE):
        return set()
    with open(POSTED_IDS_FILE, "r") as f:
        return set(json.load(f))

def save_posted_id(post_id: int) -> None:
    """投稿済みIDを追記保存する"""
    ids = load_posted_ids()
    ids.add(post_id)
    with open(POSTED_IDS_FILE, "w") as f:
        json.dump(list(ids), f)

def find_new_posts(posts: list[dict]) -> list[dict]:
    """未投稿の記事だけを返す"""
    posted_ids = load_posted_ids()
    return [p for p in posts if p["id"] not in posted_ids]

DBを使わずJSONファイルで管理するのは、シンプルさを優先した選択だ。記事本数が数千本を超えるようなメディアでは、SQLiteやRedisに切り替える方が安定する。月30本程度であれば、JSONファイルで十分に機能する。

X API v2 で自動ツイートを送信する

認証の仕組みとtweepyの使い方

X API v2でツイートを投稿するには、OAuth 1.0a認証が必要だ。4つのキーを使う。

  • Consumer Key(API Key)
  • Consumer Secret(API Key Secret)
  • Access Token
  • Access Token Secret

これらはX Developer Portal(developer.twitter.com)のアプリ設定画面から発行できる。Free tierでもアプリを作成し、Read and Writeのパーミッションを設定すれば取得できる(出典: X Developer Platform — Getting Access to the X API)。

tweepyライブラリを使うと、認証まわりのコードが大幅にシンプルになる。

import tweepy

def get_x_client() -> tweepy.Client:
    """tweepy クライアントを生成して返す"""
    return tweepy.Client(
        consumer_key=os.getenv("X_CONSUMER_KEY"),
        consumer_secret=os.getenv("X_CONSUMER_SECRET"),
        access_token=os.getenv("X_ACCESS_TOKEN"),
        access_token_secret=os.getenv("X_ACCESS_TOKEN_SECRET"),
    )

ツイートテキストの組み立て

記事タイトルとURLを組み合わせてツイート本文を作る。Xのツイート文字数上限は280文字(日本語でも同じ)だが、URLは短縮URLに変換されて23文字としてカウントされる仕様だ(出典: X Help Center — About Twitter’s character limit)。

def build_tweet_text(post: dict) -> str:
    """ツイート本文を組み立てる"""
    title = post["title"]["rendered"]
    url = post["link"]
    # タイトルが長い場合は切り詰める
    max_title_len = 100
    if len(title) > max_title_len:
        title = title[:max_title_len] + "…"
    return f"{title}\n\n{url}"

def post_tweet(client: tweepy.Client, text: str) -> bool:
    """ツイートを送信する。成功でTrue、失敗でFalseを返す"""
    try:
        response = client.create_tweet(text=text)
        return response.data is not None
    except tweepy.TweepyException as e:
        print(f"ツイート失敗: {e}")
        return False

エラーハンドリングは最低限の形で入れている。本番運用に移す場合は、ログファイルへの書き込みや、失敗時のリトライ回数の上限設定を追加することを想定している。

メインスクリプトの統合と動作確認

全体の処理フロー

flowchart TD
    A[スクリプト起動] --> B[WordPress REST API で記事取得]
    B --> C{未投稿記事あり?}
    C -- Yes --> D[ツイート本文を組み立て]
    D --> E[X API でツイート送信]
    E --> F{送信成功?}
    F -- Yes --> G[posted_ids.json に ID 保存]
    F -- No --> H[エラーログ出力]
    G --> I[次の未投稿記事へ]
    I --> C
    C -- No --> J[終了]

メイン処理

import time

def main():
    print("WordPress → X 自動投稿スクリプト起動")

    # WordPress から記事取得
    posts = get_recent_posts(per_page=10)
    new_posts = find_new_posts(posts)

    if not new_posts:
        print("未投稿の記事はありません")
        return

    print(f"未投稿記事: {len(new_posts)} 件")

    client = get_x_client()

    for post in new_posts:
        tweet_text = build_tweet_text(post)
        success = post_tweet(client, tweet_text)

        if success:
            save_posted_id(post["id"])
            print(f"投稿完了: {post['title']['rendered']}")
            # レート制限への配慮: 投稿間隔を空ける
            time.sleep(5)
        else:
            print(f"投稿失敗: {post['title']['rendered']}")

    print("処理完了")

if __name__ == "__main__":
    main()

動作確認時の手順

実際に動かす前に、確認しておくべき点がある。

  1. WordPress側でwp-json/wp/v2/postsにアクセスできるか確認する。ブラウザでURLを叩いてJSONが返ってくれば疎通している。
  2. X Developer Portalでアプリのパーミッションが「Read and Write」になっているか確認する。Read onlyのままだと401エラーが返る。パーミッション変更後はトークンの再生成が必要な場合がある。
  3. tweepy.Clientに渡すキーの順番と名前が.envと一致しているかを確認する。変数名のタイポによる認証失敗が多い。

初回実行時は、スクリプト内にprint(tweet_text)を入れて実際の送信前に本文を目視確認することを勧める。予期しないHTMLエンティティ(&など)がタイトルに混入していることがある。WordPress REST APIのタイトルフィールドはHTMLエンコードされた状態で返ってくるためだ。html.unescape(title)を挟むことで対処できる。

import html

def build_tweet_text(post: dict) -> str:
    title = html.unescape(post["title"]["rendered"])  # エンティティをデコード
    url = post["link"]
    max_title_len = 100
    if len(title) > max_title_len:
        title = title[:max_title_len] + "…"
    return f"{title}\n\n{url}"

定期実行の設定(cronの例)

VPS上でこのスクリプトを毎時実行する場合、crontabに以下を追加する。

0 * * * * /usr/bin/python3 /home/user/wp_x_post/main.py >> /var/log/wp_x_post.log 2>&1

実行頻度は記事の更新ペースに合わせて調整する。1時間ごとで十分なケースがほとんどだ。ただし、スクリプトが異常終了したり、posted_ids.jsonが破損したりすると未投稿記事が検出されなくなる。運用段階では定期的にログを確認する習慣を入れておく。


※本記事は2026-05-21時点の情報に基づきます。AI モデルや API の仕様・料金は変更されることがあります。最新は公式ドキュメントをご確認ください。

AI / tech の選択は要件や環境によって最適解が変わります。本記事は参考情報で、最終的な技術判断はご自身の検証に基づいてください。


まとめ

  • WordPress REST APIで公開記事を取得し、posted_ids.jsonで未投稿管理する構成がシンプルで動かしやすい
  • X API認証はOAuth 1.0aの4トークン方式を使う。パーミッションの設定ミスが最も多い原因なので、Read and Writeになっているかを先に確認する
  • タイトルのHTMLエンティティ問題はhtml.unescape()で処理する。小さなポイントだが、実運用で詰まりやすい箇所だ

次は、投稿本文にハッシュタグを自動付与する処理と、記事カテゴリ別のテンプレートを組み合わせる構成を試してみたい。

Photo by Jakub Żerdzicki on Unsplash