Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions inc/options/class-wpseo-option-wpseo.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ class WPSEO_Option_Wpseo extends WPSEO_Option {
'ai_free_sparks_started_on' => null,
'enable_llms_txt' => false,
'last_updated_on' => false,
'default_seo_title' => [],
'default_seo_meta_desc' => [],
];

/**
Expand Down Expand Up @@ -432,6 +434,8 @@ protected function validate_option( $dirty, $clean, $old ) {
case 'last_known_public_taxonomies':
case 'new_post_types':
case 'new_taxonomies':
case 'default_seo_title':
case 'default_seo_meta_desc':
$clean[ $key ] = $old[ $key ];

if ( isset( $dirty[ $key ] ) ) {
Expand Down
199 changes: 199 additions & 0 deletions src/alerts/application/default-seo-data/default-seo-data-alert.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Alerts\Application\Default_SEO_Data;

use Yoast\WP\SEO\Alerts\Infrastructure\Default_SEO_Data\Default_SEO_Data_Collector;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Product_Helper;
use Yoast\WP\SEO\Helpers\Short_Link_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast_Notification;
use Yoast_Notification_Center;

/**
* Default_SEO_Data_Alert class.
*/
class Default_SEO_Data_Alert implements Integration_Interface {

public const NOTIFICATION_ID = 'wpseo-default-seo-data';

/**
* The notifications center.
*
* @var Yoast_Notification_Center
*/
private $notification_center;

/**
* The default SEO data collector.
*
* @var Default_SEO_Data_Collector
*/
private $default_seo_data_collector;

/**
* The short link helper.
*
* @var Short_Link_Helper
*/
private $short_link_helper;

/**
* The product helper.
*
* @var Product_Helper
*/
private $product_helper;

/**
* The indexable helper.
*
* @var Indexable_Helper
*/
private $indexable_helper;

/**
* The post type helper.
*
* @var Post_Type_Helper
*/
private $post_type_helper;

/**
* Default_SEO_Data_Alert constructor.
*
* @param Yoast_Notification_Center $notification_center The notification center.
* @param Default_SEO_Data_Collector $default_seo_data_collector The default SEO data collector.
* @param Short_Link_Helper $short_link_helper The short link helper.
* @param Product_Helper $product_helper The product helper.
* @param Indexable_Helper $indexable_helper The indexable helper.
* @param Post_Type_Helper $post_type_helper The post type helper.
*/
public function __construct(
Yoast_Notification_Center $notification_center,
Default_SEO_Data_Collector $default_seo_data_collector,
Short_Link_Helper $short_link_helper,
Product_Helper $product_helper,
Indexable_Helper $indexable_helper,
Post_Type_Helper $post_type_helper
) {
$this->notification_center = $notification_center;
$this->default_seo_data_collector = $default_seo_data_collector;
$this->short_link_helper = $short_link_helper;
$this->product_helper = $product_helper;
$this->indexable_helper = $indexable_helper;
$this->post_type_helper = $post_type_helper;
}

/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array<string>
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}

/**
* Initializes the integration.
*
* @return void
*/
public function register_hooks() {
\add_action( 'admin_init', [ $this, 'add_notifications' ] );
}

/**
* Adds notifications (when necessary).
Comment thread
leonidasmi marked this conversation as resolved.
*
* @return void
*/
public function add_notifications() {
if ( ! $this->indexable_helper->should_index_indexables() ) {
// Do not show the notification when indexables are disabled.
$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
return;
}

if ( ! $this->post_type_helper->is_indexable( 'post' ) || ! $this->post_type_helper->has_metabox( 'post' ) ) {
// Do not show the notification when posts are not indexable or have no metabox.
$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
return;
}

$default_seo_titles = $this->default_seo_data_collector->get_posts_with_default_seo_title();
$default_seo_descs = $this->default_seo_data_collector->get_posts_with_default_seo_description();

$has_enough_posts_with_default_title = \count( $default_seo_titles ) > 4;
$has_enough_posts_with_default_desc = \count( $default_seo_descs ) > 4;

if ( ! $has_enough_posts_with_default_title && ! $has_enough_posts_with_default_desc ) {
$this->notification_center->remove_notification_by_id( self::NOTIFICATION_ID );
return;
}

$notification = $this->get_default_seo_data_notification( $has_enough_posts_with_default_title, $has_enough_posts_with_default_desc );

$this->notification_center->add_notification( $notification );
}

/**
* Build the default SEO data notification.
*
* @param bool $default_seo_titles Whether there are content types with default SEO title in their most recent posts.
* @param bool $default_seo_descs Whether there are content types with default SEO description in their most recent posts.
*
* @return Yoast_Notification The notification containing the suggested plugin.
*/
protected function get_default_seo_data_notification( $default_seo_titles, $default_seo_descs ) {
Comment thread
leonidasmi marked this conversation as resolved.
Outdated
$message = $this->get_default_seo_data_message( $default_seo_titles, $default_seo_descs );

return new Yoast_Notification(
$message,
[
'id' => self::NOTIFICATION_ID,
'type' => Yoast_Notification::WARNING,
'capabilities' => [ 'wpseo_manage_options' ],
]
);
}

/**
* Creates a message to inform users that they are using only default SEO data lately.
*
* @param bool $default_seo_titles Whether there are content types with default SEO title in their most recent posts.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to suggest maybe a has_... or something naming since I was a bit confused that this was not the actual default seo title or description haha

* @param bool $default_seo_descs Whether there are content types with default SEO description in their most recent posts.
*
* @return string The default SEO data message.
*/
protected function get_default_seo_data_message( $default_seo_titles, $default_seo_descs ) {
Comment thread
leonidasmi marked this conversation as resolved.
Outdated
$shortlink = ( $this->product_helper->is_premium() ) ? $this->short_link_helper->get( 'https://yoa.st/ai-generate-alert-premium/' ) : $this->short_link_helper->get( 'https://yoa.st/ai-generate-alert-free/' );

if ( $default_seo_titles && $default_seo_descs ) {
$default_seo_data = \esc_html__( 'SEO titles and meta descriptions', 'wordpress-seo' );
}
elseif ( $default_seo_titles ) {
$default_seo_data = \esc_html__( 'SEO titles', 'wordpress-seo' );
}
elseif ( $default_seo_descs ) {
$default_seo_data = \esc_html__( 'meta descriptions', 'wordpress-seo' );
}
else {
$default_seo_data = \esc_html__( 'SEO data', 'wordpress-seo' );
}

/* translators: %1$s expands to "SEO title" or "meta description", %2$s expands to an opening strong tag, %3$s expands to a closing strong tag, %4$s expands to an opening link tag, %5$s expands to a closing link tag. */
$message = ( $this->product_helper->is_premium() ) ? \esc_html__( 'Your recent posts are using default %1$s, making them less appealing in search. Create custom titles and descriptions instantly with %2$sYoast AI Generate%3$s. %4$sLearn how to use it%5$s.', 'wordpress-seo' ) : \esc_html__( 'Your recent posts are using default %1$s, which makes them easy to overlook. Catch attention in search with custom titles and descriptions from %2$sYoast AI Generate%3$s. %4$sTry it for free.%5$s', 'wordpress-seo' );

return \sprintf(
$message,
$default_seo_data,
'<strong>',
'</strong>',
'<a href="' . \esc_url( $shortlink ) . '">',
'</a>'
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Alerts\Infrastructure\Default_SEO_Data;

use Yoast\WP\SEO\Helpers\Options_Helper;

/**
* Class that collects default SEO data.
*/
class Default_SEO_Data_Collector {

/**
* Holds the Options_Helper instance.
*
* @var Options_Helper
*/
private $options_helper;

/**
* The constructor.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_helper = $options_helper;
}

/**
* Returns the posts with default SEO title in their most recent.
*
* @return string[] The posts with default SEO title in their most recent.
*/
public function get_posts_with_default_seo_title(): array {
return $this->options_helper->get( 'default_seo_title', [] );
}

/**
* Returns the posts with default SEO description in their most recent.
*
* @return string[] The posts with default SEO description in their most recent.
*/
public function get_posts_with_default_seo_description(): array {
return $this->options_helper->get( 'default_seo_meta_desc', [] );
}
}
Original file line number Diff line number Diff line change
@@ -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 );
}
}
Loading