Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1ad0f80
Abstract the mechanics of the indexation process
pls78 Jun 23, 2026
06de1f6
Extract the indexing error content
pls78 Jun 23, 2026
8dbe879
Introduce an exception that will be thrown when an indexable fails to…
pls78 Jun 24, 2026
13ee4fe
Throw an exception and log when an indexable creation fails
pls78 Jun 24, 2026
008d384
Catch the `Indexing_Failed_Exception` and return an error with object…
pls78 Jun 24, 2026
fe23268
Let the command catch the `Indexing_Failed_Exception` and print to te…
pls78 Jun 24, 2026
46686a6
Add object id and type
pls78 Jun 24, 2026
4c0c934
Show to the user which object failed to be indexed
pls78 Jun 24, 2026
d93dc3b
Add test to check if the error message is correctly displayed
pls78 Jun 24, 2026
e061c12
Update unit tests
pls78 Jun 24, 2026
3ecba73
Lower error threshold
pls78 Jun 24, 2026
af5e18d
Merge trunk
leonidasmi Jul 8, 2026
1749b03
Potential fix for pull request finding
pls78 Jul 15, 2026
f57f1a9
Add missing stup
pls78 Jul 16, 2026
ea831bd
Add a WP_Error double
pls78 Jul 16, 2026
4e0937f
Fix test
pls78 Jul 16, 2026
9ad2594
Assign propTypes and defaultProps as we do in the FTC step
pls78 Jul 16, 2026
5f8be02
Fix docblock
pls78 Jul 16, 2026
f26bcd4
Handle the case when object_id and/or object_subtype are null
pls78 Jul 16, 2026
4b0b59e
Fix comment
pls78 Jul 16, 2026
6de21ae
Add test
pls78 Jul 16, 2026
a60f387
Handle the case when object_id and/or object_subtype are null
pls78 Jul 16, 2026
ab9f691
Fix the case of recursive indexables build
pls78 Jul 16, 2026
ac11e6a
Add test covering the new case
pls78 Jul 16, 2026
9c2936b
Merge branch 'trunk' into 943-seo-data-optimization-error-reporting-r…
pls78 Jul 16, 2026
ff01698
Lower error threshold
pls78 Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
"Yoast\\WP\\SEO\\Composer\\Actions::check_coding_standards"
],
"check-cs-thresholds": [
"@putenv YOASTCS_THRESHOLD_ERRORS=2386",
"@putenv YOASTCS_THRESHOLD_ERRORS=2378",
"@putenv YOASTCS_THRESHOLD_WARNINGS=254",
"Yoast\\WP\\SEO\\Composer\\Actions::check_cs_thresholds"
],
Expand Down
311 changes: 311 additions & 0 deletions packages/js/src/components/AbstractIndexation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
/* global yoastIndexingData */
import { Component, flushSync } from "@wordpress/element";
import PropTypes from "prop-types";
import { addHistoryState, removeSearchParam } from "../helpers/urlHelpers";
import RequestError from "../errors/RequestError";
import ParseError from "../errors/ParseError";

export const STATE = {
/**
* When the process has not started yet, or has been stopped manually.
*/
IDLE: "idle",
/**
* When the indexing process is in progress.
*/
IN_PROGRESS: "in_progress",
/**
* When an error has occurred during the indexing process that has stopped the process.
*/
ERRORED: "errored",
/**
* When the indexing process has finished.
*/
COMPLETED: "completed",
};

/* eslint-disable react/no-unused-state -- `error`/`firstTime` are set here but read by subclass render methods, unseen by this rule across files. */

/**
* Drives the indexing process: calls each indexing endpoint in turn and tracks progress.
*
* This base class holds the full request/cursor engine and state machine shared by every
* Indexation UI. Subclasses supply only the presentation by implementing `render()` (and the
* `render*` helpers it calls); they may override `getAlreadyIndexedStateName()` to change the
* state signalled when there is nothing left to index on mount.
*/
class AbstractIndexation extends Component {
/**
* Indexing constructor.
*
* @param {Object} props The properties.
*/
constructor( props ) {
super( props );

this.settings = yoastIndexingData;

this.state = {
state: STATE.IDLE,
processed: 0,
error: null,
amount: parseInt( this.settings.amount, 10 ),
firstTime: (
this.settings.firstTime === "1"
),
};

this.startIndexing = this.startIndexing.bind( this );
this.stopIndexing = this.stopIndexing.bind( this );
}

/**
* The state name signalled to `indexingStateCallback` when there is nothing left to index on mount.
*
* @returns {string} The state name.
*/
getAlreadyIndexedStateName() {
return "completed";
}

/**
* Does an indexing request.
*
* @param {string} url The url of the indexing that should be done.
* @param {string} nonce The WordPress nonce value for in the header.
*
* @returns {Promise} The request promise.
*/
async doIndexingRequest( url, nonce ) {
const response = await fetch( url, {
method: "POST",
headers: {
"X-WP-Nonce": nonce,
},
} );

const responseText = await response.text();

let data;
try {
/*
* Sometimes, in case of a fatal error, or if WP_DEBUG is on and a DB query fails,
* non-JSON is dumped into the HTTP response body, so account for that here.
*/
data = JSON.parse( responseText );
} catch ( error ) {
throw new ParseError( "Error parsing the response to JSON.", responseText );
}

// Throw an error when the response's status code is not in the 200-299 range.
if ( ! response.ok ) {
const errorData = data.data || {};
throw new RequestError( data.message, url, "POST", response.status, errorData.stackTrace, {
objectId: errorData.object_id,
objectType: errorData.object_type,
} );
}

return data;
}

/**
* Does any registered indexing action *before* a call to an index endpoint.
*
* @param {string} endpoint The endpoint that has been called.
*
* @returns {Promise<void>} An empty promise.
*/
async doPreIndexingAction( endpoint ) {
if ( typeof this.props.preIndexingActions[ endpoint ] === "function" ) {
await this.props.preIndexingActions[ endpoint ]( this.settings );
}
}

/**
* Does any registered indexing action *after* a call to an index endpoint.
*
* @param {string} endpoint The endpoint that has been called.
* @param {Object} response The response of the call to the endpoint.
*
* @returns {Promise<void>} An empty promise.
*/
async doPostIndexingAction( endpoint, response ) {
if ( typeof this.props.indexingActions[ endpoint ] === "function" ) {
await this.props.indexingActions[ endpoint ]( response.objects, this.settings );
}
}

/**
* Does the indexing of a given endpoint.
*
* @param {string} endpoint The endpoint.
*
* @returns {Promise} The indexing promise.
*/
async doIndexing( endpoint ) {
let url = this.settings.restApi.root + this.settings.restApi.indexing_endpoints[ endpoint ];

while ( this.isState( STATE.IN_PROGRESS ) && url !== false ) {
try {
await this.doPreIndexingAction( endpoint );
const response = await this.doIndexingRequest( url, this.settings.restApi.nonce );
await this.doPostIndexingAction( endpoint, response );

flushSync( () => {
this.setState( previousState => (
{
processed: previousState.processed + response.objects.length,
firstTime: false,
}
) );
} );

url = response.next_url;
} catch ( error ) {
flushSync( () => {
this.setState( {
state: STATE.ERRORED,
error: error,
firstTime: false,
} );
} );
}
}
}

/**
* Indexes the objects by calling each indexing endpoint in turn.
*
* @returns {Promise<void>} The indexing promise.
*/
async index() {
for ( const endpoint of Object.keys( this.settings.restApi.indexing_endpoints ) ) {
await this.doIndexing( endpoint );
}
/*
* Set the indexing process as completed only when there is no error
* and the user has not stopped the process manually.
*/
if ( ! this.isState( STATE.ERRORED ) && ! this.isState( STATE.IDLE ) ) {
this.completeIndexing();
}
}

/**
* Starts the indexing process.
*
* @returns {Promise<void>} The start indexing promise.
*/
async startIndexing() {
/*
* Since `setState` is asynchronous in nature, we have to supply a callback
* to make sure the state is correctly set before trying to call the first
* endpoint.
*/
this.setState( { processed: 0, state: STATE.IN_PROGRESS }, this.index );
}

/**
* Sets the state of the indexing process to completed.
*
* @returns {void}
*/
completeIndexing() {
this.setState( { state: STATE.COMPLETED } );
}

/**
* Stops the indexing process.
*
* @returns {void}
*/
stopIndexing() {
this.setState( previousState => (
{
state: STATE.IDLE,
processed: 0,
amount: previousState.amount - previousState.processed,
}
) );
}

/**
* Start indexation on mount, when redirected from the "Start SEO data optimization" button in the dashboard notification.
*
* @returns {void}
*/
componentDidMount() {
if ( this.settings.disabled ) {
return;
}

this.props.indexingStateCallback( this.state.amount === 0 ? this.getAlreadyIndexedStateName() : this.state.state );

const shouldStart = new URLSearchParams( window.location.search ).get( "start-indexation" ) === "true";

if ( shouldStart ) {
const currentURL = removeSearchParam( window.location.href, "start-indexation" );
addHistoryState( null, document.title, currentURL );

this.startIndexing();
}
}

/**
* Signals state changes to an optional callback function.
*
* @param {Object} _prevProps The previous props, unused in the current implementation.
* @param {Object} prevState The previous state.
*
* @returns {void}
*/
componentDidUpdate( _prevProps, prevState ) {
if ( this.state.state !== prevState.state ) {
this.props.indexingStateCallback( this.state.state );
}
}

/**
* If the current state of the indexing process is the given state.
*
* @param {STATE.IDLE|STATE.ERRORED|STATE.IN_PROGRESS|STATE.COMPLETED} state The state value to check against.
*
* @returns {boolean} If the current state of the indexing process is the given state.
*/
isState( state ) {
return this.state.state === state;
}
}

/* eslint-enable react/no-unused-state */

/**
* The prop types shared by every Indexation component.
*
* Exported as a plain object (rather than read back off `AbstractIndexation.propTypes`) so
* subclasses can extend it without tripping `react/forbid-foreign-prop-types`.
*
* @type {Object}
*/
export const indexationPropTypes = {
indexingActions: PropTypes.object,
preIndexingActions: PropTypes.object,
indexingStateCallback: PropTypes.func,
};

/**
* The default props shared by every Indexation component.
*
* @type {Object}
*/
export const indexationDefaultProps = {
indexingActions: {},
preIndexingActions: {},
indexingStateCallback: () => {},
};

AbstractIndexation.propTypes = indexationPropTypes;
AbstractIndexation.defaultProps = indexationDefaultProps;

export default AbstractIndexation;
Loading
Loading