If you don't want to use a plugin (or can't find one that does what you're needing), you might want to approach it this way:
- Add a custom meta box that allows you to mark the post as
hidden
.
- Modifying the query with pre_get_posts to remove the posts you've labeled as hidden from your site (but will be available with a direct link).
UPDATE
Following the suggestion above, here is a possible solution.
Create a custom meta box
First, create the custom meta box by registering one:
function yourtextdomain_add_custom_meta_box() {
add_meta_box("demo-meta-box", "Custom Meta Box", "yourtextdomain_custom_meta_box_markup", "post", "side", "high", null);
}
add_action("add_meta_boxes", "yourtextdomain_add_custom_meta_box");
Add the markup to the metabox (a checkbox in the case):
function yourtextdomain_custom_meta_box_markup($object) {
wp_nonce_field(basename(__FILE__), "meta-box-nonce"); ?>
<div>
<br />
<label for="meta-box-checkbox">Hidden</label>
<?php $checkbox_value = get_post_meta($object->ID, "meta-box-checkbox", true);
if($checkbox_value == "") { ?>
<input name="meta-box-checkbox" type="checkbox" value="true">
<?php } else if($checkbox_value == "true") { ?>
<input name="meta-box-checkbox" type="checkbox" value="true" checked>
<?php } ?>
<p style="color: #cccccc"><i>When selected, the post will be removed from the WP loop but still accessible from a direct link.</i></p>
</div>
<?php
}
This will give you a meta box for each post that looks like this:

And finally save the meta box value:
function yourtextdomain_save_custom_meta_box($post_id, $post, $update) {
if (!isset($_POST["meta-box-nonce"]) || !wp_verify_nonce($_POST["meta-box-nonce"], basename(__FILE__)))
return $post_id;
if(!current_user_can("edit_post", $post_id))
return $post_id;
if(defined("DOING_AUTOSAVE") && DOING_AUTOSAVE)
return $post_id;
$slug = "post";
if($slug != $post->post_type)
return $post_id;
$meta_box_checkbox_value = "";
if(isset($_POST["meta-box-checkbox"])) {
$meta_box_checkbox_value = $_POST["meta-box-checkbox"];
}
update_post_meta($post_id, "meta-box-checkbox", $meta_box_checkbox_value);
}
add_action("save_post", "yourtextdomain_save_custom_meta_box", 10, 3);
In the wp_postmeta
table, you should now see the meta value 'true' assigned to the posts you've checked as hidden and saved:

Modifying the query with pre_get_posts
Now it's just a matter of filtering out those posts that are marked as hidden from the main query. We can do this with pre_get_posts
:
add_action( 'pre_get_posts', 'yourtextdomain_pre_get_posts_hidden', 9999 );
function yourtextdomain_pre_get_posts_hidden( $query ){
// Check if on frontend and main query.
if( ! is_admin() && $query->is_main_query() ) {
// For the posts we want to exclude.
$exclude = array();
// Locate our posts marked as hidden.
$hidden = get_posts(array(
'post_type' => 'post',
'meta_query' => array(
array(
'key' => 'meta-box-checkbox',
'value' => 'true',
'compare' => '==',
),
)
));
// Create an array of hidden posts.
foreach($hidden as $hide) {
$exclude[] = $hide->ID;
}
// Exclude the hidden posts.
$query->set('post__not_in', $exclude);
}
}