WordPress core empties the trash after 30 days but only via EMPTY_TRASH_DAYS constant — which is all-or-nothing and not per-post-type. This gives you per-post-type control with configurable retention and batched deletes.
/**
* Permanently delete trashed posts older than a configurable number of days.
* Runs daily via WP-Cron. Configurable per post type.
*
* Configure in wp-config.php:
* define( 'WPFOLKS_TRASH_RETENTION_DAYS', 14 ); // Default: 14 days
*
* Extend via filter:
* add_filter( 'wpfolks_trash_post_types', fn( $types ) => array_merge( $types, [ 'product' ] ) );
*/
defined( 'WPFOLKS_TRASH_RETENTION_DAYS' ) || define( 'WPFOLKS_TRASH_RETENTION_DAYS', 14 );
// ─── 1. Schedule daily cleanup ────────────────────────────────────────────────
add_action( 'init', 'wpfolks_schedule_trash_cleanup' );
function wpfolks_schedule_trash_cleanup(): void {
if ( ! wp_next_scheduled( 'wpfolks_daily_trash_cleanup' ) ) {
wp_schedule_event( time(), 'daily', 'wpfolks_daily_trash_cleanup' );
}
}
// ─── 2. Run the cleanup ───────────────────────────────────────────────────────
add_action( 'wpfolks_daily_trash_cleanup', 'wpfolks_do_trash_cleanup' );
function wpfolks_do_trash_cleanup(): void {
$days = absint( WPFOLKS_TRASH_RETENTION_DAYS );
if ( $days < 1 ) return; // Safety: never delete with 0-day retention
$post_types = apply_filters( 'wpfolks_trash_post_types', [ 'post', 'page' ] );
$post_types = array_map( 'sanitize_key', (array) $post_types );
if ( empty( $post_types ) ) return;
$trashed_posts = new WP_Query( [
'post_type' => $post_types,
'post_status' => 'trash',
'posts_per_page' => 50, // Batch — prevents memory exhaustion on large sites
'date_query' => [
[
'column' => 'post_modified_gmt',
'before' => "{$days} days ago",
],
],
'fields' => 'ids',
'no_found_rows' => true, // Skip SQL_CALC_FOUND_ROWS for performance
] );
if ( empty( $trashed_posts->posts ) ) return;
foreach ( $trashed_posts->posts as $post_id ) {
// wp_delete_post with true = force delete (bypass trash)
$deleted = wp_delete_post( (int) $post_id, true );
if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
if ( $deleted ) {
error_log( sprintf( '[WPFolks Trash] Permanently deleted post ID %d.', (int) $post_id ) );
} else {
error_log( sprintf( '[WPFolks Trash] Failed to delete post ID %d.', (int) $post_id ) );
}
}
}
}
// ─── 3. Clean up cron event on deactivation ──────────────────────────────────
register_deactivation_hook( __FILE__, function (): void {
wp_clear_scheduled_hook( 'wpfolks_daily_trash_cleanup' );
} );