Merged in feature/from-pantheon (pull request #16)
code from pantheon * code from pantheon
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators;
|
||||
|
||||
use Yoast\WP\SEO\Context\Meta_Tags_Context;
|
||||
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Pagination_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Url_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Repositories\Indexable_Repository;
|
||||
|
||||
/**
|
||||
* Represents the generator class for the breadcrumbs.
|
||||
*/
|
||||
class Breadcrumbs_Generator implements Generator_Interface {
|
||||
|
||||
/**
|
||||
* The indexable repository.
|
||||
*
|
||||
* @var Indexable_Repository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* The current page helper.
|
||||
*
|
||||
* @var Current_Page_Helper
|
||||
*/
|
||||
private $current_page_helper;
|
||||
|
||||
/**
|
||||
* The post type helper.
|
||||
*
|
||||
* @var Post_Type_Helper
|
||||
*/
|
||||
private $post_type_helper;
|
||||
|
||||
/**
|
||||
* The URL helper.
|
||||
*
|
||||
* @var Url_Helper
|
||||
*/
|
||||
private $url_helper;
|
||||
|
||||
/**
|
||||
* The pagination helper.
|
||||
*
|
||||
* @var Pagination_Helper
|
||||
*/
|
||||
private $pagination_helper;
|
||||
|
||||
/**
|
||||
* Breadcrumbs_Generator constructor.
|
||||
*
|
||||
* @param Indexable_Repository $repository The repository.
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Current_Page_Helper $current_page_helper The current page helper.
|
||||
* @param Post_Type_Helper $post_type_helper The post type helper.
|
||||
* @param Url_Helper $url_helper The URL helper.
|
||||
* @param Pagination_Helper $pagination_helper The pagination helper.
|
||||
*/
|
||||
public function __construct(
|
||||
Indexable_Repository $repository,
|
||||
Options_Helper $options,
|
||||
Current_Page_Helper $current_page_helper,
|
||||
Post_Type_Helper $post_type_helper,
|
||||
Url_Helper $url_helper,
|
||||
Pagination_Helper $pagination_helper
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
$this->options = $options;
|
||||
$this->current_page_helper = $current_page_helper;
|
||||
$this->post_type_helper = $post_type_helper;
|
||||
$this->url_helper = $url_helper;
|
||||
$this->pagination_helper = $pagination_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the breadcrumbs.
|
||||
*
|
||||
* @param Meta_Tags_Context $context The meta tags context.
|
||||
*
|
||||
* @return array An array of associative arrays that each have a 'text' and a 'url'.
|
||||
*/
|
||||
public function generate( Meta_Tags_Context $context ) {
|
||||
$static_ancestors = [];
|
||||
$breadcrumbs_home = $this->options->get( 'breadcrumbs-home' );
|
||||
if ( $breadcrumbs_home !== '' && ! \in_array( $this->current_page_helper->get_page_type(), [ 'Home_Page', 'Static_Home_Page' ], true ) ) {
|
||||
$front_page_id = $this->current_page_helper->get_front_page_id();
|
||||
if ( $front_page_id === 0 ) {
|
||||
$home_page_ancestor = $this->repository->find_for_home_page();
|
||||
if ( \is_a( $home_page_ancestor, Indexable::class ) ) {
|
||||
$static_ancestors[] = $home_page_ancestor;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$static_ancestor = $this->repository->find_by_id_and_type( $front_page_id, 'post' );
|
||||
if ( \is_a( $static_ancestor, Indexable::class ) && $static_ancestor->post_status !== 'unindexed' ) {
|
||||
$static_ancestors[] = $static_ancestor;
|
||||
}
|
||||
}
|
||||
}
|
||||
$page_for_posts = \get_option( 'page_for_posts' );
|
||||
if ( $this->should_have_blog_crumb( $page_for_posts, $context ) ) {
|
||||
$static_ancestor = $this->repository->find_by_id_and_type( $page_for_posts, 'post' );
|
||||
if ( \is_a( $static_ancestor, Indexable::class ) && $static_ancestor->post_status !== 'unindexed' ) {
|
||||
$static_ancestors[] = $static_ancestor;
|
||||
}
|
||||
}
|
||||
if (
|
||||
$context->indexable->object_type === 'post'
|
||||
&& $context->indexable->object_sub_type !== 'post'
|
||||
&& $context->indexable->object_sub_type !== 'page'
|
||||
&& $this->post_type_helper->has_archive( $context->indexable->object_sub_type )
|
||||
) {
|
||||
$static_ancestor = $this->repository->find_for_post_type_archive( $context->indexable->object_sub_type );
|
||||
if ( \is_a( $static_ancestor, Indexable::class ) ) {
|
||||
$static_ancestors[] = $static_ancestor;
|
||||
}
|
||||
}
|
||||
if ( $context->indexable->object_type === 'term' ) {
|
||||
$parent = $this->get_taxonomy_post_type_parent( $context->indexable->object_sub_type );
|
||||
if ( $parent && $parent !== 'post' && $this->post_type_helper->has_archive( $parent ) ) {
|
||||
$static_ancestor = $this->repository->find_for_post_type_archive( $parent );
|
||||
if ( \is_a( $static_ancestor, Indexable::class ) ) {
|
||||
$static_ancestors[] = $static_ancestor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get all ancestors of the indexable and append itself to get all indexables in the full crumb.
|
||||
$indexables = $this->repository->get_ancestors( $context->indexable );
|
||||
$indexables[] = $context->indexable;
|
||||
|
||||
if ( ! empty( $static_ancestors ) ) {
|
||||
\array_unshift( $indexables, ...$static_ancestors );
|
||||
}
|
||||
|
||||
$indexables = \apply_filters( 'wpseo_breadcrumb_indexables', $indexables, $context );
|
||||
$indexables = \is_array( $indexables ) ? $indexables : [];
|
||||
$indexables = \array_filter(
|
||||
$indexables,
|
||||
static function ( $indexable ) {
|
||||
return \is_a( $indexable, Indexable::class );
|
||||
}
|
||||
);
|
||||
|
||||
$callback = function ( Indexable $ancestor ) {
|
||||
$crumb = [
|
||||
'url' => $ancestor->permalink,
|
||||
'text' => $ancestor->breadcrumb_title,
|
||||
];
|
||||
switch ( $ancestor->object_type ) {
|
||||
case 'post':
|
||||
$crumb = $this->get_post_crumb( $crumb, $ancestor );
|
||||
break;
|
||||
case 'post-type-archive':
|
||||
$crumb = $this->get_post_type_archive_crumb( $crumb, $ancestor );
|
||||
break;
|
||||
case 'term':
|
||||
$crumb = $this->get_term_crumb( $crumb, $ancestor );
|
||||
break;
|
||||
case 'system-page':
|
||||
$crumb = $this->get_system_page_crumb( $crumb, $ancestor );
|
||||
break;
|
||||
case 'user':
|
||||
$crumb = $this->get_user_crumb( $crumb, $ancestor );
|
||||
break;
|
||||
case 'date-archive':
|
||||
$crumb = $this->get_date_archive_crumb( $crumb );
|
||||
break;
|
||||
}
|
||||
return $crumb;
|
||||
};
|
||||
$crumbs = \array_map( $callback, $indexables );
|
||||
|
||||
if ( $breadcrumbs_home !== '' ) {
|
||||
$crumbs[0]['text'] = $breadcrumbs_home;
|
||||
}
|
||||
|
||||
$crumbs = $this->add_paged_crumb( $crumbs, $context->indexable );
|
||||
|
||||
/**
|
||||
* Filter: 'wpseo_breadcrumb_links' - Allow the developer to filter the Yoast SEO breadcrumb links, add to them, change order, etc.
|
||||
*
|
||||
* @param array $crumbs The crumbs array.
|
||||
*/
|
||||
$filtered_crumbs = \apply_filters( 'wpseo_breadcrumb_links', $crumbs );
|
||||
|
||||
// Basic check to make sure the filtered crumbs are in an array.
|
||||
if ( ! \is_array( $filtered_crumbs ) ) {
|
||||
\_doing_it_wrong(
|
||||
'Filter: \'wpseo_breadcrumb_links\'',
|
||||
'The `wpseo_breadcrumb_links` filter should return a multi-dimensional array.',
|
||||
'YoastSEO v20.0'
|
||||
);
|
||||
}
|
||||
else {
|
||||
$crumbs = $filtered_crumbs;
|
||||
}
|
||||
|
||||
$filter_callback = static function( $link_info, $index ) use ( $crumbs ) {
|
||||
/**
|
||||
* Filter: 'wpseo_breadcrumb_single_link_info' - Allow developers to filter the Yoast SEO Breadcrumb link information.
|
||||
*
|
||||
* @api array $link_info The breadcrumb link information.
|
||||
*
|
||||
* @param int $index The index of the breadcrumb in the list.
|
||||
* @param array $crumbs The complete list of breadcrumbs.
|
||||
*/
|
||||
return \apply_filters( 'wpseo_breadcrumb_single_link_info', $link_info, $index, $crumbs );
|
||||
};
|
||||
return \array_map( $filter_callback, $crumbs, \array_keys( $crumbs ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modified post crumb.
|
||||
*
|
||||
* @param array $crumb The crumb.
|
||||
* @param Indexable $ancestor The indexable.
|
||||
*
|
||||
* @return array The crumb.
|
||||
*/
|
||||
private function get_post_crumb( $crumb, $ancestor ) {
|
||||
$crumb['id'] = $ancestor->object_id;
|
||||
|
||||
return $crumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modified post type crumb.
|
||||
*
|
||||
* @param array $crumb The crumb.
|
||||
* @param Indexable $ancestor The indexable.
|
||||
*
|
||||
* @return array The crumb.
|
||||
*/
|
||||
private function get_post_type_archive_crumb( $crumb, $ancestor ) {
|
||||
$crumb['ptarchive'] = $ancestor->object_sub_type;
|
||||
|
||||
return $crumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modified term crumb.
|
||||
*
|
||||
* @param array $crumb The crumb.
|
||||
* @param Indexable $ancestor The indexable.
|
||||
*
|
||||
* @return array The crumb.
|
||||
*/
|
||||
private function get_term_crumb( $crumb, $ancestor ) {
|
||||
$crumb['term_id'] = $ancestor->object_id;
|
||||
$crumb['taxonomy'] = $ancestor->object_sub_type;
|
||||
|
||||
return $crumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modified system page crumb.
|
||||
*
|
||||
* @param array $crumb The crumb.
|
||||
* @param Indexable $ancestor The indexable.
|
||||
*
|
||||
* @return array The crumb.
|
||||
*/
|
||||
private function get_system_page_crumb( $crumb, $ancestor ) {
|
||||
if ( $ancestor->object_sub_type === 'search-result' ) {
|
||||
$crumb['text'] = $this->options->get( 'breadcrumbs-searchprefix' ) . ' ' . \esc_html( \get_search_query() );
|
||||
$crumb['url'] = \get_search_link();
|
||||
}
|
||||
elseif ( $ancestor->object_sub_type === '404' ) {
|
||||
$crumb['text'] = $this->options->get( 'breadcrumbs-404crumb' );
|
||||
}
|
||||
|
||||
return $crumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modified user crumb.
|
||||
*
|
||||
* @param array $crumb The crumb.
|
||||
* @param Indexable $ancestor The indexable.
|
||||
*
|
||||
* @return array The crumb.
|
||||
*/
|
||||
private function get_user_crumb( $crumb, $ancestor ) {
|
||||
$display_name = \get_the_author_meta( 'display_name', $ancestor->object_id );
|
||||
$crumb['text'] = $this->options->get( 'breadcrumbs-archiveprefix' ) . ' ' . $display_name;
|
||||
|
||||
return $crumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modified date archive crumb.
|
||||
*
|
||||
* @param array $crumb The crumb.
|
||||
*
|
||||
* @return array The crumb.
|
||||
*/
|
||||
protected function get_date_archive_crumb( $crumb ) {
|
||||
$home_url = $this->url_helper->home();
|
||||
$prefix = $this->options->get( 'breadcrumbs-archiveprefix' );
|
||||
|
||||
if ( \is_day() ) {
|
||||
$day = \esc_html( \get_the_date() );
|
||||
$crumb['url'] = $home_url . \get_the_date( 'Y/m/d' ) . '/';
|
||||
$crumb['text'] = $prefix . ' ' . $day;
|
||||
}
|
||||
elseif ( \is_month() ) {
|
||||
$month = \esc_html( \trim( \single_month_title( ' ', false ) ) );
|
||||
$crumb['url'] = $home_url . \get_the_date( 'Y/m' ) . '/';
|
||||
$crumb['text'] = $prefix . ' ' . $month;
|
||||
}
|
||||
elseif ( \is_year() ) {
|
||||
$year = \get_the_date( 'Y' );
|
||||
$crumb['url'] = $home_url . $year . '/';
|
||||
$crumb['text'] = $prefix . ' ' . $year;
|
||||
}
|
||||
|
||||
return $crumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not a blog crumb should be added.
|
||||
*
|
||||
* @param int $page_for_posts The page for posts ID.
|
||||
* @param Meta_Tags_Context $context The meta tags context.
|
||||
*
|
||||
* @return bool Whether or not a blog crumb should be added.
|
||||
*/
|
||||
protected function should_have_blog_crumb( $page_for_posts, $context ) {
|
||||
// When there is no page configured as blog page.
|
||||
if ( \get_option( 'show_on_front' ) !== 'page' || ! $page_for_posts ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $context->indexable->object_type === 'term' ) {
|
||||
$parent = $this->get_taxonomy_post_type_parent( $context->indexable->object_sub_type );
|
||||
return $parent === 'post';
|
||||
}
|
||||
|
||||
if ( $this->options->get( 'breadcrumbs-display-blog-page' ) !== true ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// When the current page is the home page, searchpage or isn't a singular post.
|
||||
if ( \is_home() || \is_search() || ! \is_singular( 'post' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the post type parent of a given taxonomy.
|
||||
*
|
||||
* @param string $taxonomy The taxonomy.
|
||||
*
|
||||
* @return string|false The parent if it exists, false otherwise.
|
||||
*/
|
||||
protected function get_taxonomy_post_type_parent( $taxonomy ) {
|
||||
$parent = $this->options->get( 'taxonomy-' . $taxonomy . '-ptparent' );
|
||||
|
||||
if ( empty( $parent ) || (string) $parent === '0' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a crumb for the current page, if we're on an archive page or paginated post.
|
||||
*
|
||||
* @param array $crumbs The array of breadcrumbs.
|
||||
* @param Indexable $current_indexable The current indexable.
|
||||
*
|
||||
* @return array The breadcrumbs.
|
||||
*/
|
||||
protected function add_paged_crumb( array $crumbs, $current_indexable ) {
|
||||
$is_simple_page = $this->current_page_helper->is_simple_page();
|
||||
|
||||
// If we're not on a paged page do nothing.
|
||||
if ( ! $is_simple_page && ! $this->current_page_helper->is_paged() ) {
|
||||
return $crumbs;
|
||||
}
|
||||
|
||||
// If we're not on a paginated post do nothing.
|
||||
if ( $is_simple_page && $current_indexable->number_of_pages === null ) {
|
||||
return $crumbs;
|
||||
}
|
||||
|
||||
$current_page_number = $this->pagination_helper->get_current_page_number();
|
||||
if ( $current_page_number <= 1 ) {
|
||||
return $crumbs;
|
||||
}
|
||||
|
||||
$crumbs[] = [
|
||||
'text' => \sprintf(
|
||||
/* translators: %s expands to the current page number */
|
||||
\__( 'Page %s', 'wordpress-seo' ),
|
||||
$current_page_number
|
||||
),
|
||||
];
|
||||
|
||||
return $crumbs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators;
|
||||
|
||||
use Yoast\WP\SEO\Context\Meta_Tags_Context;
|
||||
|
||||
interface Generator_Interface {
|
||||
|
||||
/**
|
||||
* Returns a string, or other Thing that the associated presenter can handle.
|
||||
*
|
||||
* @param Meta_Tags_Context $context The meta tags context.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function generate( Meta_Tags_Context $context );
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators;
|
||||
|
||||
use Error;
|
||||
use Yoast\WP\SEO\Context\Meta_Tags_Context;
|
||||
use Yoast\WP\SEO\Helpers\Image_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Open_Graph\Image_Helper as Open_Graph_Image_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Options_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Url_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Values\Open_Graph\Images;
|
||||
|
||||
/**
|
||||
* Represents the generator class for the Open Graph images.
|
||||
*/
|
||||
class Open_Graph_Image_Generator implements Generator_Interface {
|
||||
|
||||
/**
|
||||
* The Open Graph image helper.
|
||||
*
|
||||
* @var Open_Graph_Image_Helper
|
||||
*/
|
||||
protected $open_graph_image;
|
||||
|
||||
/**
|
||||
* The image helper.
|
||||
*
|
||||
* @var Image_Helper
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* The URL helper.
|
||||
*
|
||||
* @var Url_Helper
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* The options helper.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* Images constructor.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param Open_Graph_Image_Helper $open_graph_image Image helper for Open Graph.
|
||||
* @param Image_Helper $image The image helper.
|
||||
* @param Options_Helper $options The options helper.
|
||||
* @param Url_Helper $url The url helper.
|
||||
*/
|
||||
public function __construct(
|
||||
Open_Graph_Image_Helper $open_graph_image,
|
||||
Image_Helper $image,
|
||||
Options_Helper $options,
|
||||
Url_Helper $url
|
||||
) {
|
||||
$this->open_graph_image = $open_graph_image;
|
||||
$this->image = $image;
|
||||
$this->options = $options;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the images for an indexable.
|
||||
*
|
||||
* For legacy reasons some plugins might expect we filter a WPSEO_Opengraph_Image object. That might cause
|
||||
* type errors. This is why we try/catch our filters.
|
||||
*
|
||||
* @param Meta_Tags_Context $context The context.
|
||||
*
|
||||
* @return array The images.
|
||||
*/
|
||||
public function generate( Meta_Tags_Context $context ) {
|
||||
$image_container = $this->get_image_container();
|
||||
$backup_image_container = $this->get_image_container();
|
||||
|
||||
try {
|
||||
/**
|
||||
* Filter: wpseo_add_opengraph_images - Allow developers to add images to the Open Graph tags.
|
||||
*
|
||||
* @api Yoast\WP\SEO\Values\Open_Graph\Images The current object.
|
||||
*/
|
||||
\apply_filters( 'wpseo_add_opengraph_images', $image_container );
|
||||
} catch ( Error $error ) {
|
||||
$image_container = $backup_image_container;
|
||||
}
|
||||
|
||||
$this->add_from_indexable( $context->indexable, $image_container );
|
||||
$backup_image_container = $image_container;
|
||||
|
||||
try {
|
||||
/**
|
||||
* Filter: wpseo_add_opengraph_additional_images - Allows to add additional images to the Open Graph tags.
|
||||
*
|
||||
* @api Yoast\WP\SEO\Values\Open_Graph\Images The current object.
|
||||
*/
|
||||
\apply_filters( 'wpseo_add_opengraph_additional_images', $image_container );
|
||||
} catch ( Error $error ) {
|
||||
$image_container = $backup_image_container;
|
||||
}
|
||||
|
||||
$this->add_from_templates( $context, $image_container );
|
||||
$this->add_from_default( $image_container );
|
||||
|
||||
return $image_container->get_images();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the images for an author archive indexable.
|
||||
*
|
||||
* This is a custom method to address the case of Author Archives, since they always have an Open Graph image
|
||||
* set in the indexable (even if it is an empty default Gravatar).
|
||||
*
|
||||
* @param Meta_Tags_Context $context The context.
|
||||
*
|
||||
* @return array The images.
|
||||
*/
|
||||
public function generate_for_author_archive( Meta_Tags_Context $context ) {
|
||||
$image_container = $this->get_image_container();
|
||||
|
||||
$this->add_from_templates( $context, $image_container );
|
||||
if ( $image_container->has_images() ) {
|
||||
return $image_container->get_images();
|
||||
}
|
||||
|
||||
return $this->generate( $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an image based on the given indexable.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
* @param Images $image_container The image container.
|
||||
*/
|
||||
protected function add_from_indexable( Indexable $indexable, Images $image_container ) {
|
||||
if ( $indexable->open_graph_image_meta ) {
|
||||
$image_container->add_image_by_meta( $indexable->open_graph_image_meta );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $indexable->open_graph_image_id ) {
|
||||
$image_container->add_image_by_id( $indexable->open_graph_image_id );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $indexable->open_graph_image ) {
|
||||
$meta_data = [];
|
||||
if ( $indexable->open_graph_image_meta && \is_string( $indexable->open_graph_image_meta ) ) {
|
||||
$meta_data = \json_decode( $indexable->open_graph_image_meta, true );
|
||||
}
|
||||
|
||||
$image_container->add_image(
|
||||
\array_merge(
|
||||
(array) $meta_data,
|
||||
[
|
||||
'url' => $indexable->open_graph_image,
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default Open Graph image.
|
||||
*
|
||||
* @param Images $image_container The image container.
|
||||
*/
|
||||
protected function add_from_default( Images $image_container ) {
|
||||
if ( $image_container->has_images() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$default_image_id = $this->options->get( 'og_default_image_id', '' );
|
||||
if ( $default_image_id ) {
|
||||
$image_container->add_image_by_id( $default_image_id );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$default_image_url = $this->options->get( 'og_default_image', '' );
|
||||
if ( $default_image_url ) {
|
||||
$image_container->add_image_by_url( $default_image_url );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default Open Graph image.
|
||||
*
|
||||
* @param Meta_Tags_Context $context The context.
|
||||
* @param Images $image_container The image container.
|
||||
*/
|
||||
protected function add_from_templates( Meta_Tags_Context $context, Images $image_container ) {
|
||||
if ( $image_container->has_images() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $context->presentation->open_graph_image_id ) {
|
||||
$image_container->add_image_by_id( $context->presentation->open_graph_image_id );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $context->presentation->open_graph_image ) {
|
||||
$image_container->add_image_by_url( $context->presentation->open_graph_image );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an instance of the image container.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return Images The image container.
|
||||
*/
|
||||
protected function get_image_container() {
|
||||
$image_container = new Images( $this->image, $this->url );
|
||||
$image_container->set_helpers( $this->open_graph_image );
|
||||
|
||||
return $image_container;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators;
|
||||
|
||||
use Yoast\WP\SEO\Context\Meta_Tags_Context;
|
||||
|
||||
/**
|
||||
* Class Open_Graph_Locale_Generator.
|
||||
*/
|
||||
class Open_Graph_Locale_Generator implements Generator_Interface {
|
||||
|
||||
/**
|
||||
* Generates the OG Locale.
|
||||
*
|
||||
* @param Meta_Tags_Context $context The context.
|
||||
*
|
||||
* @return string The OG locale.
|
||||
*/
|
||||
public function generate( Meta_Tags_Context $context ) {
|
||||
/**
|
||||
* Filter: 'wpseo_locale' - Allow changing the locale output.
|
||||
*
|
||||
* Note that this filter is different from `wpseo_og_locale`, which is run _after_ the OG specific filtering.
|
||||
*
|
||||
* @api string Locale string.
|
||||
*/
|
||||
$locale = \apply_filters( 'wpseo_locale', \get_locale() );
|
||||
|
||||
// Catch some weird locales served out by WP that are not easily doubled up.
|
||||
$fix_locales = [
|
||||
'ca' => 'ca_ES',
|
||||
'en' => 'en_US',
|
||||
'el' => 'el_GR',
|
||||
'et' => 'et_EE',
|
||||
'ja' => 'ja_JP',
|
||||
'sq' => 'sq_AL',
|
||||
'uk' => 'uk_UA',
|
||||
'vi' => 'vi_VN',
|
||||
'zh' => 'zh_CN',
|
||||
];
|
||||
|
||||
if ( isset( $fix_locales[ $locale ] ) ) {
|
||||
return $fix_locales[ $locale ];
|
||||
}
|
||||
|
||||
// Convert locales like "es" to "es_ES", in case that works for the given locale (sometimes it does).
|
||||
if ( \strlen( $locale ) === 2 ) {
|
||||
$locale = \strtolower( $locale ) . '_' . \strtoupper( $locale );
|
||||
}
|
||||
|
||||
// These are the locales FB supports.
|
||||
$fb_valid_fb_locales = [
|
||||
'af_ZA', // Afrikaans.
|
||||
'ak_GH', // Akan.
|
||||
'am_ET', // Amharic.
|
||||
'ar_AR', // Arabic.
|
||||
'as_IN', // Assamese.
|
||||
'ay_BO', // Aymara.
|
||||
'az_AZ', // Azerbaijani.
|
||||
'be_BY', // Belarusian.
|
||||
'bg_BG', // Bulgarian.
|
||||
'bp_IN', // Bhojpuri.
|
||||
'bn_IN', // Bengali.
|
||||
'br_FR', // Breton.
|
||||
'bs_BA', // Bosnian.
|
||||
'ca_ES', // Catalan.
|
||||
'cb_IQ', // Sorani Kurdish.
|
||||
'ck_US', // Cherokee.
|
||||
'co_FR', // Corsican.
|
||||
'cs_CZ', // Czech.
|
||||
'cx_PH', // Cebuano.
|
||||
'cy_GB', // Welsh.
|
||||
'da_DK', // Danish.
|
||||
'de_DE', // German.
|
||||
'el_GR', // Greek.
|
||||
'en_GB', // English (UK).
|
||||
'en_PI', // English (Pirate).
|
||||
'en_UD', // English (Upside Down).
|
||||
'en_US', // English (US).
|
||||
'em_ZM',
|
||||
'eo_EO', // Esperanto.
|
||||
'es_ES', // Spanish (Spain).
|
||||
'es_LA', // Spanish.
|
||||
'es_MX', // Spanish (Mexico).
|
||||
'et_EE', // Estonian.
|
||||
'eu_ES', // Basque.
|
||||
'fa_IR', // Persian.
|
||||
'fb_LT', // Leet Speak.
|
||||
'ff_NG', // Fulah.
|
||||
'fi_FI', // Finnish.
|
||||
'fo_FO', // Faroese.
|
||||
'fr_CA', // French (Canada).
|
||||
'fr_FR', // French (France).
|
||||
'fy_NL', // Frisian.
|
||||
'ga_IE', // Irish.
|
||||
'gl_ES', // Galician.
|
||||
'gn_PY', // Guarani.
|
||||
'gu_IN', // Gujarati.
|
||||
'gx_GR', // Classical Greek.
|
||||
'ha_NG', // Hausa.
|
||||
'he_IL', // Hebrew.
|
||||
'hi_IN', // Hindi.
|
||||
'hr_HR', // Croatian.
|
||||
'hu_HU', // Hungarian.
|
||||
'ht_HT', // Haitian Creole.
|
||||
'hy_AM', // Armenian.
|
||||
'id_ID', // Indonesian.
|
||||
'ig_NG', // Igbo.
|
||||
'is_IS', // Icelandic.
|
||||
'it_IT', // Italian.
|
||||
'ik_US',
|
||||
'iu_CA',
|
||||
'ja_JP', // Japanese.
|
||||
'ja_KS', // Japanese (Kansai).
|
||||
'jv_ID', // Javanese.
|
||||
'ka_GE', // Georgian.
|
||||
'kk_KZ', // Kazakh.
|
||||
'km_KH', // Khmer.
|
||||
'kn_IN', // Kannada.
|
||||
'ko_KR', // Korean.
|
||||
'ks_IN', // Kashmiri.
|
||||
'ku_TR', // Kurdish (Kurmanji).
|
||||
'ky_KG', // Kyrgyz.
|
||||
'la_VA', // Latin.
|
||||
'lg_UG', // Ganda.
|
||||
'li_NL', // Limburgish.
|
||||
'ln_CD', // Lingala.
|
||||
'lo_LA', // Lao.
|
||||
'lt_LT', // Lithuanian.
|
||||
'lv_LV', // Latvian.
|
||||
'mg_MG', // Malagasy.
|
||||
'mi_NZ', // Maori.
|
||||
'mk_MK', // Macedonian.
|
||||
'ml_IN', // Malayalam.
|
||||
'mn_MN', // Mongolian.
|
||||
'mr_IN', // Marathi.
|
||||
'ms_MY', // Malay.
|
||||
'mt_MT', // Maltese.
|
||||
'my_MM', // Burmese.
|
||||
'nb_NO', // Norwegian (bokmal).
|
||||
'nd_ZW', // Ndebele.
|
||||
'ne_NP', // Nepali.
|
||||
'nl_BE', // Dutch (Belgie).
|
||||
'nl_NL', // Dutch.
|
||||
'nn_NO', // Norwegian (nynorsk).
|
||||
'nr_ZA', // Southern Ndebele.
|
||||
'ns_ZA', // Northern Sotho.
|
||||
'ny_MW', // Chewa.
|
||||
'om_ET', // Oromo.
|
||||
'or_IN', // Oriya.
|
||||
'pa_IN', // Punjabi.
|
||||
'pl_PL', // Polish.
|
||||
'ps_AF', // Pashto.
|
||||
'pt_BR', // Portuguese (Brazil).
|
||||
'pt_PT', // Portuguese (Portugal).
|
||||
'qc_GT', // Quiché.
|
||||
'qu_PE', // Quechua.
|
||||
'qr_GR',
|
||||
'qz_MM', // Burmese (Zawgyi).
|
||||
'rm_CH', // Romansh.
|
||||
'ro_RO', // Romanian.
|
||||
'ru_RU', // Russian.
|
||||
'rw_RW', // Kinyarwanda.
|
||||
'sa_IN', // Sanskrit.
|
||||
'sc_IT', // Sardinian.
|
||||
'se_NO', // Northern Sami.
|
||||
'si_LK', // Sinhala.
|
||||
'su_ID', // Sundanese.
|
||||
'sk_SK', // Slovak.
|
||||
'sl_SI', // Slovenian.
|
||||
'sn_ZW', // Shona.
|
||||
'so_SO', // Somali.
|
||||
'sq_AL', // Albanian.
|
||||
'sr_RS', // Serbian.
|
||||
'ss_SZ', // Swazi.
|
||||
'st_ZA', // Southern Sotho.
|
||||
'sv_SE', // Swedish.
|
||||
'sw_KE', // Swahili.
|
||||
'sy_SY', // Syriac.
|
||||
'sz_PL', // Silesian.
|
||||
'ta_IN', // Tamil.
|
||||
'te_IN', // Telugu.
|
||||
'tg_TJ', // Tajik.
|
||||
'th_TH', // Thai.
|
||||
'tk_TM', // Turkmen.
|
||||
'tl_PH', // Filipino.
|
||||
'tl_ST', // Klingon.
|
||||
'tn_BW', // Tswana.
|
||||
'tr_TR', // Turkish.
|
||||
'ts_ZA', // Tsonga.
|
||||
'tt_RU', // Tatar.
|
||||
'tz_MA', // Tamazight.
|
||||
'uk_UA', // Ukrainian.
|
||||
'ur_PK', // Urdu.
|
||||
'uz_UZ', // Uzbek.
|
||||
've_ZA', // Venda.
|
||||
'vi_VN', // Vietnamese.
|
||||
'wo_SN', // Wolof.
|
||||
'xh_ZA', // Xhosa.
|
||||
'yi_DE', // Yiddish.
|
||||
'yo_NG', // Yoruba.
|
||||
'zh_CN', // Simplified Chinese (China).
|
||||
'zh_HK', // Traditional Chinese (Hong Kong).
|
||||
'zh_TW', // Traditional Chinese (Taiwan).
|
||||
'zu_ZA', // Zulu.
|
||||
'zz_TR', // Zazaki.
|
||||
];
|
||||
|
||||
// Check to see if the locale is a valid FB one, if not, use en_US as a fallback.
|
||||
if ( \in_array( $locale, $fb_valid_fb_locales, true ) ) {
|
||||
return $locale;
|
||||
}
|
||||
|
||||
$locale = \strtolower( \substr( $locale, 0, 2 ) ) . '_' . \strtoupper( \substr( $locale, 0, 2 ) );
|
||||
if ( ! \in_array( $locale, $fb_valid_fb_locales, true ) ) {
|
||||
return 'en_US';
|
||||
}
|
||||
|
||||
return $locale;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators;
|
||||
|
||||
use WP_Block_Parser_Block;
|
||||
use Yoast\WP\SEO\Context\Meta_Tags_Context;
|
||||
use Yoast\WP\SEO\Generators\Schema\Abstract_Schema_Piece;
|
||||
use Yoast\WP\SEO\Helpers\Schema\Replace_Vars_Helper;
|
||||
use Yoast\WP\SEO\Surfaces\Helpers_Surface;
|
||||
|
||||
/**
|
||||
* Class Schema_Generator.
|
||||
*/
|
||||
class Schema_Generator implements Generator_Interface {
|
||||
|
||||
/**
|
||||
* The helpers surface.
|
||||
*
|
||||
* @var Helpers_Surface
|
||||
*/
|
||||
protected $helpers;
|
||||
|
||||
/**
|
||||
* The Schema replace vars helper.
|
||||
*
|
||||
* @var Replace_Vars_Helper
|
||||
*/
|
||||
protected $schema_replace_vars_helper;
|
||||
|
||||
/**
|
||||
* Generator constructor.
|
||||
*
|
||||
* @param Helpers_Surface $helpers The helpers surface.
|
||||
* @param Replace_Vars_Helper $schema_replace_vars_helper The replace vars helper.
|
||||
*/
|
||||
public function __construct(
|
||||
Helpers_Surface $helpers,
|
||||
Replace_Vars_Helper $schema_replace_vars_helper
|
||||
) {
|
||||
$this->helpers = $helpers;
|
||||
$this->schema_replace_vars_helper = $schema_replace_vars_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Schema graph array.
|
||||
*
|
||||
* @param Meta_Tags_Context $context The meta tags context.
|
||||
*
|
||||
* @return array The graph.
|
||||
*/
|
||||
public function generate( Meta_Tags_Context $context ) {
|
||||
$pieces = $this->get_graph_pieces( $context );
|
||||
|
||||
$this->schema_replace_vars_helper->register_replace_vars( $context );
|
||||
|
||||
foreach ( \array_keys( $context->blocks ) as $block_type ) {
|
||||
/**
|
||||
* Filter: 'wpseo_pre_schema_block_type_<block-type>' - Allows hooking things to change graph output based on the blocks on the page.
|
||||
*
|
||||
* @param string $block_type The block type.
|
||||
* @param WP_Block_Parser_Block[] $blocks All the blocks of this block type.
|
||||
* @param Meta_Tags_Context $context A value object with context variables.
|
||||
*/
|
||||
\do_action( 'wpseo_pre_schema_block_type_' . $block_type, $context->blocks[ $block_type ], $context );
|
||||
}
|
||||
|
||||
// Do a loop before everything else to inject the context and helpers.
|
||||
foreach ( $pieces as $piece ) {
|
||||
if ( \is_a( $piece, Abstract_Schema_Piece::class ) ) {
|
||||
$piece->context = $context;
|
||||
$piece->helpers = $this->helpers;
|
||||
}
|
||||
}
|
||||
|
||||
$pieces_to_generate = $this->filter_graph_pieces_to_generate( $pieces );
|
||||
$graph = $this->generate_graph( $pieces_to_generate, $context );
|
||||
$graph = $this->add_schema_blocks_graph_pieces( $graph, $context );
|
||||
$graph = $this->finalize_graph( $graph, $context );
|
||||
|
||||
return [
|
||||
'@context' => 'https://schema.org',
|
||||
'@graph' => $graph,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out any graph pieces that should not be generated.
|
||||
* (Using the `wpseo_schema_needs_<graph_piece_identifier>` series of filters).
|
||||
*
|
||||
* @param array $graph_pieces The current list of graph pieces that we want to generate.
|
||||
*
|
||||
* @return array The graph pieces to generate.
|
||||
*/
|
||||
protected function filter_graph_pieces_to_generate( $graph_pieces ) {
|
||||
$pieces_to_generate = [];
|
||||
foreach ( $graph_pieces as $piece ) {
|
||||
$identifier = \strtolower( \str_replace( 'Yoast\WP\SEO\Generators\Schema\\', '', \get_class( $piece ) ) );
|
||||
if ( isset( $piece->identifier ) ) {
|
||||
$identifier = $piece->identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter: 'wpseo_schema_needs_<identifier>' - Allows changing which graph pieces we output.
|
||||
*
|
||||
* @api bool $is_needed Whether or not to show a graph piece.
|
||||
*/
|
||||
$is_needed = \apply_filters( 'wpseo_schema_needs_' . $identifier, $piece->is_needed() );
|
||||
if ( ! $is_needed ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pieces_to_generate[ $identifier ] = $piece;
|
||||
}
|
||||
|
||||
return $pieces_to_generate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the schema graph.
|
||||
*
|
||||
* @param array $graph_piece_generators The schema graph pieces to generate.
|
||||
* @param Meta_Tags_Context $context The meta tags context to use.
|
||||
*
|
||||
* @return array The generated schema graph.
|
||||
*/
|
||||
protected function generate_graph( $graph_piece_generators, $context ) {
|
||||
$graph = [];
|
||||
foreach ( $graph_piece_generators as $identifier => $graph_piece_generator ) {
|
||||
$graph_pieces = $graph_piece_generator->generate();
|
||||
// If only a single graph piece was returned.
|
||||
if ( $graph_pieces !== false && \array_key_exists( '@type', $graph_pieces ) ) {
|
||||
$graph_pieces = [ $graph_pieces ];
|
||||
}
|
||||
|
||||
if ( ! \is_array( $graph_pieces ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $graph_pieces as $graph_piece ) {
|
||||
/**
|
||||
* Filter: 'wpseo_schema_<identifier>' - Allows changing graph piece output.
|
||||
* This filter can be called with either an identifier or a block type (see `add_schema_blocks_graph_pieces()`).
|
||||
*
|
||||
* @api array $graph_piece The graph piece to filter.
|
||||
*
|
||||
* @param Meta_Tags_Context $context A value object with context variables.
|
||||
* @param Abstract_Schema_Piece $graph_piece_generator A value object with context variables.
|
||||
* @param Abstract_Schema_Piece[] $graph_piece_generators A value object with context variables.
|
||||
*/
|
||||
$graph_piece = \apply_filters( 'wpseo_schema_' . $identifier, $graph_piece, $context, $graph_piece_generator, $graph_piece_generators );
|
||||
$graph_piece = $this->type_filter( $graph_piece, $identifier, $context, $graph_piece_generator, $graph_piece_generators );
|
||||
$graph_piece = $this->validate_type( $graph_piece );
|
||||
|
||||
if ( \is_array( $graph_piece ) ) {
|
||||
$graph[] = $graph_piece;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter: 'wpseo_schema_graph' - Allows changing graph output.
|
||||
*
|
||||
* @api array $graph The graph to filter.
|
||||
*
|
||||
* @param Meta_Tags_Context $context A value object with context variables.
|
||||
*/
|
||||
$graph = \apply_filters( 'wpseo_schema_graph', $graph, $context );
|
||||
|
||||
return $graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds schema graph pieces from Gutenberg blocks on the current page to
|
||||
* the given schema graph.
|
||||
*
|
||||
* Think of blocks like the Yoast FAQ block or the How To block.
|
||||
*
|
||||
* @param array $graph The current schema graph.
|
||||
* @param Meta_Tags_Context $context The meta tags context.
|
||||
*
|
||||
* @return array The graph with the schema blocks graph pieces added.
|
||||
*/
|
||||
protected function add_schema_blocks_graph_pieces( $graph, $context ) {
|
||||
foreach ( $context->blocks as $block_type => $blocks ) {
|
||||
foreach ( $blocks as $block ) {
|
||||
$block_type = \strtolower( $block['blockName'] );
|
||||
|
||||
/**
|
||||
* Filter: 'wpseo_schema_block_<block-type>'.
|
||||
* This filter is documented in the `generate_graph()` function in this class.
|
||||
*/
|
||||
$graph = \apply_filters( 'wpseo_schema_block_' . $block_type, $graph, $block, $context );
|
||||
|
||||
if ( isset( $block['attrs']['yoast-schema'] ) ) {
|
||||
$graph[] = $this->schema_replace_vars_helper->replace( $block['attrs']['yoast-schema'], $context->presentation );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes the schema graph after all filtering is done.
|
||||
*
|
||||
* @param array $graph The current schema graph.
|
||||
* @param Meta_Tags_Context $context The meta tags context.
|
||||
*
|
||||
* @return array The schema graph.
|
||||
*/
|
||||
protected function finalize_graph( $graph, $context ) {
|
||||
$graph = $this->remove_empty_breadcrumb( $graph, $context );
|
||||
|
||||
return $graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the breadcrumb schema if empty.
|
||||
*
|
||||
* @param array $graph The current schema graph.
|
||||
* @param Meta_Tags_Context $context The meta tags context.
|
||||
*
|
||||
* @return array The schema graph with empty breadcrumbs taken out.
|
||||
*/
|
||||
protected function remove_empty_breadcrumb( $graph, $context ) {
|
||||
if ( $this->helpers->current_page->is_home_static_page() || $this->helpers->current_page->is_home_posts_page() ) {
|
||||
return $graph;
|
||||
}
|
||||
|
||||
// Remove the breadcrumb piece, if it's empty.
|
||||
$index_to_remove = 0;
|
||||
foreach ( $graph as $key => $piece ) {
|
||||
if ( \in_array( 'BreadcrumbList', $this->get_type_from_piece( $piece ), true ) ) {
|
||||
if ( isset( $piece['itemListElement'] ) && \is_array( $piece['itemListElement'] ) && \count( $piece['itemListElement'] ) === 1 ) {
|
||||
$index_to_remove = $key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the breadcrumb piece has been removed, we should remove its reference from the WebPage node.
|
||||
if ( $index_to_remove !== 0 ) {
|
||||
\array_splice( $graph, $index_to_remove, 1 );
|
||||
|
||||
// Get the type of the WebPage node.
|
||||
$webpage_types = \is_array( $context->schema_page_type ) ? $context->schema_page_type : [ $context->schema_page_type ];
|
||||
|
||||
foreach ( $graph as $key => $piece ) {
|
||||
if ( ! empty( \array_intersect( $webpage_types, $this->get_type_from_piece( $piece ) ) ) && isset( $piece['breadcrumb'] ) ) {
|
||||
unset( $piece['breadcrumb'] );
|
||||
$graph[ $key ] = $piece;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts the WebPage graph piece for password-protected posts.
|
||||
*
|
||||
* It should only have certain whitelisted properties.
|
||||
* The type should always be WebPage.
|
||||
*
|
||||
* @param array $graph_piece The WebPage graph piece that should be adapted for password-protected posts.
|
||||
*
|
||||
* @return array The WebPage graph piece that has been adapted for password-protected posts.
|
||||
*/
|
||||
public function protected_webpage_schema( $graph_piece ) {
|
||||
$properties_to_show = \array_flip(
|
||||
[
|
||||
'@type',
|
||||
'@id',
|
||||
'url',
|
||||
'name',
|
||||
'isPartOf',
|
||||
'inLanguage',
|
||||
'datePublished',
|
||||
'dateModified',
|
||||
'breadcrumb',
|
||||
]
|
||||
);
|
||||
|
||||
$graph_piece = \array_intersect_key( $graph_piece, $properties_to_show );
|
||||
$graph_piece['@type'] = 'WebPage';
|
||||
|
||||
return $graph_piece;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the graph pieces we need.
|
||||
*
|
||||
* @param Meta_Tags_Context $context The meta tags context.
|
||||
*
|
||||
* @return Abstract_Schema_Piece[] A filtered array of graph pieces.
|
||||
*/
|
||||
protected function get_graph_pieces( $context ) {
|
||||
if ( $context->indexable->object_type === 'post' && \post_password_required( $context->post ) ) {
|
||||
$schema_pieces = [
|
||||
new Schema\WebPage(),
|
||||
new Schema\Website(),
|
||||
new Schema\Organization(),
|
||||
];
|
||||
|
||||
\add_filter( 'wpseo_schema_webpage', [ $this, 'protected_webpage_schema' ], 1 );
|
||||
}
|
||||
else {
|
||||
$schema_pieces = [
|
||||
new Schema\Article(),
|
||||
new Schema\WebPage(),
|
||||
new Schema\Main_Image(),
|
||||
new Schema\Breadcrumb(),
|
||||
new Schema\Website(),
|
||||
new Schema\Organization(),
|
||||
new Schema\Person(),
|
||||
new Schema\Author(),
|
||||
new Schema\FAQ(),
|
||||
new Schema\HowTo(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter: 'wpseo_schema_graph_pieces' - Allows adding pieces to the graph.
|
||||
*
|
||||
* @param Meta_Tags_Context $context An object with context variables.
|
||||
*
|
||||
* @api array $pieces The schema pieces.
|
||||
*/
|
||||
return \apply_filters( 'wpseo_schema_graph_pieces', $schema_pieces, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows filtering the graph piece by its schema type.
|
||||
*
|
||||
* Note: We removed the Abstract_Schema_Piece type-hint from the $graph_piece_generator argument, because
|
||||
* it caused conflicts with old code, Yoast SEO Video specifically.
|
||||
*
|
||||
* @param array $graph_piece The graph piece we're filtering.
|
||||
* @param string $identifier The identifier of the graph piece that is being filtered.
|
||||
* @param Meta_Tags_Context $context The meta tags context.
|
||||
* @param Abstract_Schema_Piece $graph_piece_generator A value object with context variables.
|
||||
* @param Abstract_Schema_Piece[] $graph_piece_generators A value object with context variables.
|
||||
*
|
||||
* @return array The filtered graph piece.
|
||||
*/
|
||||
private function type_filter( $graph_piece, $identifier, Meta_Tags_Context $context, $graph_piece_generator, array $graph_piece_generators ) {
|
||||
$types = $this->get_type_from_piece( $graph_piece );
|
||||
foreach ( $types as $type ) {
|
||||
$type = \strtolower( $type );
|
||||
|
||||
// Prevent running the same filter twice. This makes sure we run f/i. for 'author' and for 'person'.
|
||||
if ( $type && $type !== $identifier ) {
|
||||
/**
|
||||
* Filter: 'wpseo_schema_<type>' - Allows changing graph piece output by @type.
|
||||
*
|
||||
* @api array $graph_piece The graph piece to filter.
|
||||
*
|
||||
* @param Meta_Tags_Context $context A value object with context variables.
|
||||
* @param Abstract_Schema_Piece $graph_piece_generator A value object with context variables.
|
||||
* @param Abstract_Schema_Piece[] $graph_piece_generators A value object with context variables.
|
||||
*/
|
||||
$graph_piece = \apply_filters( 'wpseo_schema_' . $type, $graph_piece, $context, $graph_piece_generator, $graph_piece_generators );
|
||||
}
|
||||
}
|
||||
|
||||
return $graph_piece;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the type from a graph piece.
|
||||
*
|
||||
* @param array $piece The graph piece.
|
||||
*
|
||||
* @return array An array of the piece's types.
|
||||
*/
|
||||
private function get_type_from_piece( $piece ) {
|
||||
if ( isset( $piece['@type'] ) ) {
|
||||
if ( \is_array( $piece['@type'] ) ) {
|
||||
// Return as-is, but remove unusable values, like sub-arrays, objects, null.
|
||||
return \array_filter( $piece['@type'], 'is_string' );
|
||||
}
|
||||
|
||||
return [ $piece['@type'] ];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a graph piece's type.
|
||||
*
|
||||
* When the type is an array:
|
||||
* - Ensure the values are unique.
|
||||
* - Only 1 value? Use that value without the array wrapping.
|
||||
*
|
||||
* @param array $piece The graph piece.
|
||||
*
|
||||
* @return array The graph piece.
|
||||
*/
|
||||
private function validate_type( $piece ) {
|
||||
if ( ! isset( $piece['@type'] ) ) {
|
||||
// No type to validate.
|
||||
return $piece;
|
||||
}
|
||||
|
||||
// If it is not an array, we can return immediately.
|
||||
if ( ! \is_array( $piece['@type'] ) ) {
|
||||
return $piece;
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure the types are unique.
|
||||
* Use array_values to reset the indices (e.g. no 0, 2 because 1 was a duplicate).
|
||||
*/
|
||||
$piece['@type'] = \array_values( \array_unique( $piece['@type'] ) );
|
||||
|
||||
// Use the first value if there is only 1 type.
|
||||
if ( \count( $piece['@type'] ) === 1 ) {
|
||||
$piece['@type'] = \reset( $piece['@type'] );
|
||||
}
|
||||
|
||||
return $piece;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators\Schema;
|
||||
|
||||
use Yoast\WP\SEO\Context\Meta_Tags_Context;
|
||||
use Yoast\WP\SEO\Surfaces\Helpers_Surface;
|
||||
|
||||
/**
|
||||
* Class Abstract_Schema_Piece.
|
||||
*/
|
||||
abstract class Abstract_Schema_Piece {
|
||||
|
||||
/**
|
||||
* The meta tags context.
|
||||
*
|
||||
* @var Meta_Tags_Context
|
||||
*/
|
||||
public $context;
|
||||
|
||||
/**
|
||||
* The helpers surface
|
||||
*
|
||||
* @var Helpers_Surface
|
||||
*/
|
||||
public $helpers;
|
||||
|
||||
/**
|
||||
* Optional identifier for this schema piece.
|
||||
*
|
||||
* Used in the `Schema_Generator::filter_graph_pieces_to_generate()` method.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $identifier;
|
||||
|
||||
/**
|
||||
* Generates the schema piece.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function generate();
|
||||
|
||||
/**
|
||||
* Determines whether the schema piece is needed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function is_needed();
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators\Schema;
|
||||
|
||||
use WP_User;
|
||||
use Yoast\WP\SEO\Config\Schema_IDs;
|
||||
|
||||
/**
|
||||
* Returns schema Article data.
|
||||
*/
|
||||
class Article extends Abstract_Schema_Piece {
|
||||
|
||||
/**
|
||||
* Determines whether or not a piece should be added to the graph.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_needed() {
|
||||
if ( $this->context->indexable->object_type !== 'post' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we cannot output a publisher, we shouldn't output an Article.
|
||||
if ( $this->context->site_represents === false ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we cannot output an author, we shouldn't output an Article.
|
||||
if ( ! $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->context->schema_article_type !== 'None' ) {
|
||||
$this->context->has_article = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Article data.
|
||||
*
|
||||
* @return array Article data.
|
||||
*/
|
||||
public function generate() {
|
||||
$author = \get_userdata( $this->context->post->post_author );
|
||||
$data = [
|
||||
'@type' => $this->context->schema_article_type,
|
||||
'@id' => $this->context->canonical . Schema_IDs::ARTICLE_HASH,
|
||||
'isPartOf' => [ '@id' => $this->context->main_schema_id ],
|
||||
'author' => [
|
||||
'name' => ( $author instanceof WP_User ) ? $this->helpers->schema->html->smart_strip_tags( $author->display_name ) : '',
|
||||
'@id' => $this->helpers->schema->id->get_user_schema_id( $this->context->post->post_author, $this->context ),
|
||||
],
|
||||
'headline' => $this->helpers->schema->html->smart_strip_tags( $this->helpers->post->get_post_title_with_fallback( $this->context->id ) ),
|
||||
'datePublished' => $this->helpers->date->format( $this->context->post->post_date_gmt ),
|
||||
'dateModified' => $this->helpers->date->format( $this->context->post->post_modified_gmt ),
|
||||
'mainEntityOfPage' => [ '@id' => $this->context->main_schema_id ],
|
||||
'wordCount' => $this->word_count( $this->context->post->post_content, $this->context->post->post_title ),
|
||||
];
|
||||
|
||||
if ( $this->context->post->comment_status === 'open' ) {
|
||||
$data['commentCount'] = \intval( $this->context->post->comment_count, 10 );
|
||||
}
|
||||
|
||||
if ( $this->context->site_represents_reference ) {
|
||||
$data['publisher'] = $this->context->site_represents_reference;
|
||||
}
|
||||
|
||||
$data = $this->add_image( $data );
|
||||
$data = $this->add_keywords( $data );
|
||||
$data = $this->add_sections( $data );
|
||||
$data = $this->helpers->schema->language->add_piece_language( $data );
|
||||
|
||||
if ( \post_type_supports( $this->context->post->post_type, 'comments' ) && $this->context->post->comment_status === 'open' ) {
|
||||
$data = $this->add_potential_action( $data );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds tags as keywords, if tags are assigned.
|
||||
*
|
||||
* @param array $data Article data.
|
||||
*
|
||||
* @return array Article data.
|
||||
*/
|
||||
private function add_keywords( $data ) {
|
||||
/**
|
||||
* Filter: 'wpseo_schema_article_keywords_taxonomy' - Allow changing the taxonomy used to assign keywords to a post type Article data.
|
||||
*
|
||||
* @api string $taxonomy The chosen taxonomy.
|
||||
*/
|
||||
$taxonomy = \apply_filters( 'wpseo_schema_article_keywords_taxonomy', 'post_tag' );
|
||||
|
||||
return $this->add_terms( $data, 'keywords', $taxonomy );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds categories as sections, if categories are assigned.
|
||||
*
|
||||
* @param array $data Article data.
|
||||
*
|
||||
* @return array Article data.
|
||||
*/
|
||||
private function add_sections( $data ) {
|
||||
/**
|
||||
* Filter: 'wpseo_schema_article_sections_taxonomy' - Allow changing the taxonomy used to assign keywords to a post type Article data.
|
||||
*
|
||||
* @api string $taxonomy The chosen taxonomy.
|
||||
*/
|
||||
$taxonomy = \apply_filters( 'wpseo_schema_article_sections_taxonomy', 'category' );
|
||||
|
||||
return $this->add_terms( $data, 'articleSection', $taxonomy );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a term or multiple terms, comma separated, to a field.
|
||||
*
|
||||
* @param array $data Article data.
|
||||
* @param string $key The key in data to save the terms in.
|
||||
* @param string $taxonomy The taxonomy to retrieve the terms from.
|
||||
*
|
||||
* @return mixed Article data.
|
||||
*/
|
||||
protected function add_terms( $data, $key, $taxonomy ) {
|
||||
$terms = \get_the_terms( $this->context->id, $taxonomy );
|
||||
|
||||
if ( ! \is_array( $terms ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$callback = static function( $term ) {
|
||||
// We are using the WordPress internal translation.
|
||||
return $term->name !== \__( 'Uncategorized', 'default' );
|
||||
};
|
||||
$terms = \array_filter( $terms, $callback );
|
||||
|
||||
if ( empty( $terms ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$data[ $key ] = \wp_list_pluck( $terms, 'name' );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an image node if the post has a featured image.
|
||||
*
|
||||
* @param array $data The Article data.
|
||||
*
|
||||
* @return array The Article data.
|
||||
*/
|
||||
private function add_image( $data ) {
|
||||
if ( $this->context->main_image_url !== null ) {
|
||||
$data['image'] = [
|
||||
'@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH,
|
||||
];
|
||||
$data['thumbnailUrl'] = $this->context->main_image_url;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the potential action property to the Article Schema piece.
|
||||
*
|
||||
* @param array $data The Article data.
|
||||
*
|
||||
* @return array The Article data with the potential action added.
|
||||
*/
|
||||
private function add_potential_action( $data ) {
|
||||
/**
|
||||
* Filter: 'wpseo_schema_article_potential_action_target' - Allows filtering of the schema Article potentialAction target.
|
||||
*
|
||||
* @api array $targets The URLs for the Article potentialAction target.
|
||||
*/
|
||||
$targets = \apply_filters( 'wpseo_schema_article_potential_action_target', [ $this->context->canonical . '#respond' ] );
|
||||
|
||||
$data['potentialAction'][] = [
|
||||
'@type' => 'CommentAction',
|
||||
'name' => 'Comment',
|
||||
'target' => $targets,
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a simple word count but tries to be relatively smart about it.
|
||||
*
|
||||
* @param string $post_content The post content.
|
||||
* @param string $post_title The post title.
|
||||
*
|
||||
* @return int The number of words in the content.
|
||||
*/
|
||||
private function word_count( $post_content, $post_title = '' ) {
|
||||
// Add the title to our word count.
|
||||
$post_content = $post_title . ' ' . $post_content;
|
||||
|
||||
// Strip pre/code blocks and their content.
|
||||
$post_content = \preg_replace( '@<(pre|code)[^>]*?>.*?</\\1>@si', '', $post_content );
|
||||
|
||||
// Add space between tags that don't have it.
|
||||
$post_content = \preg_replace( '@><@', '> <', $post_content );
|
||||
|
||||
// Strips all other tags.
|
||||
$post_content = \wp_strip_all_tags( $post_content );
|
||||
|
||||
$characters = '';
|
||||
|
||||
if ( \preg_match( '@[а-я]@ui', $post_content ) ) {
|
||||
// Correct counting of the number of words in the Russian and Ukrainian languages.
|
||||
$alphabet = [
|
||||
'ru' => 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя',
|
||||
'ua' => 'абвгґдеєжзиіїйклмнопрстуфхцчшщьюя',
|
||||
];
|
||||
|
||||
$characters = \implode( '', $alphabet );
|
||||
$characters = \preg_split( '//u', $characters, -1, \PREG_SPLIT_NO_EMPTY );
|
||||
$characters = \array_unique( $characters );
|
||||
$characters = \implode( '', $characters );
|
||||
$characters .= \mb_strtoupper( $characters );
|
||||
}
|
||||
|
||||
// Remove characters from HTML entities.
|
||||
$post_content = \preg_replace( '@&[a-z0-9]+;@i', ' ', \htmlentities( $post_content ) );
|
||||
|
||||
return \str_word_count( $post_content, 0, $characters );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators\Schema;
|
||||
|
||||
/**
|
||||
* Returns schema Author data.
|
||||
*/
|
||||
class Author extends Person {
|
||||
|
||||
/**
|
||||
* Determine whether we should return Person schema.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_needed() {
|
||||
if ( $this->context->indexable->object_type === 'user' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->context->indexable->object_type === 'post'
|
||||
&& $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type )
|
||||
&& $this->context->schema_article_type !== 'None'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Person Schema data.
|
||||
*
|
||||
* @return bool|array Person data on success, false on failure.
|
||||
*/
|
||||
public function generate() {
|
||||
$user_id = $this->determine_user_id();
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $this->build_person_data( $user_id );
|
||||
|
||||
if ( $this->site_represents_current_author() === false ) {
|
||||
$data['@type'] = [ 'Person' ];
|
||||
unset( $data['logo'] );
|
||||
}
|
||||
|
||||
// If this is an author page, the Person object is the main object, so we set it as such here.
|
||||
if ( $this->context->indexable->object_type === 'user' ) {
|
||||
$data['mainEntityOfPage'] = [
|
||||
'@id' => $this->context->main_schema_id,
|
||||
];
|
||||
}
|
||||
|
||||
// If this is a post and the author archives are enabled, set the author archive url as the author url.
|
||||
if ( $this->context->indexable->object_type === 'post' ) {
|
||||
if ( $this->helpers->options->get( 'disable-author' ) !== true ) {
|
||||
$data['url'] = $this->helpers->user->get_the_author_posts_url( $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines a User ID for the Person data.
|
||||
*
|
||||
* @return bool|int User ID or false upon return.
|
||||
*/
|
||||
protected function determine_user_id() {
|
||||
$user_id = 0;
|
||||
|
||||
if ( $this->context->indexable->object_type === 'post' ) {
|
||||
$user_id = (int) $this->context->post->post_author;
|
||||
}
|
||||
|
||||
if ( $this->context->indexable->object_type === 'user' ) {
|
||||
$user_id = $this->context->indexable->object_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter: 'wpseo_schema_person_user_id' - Allows filtering of user ID used for person output.
|
||||
*
|
||||
* @api int|bool $user_id The user ID currently determined.
|
||||
*/
|
||||
$user_id = \apply_filters( 'wpseo_schema_person_user_id', $user_id );
|
||||
|
||||
if ( \is_int( $user_id ) && $user_id > 0 ) {
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* An author should not have an image from options, this only applies to persons.
|
||||
*
|
||||
* @param array $data The Person schema.
|
||||
* @param string $schema_id The string used in the `@id` for the schema.
|
||||
* @param bool $add_hash Whether or not the person's image url hash should be added to the image id.
|
||||
* @param WP_User $user_data User data.
|
||||
*
|
||||
* @return array The Person schema.
|
||||
*/
|
||||
protected function set_image_from_options( $data, $schema_id, $add_hash = false, $user_data = null ) {
|
||||
if ( $this->site_represents_current_author( $user_data ) ) {
|
||||
return parent::set_image_from_options( $data, $schema_id, $add_hash, $user_data );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators\Schema;
|
||||
|
||||
use Yoast\WP\SEO\Config\Schema_IDs;
|
||||
|
||||
/**
|
||||
* Returns schema Breadcrumb data.
|
||||
*/
|
||||
class Breadcrumb extends Abstract_Schema_Piece {
|
||||
|
||||
/**
|
||||
* Determine if we should add a breadcrumb attribute.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_needed() {
|
||||
if ( $this->context->indexable->object_type === 'unknown' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->context->indexable->object_type === 'system-page' && $this->context->indexable->object_sub_type === '404' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Schema breadcrumb data to allow recognition of page's position in the site hierarchy.
|
||||
*
|
||||
* @link https://developers.google.com/search/docs/data-types/breadcrumb
|
||||
*
|
||||
* @return bool|array Array on success, false on failure.
|
||||
*/
|
||||
public function generate() {
|
||||
$breadcrumbs = $this->context->presentation->breadcrumbs;
|
||||
$list_elements = [];
|
||||
|
||||
// In case of pagination, replace the last breadcrumb, because it only contains "Page [number]" and has no URL.
|
||||
if (
|
||||
(
|
||||
$this->helpers->current_page->is_paged()
|
||||
|| $this->context->indexable->number_of_pages > 1
|
||||
) && (
|
||||
// Do not replace the last breadcrumb on static post pages.
|
||||
! $this->helpers->current_page->is_static_posts_page()
|
||||
// Do not remove the last breadcrumb if only one exists (bugfix for custom paginated frontpages).
|
||||
&& \count( $breadcrumbs ) > 1
|
||||
)
|
||||
) {
|
||||
\array_pop( $breadcrumbs );
|
||||
}
|
||||
|
||||
// Only output breadcrumbs that are not hidden.
|
||||
$breadcrumbs = \array_filter( $breadcrumbs, [ $this, 'not_hidden' ] );
|
||||
|
||||
\reset( $breadcrumbs );
|
||||
|
||||
/*
|
||||
* Check whether at least one of the breadcrumbs is broken.
|
||||
* If so, do not output anything.
|
||||
*/
|
||||
foreach ( $breadcrumbs as $breadcrumb ) {
|
||||
if ( $this->is_broken( $breadcrumb ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the last breadcrumb.
|
||||
$last_breadcrumb = \array_pop( $breadcrumbs );
|
||||
$breadcrumbs[] = $this->format_last_breadcrumb( $last_breadcrumb );
|
||||
|
||||
// If this is a static front page, prevent nested pages from creating a trail.
|
||||
if ( $this->helpers->current_page->is_home_static_page() ) {
|
||||
|
||||
// Check if we're dealing with a nested page.
|
||||
if ( \count( $breadcrumbs ) > 1 ) {
|
||||
|
||||
// Store the breadcrumbs home variable before dropping the parent page from the Schema.
|
||||
$breadcrumbs_home = $breadcrumbs[0]['text'];
|
||||
$breadcrumbs = [ \array_pop( $breadcrumbs ) ];
|
||||
|
||||
// Make the child page show the breadcrumbs home variable rather than its own title.
|
||||
$breadcrumbs[0]['text'] = $breadcrumbs_home;
|
||||
}
|
||||
}
|
||||
|
||||
$breadcrumbs = \array_filter( $breadcrumbs, [ $this, 'not_empty_text' ] );
|
||||
$breadcrumbs = \array_values( $breadcrumbs );
|
||||
|
||||
// Create intermediate breadcrumbs.
|
||||
foreach ( $breadcrumbs as $index => $breadcrumb ) {
|
||||
$list_elements[] = $this->create_breadcrumb( $index, $breadcrumb );
|
||||
}
|
||||
|
||||
return [
|
||||
'@type' => 'BreadcrumbList',
|
||||
'@id' => $this->context->canonical . Schema_IDs::BREADCRUMB_HASH,
|
||||
'itemListElement' => $list_elements,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a breadcrumb array.
|
||||
*
|
||||
* @param int $index The position in the list.
|
||||
* @param array $breadcrumb The position in the list.
|
||||
*
|
||||
* @return array A breadcrumb listItem.
|
||||
*/
|
||||
private function create_breadcrumb( $index, $breadcrumb ) {
|
||||
$crumb = [
|
||||
'@type' => 'ListItem',
|
||||
'position' => ( $index + 1 ),
|
||||
'name' => $this->helpers->schema->html->smart_strip_tags( $breadcrumb['text'] ),
|
||||
];
|
||||
|
||||
if ( ! empty( $breadcrumb['url'] ) ) {
|
||||
$crumb['item'] = $breadcrumb['url'];
|
||||
}
|
||||
|
||||
return $crumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the last breadcrumb in the breadcrumb list, omitting the URL per Google's spec.
|
||||
*
|
||||
* @link https://developers.google.com/search/docs/data-types/breadcrumb
|
||||
*
|
||||
* @param array $breadcrumb The position in the list.
|
||||
*
|
||||
* @return array The last of the breadcrumbs.
|
||||
*/
|
||||
private function format_last_breadcrumb( $breadcrumb ) {
|
||||
unset( $breadcrumb['url'] );
|
||||
|
||||
return $breadcrumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the breadcrumb is broken.
|
||||
* A breadcrumb is considered broken:
|
||||
* - when it is not an array.
|
||||
* - when it has no URL or text.
|
||||
*
|
||||
* @param array $breadcrumb The breadcrumb to test.
|
||||
*
|
||||
* @return bool `true` if the breadcrumb is broken.
|
||||
*/
|
||||
private function is_broken( $breadcrumb ) {
|
||||
// A breadcrumb is broken if it is not an array.
|
||||
if ( ! \is_array( $breadcrumb ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A breadcrumb is broken if it does not contain a URL or text.
|
||||
if ( ! \array_key_exists( 'url', $breadcrumb ) || ! \array_key_exists( 'text', $breadcrumb ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the breadcrumb is not set to be hidden.
|
||||
*
|
||||
* @param array $breadcrumb The breadcrumb array.
|
||||
*
|
||||
* @return bool If the breadcrumb should not be hidden.
|
||||
*/
|
||||
private function not_hidden( $breadcrumb ) {
|
||||
return empty( $breadcrumb['hide_in_schema'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the breadcrumb has a not empty text.
|
||||
*
|
||||
* @param array $breadcrumb The breadcrumb array.
|
||||
*
|
||||
* @return bool If the breadcrumb has a not empty text.
|
||||
*/
|
||||
private function not_empty_text( $breadcrumb ) {
|
||||
return ! empty( $breadcrumb['text'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators\Schema;
|
||||
|
||||
/**
|
||||
* Returns schema FAQ data.
|
||||
*/
|
||||
class FAQ extends Abstract_Schema_Piece {
|
||||
|
||||
/**
|
||||
* Determines whether or not a piece should be added to the graph.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_needed() {
|
||||
if ( empty( $this->context->blocks['yoast/faq-block'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! \is_array( $this->context->schema_page_type ) ) {
|
||||
$this->context->schema_page_type = [ $this->context->schema_page_type ];
|
||||
}
|
||||
$this->context->schema_page_type[] = 'FAQPage';
|
||||
$this->context->main_entity_of_page = $this->generate_ids();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the IDs so we can link to them in the main entity.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function generate_ids() {
|
||||
$ids = [];
|
||||
foreach ( $this->context->blocks['yoast/faq-block'] as $block ) {
|
||||
foreach ( $block['attrs']['questions'] as $question ) {
|
||||
if ( ! isset( $question['jsonAnswer'] ) || empty( $question['jsonAnswer'] ) ) {
|
||||
continue;
|
||||
}
|
||||
$ids[] = [ '@id' => $this->context->canonical . '#' . \esc_attr( $question['id'] ) ];
|
||||
}
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a list of questions, referencing them by ID.
|
||||
*
|
||||
* @return array Our Schema graph.
|
||||
*/
|
||||
public function generate() {
|
||||
$graph = [];
|
||||
|
||||
$questions = [];
|
||||
foreach ( $this->context->blocks['yoast/faq-block'] as $index => $block ) {
|
||||
$questions = \array_merge( $questions, $block['attrs']['questions'] );
|
||||
}
|
||||
foreach ( $questions as $index => $question ) {
|
||||
if ( ! isset( $question['jsonAnswer'] ) || empty( $question['jsonAnswer'] ) ) {
|
||||
continue;
|
||||
}
|
||||
$graph[] = $this->generate_question_block( $question, ( $index + 1 ) );
|
||||
}
|
||||
|
||||
return $graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a Question piece.
|
||||
*
|
||||
* @param array $question The question to generate schema for.
|
||||
* @param int $position The position of the question.
|
||||
*
|
||||
* @return array Schema.org Question piece.
|
||||
*/
|
||||
protected function generate_question_block( $question, $position ) {
|
||||
$url = $this->context->canonical . '#' . \esc_attr( $question['id'] );
|
||||
|
||||
$data = [
|
||||
'@type' => 'Question',
|
||||
'@id' => $url,
|
||||
'position' => $position,
|
||||
'url' => $url,
|
||||
'name' => $this->helpers->schema->html->smart_strip_tags( $question['jsonQuestion'] ),
|
||||
'answerCount' => 1,
|
||||
'acceptedAnswer' => $this->add_accepted_answer_property( $question ),
|
||||
];
|
||||
|
||||
$data = $this->helpers->schema->language->add_piece_language( $data );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the Questions `acceptedAnswer` property.
|
||||
*
|
||||
* @param array $question The question to add the acceptedAnswer to.
|
||||
*
|
||||
* @return array Schema.org Question piece.
|
||||
*/
|
||||
protected function add_accepted_answer_property( $question ) {
|
||||
$data = [
|
||||
'@type' => 'Answer',
|
||||
'text' => $this->helpers->schema->html->sanitize( $question['jsonAnswer'] ),
|
||||
];
|
||||
|
||||
$data = $this->helpers->schema->language->add_piece_language( $data );
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators\Schema;
|
||||
|
||||
use Yoast\WP\SEO\Config\Schema_IDs;
|
||||
|
||||
/**
|
||||
* Returns schema HowTo data.
|
||||
*/
|
||||
class HowTo extends Abstract_Schema_Piece {
|
||||
|
||||
/**
|
||||
* Determines whether or not a piece should be added to the graph.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_needed() {
|
||||
return ! empty( $this->context->blocks['yoast/how-to-block'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a list of questions, referencing them by ID.
|
||||
*
|
||||
* @return array Our Schema graph.
|
||||
*/
|
||||
public function generate() {
|
||||
$graph = [];
|
||||
|
||||
foreach ( $this->context->blocks['yoast/how-to-block'] as $index => $block ) {
|
||||
$this->add_how_to( $graph, $block, $index );
|
||||
}
|
||||
|
||||
return $graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the duration of the task to the Schema.
|
||||
*
|
||||
* @param array $data Our How-To schema data.
|
||||
* @param array $attributes The block data attributes.
|
||||
*/
|
||||
private function add_duration( &$data, $attributes ) {
|
||||
if ( empty( $attributes['hasDuration'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$days = empty( $attributes['days'] ) ? 0 : $attributes['days'];
|
||||
$hours = empty( $attributes['hours'] ) ? 0 : $attributes['hours'];
|
||||
$minutes = empty( $attributes['minutes'] ) ? 0 : $attributes['minutes'];
|
||||
|
||||
if ( ( $days + $hours + $minutes ) > 0 ) {
|
||||
$data['totalTime'] = \esc_attr( 'P' . $days . 'DT' . $hours . 'H' . $minutes . 'M' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the steps to our How-To output.
|
||||
*
|
||||
* @param array $data Our How-To schema data.
|
||||
* @param array $steps Our How-To block's steps.
|
||||
*/
|
||||
private function add_steps( &$data, $steps ) {
|
||||
foreach ( $steps as $step ) {
|
||||
$schema_id = $this->context->canonical . '#' . \esc_attr( $step['id'] );
|
||||
$schema_step = [
|
||||
'@type' => 'HowToStep',
|
||||
'url' => $schema_id,
|
||||
];
|
||||
|
||||
if ( isset( $step['jsonText'] ) ) {
|
||||
$json_text = $this->helpers->schema->html->sanitize( $step['jsonText'] );
|
||||
}
|
||||
|
||||
if ( isset( $step['jsonName'] ) ) {
|
||||
$json_name = $this->helpers->schema->html->smart_strip_tags( $step['jsonName'] );
|
||||
}
|
||||
|
||||
if ( empty( $json_name ) ) {
|
||||
if ( empty( $step['text'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$schema_step['text'] = '';
|
||||
|
||||
$this->add_step_image( $schema_step, $step );
|
||||
|
||||
// If there is no text and no image, don't output the step.
|
||||
if ( empty( $json_text ) && empty( $schema_step['image'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! empty( $json_text ) ) {
|
||||
$schema_step['text'] = $json_text;
|
||||
}
|
||||
}
|
||||
|
||||
elseif ( empty( $json_text ) ) {
|
||||
$schema_step['text'] = $json_name;
|
||||
}
|
||||
else {
|
||||
$schema_step['name'] = $json_name;
|
||||
|
||||
$this->add_step_description( $schema_step, $json_text );
|
||||
$this->add_step_image( $schema_step, $step );
|
||||
}
|
||||
|
||||
$data['step'][] = $schema_step;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we have a step description, if we do, add it.
|
||||
*
|
||||
* @param array $schema_step Our Schema output for the Step.
|
||||
* @param string $json_text The step text.
|
||||
*/
|
||||
private function add_step_description( &$schema_step, $json_text ) {
|
||||
$schema_step['itemListElement'] = [
|
||||
[
|
||||
'@type' => 'HowToDirection',
|
||||
'text' => $json_text,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we have a step image, if we do, add it.
|
||||
*
|
||||
* @param array $schema_step Our Schema output for the Step.
|
||||
* @param array $step The step block data.
|
||||
*/
|
||||
private function add_step_image( &$schema_step, $step ) {
|
||||
if ( isset( $step['text'] ) && \is_array( $step['text'] ) ) {
|
||||
foreach ( $step['text'] as $line ) {
|
||||
if ( \is_array( $line ) && isset( $line['type'] ) && $line['type'] === 'img' ) {
|
||||
$schema_step['image'] = $this->get_image_schema( \esc_url( $line['props']['src'] ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the HowTo schema for a block.
|
||||
*
|
||||
* @param array $graph Our Schema data.
|
||||
* @param array $block The How-To block content.
|
||||
* @param int $index The index of the current block.
|
||||
*/
|
||||
protected function add_how_to( &$graph, $block, $index ) {
|
||||
$data = [
|
||||
'@type' => 'HowTo',
|
||||
'@id' => $this->context->canonical . '#howto-' . ( $index + 1 ),
|
||||
'name' => $this->helpers->schema->html->smart_strip_tags( $this->helpers->post->get_post_title_with_fallback( $this->context->id ) ),
|
||||
'mainEntityOfPage' => [ '@id' => $this->context->main_schema_id ],
|
||||
'description' => '',
|
||||
];
|
||||
|
||||
if ( $this->context->has_article ) {
|
||||
$data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id . Schema_IDs::ARTICLE_HASH ];
|
||||
}
|
||||
|
||||
if ( isset( $block['attrs']['jsonDescription'] ) ) {
|
||||
$data['description'] = $this->helpers->schema->html->sanitize( $block['attrs']['jsonDescription'] );
|
||||
}
|
||||
|
||||
$this->add_duration( $data, $block['attrs'] );
|
||||
$this->add_steps( $data, $block['attrs']['steps'] );
|
||||
|
||||
$data = $this->helpers->schema->language->add_piece_language( $data );
|
||||
|
||||
$graph[] = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the image schema from the attachment $url.
|
||||
*
|
||||
* @param string $url Attachment url.
|
||||
*
|
||||
* @return array Image schema.
|
||||
*/
|
||||
protected function get_image_schema( $url ) {
|
||||
$schema_id = $this->context->canonical . '#schema-image-' . \md5( $url );
|
||||
|
||||
return $this->helpers->schema->image->generate_from_url( $schema_id, $url );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators\Schema;
|
||||
|
||||
use Yoast\WP\SEO\Config\Schema_IDs;
|
||||
|
||||
/**
|
||||
* Returns ImageObject schema data.
|
||||
*/
|
||||
class Main_Image extends Abstract_Schema_Piece {
|
||||
|
||||
/**
|
||||
* Determines whether or not a piece should be added to the graph.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_needed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a main image for the current URL to the schema if there is one.
|
||||
*
|
||||
* This can be either the featured image or the first image in the content of the page.
|
||||
*
|
||||
* @return false|array Image Schema.
|
||||
*/
|
||||
public function generate() {
|
||||
$image_id = $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH;
|
||||
|
||||
// The featured image.
|
||||
if ( $this->context->main_image_id ) {
|
||||
$generated_schema = $this->helpers->schema->image->generate_from_attachment_id( $image_id, $this->context->main_image_id );
|
||||
$this->context->main_image_url = $generated_schema['url'];
|
||||
|
||||
return $generated_schema;
|
||||
}
|
||||
|
||||
// The first image in the content.
|
||||
if ( $this->context->main_image_url ) {
|
||||
return $this->helpers->schema->image->generate_from_url( $image_id, $this->context->main_image_url );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators\Schema;
|
||||
|
||||
use Yoast\WP\SEO\Config\Schema_IDs;
|
||||
|
||||
/**
|
||||
* Returns schema Organization data.
|
||||
*/
|
||||
class Organization extends Abstract_Schema_Piece {
|
||||
|
||||
/**
|
||||
* Determines whether an Organization graph piece should be added.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_needed() {
|
||||
return $this->context->site_represents === 'company';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Organization Schema data.
|
||||
*
|
||||
* @return array The Organization schema.
|
||||
*/
|
||||
public function generate() {
|
||||
$logo_schema_id = $this->context->site_url . Schema_IDs::ORGANIZATION_LOGO_HASH;
|
||||
|
||||
if ( $this->context->company_logo_meta ) {
|
||||
$logo = $this->helpers->schema->image->generate_from_attachment_meta( $logo_schema_id, $this->context->company_logo_meta, $this->context->company_name );
|
||||
}
|
||||
else {
|
||||
$logo = $this->helpers->schema->image->generate_from_attachment_id( $logo_schema_id, $this->context->company_logo_id, $this->context->company_name );
|
||||
}
|
||||
|
||||
$organization = [
|
||||
'@type' => 'Organization',
|
||||
'@id' => $this->context->site_url . Schema_IDs::ORGANIZATION_HASH,
|
||||
'name' => $this->helpers->schema->html->smart_strip_tags( $this->context->company_name ),
|
||||
];
|
||||
|
||||
if ( ! empty( $this->context->company_alternate_name ) ) {
|
||||
$organization['alternateName'] = $this->context->company_alternate_name;
|
||||
}
|
||||
|
||||
$organization['url'] = $this->context->site_url;
|
||||
$organization['logo'] = $logo;
|
||||
$organization['image'] = [ '@id' => $logo['@id'] ];
|
||||
|
||||
$same_as = \array_values( \array_unique( \array_filter( $this->fetch_social_profiles() ) ) );
|
||||
if ( ! empty( $same_as ) ) {
|
||||
$organization['sameAs'] = $same_as;
|
||||
}
|
||||
|
||||
return $organization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the social profiles to display in the organization schema.
|
||||
*
|
||||
* @return array An array of social profiles.
|
||||
*/
|
||||
private function fetch_social_profiles() {
|
||||
$profiles = $this->helpers->social_profiles->get_organization_social_profiles();
|
||||
|
||||
if ( isset( $profiles['other_social_urls'] ) ) {
|
||||
$other_social_urls = $profiles['other_social_urls'];
|
||||
unset( $profiles['other_social_urls'] );
|
||||
$profiles = \array_merge( $profiles, $other_social_urls );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter: 'wpseo_schema_organization_social_profiles' - Allows filtering social profiles for the
|
||||
* represented organization.
|
||||
*
|
||||
* @api string[] $profiles
|
||||
*/
|
||||
$profiles = \apply_filters( 'wpseo_schema_organization_social_profiles', $profiles );
|
||||
|
||||
return $profiles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators\Schema;
|
||||
|
||||
use WP_User;
|
||||
use Yoast\WP\SEO\Config\Schema_IDs;
|
||||
|
||||
/**
|
||||
* Returns schema Person data.
|
||||
*/
|
||||
class Person extends Abstract_Schema_Piece {
|
||||
|
||||
/**
|
||||
* Array of the social profiles we display for a Person.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $social_profiles = [
|
||||
'facebook',
|
||||
'instagram',
|
||||
'linkedin',
|
||||
'pinterest',
|
||||
'twitter',
|
||||
'myspace',
|
||||
'youtube',
|
||||
'soundcloud',
|
||||
'tumblr',
|
||||
'wikipedia',
|
||||
];
|
||||
|
||||
/**
|
||||
* The Schema type we use for this class.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $type = [ 'Person', 'Organization' ];
|
||||
|
||||
/**
|
||||
* Determine whether we should return Person schema.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_needed() {
|
||||
// Using an author piece instead.
|
||||
if ( $this->site_represents_current_author() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->context->site_represents === 'person';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Person Schema data.
|
||||
*
|
||||
* @return bool|array Person data on success, false on failure.
|
||||
*/
|
||||
public function generate() {
|
||||
$user_id = $this->determine_user_id();
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->build_person_data( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines a User ID for the Person data.
|
||||
*
|
||||
* @return bool|int User ID or false upon return.
|
||||
*/
|
||||
protected function determine_user_id() {
|
||||
/**
|
||||
* Filter: 'wpseo_schema_person_user_id' - Allows filtering of user ID used for person output.
|
||||
*
|
||||
* @api int|bool $user_id The user ID currently determined.
|
||||
*/
|
||||
$user_id = \apply_filters( 'wpseo_schema_person_user_id', $this->context->site_user_id );
|
||||
|
||||
// It should to be an integer higher than 0.
|
||||
if ( \is_int( $user_id ) && $user_id > 0 ) {
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of social profile URLs for Person.
|
||||
*
|
||||
* @param array $same_as_urls Array of SameAs URLs.
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return string[] A list of SameAs URLs.
|
||||
*/
|
||||
protected function get_social_profiles( $same_as_urls, $user_id ) {
|
||||
/**
|
||||
* Filter: 'wpseo_schema_person_social_profiles' - Allows filtering of social profiles per user.
|
||||
*
|
||||
* @param int $user_id The current user we're grabbing social profiles for.
|
||||
*
|
||||
* @api string[] $social_profiles The array of social profiles to retrieve. Each should be a user meta field
|
||||
* key. As they are retrieved using the WordPress function `get_the_author_meta`.
|
||||
*/
|
||||
$social_profiles = \apply_filters( 'wpseo_schema_person_social_profiles', $this->social_profiles, $user_id );
|
||||
|
||||
// We can only handle an array.
|
||||
if ( ! \is_array( $social_profiles ) ) {
|
||||
return $same_as_urls;
|
||||
}
|
||||
|
||||
foreach ( $social_profiles as $profile ) {
|
||||
// Skip non-string values.
|
||||
if ( ! \is_string( $profile ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$social_url = $this->url_social_site( $profile, $user_id );
|
||||
if ( $social_url ) {
|
||||
$same_as_urls[] = $social_url;
|
||||
}
|
||||
}
|
||||
|
||||
return $same_as_urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds our array of Schema Person data for a given user ID.
|
||||
*
|
||||
* @param int $user_id The user ID to use.
|
||||
* @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( $user_id, $add_hash = false ) {
|
||||
$user_data = \get_userdata( $user_id );
|
||||
$data = [
|
||||
'@type' => $this->type,
|
||||
'@id' => $this->helpers->schema->id->get_user_schema_id( $user_id, $this->context ),
|
||||
];
|
||||
|
||||
// Safety check for the `get_userdata` WP function, which could return false.
|
||||
if ( $user_data === false ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$data['name'] = $this->helpers->schema->html->smart_strip_tags( $user_data->display_name );
|
||||
$data = $this->add_image( $data, $user_data, $add_hash );
|
||||
|
||||
if ( ! empty( $user_data->description ) ) {
|
||||
$data['description'] = $this->helpers->schema->html->smart_strip_tags( $user_data->description );
|
||||
}
|
||||
|
||||
$data = $this->add_same_as_urls( $data, $user_data, $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ImageObject for the persons avatar.
|
||||
*
|
||||
* @param array $data The Person schema.
|
||||
* @param WP_User $user_data User data.
|
||||
* @param bool $add_hash Wether or not the person's image url hash should be added to the image id.
|
||||
*
|
||||
* @return array The Person schema.
|
||||
*/
|
||||
protected function add_image( $data, $user_data, $add_hash = false ) {
|
||||
$schema_id = $this->context->site_url . Schema_IDs::PERSON_LOGO_HASH;
|
||||
|
||||
$data = $this->set_image_from_options( $data, $schema_id, $add_hash, $user_data );
|
||||
if ( ! isset( $data['image'] ) ) {
|
||||
$data = $this->set_image_from_avatar( $data, $user_data, $schema_id, $add_hash );
|
||||
}
|
||||
|
||||
if ( \is_array( $this->type ) && \in_array( 'Organization', $this->type, true ) ) {
|
||||
$data_logo = isset( $data['image']['@id'] ) ? $data['image']['@id'] : $schema_id;
|
||||
$data['logo'] = [ '@id' => $data_logo ];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the person image from our settings.
|
||||
*
|
||||
* @param array $data The Person schema.
|
||||
* @param string $schema_id The string used in the `@id` for the schema.
|
||||
* @param bool $add_hash Whether or not the person's image url hash should be added to the image id.
|
||||
* @param WP_User $user_data User data.
|
||||
*
|
||||
* @return array The Person schema.
|
||||
*/
|
||||
protected function set_image_from_options( $data, $schema_id, $add_hash = false, $user_data = null ) {
|
||||
if ( $this->context->site_represents !== 'person' ) {
|
||||
return $data;
|
||||
}
|
||||
if ( \is_array( $this->context->person_logo_meta ) ) {
|
||||
$data['image'] = $this->helpers->schema->image->generate_from_attachment_meta( $schema_id, $this->context->person_logo_meta, $data['name'], $add_hash );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the person logo from gravatar.
|
||||
*
|
||||
* @param array $data The Person schema.
|
||||
* @param WP_User $user_data User data.
|
||||
* @param string $schema_id The string used in the `@id` for the schema.
|
||||
* @param bool $add_hash Wether or not the person's image url hash should be added to the image id.
|
||||
*
|
||||
* @return array The Person schema.
|
||||
*/
|
||||
protected function set_image_from_avatar( $data, $user_data, $schema_id, $add_hash = false ) {
|
||||
// If we don't have an image in our settings, fall back to an avatar, if we're allowed to.
|
||||
$show_avatars = \get_option( 'show_avatars' );
|
||||
if ( ! $show_avatars ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$url = \get_avatar_url( $user_data->user_email );
|
||||
if ( empty( $url ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$data['image'] = $this->helpers->schema->image->simple_image_object( $schema_id, $url, $user_data->display_name, $add_hash );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an author's social site URL.
|
||||
*
|
||||
* @param string $social_site The social site to retrieve the URL for.
|
||||
* @param mixed $user_id The user ID to use function outside of the loop.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function url_social_site( $social_site, $user_id = false ) {
|
||||
$url = \get_the_author_meta( $social_site, $user_id );
|
||||
|
||||
if ( ! empty( $url ) && $social_site === 'twitter' ) {
|
||||
$url = 'https://twitter.com/' . $url;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the site is represented by the same person as this indexable.
|
||||
*
|
||||
* @param WP_User $user_data User data.
|
||||
*
|
||||
* @return bool True when the site is represented by the same person as this indexable.
|
||||
*/
|
||||
protected function site_represents_current_author( $user_data = null ) {
|
||||
// Can only be the case when the site represents a user.
|
||||
if ( $this->context->site_represents !== 'person' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Article post from the same user as the site represents.
|
||||
if (
|
||||
$this->context->indexable->object_type === 'post'
|
||||
&& $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type )
|
||||
&& $this->context->schema_article_type !== 'None'
|
||||
) {
|
||||
$user_id = ( ( ! \is_null( $user_data ) ) && ( isset( $user_data->ID ) ) ) ? $user_data->ID : $this->context->indexable->author_id;
|
||||
|
||||
return $this->context->site_user_id === $user_id;
|
||||
}
|
||||
|
||||
// Author archive from the same user as the site represents.
|
||||
return $this->context->indexable->object_type === 'user' && $this->context->site_user_id === $this->context->indexable->object_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds our SameAs array.
|
||||
*
|
||||
* @param array $data The Person schema data.
|
||||
* @param WP_User $user_data The user data object.
|
||||
* @param int $user_id The user ID to use.
|
||||
*
|
||||
* @return array The Person schema data.
|
||||
*/
|
||||
protected function add_same_as_urls( $data, $user_data, $user_id ) {
|
||||
$same_as_urls = [];
|
||||
|
||||
// Add the "Website" field from WordPress' contact info.
|
||||
if ( ! empty( $user_data->user_url ) ) {
|
||||
$same_as_urls[] = $user_data->user_url;
|
||||
}
|
||||
|
||||
// Add the social profiles.
|
||||
$same_as_urls = $this->get_social_profiles( $same_as_urls, $user_id );
|
||||
|
||||
if ( ! empty( $same_as_urls ) ) {
|
||||
$same_as_urls = \array_values( \array_unique( $same_as_urls ) );
|
||||
$data['sameAs'] = $same_as_urls;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators\Schema;
|
||||
|
||||
use WP_Post;
|
||||
use Yoast\WP\SEO\Config\Schema_IDs;
|
||||
|
||||
/**
|
||||
* Returns schema WebPage data.
|
||||
*/
|
||||
class WebPage extends Abstract_Schema_Piece {
|
||||
|
||||
/**
|
||||
* Determines whether or not a piece should be added to the graph.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_needed() {
|
||||
if ( $this->context->indexable->object_type === 'unknown' ) {
|
||||
return false;
|
||||
}
|
||||
return ! ( $this->context->indexable->object_type === 'system-page' && $this->context->indexable->object_sub_type === '404' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns WebPage schema data.
|
||||
*
|
||||
* @return array WebPage schema data.
|
||||
*/
|
||||
public function generate() {
|
||||
$data = [
|
||||
'@type' => $this->context->schema_page_type,
|
||||
'@id' => $this->context->main_schema_id,
|
||||
'url' => $this->context->canonical,
|
||||
'name' => $this->helpers->schema->html->smart_strip_tags( $this->context->title ),
|
||||
'isPartOf' => [
|
||||
'@id' => $this->context->site_url . Schema_IDs::WEBSITE_HASH,
|
||||
],
|
||||
];
|
||||
|
||||
if ( empty( $this->context->canonical ) && \is_search() ) {
|
||||
$data['url'] = $this->build_search_url();
|
||||
}
|
||||
|
||||
if ( $this->helpers->current_page->is_front_page() ) {
|
||||
if ( $this->context->site_represents_reference ) {
|
||||
$data['about'] = $this->context->site_represents_reference;
|
||||
}
|
||||
}
|
||||
|
||||
$data = $this->add_image( $data );
|
||||
|
||||
if ( $this->context->indexable->object_type === 'post' ) {
|
||||
$data['datePublished'] = $this->helpers->date->format( $this->context->post->post_date_gmt );
|
||||
$data['dateModified'] = $this->helpers->date->format( $this->context->post->post_modified_gmt );
|
||||
|
||||
if ( $this->context->indexable->object_sub_type === 'post' ) {
|
||||
$data = $this->add_author( $data, $this->context->post );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $this->context->description ) ) {
|
||||
$data['description'] = $this->helpers->schema->html->smart_strip_tags( $this->context->description );
|
||||
}
|
||||
|
||||
if ( $this->add_breadcrumbs() ) {
|
||||
$data['breadcrumb'] = [
|
||||
'@id' => $this->context->canonical . Schema_IDs::BREADCRUMB_HASH,
|
||||
];
|
||||
}
|
||||
|
||||
if ( ! empty( $this->context->main_entity_of_page ) ) {
|
||||
$data['mainEntity'] = $this->context->main_entity_of_page;
|
||||
}
|
||||
|
||||
$data = $this->helpers->schema->language->add_piece_language( $data );
|
||||
$data = $this->add_potential_action( $data );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an author property to the $data if the WebPage is not represented.
|
||||
*
|
||||
* @param array $data The WebPage schema.
|
||||
* @param WP_Post $post The post the context is representing.
|
||||
*
|
||||
* @return array The WebPage schema.
|
||||
*/
|
||||
public function add_author( $data, $post ) {
|
||||
if ( $this->context->site_represents === false ) {
|
||||
$data['author'] = [ '@id' => $this->helpers->schema->id->get_user_schema_id( $post->post_author, $this->context ) ];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have an image, make it the primary image of the page.
|
||||
*
|
||||
* @param array $data WebPage schema data.
|
||||
*/
|
||||
public function add_image( $data ) {
|
||||
if ( $this->context->has_image ) {
|
||||
$data['primaryImageOfPage'] = [ '@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH ];
|
||||
$data['image'] = [ '@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH ];
|
||||
$data['thumbnailUrl'] = $this->context->main_image_url;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we should add a breadcrumb attribute.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function add_breadcrumbs() {
|
||||
if ( $this->context->indexable->object_type === 'system-page' && $this->context->indexable->object_sub_type === '404' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the potential action property to the WebPage Schema piece.
|
||||
*
|
||||
* @param array $data The WebPage data.
|
||||
*
|
||||
* @return array The WebPage data with the potential action added.
|
||||
*/
|
||||
private function add_potential_action( $data ) {
|
||||
$url = $this->context->canonical;
|
||||
if ( $data['@type'] === 'CollectionPage' || ( \is_array( $data['@type'] ) && \in_array( 'CollectionPage', $data['@type'], true ) ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter: 'wpseo_schema_webpage_potential_action_target' - Allows filtering of the schema WebPage potentialAction target.
|
||||
*
|
||||
* @api array $targets The URLs for the WebPage potentialAction target.
|
||||
*/
|
||||
$targets = \apply_filters( 'wpseo_schema_webpage_potential_action_target', [ $url ] );
|
||||
|
||||
$data['potentialAction'][] = [
|
||||
'@type' => 'ReadAction',
|
||||
'target' => $targets,
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the search URL for use when if there is no canonical.
|
||||
*
|
||||
* @return string Search URL.
|
||||
*/
|
||||
private function build_search_url() {
|
||||
return $this->context->site_url . '?s=' . \get_search_query();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators\Schema;
|
||||
|
||||
use Yoast\WP\SEO\Config\Schema_IDs;
|
||||
|
||||
/**
|
||||
* Returns schema Website data.
|
||||
*/
|
||||
class Website extends Abstract_Schema_Piece {
|
||||
|
||||
/**
|
||||
* Determines whether or not a piece should be added to the graph.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_needed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs code to allow recognition of the internal search engine.
|
||||
*
|
||||
* @return array Website data blob.
|
||||
*/
|
||||
public function generate() {
|
||||
$data = [
|
||||
'@type' => 'WebSite',
|
||||
'@id' => $this->context->site_url . Schema_IDs::WEBSITE_HASH,
|
||||
'url' => $this->context->site_url,
|
||||
'name' => $this->helpers->schema->html->smart_strip_tags( $this->context->site_name ),
|
||||
'description' => \get_bloginfo( 'description' ),
|
||||
];
|
||||
|
||||
if ( $this->context->site_represents_reference ) {
|
||||
$data['publisher'] = $this->context->site_represents_reference;
|
||||
}
|
||||
|
||||
$data = $this->add_alternate_name( $data );
|
||||
$data = $this->internal_search_section( $data );
|
||||
$data = $this->helpers->schema->language->add_piece_language( $data );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an alternate name if one was specified in the Yoast SEO settings.
|
||||
*
|
||||
* @param array $data The website data array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function add_alternate_name( $data ) {
|
||||
if ( $this->context->alternate_site_name !== '' ) {
|
||||
$data['alternateName'] = $this->helpers->schema->html->smart_strip_tags( $this->context->alternate_site_name );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the internal search JSON LD code to the homepage if it's not disabled.
|
||||
*
|
||||
* @link https://developers.google.com/search/docs/data-types/sitelinks-searchbox
|
||||
*
|
||||
* @param array $data The website data array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function internal_search_section( $data ) {
|
||||
/**
|
||||
* Filter: 'disable_wpseo_json_ld_search' - Allow disabling of the json+ld output.
|
||||
*
|
||||
* @api bool $display_search Whether or not to display json+ld search on the frontend.
|
||||
*/
|
||||
if ( \apply_filters( 'disable_wpseo_json_ld_search', false ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter: 'wpseo_json_ld_search_url' - Allows filtering of the search URL for Yoast SEO.
|
||||
*
|
||||
* @api string $search_url The search URL for this site with a `{search_term_string}` variable.
|
||||
*/
|
||||
$search_url = \apply_filters( 'wpseo_json_ld_search_url', $this->context->site_url . '?s={search_term_string}' );
|
||||
|
||||
$data['potentialAction'][] = [
|
||||
'@type' => 'SearchAction',
|
||||
'target' => [
|
||||
'@type' => 'EntryPoint',
|
||||
'urlTemplate' => $search_url,
|
||||
],
|
||||
'query-input' => 'required name=search_term_string',
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\Generators;
|
||||
|
||||
use Yoast\WP\SEO\Context\Meta_Tags_Context;
|
||||
use Yoast\WP\SEO\Helpers\Image_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Twitter\Image_Helper as Twitter_Image_Helper;
|
||||
use Yoast\WP\SEO\Helpers\Url_Helper;
|
||||
use Yoast\WP\SEO\Models\Indexable;
|
||||
use Yoast\WP\SEO\Values\Images;
|
||||
|
||||
/**
|
||||
* Represents the generator class for the Twitter images.
|
||||
*/
|
||||
class Twitter_Image_Generator implements Generator_Interface {
|
||||
|
||||
/**
|
||||
* The image helper.
|
||||
*
|
||||
* @var Image_Helper
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* The URL helper.
|
||||
*
|
||||
* @var Url_Helper
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* The Twitter image helper.
|
||||
*
|
||||
* @var Twitter_Image_Helper
|
||||
*/
|
||||
protected $twitter_image;
|
||||
|
||||
/**
|
||||
* Twitter_Image_Generator constructor.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param Image_Helper $image The image helper.
|
||||
* @param Url_Helper $url The url helper.
|
||||
* @param Twitter_Image_Helper $twitter_image The Twitter image helper.
|
||||
*/
|
||||
public function __construct( Image_Helper $image, Url_Helper $url, Twitter_Image_Helper $twitter_image ) {
|
||||
$this->image = $image;
|
||||
$this->url = $url;
|
||||
$this->twitter_image = $twitter_image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the images for an indexable.
|
||||
*
|
||||
* @param Meta_Tags_Context $context The context.
|
||||
*
|
||||
* @return array The images.
|
||||
*/
|
||||
public function generate( Meta_Tags_Context $context ) {
|
||||
$image_container = $this->get_image_container();
|
||||
|
||||
$this->add_from_indexable( $context->indexable, $image_container );
|
||||
|
||||
return $image_container->get_images();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an image based on the given indexable.
|
||||
*
|
||||
* @param Indexable $indexable The indexable.
|
||||
* @param Images $image_container The image container.
|
||||
*/
|
||||
protected function add_from_indexable( Indexable $indexable, Images $image_container ) {
|
||||
if ( $indexable->twitter_image_id ) {
|
||||
$image_container->add_image_by_id( $indexable->twitter_image_id );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $indexable->twitter_image ) {
|
||||
$image_container->add_image_by_url( $indexable->twitter_image );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an instance of the image container.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return Images The image container.
|
||||
*/
|
||||
protected function get_image_container() {
|
||||
$image_container = new Images( $this->image, $this->url );
|
||||
$image_container->image_size = $this->twitter_image->get_image_size();
|
||||
|
||||
return $image_container;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user