From 8a799eb3b821222773d29e5adbd15b45b43ee001 Mon Sep 17 00:00:00 2001 From: Lucy Tomas Date: Wed, 20 Mar 2024 15:46:48 +0100 Subject: [PATCH 01/13] feat: remove yoast head from taxonomy terms that are not being queried --- .../classes/Integrations/YoastSEO.php | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php index f4bdd7dd6..2c492dc01 100644 --- a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php +++ b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php @@ -36,6 +36,61 @@ public function register() { // Introduce hereflangs presenter to Yoast list of presenters. add_action( 'rest_api_init', [ $this, 'wpseo_rest_api_hreflang_presenter' ], 10, 0 ); + + // Modify API response to optimise payload by removing the yoast_head and yoast_json_head where not needed. + // Embedded data is not added yet on rest_prepare_{$this->post_type}. + 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; + } + + foreach ( $result as &$post_obj ) { + // Optimise for taxonnomies. Removes yoast head from _embed terms for any term that is not in the queried params. + if ( ! empty( $post_obj['_embedded']['wp:term'] ) ) { + // Loop through each wp:term collection in _embedded. + foreach ( $post_obj['_embedded']['wp:term'] as &$terms_collection ) { + foreach ( $terms_collection as &$term_obj ) { + // Get the queried terms for the taxonomy. + $taxonomy_param = $term_obj['taxonomy'] === 'category' ? + $request->get_param('category') ?? $request->get_param('categories') : + $request->get_param( $term_obj['taxonomy'] ); + + if ( ! empty( $taxonomy_param ) ) { + $taxonomy_param = is_array( $taxonomy_param ) ? $taxonomy_param : explode( ',', $taxonomy_param ); + + // If the term slug is not in taxonomy_param array, unset yoast heads. + if ( !in_array( $term_obj['slug'], $taxonomy_param, true ) ) { + unset( $term_obj['yoast_head'], $term_obj['yoast_head_json'] ); + } + } else { + unset( $term_obj['yoast_head'], $term_obj['yoast_head_json'] ); + } + } + } + } + } + + return $result; } /** From 90a8ee8fdcf8f7eb8bce13969378e5e0dfaac081 Mon Sep 17 00:00:00 2001 From: Lucy Tomas Date: Thu, 21 Mar 2024 10:03:09 +0100 Subject: [PATCH 02/13] feat: optimise yoast payload for author --- .../classes/Integrations/YoastSEO.php | 111 ++++++++++++++---- 1 file changed, 89 insertions(+), 22 deletions(-) diff --git a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php index 2c492dc01..5ba6ca2bb 100644 --- a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php +++ b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php @@ -51,8 +51,8 @@ public function register() { * 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. + * @param \WP_REST_Server $server Server instance. + * @param \WP_REST_Request $request Request used to generate the response. * * @return array Modified response data. */ @@ -64,33 +64,100 @@ public function optimise_yoast_payload( $result, $server, $request ) { return $result; } + $first_post = true; + foreach ( $result as &$post_obj ) { - // Optimise for taxonnomies. Removes yoast head from _embed terms for any term that is not in the queried params. + if ( ! empty( $post_obj['_embedded']['wp:term'] ) ) { - // Loop through each wp:term collection in _embedded. - foreach ( $post_obj['_embedded']['wp:term'] as &$terms_collection ) { - foreach ( $terms_collection as &$term_obj ) { - // Get the queried terms for the taxonomy. - $taxonomy_param = $term_obj['taxonomy'] === 'category' ? - $request->get_param('category') ?? $request->get_param('categories') : - $request->get_param( $term_obj['taxonomy'] ); - - if ( ! empty( $taxonomy_param ) ) { - $taxonomy_param = is_array( $taxonomy_param ) ? $taxonomy_param : explode( ',', $taxonomy_param ); - - // If the term slug is not in taxonomy_param array, unset yoast heads. - if ( !in_array( $term_obj['slug'], $taxonomy_param, true ) ) { - unset( $term_obj['yoast_head'], $term_obj['yoast_head_json'] ); - } - } else { - unset( $term_obj['yoast_head'], $term_obj['yoast_head_json'] ); - } + $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 ); + } + + $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 ); } - return $result; + 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 ); } /** From 626778e8fcebd7e1f0e8c515d445478bce36f2e2 Mon Sep 17 00:00:00 2001 From: Lucy Tomas Date: Thu, 21 Mar 2024 10:05:24 +0100 Subject: [PATCH 03/13] feat: only keep yoast head for the first post --- wp/headless-wp/includes/classes/Integrations/YoastSEO.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php index 5ba6ca2bb..aa2538d10 100644 --- a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php +++ b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php @@ -76,6 +76,10 @@ public function optimise_yoast_payload( $result, $server, $request ) { $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; } From 5936925b51b8de6e4654f698af9b99d4ca9992e6 Mon Sep 17 00:00:00 2001 From: Lucy Tomas Date: Thu, 21 Mar 2024 17:29:06 +0100 Subject: [PATCH 04/13] feat: add tests --- .../classes/Integrations/YoastSEO.php | 244 +++++++++--------- .../tests/php/tests/TestYoastIntegration.php | 170 ++++++++++++ 2 files changed, 292 insertions(+), 122 deletions(-) create mode 100644 wp/headless-wp/tests/php/tests/TestYoastIntegration.php 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.' ); + } + } + } + } +} From f89c018e697ceed65597994d9759d68044cbf578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucy=20Tom=C3=A1s=20Cross?= Date: Sun, 10 Nov 2024 18:13:21 +0100 Subject: [PATCH 05/13] feat: add optimizeYoastPayload param to the fetching strategies --- .../core/src/data/strategies/AbstractFetchStrategy.ts | 7 +++++++ .../src/data/strategies/AuthorArchiveFetchStrategy.ts | 6 +++++- .../core/src/data/strategies/PostOrPostsFetchStrategy.ts | 8 +++++++- .../core/src/data/strategies/PostsArchiveFetchStrategy.ts | 8 ++++++++ packages/core/src/data/strategies/SearchFetchStrategy.ts | 4 ++++ .../core/src/data/strategies/SearchNativeFetchStrategy.ts | 8 ++++++++ .../core/src/data/strategies/SinglePostFetchStrategy.ts | 8 ++++++++ .../core/src/data/strategies/TaxonomyTermsStrategy.ts | 6 ++++++ packages/core/src/types.ts | 4 +++- projects/wp-nextjs/headstartwp.config.client.js | 1 + wp/headless-wp/includes/classes/Integrations/YoastSEO.php | 4 ++-- 11 files changed, 59 insertions(+), 5 deletions(-) diff --git a/packages/core/src/data/strategies/AbstractFetchStrategy.ts b/packages/core/src/data/strategies/AbstractFetchStrategy.ts index 615b67342..75805a36b 100644 --- a/packages/core/src/data/strategies/AbstractFetchStrategy.ts +++ b/packages/core/src/data/strategies/AbstractFetchStrategy.ts @@ -28,6 +28,13 @@ export interface EndpointParams { */ lang?: string; + /** + * The custom parameter to optimize the Yoast payload. + * + * This is only used if the YoastSEO integration is enabled + */ + optimizeYoastPayload?: boolean; + [k: string]: unknown; } diff --git a/packages/core/src/data/strategies/AuthorArchiveFetchStrategy.ts b/packages/core/src/data/strategies/AuthorArchiveFetchStrategy.ts index 30ab6308c..cc30a80f7 100644 --- a/packages/core/src/data/strategies/AuthorArchiveFetchStrategy.ts +++ b/packages/core/src/data/strategies/AuthorArchiveFetchStrategy.ts @@ -1,4 +1,4 @@ -import { getCustomTaxonomies } from '../../utils'; +import { getCustomTaxonomies, getSiteBySourceUrl } from '../../utils'; import { PostEntity } from '../types'; import { authorArchivesMatchers } from '../utils/matchers'; import { parsePath } from '../utils/parsePath'; @@ -25,6 +25,10 @@ export class AuthorArchiveFetchStrategy< const matchers = [...authorArchivesMatchers]; const customTaxonomies = getCustomTaxonomies(this.baseURL); + const config = getSiteBySourceUrl(this.baseURL); + + this.optimizeYoastPayload = !!config.integrations?.yoastSEO?.optimizeYoastPayload; + customTaxonomies?.forEach((taxonomy) => { const slug = taxonomy?.rewrite ?? taxonomy.slug; matchers.push({ diff --git a/packages/core/src/data/strategies/PostOrPostsFetchStrategy.ts b/packages/core/src/data/strategies/PostOrPostsFetchStrategy.ts index ef764b482..2ef91dbdd 100644 --- a/packages/core/src/data/strategies/PostOrPostsFetchStrategy.ts +++ b/packages/core/src/data/strategies/PostOrPostsFetchStrategy.ts @@ -8,7 +8,7 @@ import { } from './AbstractFetchStrategy'; import { PostParams, SinglePostFetchStrategy } from './SinglePostFetchStrategy'; import { PostsArchiveFetchStrategy, PostsArchiveParams } from './PostsArchiveFetchStrategy'; -import { FrameworkError, NotFoundError } from '../../utils'; +import { FrameworkError, NotFoundError, getSiteBySourceUrl } from '../../utils'; /** * The params supported by {@link PostOrPostsFetchStrategy} @@ -61,11 +61,17 @@ export class PostOrPostsFetchStrategy< postsStrategy: PostsArchiveFetchStrategy = new PostsArchiveFetchStrategy(this.baseURL); + optimizeYoastPayload: boolean = false; + getDefaultEndpoint(): string { return '@postOrPosts'; } getParamsFromURL(path: string, params: Partial

= {}): Partial

{ + const config = getSiteBySourceUrl(this.baseURL); + + this.optimizeYoastPayload = !!config.integrations?.yoastSEO?.optimizeYoastPayload; + this.urlParams = { single: this.postStrategy.getParamsFromURL(path, params.single), archive: this.postsStrategy.getParamsFromURL(path, params.archive), diff --git a/packages/core/src/data/strategies/PostsArchiveFetchStrategy.ts b/packages/core/src/data/strategies/PostsArchiveFetchStrategy.ts index 349556e7c..7c14c68c9 100644 --- a/packages/core/src/data/strategies/PostsArchiveFetchStrategy.ts +++ b/packages/core/src/data/strategies/PostsArchiveFetchStrategy.ts @@ -211,6 +211,8 @@ export class PostsArchiveFetchStrategy< locale: string = ''; + optimizeYoastPayload: boolean = false; + getDefaultEndpoint(): string { return endpoints.posts; } @@ -234,6 +236,8 @@ export class PostsArchiveFetchStrategy< this.locale = config.integrations?.polylang?.enable && params.lang ? params.lang : ''; this.path = path; + this.optimizeYoastPayload = !!config.integrations?.yoastSEO?.optimizeYoastPayload; + const matchers = [...postsMatchers]; if (typeof params.taxonomy === 'string') { @@ -457,6 +461,10 @@ export class PostsArchiveFetchStrategy< } } + if (this.optimizeYoastPayload) { + params.optimizeYoastPayload = true; + } + return super.fetcher(finalUrl, params, options); } diff --git a/packages/core/src/data/strategies/SearchFetchStrategy.ts b/packages/core/src/data/strategies/SearchFetchStrategy.ts index 849665ed7..a24bc2348 100644 --- a/packages/core/src/data/strategies/SearchFetchStrategy.ts +++ b/packages/core/src/data/strategies/SearchFetchStrategy.ts @@ -85,6 +85,10 @@ export class SearchFetchStrategy< queriedObject.search.yoast_head_json = seo_json; } + if (this.optimizeYoastPayload) { + params.optimizeYoastPayload = true; + } + const response = await super.fetcher(url, params, { ...options, throwIfNotFound: false }); return { diff --git a/packages/core/src/data/strategies/SearchNativeFetchStrategy.ts b/packages/core/src/data/strategies/SearchNativeFetchStrategy.ts index 85f74e402..40ac623a5 100644 --- a/packages/core/src/data/strategies/SearchNativeFetchStrategy.ts +++ b/packages/core/src/data/strategies/SearchNativeFetchStrategy.ts @@ -74,6 +74,8 @@ export class SearchNativeFetchStrategy< locale: string = ''; + optimizeYoastPayload: boolean = false; + getDefaultEndpoint() { return endpoints.search; } @@ -94,6 +96,8 @@ export class SearchNativeFetchStrategy< // Required for search lang url. this.locale = config.integrations?.polylang?.enable && params.lang ? params.lang : ''; + this.optimizeYoastPayload = !!config.integrations?.yoastSEO?.optimizeYoastPayload; + return parsePath(searchMatchers, path) as Partial

; } @@ -165,6 +169,10 @@ export class SearchNativeFetchStrategy< queriedObject.search.yoast_head_json = seo_json; } + if (this.optimizeYoastPayload) { + params.optimizeYoastPayload = true; + } + const response = await super.fetcher(url, params, { ...options, throwIfNotFound: false, diff --git a/packages/core/src/data/strategies/SinglePostFetchStrategy.ts b/packages/core/src/data/strategies/SinglePostFetchStrategy.ts index 7d7fdb7b1..65abf7027 100644 --- a/packages/core/src/data/strategies/SinglePostFetchStrategy.ts +++ b/packages/core/src/data/strategies/SinglePostFetchStrategy.ts @@ -90,6 +90,8 @@ export class SinglePostFetchStrategy< shouldCheckCurrentPathAgainstPostLink: boolean = true; + optimizeYoastPayload: boolean = false; + getDefaultEndpoint(): string { return endpoints.posts; } @@ -108,6 +110,8 @@ export class SinglePostFetchStrategy< this.path = nonUrlParams.fullPath ?? path; + this.optimizeYoastPayload = !!config.integrations?.yoastSEO?.optimizeYoastPayload; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { year, day, month, ...params } = parsePath(postMatchers, path); @@ -304,6 +308,10 @@ export class SinglePostFetchStrategy< } try { + if (this.optimizeYoastPayload) { + params.optimizeYoastPayload = true; + } + const result = await super.fetcher(url, params, options); return result; diff --git a/packages/core/src/data/strategies/TaxonomyTermsStrategy.ts b/packages/core/src/data/strategies/TaxonomyTermsStrategy.ts index d5b3e5a85..3df524d7d 100644 --- a/packages/core/src/data/strategies/TaxonomyTermsStrategy.ts +++ b/packages/core/src/data/strategies/TaxonomyTermsStrategy.ts @@ -102,6 +102,8 @@ export class TaxonomyTermsStrategy< > extends AbstractFetchStrategy { defaultTaxonomy = 'category'; + optimizeYoastPayload: boolean = false; + getDefaultEndpoint(): string { return endpoints.category; } @@ -140,6 +142,10 @@ export class TaxonomyTermsStrategy< params: Partial

, options?: Partial, ): Promise> { + if (this.optimizeYoastPayload) { + params.optimizeYoastPayload = true; + } + return super.fetcher(url, params, { ...options, throwIfNotFound: false }); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index a28123522..ed2f8145c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -48,7 +48,9 @@ export interface Integration { enable: boolean; } -export interface YoastSEOIntegration extends Integration {} +export interface YoastSEOIntegration extends Integration { + optimizeYoastPayload?: boolean; +} export interface PolylangIntegration extends Integration {} diff --git a/projects/wp-nextjs/headstartwp.config.client.js b/projects/wp-nextjs/headstartwp.config.client.js index bdee699a1..823c5446d 100644 --- a/projects/wp-nextjs/headstartwp.config.client.js +++ b/projects/wp-nextjs/headstartwp.config.client.js @@ -41,6 +41,7 @@ module.exports = { integrations: { yoastSEO: { enable: true, + optimizeYoastPayload: true, }, polylang: { enable: process?.env?.NEXT_PUBLIC_ENABLE_POLYLANG_INTEGRATION === 'true', diff --git a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php index 887afc65f..99d6157f0 100644 --- a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php +++ b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php @@ -344,8 +344,8 @@ function ( $presenters ) { public function optimise_yoast_payload( $result, $server, $request, $embed = false ) { $embed = $embed ?: rest_parse_embed_param( $_GET['_embed'] ?? false ); - - if ( ! $embed ) { + + if ( ! $embed || empty( $request->get_param( 'optimizeYoastPayload' ) ) ) { return $result; } From fff4834a5fcde42cb3645417a24ba70162d7eed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucy=20Tom=C3=A1s=20Cross?= Date: Sun, 10 Nov 2024 20:17:23 +0100 Subject: [PATCH 06/13] fix: fix test by adding optimizeYoastPayload to test --- wp/headless-wp/tests/php/tests/TestYoastIntegration.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wp/headless-wp/tests/php/tests/TestYoastIntegration.php b/wp/headless-wp/tests/php/tests/TestYoastIntegration.php index b9a286149..6d230989d 100644 --- a/wp/headless-wp/tests/php/tests/TestYoastIntegration.php +++ b/wp/headless-wp/tests/php/tests/TestYoastIntegration.php @@ -51,6 +51,7 @@ class TestYoastIntegration extends WP_Test_REST_TestCase { */ public function set_up() { parent::set_up(); + $this->yoast_seo = new YoastSEO(); $this->yoast_seo->register(); self::$rest_server = rest_get_server(); @@ -111,6 +112,7 @@ protected function get_posts_by_with_optimised_response( $param, $value ) { $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); $request->set_param( $param, $value ); + $request->set_param( 'optimizeYoastPayload', true ); $response = rest_do_request( $request ); $data = self::$rest_server->response_to_data( $response, true ); From 3a0849048e1d9e9801bcda4752e1cc4fd64bb1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucy=20Tom=C3=A1s=20Cross?= Date: Sun, 10 Nov 2024 22:07:16 +0100 Subject: [PATCH 07/13] fix: fix fetch with optimize yoast payload param --- .../core/src/data/strategies/PostsArchiveFetchStrategy.ts | 4 +++- packages/core/src/data/strategies/SearchFetchStrategy.ts | 4 ---- .../core/src/data/strategies/SinglePostFetchStrategy.ts | 8 ++++++-- .../core/src/data/strategies/TaxonomyTermsStrategy.ts | 6 ------ 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/core/src/data/strategies/PostsArchiveFetchStrategy.ts b/packages/core/src/data/strategies/PostsArchiveFetchStrategy.ts index 7c14c68c9..d3fe24e64 100644 --- a/packages/core/src/data/strategies/PostsArchiveFetchStrategy.ts +++ b/packages/core/src/data/strategies/PostsArchiveFetchStrategy.ts @@ -462,7 +462,9 @@ export class PostsArchiveFetchStrategy< } if (this.optimizeYoastPayload) { - params.optimizeYoastPayload = true; + finalUrl = addQueryArgs(finalUrl, { + optimizeYoastPayload: true, + }); } return super.fetcher(finalUrl, params, options); diff --git a/packages/core/src/data/strategies/SearchFetchStrategy.ts b/packages/core/src/data/strategies/SearchFetchStrategy.ts index a24bc2348..849665ed7 100644 --- a/packages/core/src/data/strategies/SearchFetchStrategy.ts +++ b/packages/core/src/data/strategies/SearchFetchStrategy.ts @@ -85,10 +85,6 @@ export class SearchFetchStrategy< queriedObject.search.yoast_head_json = seo_json; } - if (this.optimizeYoastPayload) { - params.optimizeYoastPayload = true; - } - const response = await super.fetcher(url, params, { ...options, throwIfNotFound: false }); return { diff --git a/packages/core/src/data/strategies/SinglePostFetchStrategy.ts b/packages/core/src/data/strategies/SinglePostFetchStrategy.ts index 65abf7027..732171c11 100644 --- a/packages/core/src/data/strategies/SinglePostFetchStrategy.ts +++ b/packages/core/src/data/strategies/SinglePostFetchStrategy.ts @@ -5,6 +5,7 @@ import { removeSourceUrl, NotFoundError, getSiteBySourceUrl, + addQueryArgs, } from '../../utils'; import { PostEntity } from '../types'; import { postMatchers } from '../utils/matchers'; @@ -278,6 +279,7 @@ export class SinglePostFetchStrategy< */ async fetcher(url: string, params: P, options: Partial = {}) { const { burstCache = false } = options; + let finalUrl = url; if (params.authToken) { options.previewToken = params.authToken; @@ -309,10 +311,12 @@ export class SinglePostFetchStrategy< try { if (this.optimizeYoastPayload) { - params.optimizeYoastPayload = true; + finalUrl = addQueryArgs(finalUrl, { + optimizeYoastPayload: true, + }); } - const result = await super.fetcher(url, params, options); + const result = await super.fetcher(finalUrl, params, options); return result; } catch (e) { diff --git a/packages/core/src/data/strategies/TaxonomyTermsStrategy.ts b/packages/core/src/data/strategies/TaxonomyTermsStrategy.ts index 3df524d7d..d5b3e5a85 100644 --- a/packages/core/src/data/strategies/TaxonomyTermsStrategy.ts +++ b/packages/core/src/data/strategies/TaxonomyTermsStrategy.ts @@ -102,8 +102,6 @@ export class TaxonomyTermsStrategy< > extends AbstractFetchStrategy { defaultTaxonomy = 'category'; - optimizeYoastPayload: boolean = false; - getDefaultEndpoint(): string { return endpoints.category; } @@ -142,10 +140,6 @@ export class TaxonomyTermsStrategy< params: Partial

, options?: Partial, ): Promise> { - if (this.optimizeYoastPayload) { - params.optimizeYoastPayload = true; - } - return super.fetcher(url, params, { ...options, throwIfNotFound: false }); } From 1a7ce5538dfed7d4d8a720e54b9ad1d5d1fa9805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucy=20Tom=C3=A1s=20Cross?= Date: Sun, 17 Nov 2024 09:09:11 +0100 Subject: [PATCH 08/13] fix: add _embed param to test --- .../tests/php/tests/TestYoastIntegration.php | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/wp/headless-wp/tests/php/tests/TestYoastIntegration.php b/wp/headless-wp/tests/php/tests/TestYoastIntegration.php index 6d230989d..225252d98 100644 --- a/wp/headless-wp/tests/php/tests/TestYoastIntegration.php +++ b/wp/headless-wp/tests/php/tests/TestYoastIntegration.php @@ -37,6 +37,13 @@ class TestYoastIntegration extends WP_Test_REST_TestCase { */ protected $category_id; + /** + * The tag id + * + * @var int + */ + protected $tag_id; + /** * The author id * @@ -64,6 +71,7 @@ public function set_up() { */ protected function create_posts() { $this->category_id = $this->factory()->term->create( [ 'taxonomy' => 'category', 'slug' => 'test-category' ] ); + $this->tag_id = $this->factory()->term->create( [ 'taxonomy' => 'post_tag', 'slug' => 'test-post-tag' ] ); $this->author_id = $this->factory()->user->create( [ 'role' => 'editor', @@ -75,13 +83,24 @@ protected function create_posts() { ); $random_category_id = $this->factory()->term->create( [ 'taxonomy' => 'category', 'slug' => 'random-category' ] ); + $random_tag_id = $this->factory()->term->create( [ 'taxonomy' => 'post_tag', 'slug' => 'random-post-tag' ] ); - $this->factory()->post->create_many( 2, [ + $post_1 = $this->factory()->post->create_and_get( [ 'post_type' => 'post', 'post_status' => 'publish', - 'post_category' => [ $this->category_id, $random_category_id ], 'post_author' => $this->author_id, ]); + + $post_2 = $this->factory()->post->create_and_get( [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_author' => $this->author_id, + ]); + + wp_set_post_terms( $post_1->ID, [ $this->category_id, $random_category_id ], 'category' ); + wp_set_post_terms( $post_2->ID, [ $this->category_id, $random_category_id ], 'category' ); + wp_set_post_terms( $post_1->ID, [ $this->tag_id, $random_tag_id ], 'post_tag' ); + wp_set_post_terms( $post_2->ID, [ $this->tag_id, $random_tag_id ], 'post_tag' ); } /** @@ -89,15 +108,19 @@ protected function create_posts() { * * @return void */ - public function test_optimise_category_yoast_payload() { - + public function test_optimise_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 ); + $result_category = $this->get_posts_by_with_optimised_response( 'categories', $this->category_id ); + $this->assert_yoast_head_in_response( $result_category ); + + // Perform a REST API request for the posts by tag. + // $result_tag = $this->get_posts_by_with_optimised_response( 'tags', $this->tag_id ); + // $this->assert_yoast_head_in_response( $result_tag ); // 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 ); + // $result_author = $this->get_posts_by_with_optimised_response( 'author', $this->author_id ); + // $this->assert_yoast_head_in_response( $$result_author ); } /** @@ -113,6 +136,7 @@ protected function get_posts_by_with_optimised_response( $param, $value ) { $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); $request->set_param( $param, $value ); $request->set_param( 'optimizeYoastPayload', true ); + $request->set_param( '_embed', true ); $response = rest_do_request( $request ); $data = self::$rest_server->response_to_data( $response, true ); From e087077ddc04218f3ffba1f092762d25202c53b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucy=20Tom=C3=A1s=20Cross?= Date: Mon, 18 Nov 2024 09:46:25 +0100 Subject: [PATCH 09/13] fix: phpcs --- .../classes/Integrations/YoastSEO.php | 15 ++--- .../tests/php/tests/TestYoastIntegration.php | 58 +++++++++++++------ 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php index 99d6157f0..f4d43762f 100644 --- a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php +++ b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php @@ -39,7 +39,7 @@ public function register() { // Modify API response to optimise payload by removing the yoast_head and yoast_json_head where not needed. // Embedded data is not added yet on rest_prepare_{$this->post_type}. - add_filter( 'rest_pre_echo_response' , [ $this, 'optimise_yoast_payload' ], 10, 3 ); + add_filter( 'rest_pre_echo_response', [ $this, 'optimise_yoast_payload' ], 10, 3 ); } /** @@ -338,13 +338,14 @@ function ( $presenters ) { * @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. + * @param boolean $embed Whether the response should include embedded data. * * @return array Modified response data. */ public function optimise_yoast_payload( $result, $server, $request, $embed = false ) { - $embed = $embed ?: rest_parse_embed_param( $_GET['_embed'] ?? false ); - + $embed = $embed ? $embed : filter_var( wp_unslash( $_GET['_embed'] ?? false ), FILTER_VALIDATE_BOOLEAN ); + if ( ! $embed || empty( $request->get_param( 'optimizeYoastPayload' ) ) ) { return $result; } @@ -394,9 +395,9 @@ protected function optimise_yoast_payload_for_taxonomy( &$taxonomy_groups, $requ 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'] ); + $param = 'category' === $term_obj['taxonomy'] ? + $request->get_param( 'category' ) ?? $request->get_param( 'categories' ) : + $request->get_param( $term_obj['taxonomy'] ); } if ( $first_post && ! empty( $param ) ) { @@ -438,7 +439,7 @@ protected function optimise_yoast_payload_for_author( &$authors, $request, $firs $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 ) ) { + if ( ! in_array( $author['slug'], $param, true ) && ! in_array( $author['id'], $param, true ) ) { unset( $author['yoast_head'], $author['yoast_head_json'] ); } } else { diff --git a/wp/headless-wp/tests/php/tests/TestYoastIntegration.php b/wp/headless-wp/tests/php/tests/TestYoastIntegration.php index 225252d98..3c91bad0b 100644 --- a/wp/headless-wp/tests/php/tests/TestYoastIntegration.php +++ b/wp/headless-wp/tests/php/tests/TestYoastIntegration.php @@ -16,7 +16,7 @@ * Covers the test for the Yoast integration */ class TestYoastIntegration extends WP_Test_REST_TestCase { -/** + /** * The YoastSEO instance * * @var YoastSEO @@ -70,8 +70,18 @@ public function set_up() { * Create posts for testing */ protected function create_posts() { - $this->category_id = $this->factory()->term->create( [ 'taxonomy' => 'category', 'slug' => 'test-category' ] ); - $this->tag_id = $this->factory()->term->create( [ 'taxonomy' => 'post_tag', 'slug' => 'test-post-tag' ] ); + $this->category_id = $this->factory()->term->create( + [ + 'taxonomy' => 'category', + 'slug' => 'test-category', + ] + ); + $this->tag_id = $this->factory()->term->create( + [ + 'taxonomy' => 'post_tag', + 'slug' => 'test-post-tag', + ] + ); $this->author_id = $this->factory()->user->create( [ 'role' => 'editor', @@ -82,20 +92,34 @@ protected function create_posts() { ] ); - $random_category_id = $this->factory()->term->create( [ 'taxonomy' => 'category', 'slug' => 'random-category' ] ); - $random_tag_id = $this->factory()->term->create( [ 'taxonomy' => 'post_tag', 'slug' => 'random-post-tag' ] ); + $random_category_id = $this->factory()->term->create( + [ + 'taxonomy' => 'category', + 'slug' => 'random-category', + ] + ); + $random_tag_id = $this->factory()->term->create( + [ + 'taxonomy' => 'post_tag', + 'slug' => 'random-post-tag', + ] + ); - $post_1 = $this->factory()->post->create_and_get( [ - 'post_type' => 'post', - 'post_status' => 'publish', - 'post_author' => $this->author_id, - ]); + $post_1 = $this->factory()->post->create_and_get( + [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_author' => $this->author_id, + ] + ); - $post_2 = $this->factory()->post->create_and_get( [ - 'post_type' => 'post', - 'post_status' => 'publish', - 'post_author' => $this->author_id, - ]); + $post_2 = $this->factory()->post->create_and_get( + [ + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_author' => $this->author_id, + ] + ); wp_set_post_terms( $post_1->ID, [ $this->category_id, $random_category_id ], 'category' ); wp_set_post_terms( $post_2->ID, [ $this->category_id, $random_category_id ], 'category' ); @@ -109,7 +133,7 @@ protected function create_posts() { * @return void */ public function test_optimise_yoast_payload() { - + // Perform a REST API request for the posts by category. $result_category = $this->get_posts_by_with_optimised_response( 'categories', $this->category_id ); $this->assert_yoast_head_in_response( $result_category ); @@ -179,7 +203,7 @@ protected function assert_yoast_head_in_response( $result ) { */ protected function assert_embedded_item( $embedded_obj, $type, $first_post, $id = null ) { - foreach ( $embedded_obj[ $type ] as $group) { + foreach ( $embedded_obj[ $type ] as $group ) { $items = 'wp:term' !== $type ? [ $group ] : $group; From 078469742aa7f68dc5e0da26e003b6540fc3ccfb Mon Sep 17 00:00:00 2001 From: Lucy Tomas Date: Tue, 22 Jul 2025 10:07:08 +0200 Subject: [PATCH 10/13] fix: tests with manual data --- .../tests/php/tests/TestYoastIntegration.php | 360 ++++++++++++------ 1 file changed, 237 insertions(+), 123 deletions(-) diff --git a/wp/headless-wp/tests/php/tests/TestYoastIntegration.php b/wp/headless-wp/tests/php/tests/TestYoastIntegration.php index 3c91bad0b..687cdc444 100644 --- a/wp/headless-wp/tests/php/tests/TestYoastIntegration.php +++ b/wp/headless-wp/tests/php/tests/TestYoastIntegration.php @@ -31,25 +31,11 @@ class TestYoastIntegration extends WP_Test_REST_TestCase { protected static $rest_server; /** - * The category id + * Test data IDs * - * @var int + * @var array */ - protected $category_id; - - /** - * The tag id - * - * @var int - */ - protected $tag_id; - - /** - * The author id - * - * @var int - */ - protected $author_id; + protected $test_data = []; /** * Sets up the Test class @@ -59,160 +45,288 @@ class TestYoastIntegration extends WP_Test_REST_TestCase { public function set_up() { parent::set_up(); + // Set the correct HeadstartWP REST API access option to 'none' ( publicly accessible ) + update_option( 'tenup_restrict_rest_api', 'none' ); + $this->yoast_seo = new YoastSEO(); $this->yoast_seo->register(); self::$rest_server = rest_get_server(); - $this->create_posts(); + + $this->test_data = []; + $this->set_test_data(); } /** - * Create posts for testing + * Set test data for all scenarios. + * Using string IDs to match real REST API responses. */ - protected function create_posts() { - $this->category_id = $this->factory()->term->create( - [ - 'taxonomy' => 'category', - 'slug' => 'test-category', - ] - ); - $this->tag_id = $this->factory()->term->create( - [ - 'taxonomy' => 'post_tag', - 'slug' => 'test-post-tag', - ] - ); - $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', - ] - ); + protected function set_test_data() { + $this->test_data = [ + 'jane_author' => '6', + 'other_author' => '7', + 'news_category' => '8', + 'other_category' => '9', + 'post_1' => '29', + 'post_2' => '30', + 'post_3' => '31', + ]; + } - $random_category_id = $this->factory()->term->create( + /** + * Manually construct REST API response format for testing Yoast optimization + * + * @return array + */ + protected function create_manual_rest_api_data() { + return [ [ - 'taxonomy' => 'category', - 'slug' => 'random-category', - ] - ); - $random_tag_id = $this->factory()->term->create( + 'id' => $this->test_data[ 'post_1' ], + 'title' => [ 'rendered' => 'First Post by Jane' ], + 'author' => $this->test_data[ 'jane_author' ], + 'yoast_head' => 'First Post by Jane', + 'yoast_head_json' => [ 'title' => 'First Post by Jane' ], + '_embedded' => [ + 'author' => [ + [ + 'id' => $this->test_data[ 'jane_author' ], + 'name' => 'Jane Author', + 'slug' => 'jane-author', + 'yoast_head' => 'Jane Author', + 'yoast_head_json' => [ 'title' => 'Jane Author' ], + ] + ], + 'wp:term' => [ + [ + [ + 'id' => $this->test_data[ 'news_category' ], + 'name' => 'News Category', + 'slug' => 'news-category', + 'taxonomy' => 'category', + 'yoast_head' => 'News Category', + 'yoast_head_json' => [ 'title' => 'News Category' ], + ] + ] + ] + ] + ], [ - 'taxonomy' => 'post_tag', - 'slug' => 'random-post-tag', + 'id' => $this->test_data[ 'post_2' ], + 'title' => [ 'rendered' => 'Second Post by Jane' ], + 'author' => $this->test_data[ 'jane_author' ], + 'yoast_head' => 'Second Post by Jane', + 'yoast_head_json' => [ 'title' => 'Second Post by Jane' ], + '_embedded' => [ + 'author' => [ + [ + 'id' => $this->test_data[ 'jane_author' ], + 'name' => 'Jane Author', + 'slug' => 'jane-author', + 'yoast_head' => 'Jane Author', + 'yoast_head_json' => [ 'title' => 'Jane Author' ], + ] + ], + 'wp:term' => [ + [ + [ + 'id' => $this->test_data[ 'news_category' ], + 'name' => 'News Category', + 'slug' => 'news-category', + 'taxonomy' => 'category', + 'yoast_head' => 'News Category', + 'yoast_head_json' => [ 'title' => 'News Category' ], + ] + ] + ] + ] ] - ); + ]; + } - $post_1 = $this->factory()->post->create_and_get( - [ - 'post_type' => 'post', - 'post_status' => 'publish', - 'post_author' => $this->author_id, - ] - ); + /** + * Single post query + * Only the queried post should have yoast metadata + */ + public function test_single_post_query() { + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->test_data[ 'post_1' ] ); + $request->set_param( 'optimizeYoastPayload', true ); + $request->set_param( '_embed', true ); - $post_2 = $this->factory()->post->create_and_get( - [ - 'post_type' => 'post', - 'post_status' => 'publish', - 'post_author' => $this->author_id, - ] - ); + $manual_data = $this->create_manual_rest_api_data(); + $data_for_optimization = [ $manual_data[0] ]; + + $optimized_data = $this->yoast_seo->optimise_yoast_payload( $data_for_optimization, self::$rest_server, $request, true ); + $optimized_post = $optimized_data[0]; - wp_set_post_terms( $post_1->ID, [ $this->category_id, $random_category_id ], 'category' ); - wp_set_post_terms( $post_2->ID, [ $this->category_id, $random_category_id ], 'category' ); - wp_set_post_terms( $post_1->ID, [ $this->tag_id, $random_tag_id ], 'post_tag' ); - wp_set_post_terms( $post_2->ID, [ $this->tag_id, $random_tag_id ], 'post_tag' ); + // The single post should have yoast_head + $this->assertArrayHasKey( 'yoast_head', $optimized_post, 'Single queried post should have yoast_head' ); + + // Embedded terms and authors should NOT have yoast_head since no specific term/author was queried + if ( isset( $optimized_post[ '_embedded' ] ) ) { + $this->assert_no_yoast_in_embedded( $optimized_post[ '_embedded' ], 'No embedded items should have yoast_head for single post query' ); + } } /** - * Tests optimising the Yoast SEO payload in REST API responses. - * - * @return void + * Posts by category + * Only first post and queried category should have yoast metadata */ - public function test_optimise_yoast_payload() { + public function test_posts_by_category() { + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'categories', $this->test_data[ 'news_category' ] ); + $request->set_param( 'per_page', 10 ); + $request->set_param( 'optimizeYoastPayload', true ); + $request->set_param( '_embed', true ); + + $data = $this->create_manual_rest_api_data(); + $optimized_data = $this->yoast_seo->optimise_yoast_payload( $data, self::$rest_server, $request, true ); - // Perform a REST API request for the posts by category. - $result_category = $this->get_posts_by_with_optimised_response( 'categories', $this->category_id ); - $this->assert_yoast_head_in_response( $result_category ); + $this->assertGreaterThanOrEqual( 2, count( $optimized_data ), 'Should return at least 2 posts' ); - // Perform a REST API request for the posts by tag. - // $result_tag = $this->get_posts_by_with_optimised_response( 'tags', $this->tag_id ); - // $this->assert_yoast_head_in_response( $result_tag ); + // First post should have yoast_head + $this->assertArrayHasKey( 'yoast_head', $optimized_data[0], 'First post should have yoast_head' ); - // Perform a REST API request for the posts by author. - // $result_author = $this->get_posts_by_with_optimised_response( 'author', $this->author_id ); - // $this->assert_yoast_head_in_response( $$result_author ); + // Subsequent posts should NOT have yoast_head + for ( $i = 1; $i < count( $optimized_data ); $i++ ) { + $this->assertArrayNotHasKey( 'yoast_head', $optimized_data[$i], "Post {$i} should not have yoast_head" ); + } + + // Check embedded terms - only the queried category should have yoast_head + if ( isset( $optimized_data[0][ '_embedded' ][ 'wp:term' ] ) ) { + $this->assert_yoast_in_term( $optimized_data[0][ '_embedded' ][ 'wp:term' ], $this->test_data[ 'news_category' ], 'category' ); + } } /** - * 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 + * Posts by author + * Only first post and queried author should have yoast metadata */ - protected function get_posts_by_with_optimised_response( $param, $value ) { - + public function test_posts_by_author() { $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); - $request->set_param( $param, $value ); + $request->set_param( 'author', $this->test_data[ 'jane_author' ] ); + $request->set_param( 'per_page', 10 ); $request->set_param( 'optimizeYoastPayload', true ); $request->set_param( '_embed', true ); - $response = rest_do_request( $request ); - $data = self::$rest_server->response_to_data( $response, true ); + $data = $this->create_manual_rest_api_data(); + $optimized_data = $this->yoast_seo->optimise_yoast_payload( $data, self::$rest_server, $request, true ); + + $this->assertGreaterThanOrEqual( 2, count( $optimized_data ), 'Should return at least 2 posts' ); - $this->assertGreaterThanOrEqual( 2, count( $data ), 'There should be at least two posts returned.' ); + // First post should have yoast_head + $this->assertArrayHasKey( 'yoast_head', $optimized_data[0], 'First post should have yoast_head' ); - return $this->yoast_seo->optimise_yoast_payload( $data, self::$rest_server, $request, true ); + // Subsequent posts should NOT have yoast_head + for ( $i = 1; $i < count( $optimized_data ); $i++ ) { + $this->assertArrayNotHasKey( 'yoast_head', $optimized_data[$i], "Post {$i} should not have yoast_head" ); + } + + // Check embedded authors - only the queried author should have yoast_head + if ( isset( $optimized_data[0][ '_embedded' ][ 'author' ] ) ) { + $author = $optimized_data[0][ '_embedded' ][ 'author' ][0]; + if ( $author[ 'id' ] === $this->test_data[ 'jane_author' ] ) { + $this->assertArrayHasKey( 'yoast_head', $author, 'Queried author should have yoast_head' ); + } else { + $this->assertArrayNotHasKey( 'yoast_head', $author, 'Non-queried author should not have yoast_head' ); + } + } } /** - * Asserts the presence of yoast_head in the response for each post. - * - * @param array $result The response data containing posts. - * @return void + * Posts by category and author + * Only first post, queried category, and queried author should have yoast metadata */ - protected function assert_yoast_head_in_response( $result ) { - $first_post = true; + public function test_posts_by_category_and_author() { + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'categories', $this->test_data[ 'news_category' ] ); + $request->set_param( 'author', $this->test_data[ 'jane_author' ] ); + $request->set_param( 'per_page', 10 ); + $request->set_param( 'optimizeYoastPayload', true ); + $request->set_param( '_embed', true ); + + $data = $this->create_manual_rest_api_data(); + $optimized_data = $this->yoast_seo->optimise_yoast_payload( $data, self::$rest_server, $request, true ); - foreach ( $result as $post ) { + $this->assertGreaterThanOrEqual( 1, count( $optimized_data ), 'Should return at least 1 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.' ); + // First post should have yoast_head + $this->assertArrayHasKey( 'yoast_head', $optimized_data[0], 'First post should have yoast_head' ); - $this->assert_embedded_item( $post['_embedded'], 'wp:term', $first_post, $this->category_id ); - $this->assert_embedded_item( $post['_embedded'], 'author', $first_post, null ); + // Subsequent posts should NOT have yoast_head + for ( $i = 1; $i < count( $optimized_data ); $i++ ) { + $this->assertArrayNotHasKey( 'yoast_head', $optimized_data[$i], "Post {$i} should not have yoast_head" ); + } - $first_post = false; + // Check embedded terms - only the queried category should have yoast_head + if ( isset( $optimized_data[0][ '_embedded' ][ 'wp:term' ] ) ) { + $this->assert_yoast_in_term( $optimized_data[0][ '_embedded' ][ 'wp:term' ], $this->test_data[ 'news_category' ], 'category' ); + } + + // Check embedded authors - only the queried author should have yoast_head + if ( isset( $optimized_data[0][ '_embedded' ][ 'author' ] ) ) { + $author = $optimized_data[0][ '_embedded' ][ 'author' ][0]; + if ( $author[ 'id' ] === $this->test_data[ 'jane_author' ] ) { + $this->assertArrayHasKey( 'yoast_head', $author, 'Queried author should have yoast_head' ); + } else { + $this->assertArrayNotHasKey( 'yoast_head', $author, 'Non-queried author should not have yoast_head' ); + } } } /** - * 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 + * Test that optimization only runs when optimizeYoastPayload parameter is true */ - protected function assert_embedded_item( $embedded_obj, $type, $first_post, $id = null ) { - - foreach ( $embedded_obj[ $type ] as $group ) { + public function test_optimization_only_runs_when_parameter_is_set() { + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'categories', $this->test_data[ 'news_category' ] ); + $request->set_param( 'per_page', 10 ); + $request->set_param( '_embed', true ); + + $data = $this->create_manual_rest_api_data(); + $optimized_data = $this->yoast_seo->optimise_yoast_payload( $data, self::$rest_server, $request, true ); - $items = 'wp:term' !== $type ? [ $group ] : $group; + $this->assertEquals( $data, $optimized_data, 'Data should remain unchanged when optimizeYoastPayload is not set' ); + } - foreach ( $items as $item ) { + /** + * Helper method to assert no yoast_head in any embedded items + * + * @param array $embedded The embedded data + * @param string $message The assertion message + */ + protected function assert_no_yoast_in_embedded( $embedded, $message ) { + foreach ( $embedded as $embed_type => $embed_data ) { + if ( is_array( $embed_data ) ) { + foreach ( $embed_data as $item_group ) { + $items = is_array( $item_group ) && isset( $item_group[0] ) ? $item_group : [ $item_group ]; + foreach ( $items as $item ) { + if ( is_array( $item ) ) { + $this->assertArrayNotHasKey( 'yoast_head', $item, $message . " (in {$embed_type})" ); + } + } + } + } + } + } - 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.' ); + /** + * Helper method to assert yoast_head exists only in specific term + * + * @param array $terms The terms data + * @param int $target_id The ID of the term that should have yoast_head + * @param string $taxonomy The taxonomy name + */ + protected function assert_yoast_in_term( $terms, $target_id, $taxonomy ) { + foreach ( $terms as $term_group ) { + if ( is_array( $term_group ) ) { + foreach ( $term_group as $term ) { + if ( isset( $term[ 'id' ] ) && isset( $term[ 'taxonomy' ] ) ) { + if ( $term[ 'id' ] === $target_id && $term[ 'taxonomy' ] === $taxonomy ) { + $this->assertArrayHasKey( 'yoast_head', $term, "Queried {$taxonomy} should have yoast_head" ); + } else { + $this->assertArrayNotHasKey( 'yoast_head', $term, "Non-queried {$taxonomy} should not have yoast_head" ); + } + } } } } From ee52eac66cd89eb6bb9988650de14e26626ebff9 Mon Sep 17 00:00:00 2001 From: Lucy Tomas Date: Tue, 22 Jul 2025 10:44:50 +0200 Subject: [PATCH 11/13] fix: lint --- .../tests/php/tests/TestYoastIntegration.php | 154 +++++++++--------- 1 file changed, 78 insertions(+), 76 deletions(-) diff --git a/wp/headless-wp/tests/php/tests/TestYoastIntegration.php b/wp/headless-wp/tests/php/tests/TestYoastIntegration.php index 687cdc444..c04b0226e 100644 --- a/wp/headless-wp/tests/php/tests/TestYoastIntegration.php +++ b/wp/headless-wp/tests/php/tests/TestYoastIntegration.php @@ -52,7 +52,6 @@ public function set_up() { $this->yoast_seo->register(); self::$rest_server = rest_get_server(); - $this->test_data = []; $this->set_test_data(); } @@ -63,13 +62,13 @@ public function set_up() { */ protected function set_test_data() { $this->test_data = [ - 'jane_author' => '6', - 'other_author' => '7', - 'news_category' => '8', + 'jane_author' => '6', + 'other_author' => '7', + 'news_category' => '8', 'other_category' => '9', - 'post_1' => '29', - 'post_2' => '30', - 'post_3' => '31', + 'post_1' => '29', + 'post_2' => '30', + 'post_3' => '31', ]; } @@ -81,65 +80,65 @@ protected function set_test_data() { protected function create_manual_rest_api_data() { return [ [ - 'id' => $this->test_data[ 'post_1' ], - 'title' => [ 'rendered' => 'First Post by Jane' ], - 'author' => $this->test_data[ 'jane_author' ], - 'yoast_head' => 'First Post by Jane', + 'id' => $this->test_data['post_1'], + 'title' => [ 'rendered' => 'First Post by Jane' ], + 'author' => $this->test_data['jane_author'], + 'yoast_head' => 'First Post by Jane', 'yoast_head_json' => [ 'title' => 'First Post by Jane' ], - '_embedded' => [ - 'author' => [ + '_embedded' => [ + 'author' => [ [ - 'id' => $this->test_data[ 'jane_author' ], - 'name' => 'Jane Author', - 'slug' => 'jane-author', - 'yoast_head' => 'Jane Author', + 'id' => $this->test_data['jane_author'], + 'name' => 'Jane Author', + 'slug' => 'jane-author', + 'yoast_head' => 'Jane Author', 'yoast_head_json' => [ 'title' => 'Jane Author' ], - ] + ], ], 'wp:term' => [ [ [ - 'id' => $this->test_data[ 'news_category' ], - 'name' => 'News Category', - 'slug' => 'news-category', - 'taxonomy' => 'category', - 'yoast_head' => 'News Category', + 'id' => $this->test_data['news_category'], + 'name' => 'News Category', + 'slug' => 'news-category', + 'taxonomy' => 'category', + 'yoast_head' => 'News Category', 'yoast_head_json' => [ 'title' => 'News Category' ], - ] - ] - ] - ] + ], + ], + ], + ], ], [ - 'id' => $this->test_data[ 'post_2' ], - 'title' => [ 'rendered' => 'Second Post by Jane' ], - 'author' => $this->test_data[ 'jane_author' ], - 'yoast_head' => 'Second Post by Jane', + 'id' => $this->test_data['post_2'], + 'title' => [ 'rendered' => 'Second Post by Jane' ], + 'author' => $this->test_data['jane_author'], + 'yoast_head' => 'Second Post by Jane', 'yoast_head_json' => [ 'title' => 'Second Post by Jane' ], - '_embedded' => [ - 'author' => [ + '_embedded' => [ + 'author' => [ [ - 'id' => $this->test_data[ 'jane_author' ], - 'name' => 'Jane Author', - 'slug' => 'jane-author', - 'yoast_head' => 'Jane Author', + 'id' => $this->test_data['jane_author'], + 'name' => 'Jane Author', + 'slug' => 'jane-author', + 'yoast_head' => 'Jane Author', 'yoast_head_json' => [ 'title' => 'Jane Author' ], - ] + ], ], 'wp:term' => [ [ [ - 'id' => $this->test_data[ 'news_category' ], - 'name' => 'News Category', - 'slug' => 'news-category', - 'taxonomy' => 'category', - 'yoast_head' => 'News Category', + 'id' => $this->test_data['news_category'], + 'name' => 'News Category', + 'slug' => 'news-category', + 'taxonomy' => 'category', + 'yoast_head' => 'News Category', 'yoast_head_json' => [ 'title' => 'News Category' ], - ] - ] - ] - ] - ] + ], + ], + ], + ], + ], ]; } @@ -148,7 +147,7 @@ protected function create_manual_rest_api_data() { * Only the queried post should have yoast metadata */ public function test_single_post_query() { - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->test_data[ 'post_1' ] ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $this->test_data['post_1'] ); $request->set_param( 'optimizeYoastPayload', true ); $request->set_param( '_embed', true ); @@ -160,10 +159,10 @@ public function test_single_post_query() { // The single post should have yoast_head $this->assertArrayHasKey( 'yoast_head', $optimized_post, 'Single queried post should have yoast_head' ); - + // Embedded terms and authors should NOT have yoast_head since no specific term/author was queried - if ( isset( $optimized_post[ '_embedded' ] ) ) { - $this->assert_no_yoast_in_embedded( $optimized_post[ '_embedded' ], 'No embedded items should have yoast_head for single post query' ); + if ( isset( $optimized_post['_embedded'] ) ) { + $this->assert_no_yoast_in_embedded( $optimized_post['_embedded'], 'No embedded items should have yoast_head for single post query' ); } } @@ -173,7 +172,7 @@ public function test_single_post_query() { */ public function test_posts_by_category() { $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); - $request->set_param( 'categories', $this->test_data[ 'news_category' ] ); + $request->set_param( 'categories', $this->test_data['news_category'] ); $request->set_param( 'per_page', 10 ); $request->set_param( 'optimizeYoastPayload', true ); $request->set_param( '_embed', true ); @@ -187,13 +186,14 @@ public function test_posts_by_category() { $this->assertArrayHasKey( 'yoast_head', $optimized_data[0], 'First post should have yoast_head' ); // Subsequent posts should NOT have yoast_head - for ( $i = 1; $i < count( $optimized_data ); $i++ ) { - $this->assertArrayNotHasKey( 'yoast_head', $optimized_data[$i], "Post {$i} should not have yoast_head" ); + $post_count = count( $optimized_data ); + for ( $i = 1; $i < $post_count; $i++ ) { + $this->assertArrayNotHasKey( 'yoast_head', $optimized_data[ $i ], "Post {$i} should not have yoast_head" ); } // Check embedded terms - only the queried category should have yoast_head - if ( isset( $optimized_data[0][ '_embedded' ][ 'wp:term' ] ) ) { - $this->assert_yoast_in_term( $optimized_data[0][ '_embedded' ][ 'wp:term' ], $this->test_data[ 'news_category' ], 'category' ); + if ( isset( $optimized_data[0]['_embedded']['wp:term'] ) ) { + $this->assert_yoast_in_term( $optimized_data[0]['_embedded']['wp:term'], $this->test_data['news_category'], 'category' ); } } @@ -203,7 +203,7 @@ public function test_posts_by_category() { */ public function test_posts_by_author() { $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); - $request->set_param( 'author', $this->test_data[ 'jane_author' ] ); + $request->set_param( 'author', $this->test_data['jane_author'] ); $request->set_param( 'per_page', 10 ); $request->set_param( 'optimizeYoastPayload', true ); $request->set_param( '_embed', true ); @@ -217,14 +217,15 @@ public function test_posts_by_author() { $this->assertArrayHasKey( 'yoast_head', $optimized_data[0], 'First post should have yoast_head' ); // Subsequent posts should NOT have yoast_head - for ( $i = 1; $i < count( $optimized_data ); $i++ ) { - $this->assertArrayNotHasKey( 'yoast_head', $optimized_data[$i], "Post {$i} should not have yoast_head" ); + $post_count = count( $optimized_data ); + for ( $i = 1; $i < $post_count; $i++ ) { + $this->assertArrayNotHasKey( 'yoast_head', $optimized_data[ $i ], "Post {$i} should not have yoast_head" ); } // Check embedded authors - only the queried author should have yoast_head - if ( isset( $optimized_data[0][ '_embedded' ][ 'author' ] ) ) { - $author = $optimized_data[0][ '_embedded' ][ 'author' ][0]; - if ( $author[ 'id' ] === $this->test_data[ 'jane_author' ] ) { + if ( isset( $optimized_data[0]['_embedded']['author'] ) ) { + $author = $optimized_data[0]['_embedded']['author'][0]; + if ( $author['id'] === $this->test_data['jane_author'] ) { $this->assertArrayHasKey( 'yoast_head', $author, 'Queried author should have yoast_head' ); } else { $this->assertArrayNotHasKey( 'yoast_head', $author, 'Non-queried author should not have yoast_head' ); @@ -238,8 +239,8 @@ public function test_posts_by_author() { */ public function test_posts_by_category_and_author() { $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); - $request->set_param( 'categories', $this->test_data[ 'news_category' ] ); - $request->set_param( 'author', $this->test_data[ 'jane_author' ] ); + $request->set_param( 'categories', $this->test_data['news_category'] ); + $request->set_param( 'author', $this->test_data['jane_author'] ); $request->set_param( 'per_page', 10 ); $request->set_param( 'optimizeYoastPayload', true ); $request->set_param( '_embed', true ); @@ -253,19 +254,20 @@ public function test_posts_by_category_and_author() { $this->assertArrayHasKey( 'yoast_head', $optimized_data[0], 'First post should have yoast_head' ); // Subsequent posts should NOT have yoast_head - for ( $i = 1; $i < count( $optimized_data ); $i++ ) { - $this->assertArrayNotHasKey( 'yoast_head', $optimized_data[$i], "Post {$i} should not have yoast_head" ); + $post_count = count( $optimized_data ); + for ( $i = 1; $i < $post_count; $i++ ) { + $this->assertArrayNotHasKey( 'yoast_head', $optimized_data[ $i ], "Post {$i} should not have yoast_head" ); } // Check embedded terms - only the queried category should have yoast_head - if ( isset( $optimized_data[0][ '_embedded' ][ 'wp:term' ] ) ) { - $this->assert_yoast_in_term( $optimized_data[0][ '_embedded' ][ 'wp:term' ], $this->test_data[ 'news_category' ], 'category' ); + if ( isset( $optimized_data[0]['_embedded']['wp:term'] ) ) { + $this->assert_yoast_in_term( $optimized_data[0]['_embedded']['wp:term'], $this->test_data['news_category'], 'category' ); } // Check embedded authors - only the queried author should have yoast_head - if ( isset( $optimized_data[0][ '_embedded' ][ 'author' ] ) ) { - $author = $optimized_data[0][ '_embedded' ][ 'author' ][0]; - if ( $author[ 'id' ] === $this->test_data[ 'jane_author' ] ) { + if ( isset( $optimized_data[0]['_embedded']['author'] ) ) { + $author = $optimized_data[0]['_embedded']['author'][0]; + if ( $author['id'] === $this->test_data['jane_author'] ) { $this->assertArrayHasKey( 'yoast_head', $author, 'Queried author should have yoast_head' ); } else { $this->assertArrayNotHasKey( 'yoast_head', $author, 'Non-queried author should not have yoast_head' ); @@ -278,10 +280,10 @@ public function test_posts_by_category_and_author() { */ public function test_optimization_only_runs_when_parameter_is_set() { $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); - $request->set_param( 'categories', $this->test_data[ 'news_category' ] ); + $request->set_param( 'categories', $this->test_data['news_category'] ); $request->set_param( 'per_page', 10 ); $request->set_param( '_embed', true ); - + $data = $this->create_manual_rest_api_data(); $optimized_data = $this->yoast_seo->optimise_yoast_payload( $data, self::$rest_server, $request, true ); @@ -320,8 +322,8 @@ protected function assert_yoast_in_term( $terms, $target_id, $taxonomy ) { foreach ( $terms as $term_group ) { if ( is_array( $term_group ) ) { foreach ( $term_group as $term ) { - if ( isset( $term[ 'id' ] ) && isset( $term[ 'taxonomy' ] ) ) { - if ( $term[ 'id' ] === $target_id && $term[ 'taxonomy' ] === $taxonomy ) { + if ( isset( $term['id'] ) && isset( $term['taxonomy'] ) ) { + if ( $term['id'] === $target_id && $term['taxonomy'] === $taxonomy ) { $this->assertArrayHasKey( 'yoast_head', $term, "Queried {$taxonomy} should have yoast_head" ); } else { $this->assertArrayNotHasKey( 'yoast_head', $term, "Non-queried {$taxonomy} should not have yoast_head" ); From 246fed907cbae71a37765f8c3b5388661a39ffe0 Mon Sep 17 00:00:00 2001 From: Lucy Tomas Date: Tue, 22 Jul 2025 19:59:54 +0200 Subject: [PATCH 12/13] feat: set up (parent post data) embeddable to false --- .../classes/Integrations/YoastSEO.php | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php index f4d43762f..e92f73313 100644 --- a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php +++ b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php @@ -40,6 +40,9 @@ public function register() { // Modify API response to optimise payload by removing the yoast_head and yoast_json_head where not needed. // Embedded data is not added yet on rest_prepare_{$this->post_type}. add_filter( 'rest_pre_echo_response', [ $this, 'optimise_yoast_payload' ], 10, 3 ); + + // Filter 'up' link embeddable property to prevent parent pages from being embedded with yoast_head + add_action( 'rest_api_init', [ $this, 'filter_up_link_embeddable' ] ); } /** @@ -362,7 +365,7 @@ public function optimise_yoast_payload( $result, $server, $request, $embed = fal $this->optimise_yoast_payload_for_author( $post_obj['_embedded']['author'], $request, $first_post ); } - if ( ! $first_post ) { + if ( ! $first_post && empty( $request->get_param( 'slug' ) ) ) { unset( $post_obj['yoast_head'], $post_obj['yoast_head_json'] ); } @@ -449,4 +452,53 @@ protected function optimise_yoast_payload_for_author( &$authors, $request, $firs unset( $author ); } + + /** + * Registers filters to disable embeddable property for 'up' links on hierarchical post types + * + * This prevents parent pages from being embedded with their yoast_head data, + * reducing payload size. + */ + public function filter_up_link_embeddable() { + $post_types = get_post_types( [ 'show_in_rest' => true, 'hierarchical' => true ], 'names' ); + + foreach ( $post_types as $post_type ) { + add_filter( "rest_prepare_{$post_type}", [ $this, 'disable_up_link_embeddable' ], 10, 3 ); + } + } + + /** + * Disables embeddable property for 'up' link in REST API response + * + * @param \WP_REST_Response $response The response object. + * @param \WP_Post $post Post object. + * @param \WP_REST_Request $request Request object. + * + * @return \WP_REST_Response + */ + public function disable_up_link_embeddable( $response, $post, $request ) { + // Only modify if optimizeYoastPayload parameter is present + if ( empty( $request->get_param( 'optimizeYoastPayload' ) ) ) { + return $response; + } + + // Get the links from the response object + $links = $response->get_links(); + + // Check if 'up' link exists + if ( ! empty( $links['up'] ) ) { + // Remove existing 'up' links + $response->remove_link( 'up' ); + + // Re-add each 'up' link with embeddable set to false + foreach ( $links['up'] as $up_link ) { + $href = $up_link['href']; + + // Try passing embeddable as a direct attribute + $response->add_link( 'up', $href, array( 'embeddable' => false ) ); + } + } + + return $response; + } } From b166f5348f1e88b83d4b5e1d9711cbd7dc272b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADcholas=20Oliveira?= Date: Wed, 23 Jul 2025 10:26:41 -0300 Subject: [PATCH 13/13] fix: phpcs --- .changeset/ninety-lemons-care.md | 6 ++++++ .../headstartwp.config.js | 7 +++++++ .../headstartwp.config.js | 6 ++++++ .../wp-multisite-nextjs/headstartwp.config.js | 6 ++++++ projects/wp-nextjs-app/headstartwp.config.js | 1 + .../headstartwp.config.js | 4 ++++ .../headstartwp.config.js | 1 + .../includes/classes/Integrations/YoastSEO.php | 18 ++++++++++++------ 8 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 .changeset/ninety-lemons-care.md diff --git a/.changeset/ninety-lemons-care.md b/.changeset/ninety-lemons-care.md new file mode 100644 index 000000000..0f4d5a741 --- /dev/null +++ b/.changeset/ninety-lemons-care.md @@ -0,0 +1,6 @@ +--- +"@headstartwp/headstartwp": minor +"@headstartwp/core": minor +--- + +Introducing optimizeYoastPayload to reduce payload size when using the yoast integration diff --git a/projects/wp-multisite-i18n-nextjs/headstartwp.config.js b/projects/wp-multisite-i18n-nextjs/headstartwp.config.js index 519ffa128..f14b8c69c 100644 --- a/projects/wp-multisite-i18n-nextjs/headstartwp.config.js +++ b/projects/wp-multisite-i18n-nextjs/headstartwp.config.js @@ -45,4 +45,11 @@ module.exports = { useWordPressPlugin: true, }, ], + + integrations: { + yoastSEO: { + enable: true, + optimizeYoastPayload: true, + }, + }, }; diff --git a/projects/wp-multisite-nextjs-app/headstartwp.config.js b/projects/wp-multisite-nextjs-app/headstartwp.config.js index 6565f4309..942c32a37 100644 --- a/projects/wp-multisite-nextjs-app/headstartwp.config.js +++ b/projects/wp-multisite-nextjs-app/headstartwp.config.js @@ -22,4 +22,10 @@ module.exports = { hostUrl: 'http://js1.localhost:3000', }, ], + integrations: { + yoastSEO: { + enable: true, + optimizeYoastPayload: true, + }, + }, }; diff --git a/projects/wp-multisite-nextjs/headstartwp.config.js b/projects/wp-multisite-nextjs/headstartwp.config.js index 4b2a6d6ff..e5afd25be 100644 --- a/projects/wp-multisite-nextjs/headstartwp.config.js +++ b/projects/wp-multisite-nextjs/headstartwp.config.js @@ -32,4 +32,10 @@ module.exports = { useWordPressPlugin: true, }, ], + integrations: { + yoastSEO: { + enable: true, + optimizeYoastPayload: true, + }, + }, }; diff --git a/projects/wp-nextjs-app/headstartwp.config.js b/projects/wp-nextjs-app/headstartwp.config.js index 2587f6bec..3881cd5f5 100644 --- a/projects/wp-nextjs-app/headstartwp.config.js +++ b/projects/wp-nextjs-app/headstartwp.config.js @@ -15,6 +15,7 @@ module.exports = { integrations: { yoastSEO: { enable: true, + optimizeYoastPayload: true, }, }, }; diff --git a/projects/wp-polylang-nextjs-app/headstartwp.config.js b/projects/wp-polylang-nextjs-app/headstartwp.config.js index 3a23b5906..4484d713b 100644 --- a/projects/wp-polylang-nextjs-app/headstartwp.config.js +++ b/projects/wp-polylang-nextjs-app/headstartwp.config.js @@ -18,5 +18,9 @@ module.exports = { polylang: { enable: true, }, + yoastSEO: { + enable: true, + optimizeYoastPayload: true, + }, }, }; diff --git a/test-projects/wp-nextjs-universal-blocks/headstartwp.config.js b/test-projects/wp-nextjs-universal-blocks/headstartwp.config.js index 2587f6bec..3881cd5f5 100644 --- a/test-projects/wp-nextjs-universal-blocks/headstartwp.config.js +++ b/test-projects/wp-nextjs-universal-blocks/headstartwp.config.js @@ -15,6 +15,7 @@ module.exports = { integrations: { yoastSEO: { enable: true, + optimizeYoastPayload: true, }, }, }; diff --git a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php index e92f73313..6d9acd9ab 100644 --- a/wp/headless-wp/includes/classes/Integrations/YoastSEO.php +++ b/wp/headless-wp/includes/classes/Integrations/YoastSEO.php @@ -40,7 +40,7 @@ public function register() { // Modify API response to optimise payload by removing the yoast_head and yoast_json_head where not needed. // Embedded data is not added yet on rest_prepare_{$this->post_type}. add_filter( 'rest_pre_echo_response', [ $this, 'optimise_yoast_payload' ], 10, 3 ); - + // Filter 'up' link embeddable property to prevent parent pages from being embedded with yoast_head add_action( 'rest_api_init', [ $this, 'filter_up_link_embeddable' ] ); } @@ -455,12 +455,18 @@ protected function optimise_yoast_payload_for_author( &$authors, $request, $firs /** * Registers filters to disable embeddable property for 'up' links on hierarchical post types - * + * * This prevents parent pages from being embedded with their yoast_head data, * reducing payload size. */ public function filter_up_link_embeddable() { - $post_types = get_post_types( [ 'show_in_rest' => true, 'hierarchical' => true ], 'names' ); + $post_types = get_post_types( + [ + 'show_in_rest' => true, + 'hierarchical' => true, + ], + 'names' + ); foreach ( $post_types as $post_type ) { add_filter( "rest_prepare_{$post_type}", [ $this, 'disable_up_link_embeddable' ], 10, 3 ); @@ -489,13 +495,13 @@ public function disable_up_link_embeddable( $response, $post, $request ) { if ( ! empty( $links['up'] ) ) { // Remove existing 'up' links $response->remove_link( 'up' ); - + // Re-add each 'up' link with embeddable set to false foreach ( $links['up'] as $up_link ) { $href = $up_link['href']; - + // Try passing embeddable as a direct attribute - $response->add_link( 'up', $href, array( 'embeddable' => false ) ); + $response->add_link( 'up', $href, [ 'embeddable' => false ] ); } }