Set a WordPress Post Type to Private by Default

Some WordPress post types should never be public: video tutorials for clients, quotes, internal documentation. Editors still click Publish, and one forgotten visibility setting puts the content on the open web and in search engines. This snippet sets the post type to private by default: whenever a post of that type is published, WordPress saves it as Private instead.

Private posts are only visible to logged-in users who can read private posts (editors and administrators by default), and WordPress leaves them out of feeds, searches and the sitemap for everyone else.

The snippet

<?php
/**
 * Publish posts of chosen post types as Private, so only logged-in users
 * with the right role can see them.
 */

// ===== Config: post types that are always private =====
function dpc_private_post_types(): array {
	return array( 'my_post_type' );
}
// ===== End of config =====

add_filter( 'wp_insert_post_data', 'dpc_force_private_status', 10, 2 );

function dpc_force_private_status( array $data, array $postarr ): array {
	if ( ! in_array( $data['post_type'], dpc_private_post_types(), true ) ) {
		return $data;
	}

	// Change only Published and Scheduled. Drafts, Pending, Trash and auto-drafts keep their status.
	if ( in_array( $data['post_status'], array( 'publish', 'future' ), true ) ) {
		$data['post_status'] = 'private';
	}

	return $data;
}

Replace my_post_type with the slug of your post type. Add more slugs to the array to cover several types.

Add it with Scripts Organizer

In Scripts Organizer go to Code Blocks → Add New and give the block a name. Set Trigger location to Everywhere, tick PHP under Script location, paste the code into the PHP editor, edit the config and publish. Leave Action/Hook empty. Posts are saved from the block editor through the REST API, from Quick Edit and by imports, so the code must load on every request.

The same code also works unchanged in a small plugin or an mu-plugin.

How it works

  • wp_insert_post_data runs every time a post is created or updated, right before it is written to the database.
  • When the post type is in the list and the status is publish or future (scheduled), the snippet changes it to private.
  • Every other status is left alone, so drafts, Pending Review, auto-drafts and the trash work as usual.

The older version changed every status to private. That made it impossible to save a draft or move a post to the trash: WordPress saved it as private again.

Customise it

  • Let administrators publish publicly when they really want to: return $data early with if ( current_user_can( 'manage_options' ) ).
  • Make the whole post type invisible on the front end instead: register it with 'public' => false and 'show_ui' => true. Then there are no URLs at all, which is a different job from private posts that logged-in staff can open.

Gotchas

  • Scheduled posts become private right away; WordPress has no scheduled private status. A post that was already scheduled before you added the snippet is published by cron without this filter, so check those once.
  • Existing published posts stay public until they are saved again. Switch them with Bulk Edit → Status → Private.
  • The editor still shows a Publish button; after saving it shows Private.

In the Scripts Organizer library Import it with one click from Scripts Organizer.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You’ll also get DPlugins product updates. No spam, and you can unsubscribe anytime.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You'll also get DPlugins product updates. No spam, and you can unsubscribe anytime.