WordPress offers authors the possibility to schedule the publication of posts. By setting the publication date in the future when writing a new post, WordPress should automatically publish it for you when that time comes.

But this is not flawless. If your website has to deal with a high peak or limited resources, WordPress may not have been able to publish the message for you. This in itself is not a disaster, but the problem is that WordPress will not make a second attempt. In the admin area you’ll see the message “missed schedule”, but this does not solve the problem of course.

I have written a plugin that makes WordPress try to publish posts with a “missed schedule” every 5 minutes. If you are not too technically inclined, you can download it here, but it is even more fun to write it with me using the tutorial below.

Step 1 – Basic structure

The final folder structure of the plugin will look like this:

  • tpt-missed-schedule/
    • tpt-missed-schedule.php

I use the prefix tpt for all my plugins, but you can change this in whatever you like. At the top of the plugin file you may specify the meta data:

<?php

/**
 * Plugin Name: Missed schedule
 * Author: Turpoint
 * Author URI: https://turpoint.com
 * Description: Catches missed schedules (auto publishing posts).
 */

Immediately below I create my plugin class. Personally I like to work with classes for all my WordPress development. In those classes I put hooks, filters and other functionalities.

namespace Turpoint;

new Missed_Schedule;

class Missed_Schedule
{
    /**
     * Constructor
     */
    public function __construct()
    {
        //
    }
}

Step 2 – Define your own wp-cron interval

By default WP-Cron offers only a limited number of intervals at which we can call a function:

  • Once per hour (hourly)
  • Once per day (daily)
  • Twice per day (twicedaily)

These intervals are a little too short for our case. If the automatic publishing of a scheduled message fails, a new attempt should be made no more than 5 minutes later.

Therefore I will define an additional interval in the plugin called five_minutes, this can be done via the filter cron_schedules. Declare this filter in the __construct function of the Missed_Schedule class:

// ...

public function __construct()
{
    add_filter('cron_schedules', [$this, 'add_five_minutes_cron_schedule']);
}

 /**
 * Add five minutes cron schedule
 */
public function add_five_minutes_cron_schedule($schedules)
{
    $schedules['five_minutes'] = [
        'interval' => 300,
        'display' => __('Five minutes'),
    ];
    return $schedules;
}

// ...

Step 3 – Schedule our own WP-Cron action

Now that the interval has been created, we can schedule a certain action on this interval. WP-Cron will then call this action every 5 minutes. Scheduling this action only needs to happen once, so it is best to do this when the plugin is activated.

// ...

public function __construct()
{
    // ...
    register_activation_hook(__FILE__, [$this, 'schedule_cron_job']);
}

// ...

/**
 * Schedule the cron job
 */
public function schedule_cron_job()
{
    if (wp_next_scheduled('tpt_catch_missed_schedule')) {
        return;
    }
    wp_schedule_event(time(), 'five_minutes', 'tpt_catch_missed_schedule');
}

// ...

It is important to remove this action from the schedule when our plugin is no longer used. This is not compulsory per se, but it is not necessary for WordPress to keep calling actions of plugins that are no longer active on the website.

Just as we scheduled the action via register_activation_hook, we can delete the action via register_deactivation_hook.

// ...

public function __construct()
{
    // ...
    register_deactivation_hook(__FILE__, [$this, 'unschedule_cron_job']);
}

// ...

/**
 * Unschedule the cron job
 */
public function unschedule_cron_job()
{
    $timestamp = wp_next_scheduled('tpt_catch_missed_schedule');
    wp_unschedule_event($timestamp, 'tpt_catch_missed_schedule');
}

// ...

Step 4 – Catching up on a missed schedule

And now for the most important part of the plugin. Here we check which messages were scheduled, but have not yet been published.

For this purpose we retrieve the messages which have the status future, but whose publication date lies in the past at the same time. These are the messages with “Missed schedule”.

// ...

/**
 * Constructor
 */
public function __construct()
{
    // ... 

    add_action('tpt_catch_missed_schedule', [$this, 'catch_missed_schedule']);
}

// ...

/**
 * Catch missed schedule
 */
public function catch_missed_schedule()
{
    global $wpdb;
    $now = gmdate('Y-m-d H:i:00');

    $args = [
        'public' => true,
        'exclude_from_search' => false,
        '_builtin' => false
    ];
    $post_types = get_post_types($args, 'names', 'and');
    $str = implode('\',\'',$post_types);

    if ($str) {
        $sql = "Select ID from $wpdb->posts WHERE post_type in ('post','page','$str') AND post_status='future' AND post_date_gmt<'$now'";
    }
    else { 
        $sql = "Select ID from $wpdb->posts WHERE post_type in ('post','page') AND post_status='future' AND post_date_gmt<'$now'";
    }

    $posts = $wpdb->get_results($sql);
    
    if ($posts) {
        foreach ($posts as $post) {
            wp_publish_post($post->ID);
        }
    }
}

// ...

The above piece of code does the following:

  • It retrieves the post types from the website
  • It compiles a database query that retrieves the posts with post_status = future, but whose publication date is in the past
  • It loops over the posts and publishes them one by one

Eindresultaat

For reference, the complete plugin file tpt-missed-schedule.php looks like this:

<?php

/**
 * Plugin Name: Missed schedule
 * Author: Turpoint
 * Author URI: https://turpoint.com
 * Description: Catches missed schedules (auto publishing posts).
 */

namespace Turpoint;

new Missed_Schedule;

class Missed_Schedule
{
    /**
     * Constructor
     */
    public function __construct()
    {
        register_activation_hook(__FILE__, [$this, 'schedule_cron_job']);
        register_deactivation_hook(__FILE__, [$this, 'unschedule_cron_job']);

        add_filter('cron_schedules', [$this, 'add_five_minutes_cron_schedule']);

        add_action('tpt_catch_missed_schedule', [$this, 'catch_missed_schedule']);
    }

    /**
     * Add five minutes cron schedule
     */
    public function add_five_minutes_cron_schedule($schedules)
    {
        $schedules['five_minutes'] = [
            'interval' => 300,
            'display' => __('Five minutes'),
        ];
        return $schedules;
    }

    /**
     * Schedule the cron job
     */
    public function schedule_cron_job()
    {
        if (wp_next_scheduled('tpt_catch_missed_schedule')) {
            return;
        }
        wp_schedule_event(time(), 'five_minutes', 'tpt_catch_missed_schedule');
    }

    /**
     * Unschedule the cron job
     */
    public function unschedule_cron_job()
    {
        $timestamp = wp_next_scheduled('tpt_catch_missed_schedule');
        wp_unschedule_event($timestamp, 'tpt_catch_missed_schedule');
    }

    /**
     * Catch missed schedule
     */
    public function catch_missed_schedule()
    {
        global $wpdb;
		$now = gmdate('Y-m-d H:i:00');
	
    	$args = [
            'public' => true,
	        'exclude_from_search' => false,
    	    '_builtin' => false
        ];
    	$post_types = get_post_types($args, 'names', 'and');
		$str = implode('\',\'',$post_types);

		if ($str) {
			$sql = "Select ID from $wpdb->posts WHERE post_type in ('post','page','$str') AND post_status='future' AND post_date_gmt<'$now'";
		}
		else { 
            $sql = "Select ID from $wpdb->posts WHERE post_type in ('post','page') AND post_status='future' AND post_date_gmt<'$now'";
        }

        $results = $wpdb->get_results($sql);
        
 		if ($results) {
			foreach ($results as $post) {
				wp_publish_post($post->ID);
            }
		}
    }
}