Phase 5: Content and SEO - Yoast SEO, Schema.org markup, Open Graph, favicon support, XML sitemap

This commit is contained in:
Hanson.xyz Dev
2025-11-28 17:10:24 -06:00
parent c4f29a3152
commit 91de533da4
1552 changed files with 171432 additions and 7 deletions
@@ -0,0 +1,100 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Alerts\User_Interface\Default_SEO_Data;
use Yoast\WP\SEO\Alerts\User_Interface\Default_Seo_Data\Default_SEO_Data_Cron_Scheduler;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Cron Callback integration. This handles the actual process of detecting default SEO data in recent posts and updating the relevant options.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Default_SEO_Data_Cron_Callback_Integration implements Integration_Interface {
use No_Conditionals;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The scheduler.
*
* @var Default_SEO_Data_Cron_Scheduler
*/
private $scheduler;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
private $indexable_repository;
/**
* Constructor.
*
* @param Options_Helper $options_helper The options helper.
* @param Default_SEO_Data_Cron_Scheduler $scheduler The scheduler.
* @param Indexable_Repository $indexable_repository The indexable repository.
*/
public function __construct(
Options_Helper $options_helper,
Default_SEO_Data_Cron_Scheduler $scheduler,
Indexable_Repository $indexable_repository
) {
$this->options_helper = $options_helper;
$this->scheduler = $scheduler;
$this->indexable_repository = $indexable_repository;
}
/**
* Registers the hooks.
*
* @return void
*/
public function register_hooks() {
\add_action(
Default_SEO_Data_Cron_Scheduler::CRON_HOOK,
[
$this,
'detect_default_seo_data_in_recent',
]
);
}
/**
* Detects default SEO data in recent posts and updates the relevant options.
*
* @return void
*/
public function detect_default_seo_data_in_recent(): void {
if ( ! \wp_doing_cron() ) {
return;
}
$recent_posts = $this->indexable_repository->get_recently_modified_posts( 'post', 5, false );
$recent_default_seo_title = [];
$recent_default_seo_meta_desc = [];
foreach ( $recent_posts as $post ) {
if ( $post->title === null ) {
$recent_default_seo_title[] = $post->object_id;
}
if ( $post->description === null ) {
$recent_default_seo_meta_desc[] = $post->object_id;
}
}
$this->options_helper->set( 'default_seo_title', $recent_default_seo_title );
$this->options_helper->set( 'default_seo_meta_desc', $recent_default_seo_meta_desc );
}
}
@@ -0,0 +1,52 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Alerts\User_Interface\Default_Seo_Data;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Responsible for scheduling and unscheduling the cron.
*/
class Default_SEO_Data_Cron_Scheduler implements Integration_Interface {
use No_Conditionals;
/**
* The name of the cron job.
*/
public const CRON_HOOK = 'wpseo_detect_default_seo_data';
/**
* Register hooks.
*
* @return void
*/
public function register_hooks() {
\add_action( 'admin_init', [ $this, 'schedule_default_seo_data_detection' ] );
\add_action( 'wpseo_deactivate', [ $this, 'unschedule_default_seo_data_detection' ] );
}
/**
* Schedules the default SEO data detection cron.
*
* @return void
*/
public function schedule_default_seo_data_detection(): void {
if ( ! \wp_next_scheduled( self::CRON_HOOK ) ) {
\wp_schedule_event( ( \time() + \DAY_IN_SECONDS ), 'daily', self::CRON_HOOK );
}
}
/**
* Unschedules the default SEO data detection cron.
*
* @return void
*/
public function unschedule_default_seo_data_detection() {
$scheduled = \wp_next_scheduled( self::CRON_HOOK );
if ( $scheduled ) {
\wp_unschedule_event( $scheduled, self::CRON_HOOK );
}
}
}
@@ -0,0 +1,76 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Alerts\User_Interface\Default_SEO_Data;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* This handles the process of checking for non-default SEO in the latest content and updating the relevant options right away.
*/
class Default_SEO_Data_Watcher implements Integration_Interface {
use No_Conditionals;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
private $indexable_repository;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Constructor.
*
* @param Indexable_Repository $indexable_repository The indexable repository.
* @param Options_Helper $options_helper The options helper.
*/
public function __construct(
Indexable_Repository $indexable_repository,
Options_Helper $options_helper
) {
$this->indexable_repository = $indexable_repository;
$this->options_helper = $options_helper;
}
/**
* Registers the hooks with WordPress.
*
* @return void
*/
public function register_hooks() {
\add_action( 'wpseo_saved_indexable', [ $this, 'check_for_default_seo_data' ], 10, 1 );
}
/**
* Checks for default SEO data in the saved indexable and the most recently modified posts.
*
* @param Indexable $saved_indexable The saved indexable.
*
* @return void
*/
public function check_for_default_seo_data( $saved_indexable ): void {
// We have activated this feature only for posts for now.
if ( $saved_indexable->object_type !== 'post' || $saved_indexable->object_sub_type !== 'post' ) {
return;
}
// If the title or description is null, it means it's not default SEO data so let's reset the options.
if ( $saved_indexable->title !== null ) {
$this->options_helper->set( 'default_seo_title', [] );
}
if ( $saved_indexable->description !== null ) {
$this->options_helper->set( 'default_seo_meta_desc', [] );
}
}
}
@@ -0,0 +1,91 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Alerts\User_Interface;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Helpers\User_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Registers a route to resolve an alert
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Resolve_Alert_Route implements Integration_Interface {
use No_Conditionals;
/**
* The user helper.
*
* @var User_Helper
*/
private $user_helper;
/**
* The capability helper.
*
* @var Capability_Helper
*/
private $capability_helper;
/**
* Class constructor.
*
* @param User_Helper $user_helper The user helper.
* @param Capability_Helper $capability_helper The capability helper.
*/
public function __construct(
User_Helper $user_helper,
Capability_Helper $capability_helper
) {
$this->user_helper = $user_helper;
$this->capability_helper = $capability_helper;
}
/**
* Registers all hooks to WordPress.
*
* @return void
*/
public function register_hooks() {
\add_action( 'wp_ajax_wpseo_resolve_alert', [ $this, 'resolve_alert' ] );
}
/**
* Runs the callback to resolve an alert for the current user.
*
* @return void.
*/
public function resolve_alert() {
if ( ! \check_ajax_referer( 'wpseo-resolve-alert-nonce', 'nonce', false ) || ! $this->capability_helper->current_user_can( 'wpseo_manage_options' ) ) {
\wp_send_json_error(
[
'message' => 'Security check failed.',
]
);
return;
}
if ( ! isset( $_POST['alertId'] ) ) {
\wp_send_json_error(
[
'message' => 'Alert ID is missing.',
]
);
return;
}
$alert_id = \sanitize_text_field( \wp_unslash( $_POST['alertId'] ) );
$user_id = \get_current_user_id();
$this->user_helper->update_meta( $user_id, $alert_id . '_resolved', true );
\wp_send_json_success(
[
'message' => 'Alert resolved successfully.',
]
);
}
}