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,162 @@
<?php
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\AI_HTTP_Request\Infrastructure\API_Client;
use Yoast\WP\SEO\Conditionals\AI_Conditional;
use Yoast\WP\SEO\Conditionals\AI_Editor_Conditional;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\User_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Introductions\Application\Ai_Fix_Assessments_Upsell;
use Yoast\WP\SEO\Introductions\Infrastructure\Introductions_Seen_Repository;
/**
* Ai_Generator_Integration class.
*/
class Ai_Generator_Integration implements Integration_Interface {
/**
* Represents the admin asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $asset_manager;
/**
* Represents the add-on manager.
*
* @var WPSEO_Addon_Manager
*/
private $addon_manager;
/**
* Holds the API client instance.
*
* @var API_Client
*/
private $api_client;
/**
* Represents the current page helper.
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* Represents the options manager.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Represents the user helper.
*
* @var User_Helper
*/
private $user_helper;
/**
* Represents the introductions seen repository.
*
* @var Introductions_Seen_Repository
*/
private $introductions_seen_repository;
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array<string>
*/
public static function get_conditionals() {
return [ AI_Conditional::class, AI_Editor_Conditional::class ];
}
/**
* Constructs the class.
*
* @param WPSEO_Admin_Asset_Manager $asset_manager The admin asset manager.
* @param WPSEO_Addon_Manager $addon_manager The addon manager.
* @param API_Client $api_client The API client.
* @param Current_Page_Helper $current_page_helper The current page helper.
* @param Options_Helper $options_helper The options helper.
* @param User_Helper $user_helper The user helper.
* @param Introductions_Seen_Repository $introductions_seen_repository The introductions seen repository.
*/
public function __construct(
WPSEO_Admin_Asset_Manager $asset_manager,
WPSEO_Addon_Manager $addon_manager,
API_Client $api_client,
Current_Page_Helper $current_page_helper,
Options_Helper $options_helper,
User_Helper $user_helper,
Introductions_Seen_Repository $introductions_seen_repository
) {
$this->asset_manager = $asset_manager;
$this->addon_manager = $addon_manager;
$this->api_client = $api_client;
$this->current_page_helper = $current_page_helper;
$this->options_helper = $options_helper;
$this->user_helper = $user_helper;
$this->introductions_seen_repository = $introductions_seen_repository;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
// Enqueue after Elementor_Premium integration, which re-registers the assets.
\add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 );
}
/**
* Gets the subscription status for Yoast SEO Premium and Yoast WooCommerce SEO.
*
* @return array<string, bool>
*/
public function get_product_subscriptions() {
return [
'premiumSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ),
'wooCommerceSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ),
];
}
/**
* Returns the data that should be passed to the script.
*
* @return array<string|array<string>>
*/
public function get_script_data() {
$user_id = $this->user_helper->get_current_user_id();
return [
'hasConsent' => $this->user_helper->get_meta( $user_id, '_yoast_wpseo_ai_consent', true ),
'productSubscriptions' => $this->get_product_subscriptions(),
'hasSeenIntroduction' => $this->introductions_seen_repository->is_introduction_seen( $user_id, AI_Fix_Assessments_Upsell::ID ),
'requestTimeout' => $this->api_client->get_request_timeout(),
'isFreeSparks' => $this->options_helper->get( 'ai_free_sparks_started_on', null ) !== null,
];
}
/**
* Enqueues the required assets.
*
* @return void
*/
public function enqueue_assets() {
$this->asset_manager->enqueue_script( 'ai-generator' );
$this->asset_manager->localize_script( 'ai-generator', 'wpseoAiGenerator', $this->get_script_data() );
$this->asset_manager->enqueue_style( 'ai-generator' );
}
}
@@ -0,0 +1,89 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
use WP_REST_Response;
use WPSEO_Addon_Manager;
use Yoast\WP\SEO\Conditionals\AI_Conditional;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Registers a route to bust the subscription cache.
*
* @makePublic
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Bust_Subscription_Cache_Route implements Route_Interface {
use Route_Permission_Trait;
/**
* The namespace for this route.
*
* @var string
*/
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
/**
* The prefix for this route.
*
* @var string
*/
public const ROUTE_PREFIX = '/ai_generator/bust_subscription_cache';
/**
* The addon manager instance.
*
* @var WPSEO_Addon_Manager
*/
private $addon_manager;
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array<string> The conditionals.
*/
public static function get_conditionals() {
return [ AI_Conditional::class ];
}
/**
* Class constructor.
*
* @param WPSEO_Addon_Manager $addon_manager The addon manager instance.
*/
public function __construct( WPSEO_Addon_Manager $addon_manager ) {
$this->addon_manager = $addon_manager;
}
/**
* Registers routes with WordPress.
*
* @return void
*/
public function register_routes() {
\register_rest_route(
self::ROUTE_NAMESPACE,
self::ROUTE_PREFIX,
[
'methods' => 'POST',
'args' => [],
'callback' => [ $this, 'bust_subscription_cache' ],
'permission_callback' => [ $this, 'check_permissions' ],
]
);
}
/**
* Runs the callback that busts the subscription cache.
*
* @return WP_REST_Response The response of the callback action.
*/
public function bust_subscription_cache(): WP_REST_Response {
$this->addon_manager->remove_site_information_transients();
return new WP_REST_Response( 'Subscription cache successfully busted.' );
}
}
@@ -0,0 +1,174 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
use RuntimeException;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\AI_Generator\Application\Suggestions_Provider;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Remote_Request_Exception;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
use Yoast\WP\SEO\Conditionals\AI_Conditional;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Registers a route to get suggestions from the AI API
*
* @makePublic
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Get_Suggestions_Route implements Route_Interface {
use No_Conditionals;
use Route_Permission_Trait;
/**
* The namespace for this route.
*
* @var string
*/
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
/**
* The prefix for this route.
*
* @var string
*/
public const ROUTE_PREFIX = '/ai_generator/get_suggestions';
/**
* The suggestions provider instance.
*
* @var Suggestions_Provider
*/
private $suggestions_provider;
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array<string> The conditionals.
*/
public static function get_conditionals() {
return [ AI_Conditional::class ];
}
/**
* Class constructor.
*
* @param Suggestions_Provider $suggestions_provider The suggestions provider instance.
*/
public function __construct( Suggestions_Provider $suggestions_provider ) {
$this->suggestions_provider = $suggestions_provider;
}
/**
* Registers routes with WordPress.
*
* @return void
*/
public function register_routes() {
\register_rest_route(
self::ROUTE_NAMESPACE,
self::ROUTE_PREFIX,
[
'methods' => 'POST',
'args' => [
'type' => [
'required' => true,
'type' => 'string',
'enum' => [
'seo-title',
'meta-description',
'product-seo-title',
'product-meta-description',
'product-taxonomy-seo-title',
'product-taxonomy-meta-description',
'taxonomy-seo-title',
'taxonomy-meta-description',
],
'description' => 'The type of suggestion requested.',
],
'prompt_content' => [
'required' => true,
'type' => 'string',
'description' => 'The content needed by the prompt to ask for suggestions.',
],
'focus_keyphrase' => [
'required' => true,
'type' => 'string',
'description' => 'The focus keyphrase associated to the post.',
],
'language' => [
'required' => true,
'type' => 'string',
'description' => 'The language the post is written in.',
],
'platform' => [
'required' => true,
'type' => 'string',
'enum' => [
'Google',
'Facebook',
'Twitter',
],
'description' => 'The platform the post is intended for.',
],
'editor' => [
'required' => true,
'type' => 'string',
'enum' => [
'classic',
'elementor',
'gutenberg',
],
'description' => 'The current editor.',
],
],
'callback' => [ $this, 'get_suggestions' ],
'permission_callback' => [ $this, 'check_permissions' ],
]
);
}
/**
* Runs the callback to get AI-generated suggestions.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response of the get_suggestions action.
*/
public function get_suggestions( WP_REST_Request $request ): WP_REST_Response {
try {
$user = \wp_get_current_user();
$data = $this->suggestions_provider->get_suggestions(
$user,
$request->get_param( 'type' ),
$request->get_param( 'prompt_content' ),
$request->get_param( 'focus_keyphrase' ),
$request->get_param( 'language' ),
$request->get_param( 'platform' ),
$request->get_param( 'editor' )
);
} catch ( Remote_Request_Exception $e ) {
$message = [
'message' => $e->getMessage(),
'errorIdentifier' => $e->get_error_identifier(),
];
if ( $e instanceof Payment_Required_Exception || $e instanceof Too_Many_Requests_Exception ) {
$message['missingLicenses'] = $e->get_missing_licenses();
}
return new WP_REST_Response(
$message,
$e->getCode()
);
} catch ( RuntimeException $e ) {
return new WP_REST_Response( 'Failed to get suggestions.', 500 );
}
return new WP_REST_Response( $data );
}
}
@@ -0,0 +1,162 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
use WP_REST_Response;
use WPSEO_Addon_Manager;
use Yoast\WP\SEO\AI_Authorization\Application\Token_Manager;
use Yoast\WP\SEO\AI_HTTP_Request\Application\Request_Handler;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Remote_Request_Exception;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\WP_Request_Exception;
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Request;
use Yoast\WP\SEO\Conditionals\AI_Conditional;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Registers a route to get suggestions from the AI API
*
* @makePublic
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Get_Usage_Route implements Route_Interface {
use Route_Permission_Trait;
/**
* The namespace for this route.
*
* @var string
*/
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
/**
* The prefix for this route.
*
* @var string
*/
public const ROUTE_PREFIX = '/ai_generator/get_usage';
/**
* The token manager instance.
*
* @var Token_Manager
*/
private $token_manager;
/**
* The request handler instance.
*
* @var Request_Handler
*/
private $request_handler;
/**
* Represents the add-on manager.
*
* @var WPSEO_Addon_Manager
*/
private $addon_manager;
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array<string> The conditionals.
*/
public static function get_conditionals() {
return [ AI_Conditional::class ];
}
/**
* Class constructor.
*
* @param Token_Manager $token_manager The token manager instance.
* @param Request_Handler $request_handler The request handler instance.
* @param WPSEO_Addon_Manager $addon_manager The add-on manager instance.
*/
public function __construct( Token_Manager $token_manager, Request_Handler $request_handler, WPSEO_Addon_Manager $addon_manager ) {
$this->addon_manager = $addon_manager;
$this->token_manager = $token_manager;
$this->request_handler = $request_handler;
}
/**
* Registers routes with WordPress.
*
* @return void
*/
public function register_routes() {
\register_rest_route(
self::ROUTE_NAMESPACE,
self::ROUTE_PREFIX,
[
'methods' => 'POST',
'args' => [
'is_woo_product_entity' => [
'type' => 'boolean',
'default' => false,
],
],
'callback' => [ $this, 'get_usage' ],
'permission_callback' => [ $this, 'check_permissions' ],
]
);
}
/**
* Runs the callback that gets the monthly usage of the user.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response of the callback action.
*/
public function get_usage( $request ): WP_REST_Response {
$is_woo_product_entity = $request->get_param( 'is_woo_product_entity' );
$user = \wp_get_current_user();
try {
$token = $this->token_manager->get_or_request_access_token( $user );
$request_headers = [
'Authorization' => "Bearer $token",
];
$action_path = $this->get_action_path( $is_woo_product_entity );
$response = $this->request_handler->handle( new Request( $action_path, [], $request_headers, false ) );
$data = \json_decode( $response->get_body() );
} catch ( Remote_Request_Exception | WP_Request_Exception $e ) {
$message = [
'errorMessage' => $e->getMessage(),
'errorIdentifier' => $e->get_error_identifier(),
'errorCode' => $e->getCode(),
];
if ( $e instanceof Too_Many_Requests_Exception ) {
$message['missingLicenses'] = $e->get_missing_licenses();
}
return new WP_REST_Response(
$message,
$e->getCode()
);
}
return new WP_REST_Response( $data );
}
/**
* Get action path for the request.
*
* @param bool $is_woo_product_entity Whether the request is for a WooCommerce product entity.
*
* @return string The action path.
*/
public function get_action_path( $is_woo_product_entity = false ): string {
$unlimited = '/usage/' . \gmdate( 'Y-m' );
if ( $is_woo_product_entity && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ) {
return $unlimited;
}
if ( ! $is_woo_product_entity && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) {
return $unlimited;
}
return '/usage/free-usages';
}
}
@@ -0,0 +1,25 @@
<?php
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
/**
* Trait for common permission checks in route classes.
*/
trait Route_Permission_Trait {
/**
* Checks:
* - if the user is logged
* - if the user can edit posts
*
* @return bool Whether the user is logged in, can edit posts and the feature is active.
*/
public function check_permissions(): bool {
$user = \wp_get_current_user();
if ( $user === null || $user->ID < 1 ) {
return false;
}
return \user_can( $user, 'edit_posts' );
}
}