diff --git a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php index aa2538d10..887afc65f 100644 --- a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php +++ b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php @@ -42,128 +42,6 @@ public function register() { add_filter( 'rest_pre_echo_response' , [ $this, 'optimise_yoast_payload' ], 10, 3 ); } - /** - * Optimises the Yoast SEO payload in REST API responses. - * - * This method modifies the API response to reduce the payload size by removing - * the 'yoast_head' and 'yoast_json_head' fields from the response when they are - * not needed for the nextjs app. - * See https://github.com/10up/headstartwp/issues/563 - * - * @param array $result The response data to be served, typically an array. - * @param \WP_REST_Server $server Server instance. - * @param \WP_REST_Request $request Request used to generate the response. - * - * @return array Modified response data. - */ - public function optimise_yoast_payload( $result, $server, $request ) { - - $embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false; - - if ( ! $embed ) { - return $result; - } - - $first_post = true; - - foreach ( $result as &$post_obj ) { - - if ( ! empty( $post_obj['_embedded']['wp:term'] ) ) { - $this->optimise_yoast_payload_for_taxonomy( $post_obj['_embedded']['wp:term'], $request, $first_post ); - } - - if ( ! empty( $post_obj['_embedded']['author'] ) ) { - $this->optimise_yoast_payload_for_author( $post_obj['_embedded']['author'], $request, $first_post ); - } - - if ( ! $first_post ) { - unset( $post_obj['yoast_head'], $post_obj['yoast_head_json'] ); - } - - $first_post = false; - } - - unset( $post_obj ); - - return $result; - } - - /** - * Optimises the Yoast SEO payload for taxonomies. - * Removes yoast head from _embed terms for any term that is not in the queried params. - * Logic runs for the first post, yoast head metadata is removed completely for other posts. - * - * @param array $taxonomy_groups The _embedded wp:term collections. - * @param \WP_REST_Request $request Request used to generate the response. - * @param boolean $first_post Whether this is the first post in the response. - * - * @return void - */ - protected function optimise_yoast_payload_for_taxonomy( &$taxonomy_groups, $request, $first_post ) { - - foreach ( $taxonomy_groups as &$taxonomy_group ) { - - foreach ( $taxonomy_group as &$term_obj ) { - - $param = null; - - if ( $first_post ) { - // Get the queried terms for the taxonomy. - $param = $term_obj['taxonomy'] === 'category' ? - $request->get_param('category') ?? $request->get_param('categories') : - $request->get_param( $term_obj['taxonomy'] ); - } - - if ( $first_post && ! empty( $param ) ) { - $param = is_array( $param ) ? $param : explode( ',', $param ); - - // If the term slug is not in param array, unset yoast heads. - if ( ! in_array( $term_obj['slug'], $param, true ) && ! in_array( $term_obj['id'], $param, true ) ) { - unset( $term_obj['yoast_head'], $term_obj['yoast_head_json'] ); - } - } else { - unset( $term_obj['yoast_head'], $term_obj['yoast_head_json'] ); - } - } - - unset( $term_obj ); - } - - unset( $taxonomy_group ); - } - - /** - * Optimises the Yoast SEO payload for author. - * Removes yoast head from _embed author for any author that is not in the queried params. - * Logic runs for the first post, yoast head metadata is removed completely for other posts. - * - * @param array $authors The _embedded author collections. - * @param \WP_REST_Request $request Request used to generate the response. - * @param boolean $first_post Whether this is the first post in the response. - * - * @return void - */ - protected function optimise_yoast_payload_for_author( &$authors, $request, $first_post ) { - - foreach ( $authors as &$author ) { - - $param = $first_post ? $request->get_param( 'author' ) : null; - - if ( $first_post && ! empty( $param ) ) { - $param = is_array( $param ) ? $param : explode( ',', $param ); - - // If the term slug is not in param array, unset yoast heads. - if ( ! in_array( $author['slug'], $param, true ) && ! in_array( $author['id'], $param, true ) ) { - unset( $author['yoast_head'], $author['yoast_head_json'] ); - } - } else { - unset( $author['yoast_head'], $author['yoast_head_json'] ); - } - } - - unset( $author ); - } - /** * Checks if Yoast SEO Urls should be rewritten * @@ -448,4 +326,126 @@ function ( $presenters ) { } ); } + + /** + * Optimises the Yoast SEO payload in REST API responses. + * + * This method modifies the API response to reduce the payload size by removing + * the 'yoast_head' and 'yoast_json_head' fields from the response when they are + * not needed for the nextjs app. + * See https://github.com/10up/headstartwp/issues/563 + * + * @param array $result The response data to be served, typically an array. + * @param \WP_REST_Server $server Server instance. + * @param \WP_REST_Request $request Request used to generate the response. + * + * @return array Modified response data. + */ + public function optimise_yoast_payload( $result, $server, $request, $embed = false ) { + + $embed = $embed ?: rest_parse_embed_param( $_GET['_embed'] ?? false ); + + if ( ! $embed ) { + return $result; + } + + $first_post = true; + + foreach ( $result as &$post_obj ) { + + if ( ! empty( $post_obj['_embedded']['wp:term'] ) ) { + $this->optimise_yoast_payload_for_taxonomy( $post_obj['_embedded']['wp:term'], $request, $first_post ); + } + + if ( ! empty( $post_obj['_embedded']['author'] ) ) { + $this->optimise_yoast_payload_for_author( $post_obj['_embedded']['author'], $request, $first_post ); + } + + if ( ! $first_post ) { + unset( $post_obj['yoast_head'], $post_obj['yoast_head_json'] ); + } + + $first_post = false; + } + + unset( $post_obj ); + + return $result; + } + + /** + * Optimises the Yoast SEO payload for taxonomies. + * Removes yoast head from _embed terms for any term that is not in the queried params. + * Logic runs for the first post, yoast head metadata is removed completely for other posts. + * + * @param array $taxonomy_groups The _embedded wp:term collections. + * @param \WP_REST_Request $request Request used to generate the response. + * @param boolean $first_post Whether this is the first post in the response. + * + * @return void + */ + protected function optimise_yoast_payload_for_taxonomy( &$taxonomy_groups, $request, $first_post ) { + + foreach ( $taxonomy_groups as &$taxonomy_group ) { + + foreach ( $taxonomy_group as &$term_obj ) { + + $param = null; + + if ( $first_post ) { + // Get the queried terms for the taxonomy. + $param = $term_obj['taxonomy'] === 'category' ? + $request->get_param('category') ?? $request->get_param('categories') : + $request->get_param( $term_obj['taxonomy'] ); + } + + if ( $first_post && ! empty( $param ) ) { + $param = is_array( $param ) ? $param : explode( ',', $param ); + + // If the term slug is not in param array, unset yoast heads. + if ( ! in_array( $term_obj['slug'], $param, true ) && ! in_array( $term_obj['id'], $param, true ) ) { + unset( $term_obj['yoast_head'], $term_obj['yoast_head_json'] ); + } + } else { + unset( $term_obj['yoast_head'], $term_obj['yoast_head_json'] ); + } + } + + unset( $term_obj ); + } + + unset( $taxonomy_group ); + } + + /** + * Optimises the Yoast SEO payload for author. + * Removes yoast head from _embed author for any author that is not in the queried params. + * Logic runs for the first post, yoast head metadata is removed completely for other posts. + * + * @param array $authors The _embedded author collections. + * @param \WP_REST_Request $request Request used to generate the response. + * @param boolean $first_post Whether this is the first post in the response. + * + * @return void + */ + protected function optimise_yoast_payload_for_author( &$authors, $request, $first_post ) { + + foreach ( $authors as &$author ) { + + $param = $first_post ? $request->get_param( 'author' ) : null; + + if ( $first_post && ! empty( $param ) ) { + $param = is_array( $param ) ? $param : explode( ',', $param ); + + // If the term slug is not in param array, unset yoast heads. + if ( ! in_array( $author['slug'], $param, true ) && ! in_array( $author['id'], $param, true ) ) { + unset( $author['yoast_head'], $author['yoast_head_json'] ); + } + } else { + unset( $author['yoast_head'], $author['yoast_head_json'] ); + } + } + + unset( $author ); + } } diff --git a/wp/headless-wp/tests/php/tests/TestYoastIntegration.php b/wp/headless-wp/tests/php/tests/TestYoastIntegration.php new file mode 100644 index 000000000..b9a286149 --- /dev/null +++ b/wp/headless-wp/tests/php/tests/TestYoastIntegration.php @@ -0,0 +1,170 @@ +yoast_seo = new YoastSEO(); + $this->yoast_seo->register(); + self::$rest_server = rest_get_server(); + + $this->create_posts(); + } + + /** + * Create posts for testing + */ + protected function create_posts() { + $this->category_id = $this->factory()->term->create( [ 'taxonomy' => 'category', 'slug' => 'test-category' ] ); + $this->author_id = $this->factory()->user->create( + [ + 'role' => 'editor', + 'user_login' => 'test_author', + 'user_pass' => 'password', + 'user_email' => 'testauthor@example.com', + 'display_name' => 'Test Author', + ] + ); + + $random_category_id = $this->factory()->term->create( [ 'taxonomy' => 'category', 'slug' => 'random-category' ] ); + + $this->factory()->post->create_many( 2, [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_category' => [ $this->category_id, $random_category_id ], + 'post_author' => $this->author_id, + ]); + } + + /** + * Tests optimising the Yoast SEO payload in REST API responses. + * + * @return void + */ + public function test_optimise_category_yoast_payload() { + + // Perform a REST API request for the posts by category. + $result = $this->get_posts_by_with_optimised_response( 'categories', $this->category_id ); + $this->assert_yoast_head_in_response( $result ); + + // Perform a REST API request for the posts by author. + // $result = $this->get_posts_by_with_optimised_response( 'author', $this->author_id ); + // $this->assert_yoast_head_in_response( $result ); + } + + /** + * Get the optimised response from headstartwp Yoast integration by param. (category, author) + * + * @param string $param The param to filter by (category, author) + * @param int|string $value The value of the param + * + * @return \WP_REST_Response + */ + protected function get_posts_by_with_optimised_response( $param, $value ) { + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( $param, $value ); + + $response = rest_do_request( $request ); + $data = self::$rest_server->response_to_data( $response, true ); + + $this->assertGreaterThanOrEqual( 2, count( $data ), 'There should be at least two posts returned.' ); + + return $this->yoast_seo->optimise_yoast_payload( $data, self::$rest_server, $request, true ); + } + + /** + * Asserts the presence of yoast_head in the response for each post. + * + * @param array $result The response data containing posts. + * @return void + */ + protected function assert_yoast_head_in_response( $result ) { + $first_post = true; + + foreach ( $result as $post ) { + + $this->assertArrayHasKey( '_embedded', $post, 'The _embedded key should exist in the response.' ); + $this->assertArrayHasKey( 'wp:term', $post['_embedded'], 'The wp:term in _embedded key should exist in the response.' ); + $this->assertArrayHasKey( 'author', $post['_embedded'], 'The author in _embedded key should exist in the response.' ); + + $this->assert_embedded_item( $post['_embedded'], 'wp:term', $first_post, $this->category_id ); + $this->assert_embedded_item( $post['_embedded'], 'author', $first_post, null ); + + $first_post = false; + } + } + + /** + * Asserts the presence of yoast_head of the expected embedded item in the response. + * + * @param array $embedded_obj The embedded object containing the items. + * @param string $type The type of embedded item to check. + * @param bool $first_post Whether it is the first post in the response. + * @param int $id The ID of the item to check + * @return void + */ + protected function assert_embedded_item( $embedded_obj, $type, $first_post, $id = null ) { + + foreach ( $embedded_obj[ $type ] as $group) { + + $items = 'wp:term' !== $type ? [ $group ] : $group; + + foreach ( $items as $item ) { + + if ( $first_post && $item['id'] === $id ) { + $this->assertArrayHasKey( 'yoast_head', $item, 'The requested ' . $type . ' should have yoast_head in the response for the first post.' ); + } else { + $this->assertArrayNotHasKey( 'yoast_head', $item, 'yoast_head in ' . $type . ' should not be present for posts other than the first post and if not requested.' ); + } + } + } + } +}