-
Notifications
You must be signed in to change notification settings - Fork 953
Add notification for unused SEO title and description features #22608
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
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
a37867f
Check for default SEO data on recent content upon post indexable save
leonidasmi 443d851
Add alert when users use only default SEO data lately
leonidasmi 127c389
Add unit tests
leonidasmi e65cc4e
Save IDs in the DB and also link to appropriate pages in yoast site
leonidasmi b86ac40
Reset data when posts are noindexed or without metabox
leonidasmi eeccfcf
Minor UX improvements on the notification
leonidasmi 6dece8f
Merge branch 'feature/adoption-1' into add/alert-for-unused-ai-generate
leonidasmi 72511fd
Never show the notification when indexables are disabled
leonidasmi 265f3fd
Detect default SEO data behind a cron action
leonidasmi b662cec
Finalize copy
leonidasmi 5824be9
Remove tests for now
leonidasmi 1449e4e
Improve doc blocks
leonidasmi 23433ff
Improve docblock for adding notifications to make the functions purpo…
leonidasmi 18bc22d
Update src/alerts/application/default-seo-data/default-seo-data-alert…
leonidasmi 09181a7
Update src/alerts/application/default-seo-data/default-seo-data-alert…
leonidasmi 9f65b28
Update src/alerts/user-interface/default-seo-data/default-seo-data-cr…
leonidasmi f5d4daf
Rename variables in the alert application layer
leonidasmi aa0ae8c
Revert unscheduling change
leonidasmi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
202 changes: 202 additions & 0 deletions
202
src/alerts/application/default-seo-data/default-seo-data-alert.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,202 @@ | ||
| <?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). | ||
| * | ||
| * We want to show this notification only when there are enough posts that have the default SEO title or meta description, or both. | ||
| * If this is not the case we will not show the notification at all since it does not serve a purpose yet. | ||
| * | ||
| * @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; | ||
| } | ||
|
|
||
| $posts_with_default_seo_title = $this->default_seo_data_collector->get_posts_with_default_seo_title(); | ||
| $posts_with_default_seo_description = $this->default_seo_data_collector->get_posts_with_default_seo_description(); | ||
|
|
||
| $has_enough_posts_with_default_title = \count( $posts_with_default_seo_title ) > 4; | ||
| $has_enough_posts_with_default_desc = \count( $posts_with_default_seo_description ) > 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 $has_enough_posts_with_default_title Whether there are content types with default SEO title in their most recent posts. | ||
| * @param bool $has_enough_posts_with_default_desc Whether there are content types with default SEO description in their most recent posts. | ||
| * | ||
| * @return Yoast_Notification The notification containing the suggested plugin. | ||
| */ | ||
| private function get_default_seo_data_notification( $has_enough_posts_with_default_title, $has_enough_posts_with_default_desc ): Yoast_Notification { | ||
| $message = $this->get_default_seo_data_message( $has_enough_posts_with_default_title, $has_enough_posts_with_default_desc ); | ||
|
|
||
| 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 $has_enough_posts_with_default_title Whether there are content types with default SEO title in their most recent posts. | ||
| * @param bool $has_enough_posts_with_default_desc Whether there are content types with default SEO description in their most recent posts. | ||
| * | ||
| * @return string The default SEO data message. | ||
| */ | ||
| private function get_default_seo_data_message( $has_enough_posts_with_default_title, $has_enough_posts_with_default_desc ): string { | ||
| $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 ( $has_enough_posts_with_default_title && $has_enough_posts_with_default_desc ) { | ||
| $default_seo_data = \esc_html__( 'SEO titles and meta descriptions', 'wordpress-seo' ); | ||
| } | ||
| elseif ( $has_enough_posts_with_default_title ) { | ||
| $default_seo_data = \esc_html__( 'SEO titles', 'wordpress-seo' ); | ||
| } | ||
| elseif ( $has_enough_posts_with_default_desc ) { | ||
| $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>' | ||
| ); | ||
| } | ||
| } | ||
45 changes: 45 additions & 0 deletions
45
src/alerts/infrastructure/default-seo-data/default-seo-data-collector.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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', [] ); | ||
| } | ||
| } |
100 changes: 100 additions & 0 deletions
100
src/alerts/user-interface/default-seo-data/default-seo-data-cron-callback-integration.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 ); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.