diff --git a/src/generators/schema/third-party/coauthor.php b/src/generators/schema/third-party/coauthor.php index 49d09dd6e00..d231d98df1a 100644 --- a/src/generators/schema/third-party/coauthor.php +++ b/src/generators/schema/third-party/coauthor.php @@ -2,8 +2,10 @@ namespace Yoast\WP\SEO\Generators\Schema\Third_Party; +use Yoast\WP\SEO\Config\Schema_IDs; use Yoast\WP\SEO\Generators\Schema\Author; + /** * Returns schema Author data for the CoAuthor Plus assigned user on a post. */ @@ -12,7 +14,7 @@ class CoAuthor extends Author { /** * The user ID of the author we're generating data for. * - * @var int + * @var int $user_id */ private $user_id; @@ -62,6 +64,29 @@ public function generate_from_user_id( $user_id ) { return $this->generate(); } + /** + * Generate the Person data given a Guest Author object. + * + * @param object $guest_author The Guest Author object. + * + * @return array|bool + */ + public function generate_from_guest_author( $guest_author ) { + $this->user_id = $user_id; + + $data = $this->build_person_data_for_guest_author( $guest_author, true ); + + $data['@type'] = 'Person'; + unset( $data['logo'] ); + + // If this is a post and the author archives are enabled, set the author archive url as the author url. + if ( $this->helpers->options->get( 'disable-author' ) !== true ) { + $data['url'] = \get_author_posts_url( $guest_author->ID, $guest_author->user_nicename ); + } + + return $data; + } + /** * Determines a User ID for the Person data. * @@ -70,4 +95,51 @@ public function generate_from_user_id( $user_id ) { protected function determine_user_id() { return $this->user_id; } + + /** + * Builds our array of Schema Person data for a given Guest Author. + * + * @param object $guest_author The Guest Author object. + * @param bool $add_hash Wether or not the person's image url hash should be added to the image id. + * + * @return array An array of Schema Person data. + */ + protected function build_person_data_for_guest_author( $guest_author, $add_hash = false ) { + $schema_id = $this->context->site_url . Schema_IDs::PERSON_LOGO_HASH; + $data = [ + '@type' => $this->type, + '@id' => $schema_id . \wp_hash( $guest_author->user_login . $guest_author->ID . 'guest' ), + ]; + + $data['name'] = $this->helpers->schema->html->smart_strip_tags( $guest_author->display_name ); + + $data = $this->set_image_from_avatar( $data, $guest_author, $schema_id, $add_hash ); + + // If local avatar is present, override. + $avatar_meta = \wp_get_attachment_image_src( \get_post_thumbnail_id( $guest_author->ID ) ); + if ( $avatar_meta ) { + $avatar_meta = [ + 'url' => $avatar_meta[0], + 'width' => $avatar_meta[1], + 'height' => $avatar_meta[2], + ]; + $data['image'] = $this->helpers->schema->image->generate_from_attachment_meta( $schema_id, $avatar_meta, $data['name'], $add_hash ); + } + + if ( ! empty( $guest_author->description ) ) { + $data['description'] = $this->helpers->schema->html->smart_strip_tags( $guest_author->description ); + } + + $data = $this->add_same_as_urls( $data, $guest_author, $user_id ); + + /** + * Filter: 'wpseo_schema_person_data' - Allows filtering of schema data per user. + * + * @param array $data The schema data we have for this person. + * @param int $user_id The current user we're collecting schema data for. + */ + $data = \apply_filters( 'wpseo_schema_person_data', $data, $user_id ); + + return $data; + } } diff --git a/src/integrations/third-party/coauthors-plus.php b/src/integrations/third-party/coauthors-plus.php index 32b577463e4..4654f819063 100644 --- a/src/integrations/third-party/coauthors-plus.php +++ b/src/integrations/third-party/coauthors-plus.php @@ -2,10 +2,9 @@ namespace Yoast\WP\SEO\Integrations\Third_Party; -use WP_User; +use Yoast\WP\SEO\Config\Schema_Types; use Yoast\WP\SEO\Conditionals\Third_Party\CoAuthors_Plus_Activated_Conditional; use Yoast\WP\SEO\Conditionals\Third_Party\CoAuthors_Plus_Flag_Conditional; -use Yoast\WP\SEO\Config\Schema_Types; use Yoast\WP\SEO\Context\Meta_Tags_Context; use Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece; use Yoast\WP\SEO\Generators\Schema\Third_Party\CoAuthor; @@ -32,6 +31,8 @@ class CoAuthors_Plus implements Integration_Interface { public function register_hooks() { \add_filter( 'wpseo_schema_graph', [ $this, 'filter_graph' ], 11, 2 ); \add_filter( 'wpseo_schema_author', [ $this, 'filter_author_graph' ], 11, 4 ); + \add_filter( 'wpseo_schema_profilepage', [ $this, 'filter_schema_profilepage' ], 11, 4 ); + \add_filter( 'wpseo_meta_author', [ $this, 'filter_author_meta' ], 11, 2 ); } /** @@ -57,6 +58,36 @@ public function __construct( Helpers_Surface $helpers ) { $this->helpers = $helpers; } + /** + * Filters the graph output of authors archive for guest authors. + * + * @param array $data The schema graph. + * @param Meta_Tags_Context $context The context object. + * @param Abstract_Schema_Piece $graph_piece_generator The graph piece generator. + * @param Abstract_Schema_Piece[] $graph_piece_generators The graph piece generators. + * + * @return array The (potentially altered) schema graph. + */ + public function filter_schema_profilepage( $data, $context, $graph_piece_generator, $graph_piece_generators ) { + + if ( ! is_author() ) { + return $data; + } + + $user = \get_queried_object(); + + if ( empty( $user->type ) || $user->type !== 'guest-author' ) { + return $data; + } + + // Fix author URL. + $author_url = \get_author_posts_url( $user->ID, $user->user_nicename ); + $graph_piece_generator->context->canonical = $author_url; + $graph_piece_generator->context->main_schema_id = $author_url; + + return $graph_piece_generator->generate(); + } + /** * Filters the graph output to add authors. * @@ -103,29 +134,31 @@ public function filter_graph( $data, $context ) { /** * Contains the authors from the CoAuthors Plus plugin. * - * @var WP_User[] $author_objects + * @var \WP_User[] $author_objects */ $author_objects = \get_coauthors( $context->post->ID ); - if ( \count( $author_objects ) <= 1 ) { - return $data; - } - $ids = []; + $ids = []; + $authors = []; // Add the authors to the schema. foreach ( $author_objects as $author ) { - if ( $author->ID === (int) $context->post->post_author ) { - continue; - } $author_generator = new CoAuthor(); $author_generator->context = $context; $author_generator->helpers = $this->helpers; - $author_data = $author_generator->generate_from_user_id( $author->ID ); + + if ( $author instanceof \WP_User ) { + $author_data = $author_generator->generate_from_user_id( $author->ID ); + } + elseif ( ! empty( $author->type ) && $author->type === 'guest-author' ) { + $author_data = $author_generator->generate_from_guest_author( $author ); + } + if ( ! empty( $author_data ) ) { - $ids[] = [ '@id' => $author_data['@id'] ]; + $ids[] = [ '@id' => $author_data['@id'] ]; + $authors[] = $author_data; } } - $schema_types = new Schema_Types(); $article_types = $schema_types->get_article_type_options_values(); @@ -133,20 +166,53 @@ public function filter_graph( $data, $context ) { $add_to_graph = false; foreach ( $data as $key => $piece ) { if ( \in_array( $piece['@type'], $article_types, true ) ) { - $data[ $key ]['author'] = \array_merge( [ $piece['author'] ], $ids ); + $data[ $key ]['author'] = $ids; $add_to_graph = true; break; } } if ( $add_to_graph ) { + // Clean all Persons from the schema, as the user stored as post owner might be incorrectly added if the post post has only guest authors as authors. + $data = \array_filter( + $data, + function( $piece ) { + return empty( $piece['@type'] ) || $piece['@type'] !== 'Person'; + } + ); + if ( ! empty( $author_data ) ) { if ( $context->site_represents !== 'person' || $author->ID !== $context->site_user_id ) { - $data[] = $author_data; + $data = \array_merge( $data, $authors ); } } } return $data; } + + /** + * Filters the author meta tag + * + * @param string $author_name The article author's display name. Return empty to disable the tag. + * @param Indexable_Presentation $presentation The presentation of an indexable. + * @return string + */ + public function filter_author_meta( $author_name, $presentation ) { + $author_objects = \get_coauthors( $presentation->context->post->id ); + + // Fallback in case of error. + if ( empty( $author_objects ) ) { + return $author_name; + } + + $output = ''; + foreach ( $author_objects as $i => $author ) { + $output .= $author->display_name; + if ( $i <= ( count( $author_objects ) - 2 ) ) { + $output .= ', '; + } + } + return $output; + } }