Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/yoast payload optimize #727

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
7 changes: 7 additions & 0 deletions packages/core/src/data/strategies/AbstractFetchStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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<P> = {}): Partial<P> {
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),
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/data/strategies/PostsArchiveFetchStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ export class PostsArchiveFetchStrategy<

locale: string = '';

optimizeYoastPayload: boolean = false;

getDefaultEndpoint(): string {
return endpoints.posts;
}
Expand All @@ -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') {
Expand Down Expand Up @@ -457,6 +461,12 @@ export class PostsArchiveFetchStrategy<
}
}

if (this.optimizeYoastPayload) {
finalUrl = addQueryArgs(finalUrl, {
optimizeYoastPayload: true,
});
}

return super.fetcher(finalUrl, params, options);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export class SearchNativeFetchStrategy<

locale: string = '';

optimizeYoastPayload: boolean = false;

getDefaultEndpoint() {
return endpoints.search;
}
Expand All @@ -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<P>;
}

Expand Down Expand Up @@ -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,
Expand Down
14 changes: 13 additions & 1 deletion packages/core/src/data/strategies/SinglePostFetchStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
removeSourceUrl,
NotFoundError,
getSiteBySourceUrl,
addQueryArgs,
} from '../../utils';
import { PostEntity } from '../types';
import { postMatchers } from '../utils/matchers';
Expand Down Expand Up @@ -90,6 +91,8 @@ export class SinglePostFetchStrategy<

shouldCheckCurrentPathAgainstPostLink: boolean = true;

optimizeYoastPayload: boolean = false;

getDefaultEndpoint(): string {
return endpoints.posts;
}
Expand All @@ -108,6 +111,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);

Expand Down Expand Up @@ -274,6 +279,7 @@ export class SinglePostFetchStrategy<
*/
async fetcher(url: string, params: P, options: Partial<FetchOptions> = {}) {
const { burstCache = false } = options;
let finalUrl = url;

if (params.authToken) {
options.previewToken = params.authToken;
Expand Down Expand Up @@ -304,7 +310,13 @@ export class SinglePostFetchStrategy<
}

try {
const result = await super.fetcher(url, params, options);
if (this.optimizeYoastPayload) {
finalUrl = addQueryArgs(finalUrl, {
optimizeYoastPayload: true,
});
}

const result = await super.fetcher(finalUrl, params, options);

return result;
} catch (e) {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

Expand Down
1 change: 1 addition & 0 deletions projects/wp-nextjs/headstartwp.config.client.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ module.exports = {
integrations: {
yoastSEO: {
enable: true,
optimizeYoastPayload: true,
},
polylang: {
enable: process?.env?.NEXT_PUBLIC_ENABLE_POLYLANG_INTEGRATION === 'true',
Expand Down
127 changes: 127 additions & 0 deletions wp/headless-wp/includes/classes/Integrations/YoastSEO.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ 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 );
}

/**
Expand Down Expand Up @@ -322,4 +326,127 @@ 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.
* @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 ? $embed : filter_var( wp_unslash( $_GET['_embed'] ?? false ), FILTER_VALIDATE_BOOLEAN );

if ( ! $embed || empty( $request->get_param( 'optimizeYoastPayload' ) ) ) {
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 = 'category' === $term_obj['taxonomy'] ?
$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 );
}
}
Loading
Loading