share( Services\PostAnalyzer::class, static fn( Container $c ) => new Services\PostAnalyzer( $c->get( Options::class ) ) ); } public function boot( Container $container ): void { add_action( 'rest_api_init', static function () use ( $container ): void { ( new Rest\ContentController( $container->get( Services\PostAnalyzer::class ) ) )->register_routes(); } ); // The library counts are cached for five minutes, which is fine for // drift but wrong immediately after an edit: somebody who just filled // in a meta description should see the "missing" count go down. foreach ( array( 'save_post', 'deleted_post', 'untrashed_post' ) as $hook ) { add_action( $hook, array( Services\Library::class, 'flush' ) ); } /* * A change to a scored field invalidates the analysis. * * wp_after_insert_post below covers a post being saved, but the drawer * and the AI apply write post meta directly — and post meta does not * move post_modified, which is the only thing staleness was judged on. * So an AI-written title was stored, shown in the field, and then * audited against the title it had replaced: the checks kept failing, * the score kept still, and nothing the reader did changed either. * * The analysis is marked rather than recomputed. Recomputing here would * run the whole audit twice for one apply, which writes two keys, and * would do it inside the request that is trying to save. Marking lets * the next read recompute, which is exactly when the answer is wanted, * and lets the list go on showing the previous score with the "out of * date" flag it already renders. */ foreach ( array( 'added_post_meta', 'updated_post_meta', 'deleted_post_meta' ) as $meta_hook ) { add_action( $meta_hook, static function ( $meta_id, $object_id, $meta_key ): void { unset( $meta_id ); if ( ! in_array( (string) $meta_key, Services\PostAnalyzer::SCORED_META, true ) ) { return; } Services\PostAnalyzer::mark_dirty( (int) $object_id ); Services\Library::flush(); }, 10, 3 ); } // Re-analyze on save. Until now nothing did: the editor sidebar // analyzed while you typed, but a page edited any other way — quick // edit, the block editor without our panel, an importer, a stock sync // — kept its old scores, and the dashboard's "pages needing attention" // ranks by exactly those stored scores. The user-visible symptom was a // dashboard that lists the same pages for ever, however often you fix // them. wp_after_insert_post rather than save_post, so terms and meta // are already saved and the analysis sees the post as published. add_action( 'wp_after_insert_post', static function ( int $post_id, \WP_Post $post, bool $update ) use ( $container ): void { unset( $update ); if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) { return; } if ( 'publish' !== $post->post_status ) { return; } if ( ! in_array( $post->post_type, \AiRankSuite\Support\Inventory::eligible_types(), true ) ) { return; } try { $container->get( Services\PostAnalyzer::class )->analyze( $post ); } catch ( \Throwable $e ) { // An analysis failure must never block a save. unset( $e ); } }, 20, 3 ); } }