Trigger 404 template from a filter

I need to trigger the 404.blade.php template when a “post” page is opened.
Basing on this article, I’m trying this inside filters.php:

add_action( 'template_redirect', function() {
    if ( ! is_singular( 'post' ) )
        return;

    // 1. Ensure `is_*` functions work
    global $wp_query;
    $wp_query->set_404();

    // 2. Fix HTML title
    add_action( 'wp_title', function () {
        return '404: Not Found';
    }, 9999 );

    // 3. Throw 404
    status_header( 404 );
    nocache_headers();

    // 4. Show 404 template
    require get_404_template();
    //get_template_part( 404 );
    //include( get_query_template( '404' ) );

    // 5. Stop execution
    exit;
} );

but it returns a blank page with the blade code inside 404.blade.php:

@extends('layouts.app') @section('content') 

@if (!have_posts())

{{-- {!! get_search_form(false) !!} --}} @endif

@endsection

How can solve?
Thanks

Blade templates need to be compiled to PHP and then output, rather than included directly like a standard WordPress template file. So, if everything is working except you’re seeing the raw Blade code rather than the rendered template, then you probably just need to use the template() function to compile your template and then echo the result.

Try something like echo template(get_404_template()); instead of require get_404_template();.

Hi @mmirus
awesome, we’re almost there. Now it renders the 404 template but from there I don’t have the variables passed by the App.php controller.
I placed @debug in the 404 template and indeed it prints nothing.
You know why?
Thanks!

When you call a Blade template using template(get_404_template()) you need to pass variables too it directly; it won’t automatically inherit them from context. i.e. template(get_404_template(), ['variable_name' => 'some data']).

Is there a reason you can’t do the same thing with pre_get_posts and just turn all post requests into 404s before you get to template_redirect? i.e.:

add_action('pre_get_posts', function(query) {
   if ( $query->is_single ) {
      $query->set_404();
   }
});
2 Likes

Yes, I could use pre_get_posts but your code catch all single (including custom post type single), while I need to catch only “post” single (not custom post types). How can do it?

Also, should I add this steps?

// 2. Fix HTML title
add_action( 'wp_title', function () {
    return '404: Not Found';
}, 9999 );

// 3. Throw 404
status_header( 404 );
nocache_headers();

// 5. Stop execution
exit;

My code is just an example to get you started, not a literal replacement. There are a lot of conditionals on the $query object that you can check to make sure it only runs on the correct post type: https://codex.wordpress.org/Class_Reference/WP_Query#Methods_and_Properties

Does it work if you don’t?

1 Like

Hi @alwaysblank I’m trying to catch the “post” single but I can’t make it works. :frowning:

None of the following worked:

if ( $query->is_singular('post') ) ...

if ( $query->is_single('post') ) ...

if ( $query->query_vars['post_type'] == 'post' ) ...

if ( $query->get('post_type') === 'post' ) { ...

Some help?

1 Like

My first step is to check if my conditional is working by adding some code that I know with have an effect I’ll see, like die("I work!"), to see if it’s my conditional that’s not working, or the actual effect I’m shooting for (in your case forcing a 404) not firing.

i.e.:

add_action('pre_get_posts', function(query) {
   if ( $query->is_single) {
      die('I work!');
   }
});

Also make sure that the code you’re hooking into pre_get_posts with is firing at a time when it’s possible for it to hook into that action.

Hi @alwaysblank, tried your code and it prints I work! on any single post types, while I need it worked just for default single post (not custom post types)
I think the conditional $query->is_single catches any single and I can’t find the one I need…

Maybe try var_dump($query) before die() and then load one page with the standard post type and one with a custom post type? That way you should be able to find a property of $query that you can use in combination with the code that you got from @alwaysblank to target only the standard post type.

Hi @folbert, tried your idea and these are what they outputs:

"post" type => http://localhost:3000/hello/
object(WP_Query)#340 (47) { ["query"]=> array(2) { ["page"]=> string(0) "" ["name"]=> string(4) "hello" } ["query_vars"]=> array(53) { ["page"]=> string(0) "" ["name"]=> string(4) "hello" ["error"]=> string(0) "" ["m"]=> string(0) "" ["p"]=> int(0) ["post_parent"]=> string(0) "" ["subpost"]=> string(0) "" ["subpost_id"]=> string(0) "" ["attachment"]=> string(0) "" ["attachment_id"]=> int(0) ["static"]=> string(0) "" ["pagename"]=> string(0) "" ["page_id"]=> int(0) ["second"]=> string(0) "" ["minute"]=> string(0) "" ["hour"]=> string(0) "" ["day"]=> int(0) ["monthnum"]=> int(0) ["year"]=> int(0) ["w"]=> int(0) ["category_name"]=> string(0) "" ["tag"]=> string(0) "" ["cat"]=> string(0) "" ["tag_id"]=> string(0) "" ["author"]=> string(0) "" ["author_name"]=> string(0) "" ["feed"]=> string(0) "" ["tb"]=> string(0) "" ["paged"]=> int(0) ["meta_key"]=> string(0) "" ["meta_value"]=> string(0) "" ["preview"]=> string(0) "" ["s"]=> string(0) "" ["sentence"]=> string(0) "" ["title"]=> string(0) "" ["fields"]=> string(0) "" ["menu_order"]=> string(0) "" ["embed"]=> string(0) "" ["category__in"]=> array(0) { } ["category__not_in"]=> array(0) { } ["category__and"]=> array(0) { } ["post__in"]=> array(0) { } ["post__not_in"]=> array(0) { } ["post_name__in"]=> array(0) { } ["tag__in"]=> array(0) { } ["tag__not_in"]=> array(0) { } ["tag__and"]=> array(0) { } ["tag_slug__in"]=> array(0) { } ["tag_slug__and"]=> array(0) { } ["post_parent__in"]=> array(0) { } ["post_parent__not_in"]=> array(0) { } ["author__in"]=> array(0) { } ["author__not_in"]=> array(0) { } } ["tax_query"]=> NULL ["meta_query"]=> bool(false) ["date_query"]=> bool(false) ["post_count"]=> int(0) ["current_post"]=> int(-1) ["in_the_loop"]=> bool(false) ["comment_count"]=> int(0) ["current_comment"]=> int(-1) ["found_posts"]=> int(0) ["max_num_pages"]=> int(0) ["max_num_comment_pages"]=> int(0) ["is_single"]=> bool(true) ["is_preview"]=> bool(false) ["is_page"]=> bool(false) ["is_archive"]=> bool(false) ["is_date"]=> bool(false) ["is_year"]=> bool(false) ["is_month"]=> bool(false) ["is_day"]=> bool(false) ["is_time"]=> bool(false) ["is_author"]=> bool(false) ["is_category"]=> bool(false) ["is_tag"]=> bool(false) ["is_tax"]=> bool(false) ["is_search"]=> bool(false) ["is_feed"]=> bool(false) ["is_comment_feed"]=> bool(false) ["is_trackback"]=> bool(false) ["is_home"]=> bool(false) ["is_privacy_policy"]=> bool(false) ["is_404"]=> bool(false) ["is_embed"]=> bool(false) ["is_paged"]=> bool(false) ["is_admin"]=> bool(false) ["is_attachment"]=> bool(false) ["is_singular"]=> bool(true) ["is_robots"]=> bool(false) ["is_posts_page"]=> bool(false) ["is_post_type_archive"]=> bool(false) ["query_vars_hash":"WP_Query":private]=> string(32) "267f226a4936902f26db6f23ef85759d" ["query_vars_changed":"WP_Query":private]=> bool(false) ["thumbnails_cached"]=> bool(false) ["stopwords":"WP_Query":private]=> NULL ["compat_fields":"WP_Query":private]=> array(2) { [0]=> string(15) "query_vars_hash" [1]=> string(18) "query_vars_changed" } ["compat_methods":"WP_Query":private]=> array(2) { [0]=> string(16) "init_query_flags" [1]=> string(15) "parse_tax_query" } } I work!

"experience" post type => http://localhost:3000/experiences/experience-1/
object(WP_Query)#340 (47) { ["query"]=> array(4) { ["page"]=> string(0) "" ["experience"]=> string(27) "experience-1" ["post_type"]=> string(10) "experience" ["name"]=> string(27) "experience-1" } ["query_vars"]=> array(55) { ["page"]=> string(0) "" ["experience"]=> string(27) "experience-1" ["post_type"]=> string(10) "experience" ["name"]=> string(27) "experience-1" ["error"]=> string(0) "" ["m"]=> string(0) "" ["p"]=> int(0) ["post_parent"]=> string(0) "" ["subpost"]=> string(0) "" ["subpost_id"]=> string(0) "" ["attachment"]=> string(0) "" ["attachment_id"]=> int(0) ["static"]=> string(0) "" ["pagename"]=> string(0) "" ["page_id"]=> int(0) ["second"]=> string(0) "" ["minute"]=> string(0) "" ["hour"]=> string(0) "" ["day"]=> int(0) ["monthnum"]=> int(0) ["year"]=> int(0) ["w"]=> int(0) ["category_name"]=> string(0) "" ["tag"]=> string(0) "" ["cat"]=> string(0) "" ["tag_id"]=> string(0) "" ["author"]=> string(0) "" ["author_name"]=> string(0) "" ["feed"]=> string(0) "" ["tb"]=> string(0) "" ["paged"]=> int(0) ["meta_key"]=> string(0) "" ["meta_value"]=> string(0) "" ["preview"]=> string(0) "" ["s"]=> string(0) "" ["sentence"]=> string(0) "" ["title"]=> string(0) "" ["fields"]=> string(0) "" ["menu_order"]=> string(0) "" ["embed"]=> string(0) "" ["category__in"]=> array(0) { } ["category__not_in"]=> array(0) { } ["category__and"]=> array(0) { } ["post__in"]=> array(0) { } ["post__not_in"]=> array(0) { } ["post_name__in"]=> array(0) { } ["tag__in"]=> array(0) { } ["tag__not_in"]=> array(0) { } ["tag__and"]=> array(0) { } ["tag_slug__in"]=> array(0) { } ["tag_slug__and"]=> array(0) { } ["post_parent__in"]=> array(0) { } ["post_parent__not_in"]=> array(0) { } ["author__in"]=> array(0) { } ["author__not_in"]=> array(0) { } } ["tax_query"]=> NULL ["meta_query"]=> bool(false) ["date_query"]=> bool(false) ["post_count"]=> int(0) ["current_post"]=> int(-1) ["in_the_loop"]=> bool(false) ["comment_count"]=> int(0) ["current_comment"]=> int(-1) ["found_posts"]=> int(0) ["max_num_pages"]=> int(0) ["max_num_comment_pages"]=> int(0) ["is_single"]=> bool(true) ["is_preview"]=> bool(false) ["is_page"]=> bool(false) ["is_archive"]=> bool(false) ["is_date"]=> bool(false) ["is_year"]=> bool(false) ["is_month"]=> bool(false) ["is_day"]=> bool(false) ["is_time"]=> bool(false) ["is_author"]=> bool(false) ["is_category"]=> bool(false) ["is_tag"]=> bool(false) ["is_tax"]=> bool(false) ["is_search"]=> bool(false) ["is_feed"]=> bool(false) ["is_comment_feed"]=> bool(false) ["is_trackback"]=> bool(false) ["is_home"]=> bool(false) ["is_privacy_policy"]=> bool(false) ["is_404"]=> bool(false) ["is_embed"]=> bool(false) ["is_paged"]=> bool(false) ["is_admin"]=> bool(false) ["is_attachment"]=> bool(false) ["is_singular"]=> bool(true) ["is_robots"]=> bool(false) ["is_posts_page"]=> bool(false) ["is_post_type_archive"]=> bool(false) ["query_vars_hash":"WP_Query":private]=> string(32) "438563e1c3e1141da50810663a05029d" ["query_vars_changed":"WP_Query":private]=> bool(false) ["thumbnails_cached"]=> bool(false) ["stopwords":"WP_Query":private]=> NULL ["compat_fields":"WP_Query":private]=> array(2) { [0]=> string(15) "query_vars_hash" [1]=> string(18) "query_vars_changed" } ["compat_methods":"WP_Query":private]=> array(2) { [0]=> string(16) "init_query_flags" [1]=> string(15) "parse_tax_query" } } I work!

I can’t see a property to targets the standard post type. Am I wrong?

p.s. notice that I also have to trigger the 404 for standard “post” archives

My bad. $query doesn’t seem to hold any post data that can be easily used to determine the post type when called using the pre_get_posts filter.

But what if you use the filter template_include instead of the action template_redirect that you used in your original post? As taken from the documentation for template redirect: “Loading a different template is not a good use of this action hook.”

Something like:

add_action('template_include', function($template) {

  if (is_singular('post')) {

    global $wp_query;

    $wp_query->set_404();

    // In case you need to make sure that `have_posts()` return false.
    // Maybe there's a reset function on WP_Query but I couldn't find one.
    $wp_query->post_count = 0;
    $wp_query->posts = [];
    $wp_query->post = false;

    status_header(404);

    $template = get_404_template();

  }

  return $template;

});
2 Likes

Wow @folbert it seems work well! It renders the Blade template and I have also the variables passed by App.php controller. Awesome.

Just some details to understand.

  1. Basing on this article, he adds add_action(wp_title)... and nocache_headers(); and exit at the end. You didn’t add them in your code. Are they unnecessary?

  2. How can I return 404 also for standard post archive? In general I need to return 404 for all URL related to standard post

Thanks a lot

Great.

  • Title - If the <title> for the 404-page that you get is not what you want, use the code from the article. If you want to use the same custom title for all your 404 pages, put the code outside of my example code.

  • nocache_headers() - since WPs execution will continue as normal, I assume that WP core will take care of applying that function if needed.

  • exit - what happens if you add exit anywhere in the code?

  1. Not to sound harsh but that is more a general WP question than related to Sage so to paraphrase Neo: "Where you go from here, is a choice I leave to you. " :slight_smile:

Thanks @folbert

Placing exit before the return I get browser 404 page (no WP template loaded), while placing it after the return it works identical (it loads the WP 404 template).

Do you think is better to add it or not?

If you read the manual for exit (and maybe also return), I am sure that you will find the answer yourself :slight_smile:

This topic was automatically closed after 42 days. New replies are no longer allowed.