| @@ -948,14 +948,40 @@ | ||
| 948 | 948 | |
| 949 | 949 | // Check if we have 'docs' query var (alternative to 'name') |
| 950 | 950 | if ( isset( $wp_query->query_vars['docs'] ) && ! empty( $wp_query->query_vars['docs'] ) ) { |
| 951 | 951 | |
| 952 | + // Verify the requested doc actually exists (and is visible to the | |
| 953 | + // current user) before forcing a single-doc render. Category-in-path | |
| 954 | + // permalinks put the doc slug in the `docs` var alongside a valid | |
| 955 | + // doc_category; without this guard a missing slug under a real | |
| 956 | + // category rendered the single template against a null post — an | |
| 957 | + // HTTP 200 soft-404 plus "read property on null" warnings on every | |
| 958 | + // (often bot) hit. See betterdocs/betterdocs#169. | |
| 959 | + $doc_post = get_page_by_path( $wp_query->query_vars['docs'], OBJECT, 'docs' ); | |
| 960 | + $doc_visible = $doc_post | |
| 961 | + && ( 'private' !== $doc_post->post_status || current_user_can( 'read_private_docs' ) ); | |
| 962 | + | |
| 963 | + if ( ! $doc_visible ) { | |
| 964 | + // No such doc (or private and not permitted) — serve a genuine | |
| 965 | + // 404 instead of a single render against a null post. | |
| 966 | + $wp_query->set_404(); | |
| 967 | + status_header( 404 ); | |
| 968 | + nocache_headers(); | |
| 969 | + add_filter( 'template_include', function( $template ) { | |
| 970 | + $not_found = get_404_template(); | |
| 971 | + return $not_found ? $not_found : $template; | |
| 972 | + }, 999 ); | |
| 973 | + return; | |
| 974 | + } | |
| 975 | + | |
| 952 | 976 | // Explicitly set this as a single post |
| 953 | - $wp_query->is_single = true; | |
| 954 | - $wp_query->is_singular = true; | |
| 955 | - $wp_query->is_404 = false; | |
| 956 | - $wp_query->is_archive = false; | |
| 957 | - $wp_query->is_tax = false; | |
| 977 | + $wp_query->is_single = true; | |
| 978 | + $wp_query->is_singular = true; | |
| 979 | + $wp_query->is_404 = false; | |
| 980 | + $wp_query->is_archive = false; | |
| 981 | + $wp_query->is_tax = false; | |
| 982 | + $wp_query->queried_object = $doc_post; | |
| 983 | + $wp_query->queried_object_id = $doc_post->ID; | |
| 958 | 984 | return; |
| 959 | 985 | } |
| 960 | 986 | |
| 961 | 987 | // If 'name' is set, check if a post with that name exists |
| @@ -1075,8 +1101,24 @@ | ||
| 1075 | 1101 | if ( $code == 404 && ( |
| 1076 | 1102 | (isset($wp_query->query_vars['doc_category']) && ! empty($wp_query->query_vars['doc_category'])) || |
| 1077 | 1103 | (isset($wp_query->query_vars['doc_tag']) && ! empty($wp_query->query_vars['doc_tag'])) |
| 1078 | 1104 | ) ) { |
| 1105 | + // A single-doc request (category-in-path permalinks carry the doc slug | |
| 1106 | + // in the `docs`/`name` var alongside doc_category) must resolve to a real | |
| 1107 | + // doc. A valid doc is already served 200 by the queried-object branch | |
| 1108 | + // above; if we reach here with a single-doc indicator but no resolvable | |
| 1109 | + // post, the doc does not exist — keep the genuine 404 rather than forcing | |
| 1110 | + // a soft-404 200 off the category term alone. See betterdocs/betterdocs#169. | |
| 1111 | + $single_doc_slug = ''; | |
| 1112 | + if ( isset( $wp_query->query_vars['docs'] ) && ! empty( $wp_query->query_vars['docs'] ) ) { | |
| 1113 | + $single_doc_slug = $wp_query->query_vars['docs']; | |
| 1114 | + } elseif ( isset( $wp_query->query_vars['name'] ) && ! empty( $wp_query->query_vars['name'] ) ) { | |
| 1115 | + $single_doc_slug = $wp_query->query_vars['name']; | |
| 1116 | + } | |
| 1117 | + if ( '' !== $single_doc_slug && ! get_page_by_path( $single_doc_slug, OBJECT, 'docs' ) ) { | |
| 1118 | + return $status_header; | |
| 1119 | + } | |
| 1120 | + | |
| 1079 | 1121 | // Validate existence before forcing 200 |
| 1080 | 1122 | // Use encoded fallback so Bengali/Arabic/CJK slugs are found correctly. |
| 1081 | 1123 | if ( isset($wp_query->query_vars['doc_category']) && ! empty($wp_query->query_vars['doc_category']) ) { |
| 1082 | 1124 | $term = $this->get_term_by_slug_or_encoded( $wp_query->query_vars['doc_category'], 'doc_category' ); |