My idea is simple: register the CPT using 'publicly_queryable' => TRUE
and then conditionally makes the post type not publicly queryable when a single news that has no content is queried.
This implies that we have to change 'publicly_queryable'
argument after the post type is registered. Something easy: all post types object are saved in the global variable $wp_post_types
so, assuming CPT slug is 'news', simply using
$GLOBALS['wp_post_types']['news']->publicly_queryable = FALSE;
we will be able to disable query for news CPT.
Second problem is when conditionally disable.
We know that all posts have an url, even if non-queryable, however when the url for a singular post of a non-queryable CPT is visited, WordPress send a 404 response.
This happen inside the WP::parse_request()
method, so best place to run our conditional logic is just before the request parsing happen, and so best choice is the filter hook 'do_parse_request'
(fired in first lines of WP::parse_request()
).
So our workflow should be:
- inside
'do_parse_request'
check if the request is for a single news
- if #1 is yes, check if the requested news has no content
- if #2 is yes, set
publicly_queryable
argument to FALSE for news CPT
- reset
publicly_queryable
argument to TRUE after main query happen
Hardest part is #1, because once request has not yet parsed by WordPress we can't use any of the conditional tags, i.e. is too early to call is_singular( 'news' )
.
Only possiblity is to look at the url, luckily url_to_postid()
function will help us on this task.
That said we can write a simple class to implement our workflow:
class SingleCptEnabler {
private $id = -1;
private $cpt_slug;
function __construct( $cpt_slug ) {
$this->cpt_slug = $cpt_slug;
}
/**
* Run on 'do_parse_request' filter, and enable publicly_queryable
* when a single news having content is required
*/
function setup() {
if (
current_filter() === 'do_parse_request'
&& $this->isSingle()
&& ! $this->hasContent()
) {
// when 'wp' hook is fired main query already happen
add_action( 'wp', array( $this, 'enable' ) );
$this->disable();
}
}
/**
* Query DB to get post content of the current queried news
*/
function hasContent() {
if ( (int) $this->id <= 0 ) {
return;
}
$post = get_post( $this->id );
$content = ! empty( $post ) && $post->post_type === $this->cpt_slug
? apply_filters( 'the_content', $post->post_content )
: FALSE;
return ! empty( $content );
}
/**
* Enable publicly_queryable argument for news CPT
*/
function enable() {
$GLOBALS['wp_post_types'][$this->cpt_slug]->publicly_queryable = TRUE;
}
/**
* Disable publicly_queryable argument and reset id
*/
function disable() {
$GLOBALS['wp_post_types'][$this->cpt_slug]->publicly_queryable = FALSE;
$this->id = -1;
}
/**
* Check if the current url is for a singular news
*/
function isSingle() {
$this->id = -1;
if ( ! is_admin() ) {
$this->id = (int) url_to_postid( add_query_arg( array() ) );
}
return (int) $this->id > 0;
}
}
After having this class in an active plugin or in theme functions.php
(or better in a file required from there) we need just to call the SingleCptEnabler::setup()
on the 'do_parse_request'
filter hook, passing to class constructor the CPT slug:
add_filter( 'do_parse_request', function( $do ) {
$news_enabler = new SingleCptEnabler( 'news' );
$news_enabler->setup();
return $do; // we don't want to affect the filter results
} );
Class is reusable and it can be also used for more than one CPT, e.g. if we want same behavior for 'news' and 'commentary' CPTs we can do:
add_filter( 'do_parse_request', function( $do ) {
$news_enabler = new SingleCptEnabler( 'news' );
$commentary_enabler = new SingleCptEnabler( 'commentary' );
$news_enabler->setup();
$commentary_enabler->setup();
return $do; // we don't want to affect the filter results
} );
Now, when you want some news to have a full content, just fill the post content (editor), otherwise just fill the excerpt.
Only downside is that singular news page open will slow down, because of the additional work required.