テクニック

WordPress予約投稿と自動投稿の仕組み|wp-cronの弱点と設計

この記事の対象: WordPressサイトを納品・運用している制作者、予約投稿が動かない現場を抱えている担当者
読了時間の目安: 約12分

「予約投稿にしておいたのに公開されていない」という連絡は、WordPressを扱っていれば一度は受けます。原因のほとんどは wp-cron の仕組みそのもので、設定ミスではありません。WordPressの予約実行は、誰かがサイトにアクセスした瞬間に「そろそろ時間だ」と気づいて動く作りになっているためです。アクセスの少ないサイトほど遅れ、深夜の予約は翌朝まで放置されます。この記事では wp-cron を止めてサーバー側の cron に置き換える手順と、外部から記事を自動投稿する仕組みの作り方を、コピーして動くコードで解説します。


この記事の前提

作業前に、環境が揃っているか確認してください。ここが揃っていないと途中で止まります。

項目 必要なもの
WordPress 5.6以降(Application Passwords が標準搭載されたバージョン。REST API を使う章で必要)
テーマ ブロックテーマ・クラシックテーマどちらでも可。コードは子テーマの functions.php かプラグインに置く
権限 wp-config.php の編集権限、サーバーのcron設定画面(またはSSH)
サーバー cron機能が使えるレンタルサーバー、またはSSHが通るVPS
PHP 7.4以降を想定。8.x でも動作する書き方にしています

子テーマの作り方や、テーマ選定そのものに迷いがある場合は先にこちらを確認してください。


wp-cronはなぜ動かないことがあるのか

実態は「擬似cron」

WordPressの予約投稿・自動更新・プラグインの定期処理は、すべて wp-cron.php が担当しています。名前に cron と付いていますが、サーバーのcronデーモンとは無関係です。

動きはこうです。サイトに誰かがアクセスすると、WordPressが「予約されたタスクで実行時刻を過ぎているものはあるか」をDBに問い合わせます。あれば wp-cron.php を非同期で呼び出し、そこで処理が走ります。

wp-cronが動くまでの流れ
アクセス発生誰かがページを開く
予約の照会期限切れタスクをDBで確認
wp-cron.php呼出非同期でHTTPリクエスト
処理実行予約投稿が公開される

起点が「アクセス」であることが全てです。アクセスがなければ2番目以降は永久に始まりません。

具体的に起きること

  • 公開が遅れる:10時予約の記事が、その日最初のアクセスが14時なら14時に公開される
  • 公開に失敗する:処理途中でタイムアウトすると「予約投稿を公開できませんでした」と管理画面に出る
  • 逆にアクセスが多すぎて重い:PVの数だけ「予約の照会」が走るため、DBへの無駄な問い合わせが積み上がる
  • ページキャッシュで動かない:キャッシュプラグインやCDNが効いていると、PHPが実行されないままHTMLが返るため照会自体が起きない

最後の項目は見落とされがちです。表示速度のためにキャッシュを強くした結果、予約投稿が止まるという因果関係は、担当者からは絶対に見えません。

判断の分かれ目

wp-cronとサーバーcronの性質

サーバーcron(推奨)

  • アクセスと無関係に動く
  • 時刻が正確
  • 実行間隔を自分で決められる
  • ログが残せる

wp-cron(既定)

  • アクセスがないと動かない
  • 時刻がずれる
  • PV数だけ照会が走る
  • 失敗が見えにくい

アクセスが安定して多く、キャッシュも使っていないサイトなら wp-cron のままで実害は出ません。それ以外、つまり新規公開サイト・コーポレートサイト・オウンドメディアの立ち上げ期は、ほぼ全て置き換える対象です。


wp-cronを止めてサーバーcronに置き換える

wp-cronを止めてサーバーcronに置き換える

手順1:wp-cronを無効化する

wp-config.php を開きます。ファイルの場所はWordPressのルート直下です。

/wp-config.php

/* 編集が必要なのはここまでです ! WordPress のインストールをお楽しみ ! */ という行を探し、その上に次の1行を追加します。

define( 'DISABLE_WP_CRON', true );

追加後のファイルはこうなります。

define( 'WP_DEBUG', false );

/** wp-cron を無効化し、サーバーの cron から実行する */
define( 'DISABLE_WP_CRON', true );

/* 編集が必要なのはここまでです ! WordPress のインストールをお楽しみ ! */

/** Absolute path to the WordPress directory. */
if ( ! defined( 'ABSPATH' ) ) {
    define( 'ABSPATH', __DIR__ . '/' );
}

置き場所を間違えて require_once ABSPATH . 'wp-settings.php'; より下に書くと効きません。WordPressの読み込みが終わった後に定義しても、参照済みだからです。

この時点で予約投稿は完全に止まります。次の手順まで一気にやってください。

手順2:サーバーのcronに登録する

WordPressルートの絶対パスを確認します。SSHが使えるなら次で出ます。

pwd

主要な共用レンタルサーバーの多くは、コントロールパネルにcron設定画面を持っています。SSHが通る環境なら crontab -e で直接編集します。

WP-CLIが使える場合(推奨)

*/5 * * * * cd /home/username/public_html && /usr/local/bin/wp cron event run --due-now > /dev/null 2>&1

WP-CLIがない場合(wgetで叩く)

*/5 * * * * /usr/bin/wget -q -O - "https://example.com/wp-cron.php?doing_wp_cron" > /dev/null 2>&1

curlしか無い環境なら同じことをcurlで書きます

*/5 * * * * /usr/bin/curl -s "https://example.com/wp-cron.php?doing_wp_cron" > /dev/null 2>&1

WP-CLIを推す理由は、Webサーバーを経由しないためタイムアウトやWAFの影響を受けないことです。wget/curl 方式は wp-cron.php をHTTPで叩くので、PHPの実行時間制限やリバースプロキシのタイムアウトに引っかかる余地が残ります。

コピー後に変更する箇所は3つです。

箇所 変更内容
/home/username/public_html WordPressルートの絶対パス
https://example.com 実際のサイトURL(wwwの有無・httpsまで正確に)
*/5 実行間隔(5分ごと。15分でよければ */15

/usr/local/bin/wp/usr/bin/wget のパスはサーバーによって違います。which wp which wget で確認してから貼ってください。cronはログイン時のPATHを引き継がないため、コマンド名だけ書くと動きません。ここが最も多い失敗です。

手順3:動いているか確かめる

WP-CLIが使えるなら、予約されているタスクの一覧が見られます。

wp cron event list

next_run_relative の列に now や過去の時刻が並んでいたら、実行が滞っている証拠です。手動で走らせて解消するか確認します。

wp cron event run --due-now

WP-CLIがない環境では、管理画面から確認できるコードを子テーマに入れます。functions.php の末尾に追加してください。

/wp-content/themes/子テーマ名/functions.php
/**
 * 管理バーに次回 cron 実行時刻を表示する(管理者のみ)
 */
add_action( 'admin_bar_menu', function ( $wp_admin_bar ) {
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }

    $next = wp_next_scheduled( 'wp_version_check' );

    if ( ! $next ) {
        $label = 'cron: 予約なし';
    } else {
        $diff  = $next - time();
        $label = $diff > 0
            ? 'cron: あと' . human_time_diff( time(), $next )
            : 'cron: ' . human_time_diff( $next, time() ) . '遅延';
    }

    $wp_admin_bar->add_node( array(
        'id'    => 'cron-status',
        'title' => esc_html( $label ),
        'href'  => admin_url( 'site-health.php' ),
    ) );
}, 100 );

管理バーに「cron: あと3時間」のように出ます。「◯◯遅延」と表示されたら cron が回っていません。wp_version_check はWordPress本体が必ず登録する定期タスクなので、判定の基準として使っています。

管理画面の ツール → サイトヘルス にも「WP-Cronが正常に動作していません」という趣旨の項目が出ます。ただしこちらは表示のたびに判定するため、担当者が開かなければ誰も気づきません。常時見える場所に出しておく意味は、そこにあります。


独自の定期処理を仕込む

予約投稿の修復だけでなく、「毎日決まった時刻に何かをする」処理も同じ土台の上に載せられます。

実行間隔を追加する

WordPressが標準で持つ間隔は hourly twicedaily daily weekly の4つです。それ以外が必要なら自分で足します。

/**
 * cron の実行間隔に「15分ごと」を追加する
 */
add_filter( 'cron_schedules', function ( $schedules ) {
    $schedules['every_fifteen_minutes'] = array(
        'interval' => 15 * MINUTE_IN_SECONDS,
        'display'  => __( '15分ごと', 'textdomain' ),
    );
    return $schedules;
} );

every_fifteen_minutes の部分は自由に決めてかまいませんが、他プラグインと衝突しないよう接頭辞を付けるのが安全です。

タスクを登録する

登録・実行・解除の3点セットで書きます。バラバラに書くと、テーマを切り替えたときにタスクだけ残って動き続けます。

/**
 * 定期タスクの登録
 * mysite_next_run_at() は次項「タイムゾーンの落とし穴」で定義する
 */
add_action( 'init', function () {
    if ( ! wp_next_scheduled( 'mysite_daily_task' ) ) {
        wp_schedule_event( mysite_next_run_at( '04:00' ), 'daily', 'mysite_daily_task' );
    }
} );

/**
 * 実際の処理
 */
add_action( 'mysite_daily_task', function () {
    // 例:30日以上前のリビジョンを削除する
    global $wpdb;

    $threshold = gmdate( 'Y-m-d H:i:s', strtotime( '-30 days' ) );

    $revision_ids = $wpdb->get_col( $wpdb->prepare(
        "SELECT ID FROM {$wpdb->posts}
         WHERE post_type = 'revision' AND post_modified_gmt < %s
         LIMIT 200",
        $threshold
    ) );

    foreach ( $revision_ids as $revision_id ) {
        wp_delete_post_revision( (int) $revision_id );
    }

    error_log( sprintf( '[mysite_daily_task] リビジョン %d 件を削除', count( $revision_ids ) ) );
} );

/**
 * テーマ切り替え時にタスクを解除する
 */
add_action( 'switch_theme', function () {
    wp_clear_scheduled_hook( 'mysite_daily_task' );
} );

コピー後に変更する箇所は次の通りです。

  • mysite_daily_task:フック名。サイトごとにユニークな接頭辞に変える
  • '04:00':初回実行時刻
  • daily:実行間隔(前項で追加した every_fifteen_minutes も指定できる)
  • LIMIT 200:1回あたりの処理件数。多すぎるとタイムアウトする

登録を init に置いているのは、プラグイン化していない子テーマでも確実に走らせるためです。プラグインとして実装するなら register_activation_hook() で1回だけ登録し、register_deactivation_hook() で解除する形が本来の作法です。

LIMIT を付けている理由は、cronの1回の実行がPHPの max_execution_time に縛られるためです。1万件のリビジョンを一度に消そうとすると途中で落ち、しかも「落ちた」ことがどこにも記録されません。少しずつ削って何回かに分ける方が確実です。

タイムゾーンの落とし穴

wp_schedule_event() に渡すタイムスタンプはUTC基準です。current_time( 'timestamp' ) はサイト設定のタイムゾーン(日本なら JST)を返すので、この2つを混ぜると9時間ずれます。「毎朝4時のはずが13時に動く」という不具合はほぼこれです。

安全な書き方はこうです。

/**
 * サイトのタイムゾーンで「次の 04:00」を UTC タイムスタンプとして得る
 */
function mysite_next_run_at( $time_string = '04:00' ) {
    $tz  = wp_timezone(); // WordPress 5.3 以降
    $now = new DateTimeImmutable( 'now', $tz );

    $target = new DateTimeImmutable( 'today ' . $time_string, $tz );
    if ( $target <= $now ) {
        $target = $target->modify( '+1 day' );
    }

    return $target->getTimestamp(); // getTimestamp() は常に UTC 基準
}

DateTimeImmutable にタイムゾーンを渡して組み立て、最後に getTimestamp() で取り出す。この形なら計算の途中でサイト時刻とUTCが混ざりません。wp_timezone() は WordPress 5.3 以降で使えます。それより前のバージョンを保守している場合は get_option( 'timezone_string' ) から DateTimeZone を組み立てる必要がありますが、5.3未満のサイトはそもそもバージョンアップが先です。


外部から記事を自動投稿する

「予約」と「投稿の生成」は別の話です。CSVやスプレッドシート、他システムから記事を流し込みたい場合は、REST API を使います。

Application Password を発行する

WordPress 5.6以降、標準機能として使えます。httpsが有効でないと管理画面に項目が出ません

  1. 管理画面 → ユーザー → 対象ユーザーを編集
  2. ページ下部の「アプリケーションパスワード」欄に用途名を入力(例:csv-importer
  3. 「新しいアプリケーションパスワードを追加」をクリック
  4. 表示された xxxx xxxx xxxx xxxx xxxx xxxx を控える(再表示されません

投稿用のユーザーは、記事投稿だけができる権限(編集者など)で新規に作るのが安全です。管理者アカウントのパスワードを外部システムに置かない。これは運用ルールの問題ではなく、漏れたときの被害範囲の問題です。

投稿を作るスクリプト

サーバー上に置いて cron から叩く形にします。WordPressのファイル群とは別のディレクトリに置いてください。公開ディレクトリ内に置くと、URLを叩かれて実行される危険があります。

/home/username/scripts/wp-post.php
<?php
/**
 * REST API 経由で WordPress に投稿を作成する
 * 実行: php /home/username/scripts/wp-post.php
 */

$site_url = 'https://example.com';
$user     = 'editor-bot';
$app_pass = 'xxxx xxxx xxxx xxxx xxxx xxxx';

$payload = array(
    'title'   => 'テスト投稿タイトル',
    'content' => "<p>本文の1段落目です。</p>\n<p>2段落目です。</p>",
    'status'  => 'future', // draft / publish / future
    'date'    => '2026-09-10T10:00:00', // status=future のとき必須(サイトのタイムゾーン基準)
);

$ch = curl_init( $site_url . '/wp-json/wp/v2/posts' );

curl_setopt_array( $ch, array(
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode( $payload, JSON_UNESCAPED_UNICODE ),
    CURLOPT_HTTPHEADER     => array(
        'Content-Type: application/json',
        'Authorization: Basic ' . base64_encode( $user . ':' . $app_pass ),
    ),
    CURLOPT_TIMEOUT        => 30,
) );

$response = curl_exec( $ch );
$code     = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );

if ( 201 === $code ) {
    $post = json_decode( $response, true );
    echo "作成しました: ID {$post['id']} / {$post['link']}\n";
} else {
    echo "失敗 (HTTP {$code}): {$response}\n";
    exit( 1 );
}

変更が必要な箇所は上部の3つの変数と $payload の中身です。statusfuture にして date を未来の日時にすると、予約投稿として登録されます。ここで作られた予約投稿も、公開の実行は cron に委ねられるので、前章の設定が効いていることが前提になります。

最初は statusdraft にして通し、管理画面に下書きが1本増えることを確認してから future に切り替えると事故が減ります。認証と経路の検証を、公開される記事で兼ねない方がいいという単純な話です。

CSVから連続で投稿する

1本ずつ書き換えるのは現実的でないので、CSVを読んで回す形にします。

/home/username/scripts/articles.csv
title,content,date,status
記事タイトル1,"<p>本文1</p>",2026-09-10T10:00:00,future
記事タイトル2,"<p>本文2</p>",2026-09-11T10:00:00,future
記事タイトル3,"<p>本文3</p>",2026-09-12T10:00:00,future
/home/username/scripts/wp-bulk-post.php
<?php
/**
 * CSV を読んで WordPress に一括投入する
 * 実行: php /home/username/scripts/wp-bulk-post.php
 */

$site_url  = 'https://example.com';
$user      = 'editor-bot';
$app_pass  = 'xxxx xxxx xxxx xxxx xxxx xxxx';
$csv_path  = __DIR__ . '/articles.csv';
$done_path = __DIR__ . '/posted.log';

// 投入済みタイトルを読み込む(二重投稿の防止)
$done = file_exists( $done_path )
    ? array_map( 'trim', file( $done_path ) )
    : array();

$handle = fopen( $csv_path, 'r' );
if ( ! $handle ) {
    exit( "CSV を開けません: {$csv_path}\n" );
}

$header = fgetcsv( $handle ); // 1行目はヘッダーとして読み捨てる
$count  = 0;

while ( ( $row = fgetcsv( $handle ) ) !== false ) {
    if ( count( $header ) !== count( $row ) ) {
        echo "列数が合わない行を飛ばしました: " . implode( ',', $row ) . "\n";
        continue;
    }

    $data = array_combine( $header, $row );

    if ( in_array( $data['title'], $done, true ) ) {
        echo "スキップ(投入済み): {$data['title']}\n";
        continue;
    }

    $payload = array(
        'title'   => $data['title'],
        'content' => $data['content'],
        'status'  => $data['status'],
    );

    if ( 'future' === $data['status'] ) {
        $payload['date'] = $data['date'];
    }

    $ch = curl_init( $site_url . '/wp-json/wp/v2/posts' );
    curl_setopt_array( $ch, array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode( $payload, JSON_UNESCAPED_UNICODE ),
        CURLOPT_HTTPHEADER     => array(
            'Content-Type: application/json',
            'Authorization: Basic ' . base64_encode( $user . ':' . $app_pass ),
        ),
        CURLOPT_TIMEOUT        => 30,
    ) );

    $response = curl_exec( $ch );
    $code     = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
    curl_close( $ch );

    if ( 201 === $code ) {
        file_put_contents( $done_path, $data['title'] . "\n", FILE_APPEND );
        $count++;
        echo "作成: {$data['title']}\n";
    } else {
        echo "失敗 (HTTP {$code}): {$data['title']} / {$response}\n";
    }

    sleep( 1 ); // サーバー負荷を避けるため1秒空ける
}

fclose( $handle );
echo "完了: {$count} 件\n";

posted.log に投入済みのタイトルを記録し、次回実行時に読み飛ばします。cronで毎日回す運用にしても、同じ記事が何度も作られることはありません。判定にタイトルを使っているので、CSV側で同名タイトルを使い回すと2本目が投入されない点だけ注意してください。厳密にやるなら、CSVに一意のIDの列を足してそちらで突き合わせます。

変更する箇所は上部4つの変数です。sleep( 1 ) は共用サーバーで連続リクエストを弾かれないための保険で、件数が少なければ削っても問題ありません。

投稿のたびにアイキャッチやカテゴリも設定したい場合、$payloadfeatured_media(メディアID)や categories(カテゴリIDの配列)を足します。IDは /wp-json/wp/v2/categories を GET すれば一覧で取れます。アイキャッチ画像そのものを送るなら、先に /wp-json/wp/v2/media へファイルをPOSTして返ってきたIDを使います。


止まらない仕組みにする

自動化は「動き始めること」より「止まったと気づけること」の方が難しい部分です。

実行ログを残す

cronの行を書き換えて、標準出力をファイルに落とします。

*/5 * * * * cd /home/username/public_html && /usr/local/bin/wp cron event run --due-now >> /home/username/logs/wp-cron.log 2>&1

> /dev/null>> ファイルパス に変えただけです。2>&1 はエラー出力も同じファイルに送る指定で、これがないとエラーだけが消えます。

ログディレクトリは事前に作っておきます。

mkdir -p /home/username/logs

放置するとログが肥大化するので、定期的に削る行も足しておきます。

0 4 * * 0 find /home/username/logs -name "*.log" -mtime +30 -delete

毎週日曜4時に、30日より古いログを削除します。

失敗したときに気づく

管理画面を毎日見る運用は続きません。公開予定を過ぎたまま残っている予約投稿を検出して、メールを飛ばす処理を入れます。

/wp-content/themes/子テーマ名/functions.php
/**
 * 公開予定時刻を1時間以上過ぎた future 投稿を検出して通知する
 */
add_action( 'mysite_check_missed_schedule', function () {
    $missed = get_posts( array(
        'post_status'    => 'future',
        'post_type'      => 'post',
        'posts_per_page' => 20,
        'date_query'     => array(
            array(
                'before' => '1 hour ago',
                'column' => 'post_date',
            ),
        ),
    ) );

    if ( empty( $missed ) ) {
        return;
    }

    $lines = array( '公開予定を過ぎた予約投稿があります。', '' );
    foreach ( $missed as $post ) {
        $lines[] = sprintf(
            '- %s(予定: %s) %s',
            $post->post_title,
            $post->post_date,
            admin_url( 'post.php?post=' . $post->ID . '&action=edit' )
        );
    }

    wp_mail(
        get_option( 'admin_email' ),
        '[' . get_bloginfo( 'name' ) . '] 予約投稿が公開されていません',
        implode( "\n", $lines )
    );
} );

/**
 * 上記チェックを1時間ごとに登録する
 */
add_action( 'init', function () {
    if ( ! wp_next_scheduled( 'mysite_check_missed_schedule' ) ) {
        wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', 'mysite_check_missed_schedule' );
    }
} );

add_action( 'switch_theme', function () {
    wp_clear_scheduled_hook( 'mysite_check_missed_schedule' );
} );

編集リンクに get_edit_post_link() を使っていないのは、cron実行時にはログインユーザーがいないためです。権限チェックが走って空文字が返るので、admin_url() で組み立てています。cronから通知を出すコードでは、この手の「画面がある前提の関数」が黙って失敗します。

1 hour ago の部分が検出の閾値です。cronを5分間隔で回しているなら 30 minutes ago まで詰めても誤検知しません。逆にcronが1時間間隔なら、閾値も広げないと毎回メールが飛びます。

この通知自体がcronで動くという入れ子構造なので、cronが完全に死んだ場合は通知も来ません

外側から生存を確認する

内側の監視だけでは、内側ごと止まったときに気づけません。外部の死活監視サービスから、数分おきにサイトのURLを叩く経路を1本足します。監視サービス側で「応答が返らない」「特定の文字列が消えた」を条件にアラートを飛ばせます。

監視用に軽量なエンドポイントを1つ用意しておくと、トップページの重さに引きずられません。

/**
 * /?mysite_health=1 で cron の遅延状況を返す
 */
add_action( 'init', function () {
    if ( empty( $_GET['mysite_health'] ) ) {
        return;
    }

    $next   = wp_next_scheduled( 'wp_version_check' );
    $delay  = $next ? time() - $next : 0;
    $status = ( $delay > 2 * HOUR_IN_SECONDS ) ? 'NG' : 'OK';

    header( 'Content-Type: text/plain; charset=utf-8' );
    echo $status . ' delay=' . max( 0, $delay );
    exit;
} );

監視サービス側に「本文に OK が含まれること」を条件として設定すれば、cronが2時間以上滞った時点でアラートが飛びます。URLは推測されにくいクエリ名にしておいてください。

なお、cronがそもそも使えない契約プランなら、この経路を逆向きに使えます。外部の監視サービスから https://example.com/wp-cron.php?doing_wp_cron を定期的に叩けば、アクセスを人為的に発生させる形でサーバーcronの代役になります。

落ちたときに戻せるようにする

自動投稿を仕込むということは、外部からDBに書き込む経路を作るということです。誤ったCSVを流し込んで想定外の本数の記事が入る事故は現実に起きます。実行の前にバックアップの体制を確認しておいてください。


サーバー環境による違い

サーバー環境による違い

cronが使えるかどうかは、契約しているサーバーによって差があります。

環境 cron WP-CLI 備考
一般的な共用レンタルサーバー 管理画面から設定 プランにより異なる 実行間隔の最小値に制限がある場合あり
VPS・専用サーバー crontab -e で自由 自分で導入 制限なし
マネージドWordPress 提供元が管理 提供元による wp-cron無効化が既定のことも多い

共用サーバーでは「1分ごと」の実行が禁止されていたり、cronの同時実行数に上限があったりします。契約プランの仕様を確認してから間隔を決めてください。

サーバー選定そのものから関わる案件なら、cronとWP-CLIの可否は選定基準に入れるべき項目です。

なお、プラグインを増やさずに素の実装で済ませる考え方は、cron以外の場面でも同じです。ブログカードを外部サービスに頼らず作る手順も同じ発想で書いています。


うまくいかないとき

予約投稿が一切公開されなくなった

DISABLE_WP_CRON を定義したあと、cronの登録を忘れていないか確認します。この2つは必ずセットです。応急処置として、wp-config.php の該当行を一時的に false に戻せば元の挙動に戻ります。

define( 'DISABLE_WP_CRON', false );

cronは設定したのに動かない

コマンドのパスを疑ってください。cronはログインシェルの環境変数を引き継がないため、wpwget と書いただけでは「コマンドが見つからない」で終わります。

which wp
which wget
which curl

出力された絶対パスをそのまま cron に書きます。それでも動かない場合、cronの行末に一時的にログ出力を足して原因を見ます。

*/5 * * * * cd /home/username/public_html && /usr/local/bin/wp cron event run --due-now >> /tmp/cron-debug.log 2>&1

/tmp/cron-debug.log に何も書かれていなければ、cron自体が起動していません。設定画面での登録ミスか、サーバー側でcronが無効になっています。

REST APIが401を返す

順に確認します。

  • httpsになっているか:Application Passwords はhttps必須です
  • Authorization ヘッダーが届いているか:Apacheの一部設定では削除されます。次項参照
  • パスワードの空白xxxx xxxx xxxx の空白は含めたままで正しいです。詰めると通りません
  • ユーザー名:メールアドレスではなくログインIDを使います

Authorization ヘッダーが剥がれる環境では、WordPressルートの .htaccess に次を追加します。

/.htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [E=HTTP_AUTHORIZATION:%1]
</IfModule>

# BEGIN WordPress よりに書いてください。WordPressが自動で書き換える領域の中に入れると、更新時に消えます。

REST APIが403を返す

セキュリティプラグインがREST APIを止めている可能性が高いです。「REST APIの無効化」「XML-RPCの無効化」といった項目を確認します。完全に開放するのではなく、/wp-json/wp/v2/posts への認証済みリクエストだけ通す設定にできるプラグインが多いです。

サーバー側のWAFが弾いているケースもあります。管理画面から一時的にWAFを切って再試行し、通るならWAFの除外設定に該当パスを追加します。本文にHTMLタグを含むPOSTは、WAFの既定ルールに引っかかりやすい典型です。

予約時刻がずれる

3か所を確認します。

  1. WordPressのタイムゾーン:設定 → 一般 → タイムゾーンが 東京 または UTC+9 になっているか
  2. サーバーのタイムゾーンdate コマンドで確認。UTC設定のサーバーは珍しくありません
  3. コード内の混在time() はUTC、current_time( 'timestamp' ) はサイト時刻。混ぜない

REST APIの date フィールドはサイトのタイムゾーン基準、date_gmt はUTC基準です。片方だけ指定すればもう片方は自動で計算されるので、両方を書く必要はありません。

記事が二重に投稿される

cronが重複登録されている可能性があります。WP-CLIで確認します。

wp cron event list --fields=hook,next_run_relative

同じフック名が複数並んでいたら、一度全部消してから登録し直します。

wp cron event delete mysite_daily_task

CSV投入スクリプト側の二重投稿は、前述の posted.log で防いでいます。ログファイルが消えると再投入されるので、削除しないよう運用ルールに含めてください。

処理が途中で止まる

PHPの実行時間制限に当たっています。件数を減らすのが第一手です。LIMIT 200LIMIT 50 にする、CSVを分割する、といった対応で解決します。

WP-CLI経由の実行はWebサーバーのタイムアウトと無関係なので、重い処理はWP-CLIから叩く形にすると安定します。これも wp-cron を捨ててサーバーcronに寄せる理由のひとつです。


よくある質問

wp-cronを無効化すると何か不具合はありますか

予約投稿・自動更新・プラグインの定期処理がすべて止まるため、サーバーcronの登録とセットで行う必要があります。逆に言えば、cronさえ正しく登録すれば機能上の欠損はありません。むしろアクセスごとの無駄なDB照会が消えるため、表示速度には有利に働きます。

共用レンタルサーバーでもサーバーcronは使えますか

多くの共用サーバーはコントロールパネルからcronを設定できます。ただし実行間隔の最小値や同時実行数に制限があるプランもあるため、契約中のプランの仕様を確認してください。cronが使えないプランなら、外部の死活監視サービスから定期的に wp-cron.php を叩く方法で代替できます。

実行間隔は何分が適切ですか

予約投稿の運用が中心なら5分から15分で十分です。分単位の正確さが必要な用途でなければ、間隔を短くしてもサーバー負荷が増えるだけで得るものはありません。1日1回だけ動けばよい処理しかないサイトなら、1時間間隔でも問題ありません。

Application Passwordは管理者アカウントで発行してよいですか

投稿だけが目的なら、編集者権限の専用ユーザーを新規作成して発行することを推奨します。パスワードが外部システムやスクリプトファイルに保存される以上、漏洩時の被害範囲は権限の広さに比例します。用途ごとに発行し、不要になったら管理画面から削除してください。

プラグインで同じことはできませんか

cron管理系のプラグインは登録されたタスクの可視化・手動実行に有効ですが、wp-cronがアクセス起点で動くという構造そのものは変えられません。プラグインを入れても DISABLE_WP_CRON とサーバーcronの組み合わせは必要です。可視化のためだけに1本入れる判断はあり得ます。


まとめ

やることは3つです。

止まらない自動投稿を作る3工程
wp-cronを止めるwp-config.php に1行追加
サーバーcronを登録絶対パスで5〜15分間隔
気づける状態にするログ出力と遅延通知

3つ目が抜けている現場をよく見ます。1と2だけでも予約投稿は正確に動きますが、サーバー移転やPHPバージョン更新でcronの設定が飛んだとき、誰も気づかないまま数週間が過ぎます。

自動化した処理は、動いていることを定期的に確認する経路がなければ完成しません。ログを残し、遅延を検知し、メールが飛ぶところまで作って初めて「止まらない仕組み」になります。