
🔒 4 Ways to programmatically unpublish a post in WordPress
The site went down after a plugin update, you need to urgently hide the problematic post before it's too late. And a week later, bring it back when the bug is fixed. Or a client asks you to remove an outdated article from search results, but not delete it permanently.
Manually switching the status via the admin panel works for one or two posts. But when there are dozens of them or the logic needs to fire automatically, you need a programmatic approach. WordPress gives you four ways to unpublish a post via PHP: from a safe draft to full deletion.
Below, each method with ready-to-use code, an explanation, and a hint on when to use which one.
💡 Quick overview:
- Turned a post into a draft via
wp_update_postwith thedraftstatus, the safest and most reversible way - Made a post private (
private), visible only to administrators and editors - Sent a post into the future via
post_date, the post disappears from search results until the specified date arrives - Permanently deleted a post via
wp_delete_post, a last resort with warnings and a backup
Step 1. Draft: unpublish a post without losing data
The most common scenario: you need to temporarily hide a post but keep all the content, URL, and the ability to bring it back with one click. Switching to draft is the ideal option.
Only the post_status field in the wp_posts table changes. The post itself, its meta fields, attachments, and URL remain untouched. When you decide to bring it back, you change the status back to publish.
Code to change the status to draft. Add it to your child theme's functions.php or via the Code Snippets plugin:
1 /** 2 * Converts a post to draft by ID. 3 * 4 * @param int $post_id ID of the post to unpublish. 5 */ 6 function sd_unpublish_to_draft( $post_id ) { 7 wp_update_post( array( 8 'ID' => $post_id, 9 'post_status' => 'draft', 10 ) ); 11 } 12 13 // Example call: unpublish post with ID = 42 14 sd_unpublish_to_draft( 42 );
wp_update_post() updates a record in the database. We only pass the ID and the new post_status value, WordPress handles everything else on its own. No other fields are changed.
When to use: temporarily hiding a post for revision, automatically deactivating posts with expired relevance (e.g., promotions), programmatic moderation of user-generated content.
Step 2. Private post: hide from visitors, keep for editors
The private status is a middle ground between public and hidden. The post is not visible to regular visitors but is accessible to administrators and editors in the admin panel. Handy for internal materials: team instructions, client content drafts, private pages.
Difference from a draft: a private post is technically "published" and can have its own URL, but WordPress checks user permissions before displaying it. A visitor without the read_private_posts capability will see a 404.
The code is similar to the previous one, only the status changes:
1 /** 2 * Makes a post private — visible only to admins and editors. 3 * 4 * @param int $post_id Post ID. 5 */ 6 function sd_unpublish_to_private( $post_id ) { 7 wp_update_post( array( 8 'ID' => $post_id, 9 'post_status' => 'private', 10 ) ); 11 } 12 13 // Example call 14 sd_unpublish_to_private( 42 );
Note: if the site has custom user roles with custom capabilities, check them before mass use. By default, private posts are visible to editor and administrator roles.
When to use: premium subscription content (paired with membership plugins), internal team documentation, hiding posts for re-approval with a client before republishing.
Step 3. Future date: delayed unpublishing
An interesting trick: instead of changing the status, you can "send a post into the future", set the publication date to the year 2050. The post instantly disappears from search results because WordPress only shows posts with a date ≤ the current moment.
This method does not change post_status: the post remains publish. It simply "hasn't happened yet" from WordPress's perspective. A plus: if needed, you can restore the real date and the post will reappear.
The code uses the post_date and post_date_gmt fields:
1 /** 2 * Hides a post by setting its publication date far into the future. 3 * 4 * @param int $post_id Post ID. 5 */ 6 function sd_unpublish_to_future( $post_id ) { 7 $future_date = '2050-12-31 23:59:59'; 8 9 wp_update_post( array( 10 'ID' => $post_id, 11 'post_date' => $future_date, 12 'post_date_gmt' => get_gmt_from_date( $future_date ), 13 ) ); 14 } 15 16 // Example call 17 sd_unpublish_to_future( 42 );
get_gmt_from_date() converts local time to GMT, WordPress stores both versions of the date. Don't neglect the GMT field: without it, behavior when the site's timezone changes becomes unpredictable.
When to use: "scheduled" content publication, temporarily hiding news without changing the status, scenarios where post_status must remain publish for backward compatibility with other plugins.
Step 4. Deletion: when the post is not needed at all
wp_delete_post() is an irreversible operation. The post is deleted from the database, along with all its meta fields, taxonomy relationships, and (optionally) attachments.
This is not "unpublishing" in the strict sense. But in the context of programmatic content management, deletion is the fourth, most drastic tool. And it requires safeguards.
Before running, make a full database backup. The script below first outputs a list of what will be deleted, and only then, the production version.
1 /** 2 * Deletes a post. First — dry-run with info output, then — actual deletion. 3 * 4 * WARNING: irreversible operation. Backup before running. 5 * 6 * @param int $post_id Post ID. 7 * @param bool $force_delete true — delete permanently (skip trash), false — move to trash. 8 */ 9 function sd_delete_post_safe( $post_id, $force_delete = false ) { 10 $post = get_post( $post_id ); 11 12 if ( ! $post ) { 13 error_log( "Post with ID {$post_id} not found." ); 14 return; 15 } 16 17 // Dry-run: output info without deleting 18 error_log( sprintf( 19 'READY TO DELETE: ID=%d, title="%s", status=%s, attachments=%d', 20 $post->ID, 21 $post->post_title, 22 $post->post_status, 23 count( get_attached_media( '', $post_id ) ) 24 ) ); 25 26 // Uncomment the following line for actual deletion: 27 // wp_delete_post( $post_id, $force_delete ); 28 } 29 30 // Dry-run: only outputs info 31 sd_delete_post_safe( 12341, false );
The $force_delete flag:
false, the post goes to the Trash, it can be restored within 30 days.true, permanent deletion, cannot be restored even via the database (without a backup).
The function logs via error_log(), messages will appear in wp-content/debug.log when WP_DEBUG is enabled. In production, replace it with your own notification mechanism.
When to use: automatic cleanup of spam posts, deleting expired content (job listings, events), programmatic content rotation with full removal of old entries.
Comparison of the four methods
Method | Post status | Reversibility | Visibility to readers | Visibility in admin | When to use |
|---|---|---|---|---|---|
Draft |
| Full | Hidden | All roles with post access | Temporary hiding, revision |
Private |
| Full | Hidden | Admins and editors | Internal content, premium |
Future date |
| Full | Hidden until the date | Everyone | Scheduled publication, timetable |
Deletion | - | Only from Trash (30 days) | - | Admins only | Full deletion, cleanup |
⁉️🤔 Frequent questions
What is the difference between unpublishing and deleting?
Unpublishing (draft/private/future) keeps the post in the database: content, URL, attachments, and SEO history remain. Deletion (
wp_delete_post) erases the record completely. For temporary hiding, always use a draft, it's safe and reversible in a second.
Which method does not require changing post_status?
Sending into the future via
post_date. The post remainspublish, but WordPress considers it "not yet happened" and does not show it to visitors. This can be important if other plugins or snippets depend on thepublishstatus.
Can I unpublish several posts at once?
Yes, wrap the function call in a loop over an array of IDs. Add
wp_die()or a limit on the number of posts per run to avoid crashing the site during a mass operation:array_slice($post_ids, 0, 50)for a batch of 50.
Do I need to clear the cache after a programmatic status change?
Absolutely. WordPress flushes the internal post cache when
wp_update_post()is called, but external cache (plugins like WP Rocket, server cache, CDN) must be cleared separately. Add awp_cache_flush()call or theclean_post_cachehook after the status change.
Is it safe to run wp_delete_post in production?
Only with safeguards. Before calling: (1) check
current_user_can('delete_posts'), (2) request confirmation via a separate nonce token, (3) log the ID and title of the post being deleted. And most importantly, a backup. Even in the Trash, a post lives for 30 days, after which WordPress deletes it automatically.
What to use in your case: the bottom line
The four methods cover almost any scenario of programmatic publication management. The choice boils down to one question: do you need to keep the post?
- If you need to temporarily hide a post for revision, go with a draft (
draft). A couple of lines, zero risk. - If the content is for a limited circle of people, private status (
private). Editors see it, visitors don't. - If you need to hide a post without changing its status, a future date (
post_dateset to 2050). A clever but working trick. - If the post is definitely not needed, deletion (
wp_delete_post). But first, a dry run and a full backup.
Start with a wrapper in functions.php for one method, for example, a draft. Once you understand the logic of wp_update_post(), the other three methods will come together in five minutes.
And which method do you use for programmatic post management? Write in the comments, it's interesting to compare approaches.



