6

Imagine a CPT, let us call it "News". This News Post type is mainly used for displaying short News inline on the front page. But some of those News will be longer, mandating to only display an excerpt on the front page and having a single view. The rule would probably be that I put the text used on the front page in the excerpt all the time, while only entering content sometimes.

What I've thought of so far:

If none would need a separate page I'd just disable publicly_queryable for it.

Just leaving a single view for everything and doing a redirect back to the frontpage if the content is empty sounds like a dirty hack.

Just filtering those without content probably wouldn't suffice. What about rewrite rules? Plugins that generate Sitemaps? Search?

Is there anything else I missed? I am mainly looking for hints on the general direction to implement this, not necessarily an implementation.

2 Answers 2

4

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.

5
  • Wow, great idea. The only question that remains for me is how this plays along with e.g. Yoasts SEO when generating XML Sitemaps. I'll try this as soon as I find time and report back. Thanks so far.
    – kraftner
    Aug 13, 2014 at 15:40
  • Yes this is what I'd expect. The thing is that for those news that do have a single view it would also be nice to have them in the Sitemap.
    – kraftner
    Aug 13, 2014 at 15:52
  • Generally speaking it would be nice if the news would be seen as publicly_queryableeverywhere.
    – kraftner
    Aug 13, 2014 at 16:02
  • Yeah, you're probably right, this is as close as it gets without special handling for each plugin. Thanks a lot!
    – kraftner
    Aug 13, 2014 at 16:15
  • @kraftner I've updated the answer to work in the reversed way: now publicly_queryable is always TRUE (remember to register cpt in this way) but if you visit a single url for news with no content you get a 404. After that you have to filter out news with no content from anywhere you don't want to see them, but you need plugin specific code.
    – gmazzap
    Aug 13, 2014 at 16:15
1

Just filtering those without content probably wouldn't suffice

Why not? That would seem to be the simplest solution.

Unless I am misunderstanding your intent, it seems that checking to see if the post have content and only showing a link if it does will get you where you want to be.

On the single template for the post type, just do a check and redirect back to the homepage if there is no post content.

EDIT:

You could try creating virtual pages for the news items that have content using add_rewrite_rule(). Add the new query vars using the the query_vars filter and then use the template_redirect action to load the appropriate template.

2
  • Downvoting because I explicitly said that just a redirect back is probably a dirty hack. Just to give you an example this leaves behind a lot of "garbage" e.g. for plugins that create sitemaps automatically.
    – kraftner
    Aug 13, 2014 at 14:13
  • Ok - answer updated
    – Welcher
    Aug 13, 2014 at 14:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.