Limit CPT to only have a single view page sometimes

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:

  1. inside 'do_parse_request' check if the request is for a single news
  2. if #1 is yes, check if the requested news has no content
  3. if #2 is yes, set publicly_queryable argument to FALSE for news CPT
  4. 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.

Leave a Comment