This commit is contained in:
KhaiNguyen
2020-02-13 10:39:37 +07:00
commit 59401cb805
12867 changed files with 4646216 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns schema Article data.
*
* @since 10.2
*/
class WPSEO_Schema_Article implements WPSEO_Graph_Piece {
/**
* The date helper.
*
* @var WPSEO_Date_Helper
*/
protected $date;
/**
* A value object with context variables.
*
* @var WPSEO_Schema_Context
*/
private $context;
/**
* WPSEO_Schema_Article constructor.
*
* @param WPSEO_Schema_Context $context A value object with context variables.
*/
public function __construct( WPSEO_Schema_Context $context ) {
$this->context = $context;
$this->date = new WPSEO_Date_Helper();
}
/**
* Determines whether or not a piece should be added to the graph.
*
* @return bool
*/
public function is_needed() {
if ( ! is_singular() ) {
return false;
}
if ( $this->context->site_represents === false ) {
return false;
}
return self::is_article_post_type( get_post_type() );
}
/**
* Returns Article data.
*
* @return array $data Article data.
*/
public function generate() {
$post = get_post( $this->context->id );
$comment_count = get_comment_count( $this->context->id );
$data = [
'@type' => 'Article',
'@id' => $this->context->canonical . WPSEO_Schema_IDs::ARTICLE_HASH,
'isPartOf' => [ '@id' => $this->context->canonical . WPSEO_Schema_IDs::WEBPAGE_HASH ],
'author' => [ '@id' => WPSEO_Schema_Utils::get_user_schema_id( $post->post_author, $this->context ) ],
'headline' => get_the_title(),
'datePublished' => $this->date->format( $post->post_date_gmt ),
'dateModified' => $this->date->format( $post->post_modified_gmt ),
'commentCount' => $comment_count['approved'],
'mainEntityOfPage' => [ '@id' => $this->context->canonical . WPSEO_Schema_IDs::WEBPAGE_HASH ],
];
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 );
return $data;
}
/**
* Determines whether a given post type should have Article schema.
*
* @param string $post_type Post type to check.
*
* @return bool True if it has article schema, false if not.
*/
public static function is_article_post_type( $post_type = null ) {
if ( is_null( $post_type ) ) {
$post_type = get_post_type();
}
/**
* Filter: 'wpseo_schema_article_post_types' - Allow changing for which post types we output Article schema.
*
* @api string[] $post_types The post types for which we output Article.
*/
$post_types = apply_filters( 'wpseo_schema_article_post_types', [ 'post' ] );
return in_array( $post_type, $post_types );
}
/**
* Adds tags as keywords, if tags are assigned.
*
* @param array $data Article data.
*
* @return array $data 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 $data 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 array $data Article data.
*/
private function add_terms( $data, $key, $taxonomy ) {
$terms = get_the_terms( $this->context->id, $taxonomy );
if ( is_array( $terms ) ) {
$keywords = [];
foreach ( $terms as $term ) {
// We are checking against the WordPress internal translation.
// @codingStandardsIgnoreLine
if ( $term->name !== __( 'Uncategorized', 'default' ) ) {
$keywords[] = $term->name;
}
}
$data[ $key ] = implode( ',', $keywords );
}
return $data;
}
/**
* Adds an image node if the post has a featured image.
*
* @param array $data The Article data.
*
* @return array $data The Article data.
*/
private function add_image( $data ) {
if ( $this->context->has_image ) {
$data['image'] = [
'@id' => $this->context->canonical . WPSEO_Schema_IDs::PRIMARY_IMAGE_HASH,
];
}
return $data;
}
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns schema Person data.
*
* @since 10.2
*
* @property WPSEO_Schema_Context $context A value object with context variables.
*/
class WPSEO_Schema_Author extends WPSEO_Schema_Person implements WPSEO_Graph_Piece {
/**
* A value object with context variables.
*
* @var WPSEO_Schema_Context
*/
private $context;
/**
* The Schema type we use for this class.
*
* @var string[]
*/
protected $type = [ 'Person' ];
/**
* WPSEO_Schema_Author constructor.
*
* @param WPSEO_Schema_Context $context A value object with context variables.
*/
public function __construct( WPSEO_Schema_Context $context ) {
parent::__construct( $context );
$this->context = $context;
$this->image_hash = WPSEO_Schema_IDs::AUTHOR_LOGO_HASH;
}
/**
* Determine whether we should return Person schema.
*
* @return bool
*/
public function is_needed() {
if ( is_author() ) {
return true;
}
if ( $this->is_post_author() ) {
$post = get_post( $this->context->id );
// If the author is the user the site represents, no need for an extra author block.
if ( (int) $post->post_author === $this->context->site_user_id ) {
return false;
}
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 is an author page, the Person object is the main object, so we set it as such here.
if ( is_author() ) {
$data['mainEntityOfPage'] = [
'@id' => $this->context->canonical . WPSEO_Schema_IDs::WEBPAGE_HASH,
];
}
return $data;
}
/**
* Determine whether the current URL is worthy of Article schema.
*
* @return bool
*/
protected function is_post_author() {
if ( is_singular() && WPSEO_Schema_Article::is_article_post_type() ) {
return true;
}
return false;
}
/**
* Determines a User ID for the Person data.
*
* @return bool|int User ID or false upon return.
*/
protected function determine_user_id() {
switch ( true ) {
case is_author():
$user_id = get_queried_object_id();
break;
default:
$post = get_post( $this->context->id );
$user_id = (int) $post->post_author;
break;
}
/**
* 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.
*/
return apply_filters( 'wpseo_schema_person_user_id', $user_id );
}
/**
* 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.
*
* @return array The Person schema.
*/
private function set_image_from_options( $data, $schema_id ) {
return $data;
}
/**
* Gets the Schema type we use for this class.
*
* @return string[] The schema type.
*/
public static function get_type() {
return self::$type;
}
}

View File

@@ -0,0 +1,153 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns schema Breadcrumb data.
*
* @since 10.2
*/
class WPSEO_Schema_Breadcrumb implements WPSEO_Graph_Piece {
/**
* A value object with context variables.
*
* @var WPSEO_Schema_Context
*/
private $context;
/**
* Current position in the List.
*
* @var int
*/
private $index;
/**
* WPSEO_Schema_Breadcrumb constructor.
*
* @param WPSEO_Schema_Context $context A value object with context variables.
*/
public function __construct( WPSEO_Schema_Context $context ) {
$this->context = $context;
}
/**
* Determine if we should add a breadcrumb attribute.
*
* @return bool
*/
public function is_needed() {
if ( is_404() ) {
return false;
}
if ( is_front_page() ) {
return false;
}
if ( $this->context->breadcrumbs_enabled ) {
return true;
}
return false;
}
/**
* 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_instance = WPSEO_Breadcrumbs::get_instance();
$breadcrumbs = $breadcrumbs_instance->get_links();
$broken = false;
$list_elements = [];
foreach ( $breadcrumbs as $index => $breadcrumb ) {
if ( ! empty( $breadcrumb['hide_in_schema'] ) ) {
continue;
}
if ( ! array_key_exists( 'url', $breadcrumb ) || ! array_key_exists( 'text', $breadcrumb ) ) {
$broken = true;
break;
}
$list_elements[] = $this->add_breadcrumb( $index, $breadcrumb );
$this->index = $index;
}
if ( is_paged() ) {
$list_elements[] = $this->add_paginated_state();
}
$data = [
'@type' => 'BreadcrumbList',
'@id' => $this->context->canonical . WPSEO_Schema_IDs::BREADCRUMB_HASH,
'itemListElement' => $list_elements,
];
// Only output if JSON is correctly formatted.
if ( ! $broken ) {
return $data;
}
return false;
}
/**
* Returns a breadcrumb array.
*
* @param int $index The position in the list.
* @param array $breadcrumb The breadcrumb array.
*
* @return array A breadcrumb listItem.
*/
private function add_breadcrumb( $index, $breadcrumb ) {
if ( empty( $breadcrumb['url'] ) ) {
if ( is_paged() ) {
// Retrieve the un-paginated state of the current page.
$breadcrumb['url'] = WPSEO_Frontend::get_instance()->canonical( false, true );
}
else {
$breadcrumb['url'] = $this->context->canonical;
}
}
if ( empty( $breadcrumb['text'] ) ) {
$breadcrumb['text'] = $this->context->title;
}
return [
'@type' => 'ListItem',
'position' => ( $index + 1 ),
'item' => [
'@type' => 'WebPage',
'@id' => $breadcrumb['url'],
'url' => $breadcrumb['url'], // For future proofing, we're trying to change the standard for this.
'name' => $breadcrumb['text'],
],
];
}
/**
* Adds the paginated state to the breadcrumb array.
*
* @return array A breadcrumb listItem.
*/
private function add_paginated_state() {
$this->index++;
return $this->add_breadcrumb(
$this->index,
[
'url' => $this->context->canonical,
'text' => $this->context->title,
]
);
}
}

View File

@@ -0,0 +1,243 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Context variables for Schema generation.
*
* @property string $canonical The current page's canonical.
* @property string $company_name Holds the company name, if the site represents a company.
* @property int $company_logo_id Holds the company logo's ID, if the site represents a company.
* @property int $id The post ID, if there is one.
* @property string $site_name The site's name.
* @property string $site_description The site's tagline.
* @property string $site_represents Whether this site represents a `company` or a `person`.
* @property string $site_url The site's URL.
* @property int $site_user_id The site's user ID if a site represents a `person`.
* @property string $title Page title.
* @property string $description Page description.
* @property bool $breadcrumbs_enabled Whether or not this site has breadcrumbs enabled.
* @property array $site_represents_reference A schema @id reference to the piece the site represents.
* @property bool $has_image A boolean that determines whether the current URL has a primary image.
*
* @since 10.2
*/
class WPSEO_Schema_Context {
/**
* The current page's canonical.
*
* @var string
*/
public $canonical;
/**
* Holds the company name, if the site represents a company.
*
* @var string
*/
public $company_name;
/**
* Holds the company logo's ID, if the site represents a company.
*
* @var int
*/
public $company_logo_id;
/**
* The queried object ID, if there is one.
*
* @var int
*/
public $id;
/**
* Whether this site represents a `company` or a `person`.
*
* @var string
*/
public $site_represents;
/**
* The site's Name.
*
* @var string
*/
public $site_name;
/**
* The site's tagline.
*
* @var string
*/
public $site_description;
/**
* The site's URL.
*
* @var string
*/
public $site_url;
/**
* Page title.
*
* @var string
*/
public $title;
/**
* User ID when the site represents a Person.
*
* @var int
*/
public $site_user_id;
/**
* Page description.
*
* @var string
*/
public $description;
/**
* Whether or not this site has breadcrumbs enabled.
*
* @var bool
*/
public $breadcrumbs_enabled;
/**
* A schema @id reference to the piece the site represents.
*
* @var array
*/
public $site_represents_reference;
/**
* A boolean that determines whether the current URL has a primary image.
*
* @var bool
*/
public $has_image = false;
/**
* WPSEO_Schema_Context constructor.
*/
public function __construct() {
$this->build_data();
}
/**
* Builds all the required data for the context object.
*/
private function build_data() {
// Page level variables.
$front = WPSEO_Frontend::get_instance();
$this->canonical = $front->canonical( false, false, true );
$this->title = $front->title( '' );
$this->description = $front->metadesc( false );
$this->id = get_queried_object_id();
// Site level variables.
$this->site_name = $this->set_site_name();
$this->site_description = get_bloginfo( 'description' );
$this->site_url = trailingslashit( WPSEO_Utils::home_url() );
$this->set_breadcrumbs_variables();
$this->set_site_represents_variables();
$this->set_site_represents_reference();
}
/**
* Retrieves the site's name from settings.
*
* @return string
*/
private function set_site_name() {
if ( WPSEO_Options::get( 'website_name', '' ) !== '' ) {
return WPSEO_Options::get( 'website_name' );
}
return get_bloginfo( 'name' );
}
/**
* Sets our site represents reference for easy use.
*/
private function set_site_represents_reference() {
$this->site_represents_reference = false;
if ( $this->site_represents === 'person' ) {
$this->site_represents_reference = [ '@id' => WPSEO_Schema_Utils::get_user_schema_id( $this->site_user_id, $this ) ];
}
if ( $this->site_represents === 'company' ) {
$this->site_represents_reference = [ '@id' => $this->site_url . WPSEO_Schema_IDs::ORGANIZATION_HASH ];
}
}
/**
* Determines what our site represents, and grabs their values.
*/
private function set_site_represents_variables() {
$this->site_represents = WPSEO_Options::get( 'company_or_person', false );
switch ( $this->site_represents ) {
case 'company':
$company_name = WPSEO_Options::get( 'company_name' );
/**
* Filter: 'wpseo_schema_company_name' - Allows filtering company name
*
* @api string $company_name.
*/
$this->company_name = apply_filters( 'wpseo_schema_company_name', $company_name );
// Do not use a non-named company.
if ( empty( $this->company_name ) ) {
$this->site_represents = false;
break;
}
$company_logo_id = WPSEO_Image_Utils::get_attachment_id_from_settings( 'company_logo' );
/**
* Filter: 'wpseo_schema_company_logo_id' - Allows filtering company logo id
*
* @api integer $company_logo_id.
*/
$this->company_logo_id = apply_filters( 'wpseo_schema_company_logo_id', $company_logo_id );
/*
* Do not use a company without a logo.
* This is not a false check due to how `get_attachment_id_from_settings` works.
*/
if ( $this->company_logo_id < 1 ) {
$this->site_represents = false;
}
break;
case 'person':
$this->site_user_id = WPSEO_Options::get( 'company_or_person_user_id', false );
// Do not use a non-existing user.
if ( $this->site_user_id !== false && get_user_by( 'id', $this->site_user_id ) === false ) {
$this->site_represents = false;
}
break;
}
}
/**
* Determines whether the site uses Yoast SEO breadcrumbs.
*/
private function set_breadcrumbs_variables() {
$this->breadcrumbs_enabled = current_theme_supports( 'yoast-seo-breadcrumbs' );
if ( ! $this->breadcrumbs_enabled ) {
$this->breadcrumbs_enabled = WPSEO_Options::get( 'breadcrumbs-enable', false );
}
}
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns a question object for each question in an FAQ block.
*
* @since 11.1
*
* @property array $data The Schema array.
* @property WP_Block_Parser_Block[] $blocks The block we're taking the questions out of.
* @property WPSEO_Schema_Context context A value object with context variables.
* @property array ids
* @property int count
*/
class WPSEO_Schema_FAQ_Question_List {
/**
* The Schema array.
*
* @var array
*/
private $data = [];
/**
* All the blocks of this block-type.
*
* @var WP_Block_Parser_Block
*/
private $blocks;
/**
* Number of questions on the page.
*
* @var int
*/
private $count;
/**
* IDs of the questions on the page.
*
* @var array
*/
private $ids;
/**
* WPSEO_Schema_FAQ_Question_List constructor.
*
* @param WP_Block_Parser_Block[] $blocks An array of the FAQ blocks on this page.
* @param WPSEO_Schema_Context $context A value object with context variables.
*/
public function __construct( $blocks, $context ) {
$this->blocks = $blocks;
$this->context = $context;
$this->count = 1;
}
/**
* Find an image based on its URL and generate a Schema object for it.
*
* @return array The Schema with a question list added.
*/
public function generate() {
$this->prepare_blocks();
$this->data[] = [
'@type' => 'ItemList',
'mainEntityOfPage' => [ '@id' => $this->get_schema_id() ],
'numberOfItems' => $this->count,
'itemListElement' => $this->ids,
];
return $this->data;
}
/**
* Determine whether we're part of an article or a webpage.
*
* @return string A reference URL.
*/
private function get_schema_id() {
if ( $this->context->site_represents !== false && WPSEO_Schema_Article::is_article_post_type() ) {
return $this->context->canonical . WPSEO_Schema_IDs::ARTICLE_HASH;
}
return $this->context->canonical . WPSEO_Schema_IDs::WEBPAGE_HASH;
}
/**
* Loop through the blocks of our type.
*/
private function prepare_blocks() {
foreach ( $this->blocks as $block ) {
$this->prepare_questions( $block );
}
}
/**
* Prepare our data.
*
* @param WP_Block_Parser_Block[] $block The block to prepare the questions for.
*/
private function prepare_questions( $block ) {
foreach ( $block['attrs']['questions'] as $question ) {
if ( ! isset( $question['jsonAnswer'] ) || empty( $question['jsonAnswer'] ) ) {
continue;
}
$this->count ++;
$this->ids[] = [ '@id' => $this->context->canonical . '#' . esc_attr( $question['id'] ) ];
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns a question object for each question in an FAQ block.
*
* @since 11.1
*
* @property array $data The Schema array.
* @property WP_Block_Parser_Block $block The block we're taking the questions out of.
* @property WPSEO_Schema_Context $context A value object with context variables.
* @property int $position The position in the list.
*/
class WPSEO_Schema_FAQ_Questions {
/**
* The Schema array.
*
* @var array
*/
private $data;
/**
* All the blocks of this block-type.
*
* @var WP_Block_Parser_Block
*/
private $block;
/**
* Position in the list.
*
* @var int
*/
private $position;
/**
* WPSEO_Schema_FAQ_Questions constructor.
*
* @param array $data Our schema graph.
* @param WP_Block_Parser_Block $block The FAQ block of this type.
* @param WPSEO_Schema_Context $context A value object with context variables.
*/
public function __construct( $data, $block, $context ) {
$this->data = $data;
$this->block = $block;
$this->context = $context;
$this->position = 0;
}
/**
* Find an image based on its URL and generate a Schema object for it.
*
* @return array The Schema with Questions added.
*/
public function generate() {
foreach ( $this->block['attrs']['questions'] as $question ) {
if ( ! isset( $question['jsonAnswer'] ) || empty( $question['jsonAnswer'] ) ) {
continue;
}
$this->data[] = $this->generate_question_block( $question );
}
return $this->data;
}
/**
* Generate a Question piece.
*
* @param array $question The question to generate schema for.
*
* @return array Schema.org Question piece.
*/
protected function generate_question_block( $question ) {
$url = $this->context->canonical . '#' . esc_attr( $question['id'] );
return [
'@type' => 'Question',
'@id' => $url,
'position' => $this->position ++,
'url' => $url,
'name' => wp_strip_all_tags( $question['jsonQuestion'] ),
'answerCount' => 1,
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => strip_tags( $question['jsonAnswer'], '<h1><h2><h3><h4><h5><h6><br><ol><ul><li><a><p><b><strong><i><em>' ),
],
];
}
}

View File

@@ -0,0 +1,111 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns schema FAQ data.
*
* @since 11.3
*/
class WPSEO_Schema_FAQ implements WPSEO_Graph_Piece {
/**
* Determine whether this graph piece is needed or not.
*
* @var bool
*/
private $is_needed = false;
/**
* The FAQ blocks on the current page.
*
* @var array
*/
private $blocks;
/**
* A value object with context variables.
*
* @var WPSEO_Schema_Context
*/
private $context;
/**
* WPSEO_Schema_FAQ constructor.
*
* @param WPSEO_Schema_Context $context A value object with context variables.
*/
public function __construct( WPSEO_Schema_Context $context ) {
$this->context = $context;
add_action( 'wpseo_pre_schema_block_type_yoast/faq-block', [ $this, 'prepare_schema' ], 10, 1 );
add_filter( 'wpseo_schema_block_yoast/faq-block', [ $this, 'render_schema_questions' ], 10, 3 );
}
/**
* If this fires, we know there's an FAQ block ont he page, so filter the page type.
*
* @param array $blocks The blocks of this type on the current page.
*/
public function prepare_schema( $blocks ) {
$this->blocks = $blocks;
$this->is_needed = true;
add_filter( 'wpseo_schema_webpage_type', [ $this, 'change_schema_page_type' ] );
}
/**
* Change the page type to an array if it isn't one, include FAQPage.
*
* @param array|string $page_type The page type.
*
* @return array $page_type The page type that's now an array.
*/
public function change_schema_page_type( $page_type ) {
if ( ! is_array( $page_type ) ) {
$page_type = [ $page_type ];
}
$page_type[] = 'FAQPage';
return $page_type;
}
/**
* Render a list of questions, referencing them by ID.
*
* @return array $data Our Schema graph.
*/
public function generate() {
$question_list = new WPSEO_Schema_FAQ_Question_List( $this->blocks, $this->context );
$graph = $question_list->generate();
return $graph;
}
/**
* Add the Questions in our FAQ blocks as separate pieces to the graph.
*
* @param array $graph Schema data for the current page.
* @param WP_Block_Parser_Block $block The block data array.
* @param WPSEO_Schema_Context $context A value object with context variables.
*
* @return array $data Our Schema graph.
*/
public function render_schema_questions( $graph, $block, $context ) {
$questions = new WPSEO_Schema_FAQ_Questions( $graph, $block, $context );
$graph = $questions->generate();
return $graph;
}
/**
* Determines whether or not a piece should be added to the graph.
*
* @return bool
*/
public function is_needed() {
return $this->is_needed;
}
}

View File

@@ -0,0 +1,243 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns schema HowTo data.
*
* @since 11.5
*/
class WPSEO_Schema_HowTo implements WPSEO_Graph_Piece {
/**
* Determine whether this graph piece is needed or not.
*
* Always false, because this graph piece adds itself using the filter API.
*
* @var bool
*/
private $is_needed = false;
/**
* The HowTo blocks count on the current page.
*
* @var int
*/
private $counter;
/**
* A value object with context variables.
*
* @var WPSEO_Schema_Context
*/
private $context;
/**
* Holds the allowed HTML tags for the jsonText.
*
* @var string
*/
private $allowed_json_text_tags = '<h1><h2><h3><h4><h5><h6><br><ol><ul><li><a><p><b><strong><i><em>';
/**
* WPSEO_Schema_HowTo constructor.
*
* @param WPSEO_Schema_Context $context A value object with context variables.
*
* @codeCoverageIgnore
*/
public function __construct( WPSEO_Schema_Context $context ) {
$this->counter = 0;
$this->context = $context;
add_filter( 'wpseo_schema_block_yoast/how-to-block', [ $this, 'render' ], 10, 2 );
}
/**
* Renders a list of questions, referencing them by ID.
*
* @return array $data Our Schema graph.
*/
public function generate() {
return [];
}
/**
* Renders the How-To block into our graph.
*
* @param array $graph Our Schema data.
* @param array $block The How-To block content.
*
* @return mixed
*/
public function render( $graph, $block ) {
$this->counter++;
$data = [
'@type' => 'HowTo',
'@id' => $this->context->canonical . '#howto-' . $this->counter,
'name' => $this->context->title,
'mainEntityOfPage' => [ '@id' => $this->get_main_schema_id() ],
'description' => '',
];
$json_description = strip_tags( $block['attrs']['jsonDescription'], '<h1><h2><h3><h4><h5><h6><br><ol><ul><li><a><p><b><strong><i><em>' );
if ( isset( $json_description ) ) {
$data['description'] = $json_description;
}
$this->add_duration( $data, $block['attrs'] );
$this->add_steps( $data, $block['attrs']['steps'] );
$graph[] = $data;
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.
*
* @return array $data Our schema data.
*/
private function add_duration( &$data, $attributes ) {
if ( ! empty( $attributes['hasDuration'] ) && $attributes['hasDuration'] ) {
$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' );
}
}
return $data;
}
/**
* Determines whether we're part of an article or a webpage.
*
* @return string A reference URL.
*/
protected function get_main_schema_id() {
if ( $this->context->site_represents !== false && WPSEO_Schema_Article::is_article_post_type() ) {
return $this->context->canonical . WPSEO_Schema_IDs::ARTICLE_HASH;
}
return $this->context->canonical . WPSEO_Schema_IDs::WEBPAGE_HASH;
}
/**
* Determines whether or not a piece should be added to the graph.
*
* @return bool
*/
public function is_needed() {
return $this->is_needed;
}
/**
* 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,
];
$json_text = strip_tags( $step['jsonText'], $this->allowed_json_text_tags );
$json_name = wp_strip_all_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, $step );
$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 array $step The step block data.
*/
private function add_step_description( &$schema_step, $step ) {
$json_text = strip_tags( $step['jsonText'], $this->allowed_json_text_tags );
if ( empty( $json_text ) ) {
return;
}
$schema_step['itemListElement'] = [];
$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 ) {
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 image schema from the attachment $url.
*
* @param string $url Attachment url.
*
* @return array Image schema.
*
* @codeCoverageIgnore
*/
protected function get_image_schema( $url ) {
$image = new WPSEO_Schema_Image( $this->context->canonical . '#schema-image-' . md5( $url ) );
return $image->generate_from_url( $url );
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Constants used for @id variables.
*
* @since 10.2
*/
class WPSEO_Schema_IDs {
/**
* Hash used for the Author `@id`.
*/
const AUTHOR_HASH = '#author';
/**
* Hash used for the Author Logo's `@id`.
*/
const AUTHOR_LOGO_HASH = '#authorlogo';
/**
* Hash used for the Breadcrumb's `@id`.
*/
const BREADCRUMB_HASH = '#breadcrumb';
/**
* Hash used for the Person `@id`.
*/
const PERSON_HASH = '#/schema/person/';
/**
* Hash used for the Article `@id`.
*/
const ARTICLE_HASH = '#article';
/**
* Hash used for the Organization `@id`.
*/
const ORGANIZATION_HASH = '#organization';
/**
* Hash used for the Organization `@id`.
*/
const ORGANIZATION_LOGO_HASH = '#logo';
/**
* Hash used for the logo `@id`.
*/
const PERSON_LOGO_HASH = '#personlogo';
/**
* Hash used for an Article's primary image `@id`.
*/
const PRIMARY_IMAGE_HASH = '#primaryimage';
/**
* Hash used for the WebPage's `@id`.
*/
const WEBPAGE_HASH = '#webpage';
/**
* Hash used for the Website's `@id`.
*/
const WEBSITE_HASH = '#website';
}

View File

@@ -0,0 +1,154 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns schema image data.
*
* @since 11.1
*
* @property string $schema_id The `@id` to use for the returned image.
* @property array $data The ImageObject Schema array.
* @property int $attachment_id The ID of the attachment used to generate the object.
*/
class WPSEO_Schema_Image {
/**
* The `@id` to use for the returned image.
*
* @var string
*/
private $schema_id;
/**
* The ImageObject Schema array.
*
* @var array
*/
private $data;
/**
* The ID of the attachment used to generate the object.
*
* @var int
*/
private $attachment_id;
/**
* WPSEO_Schema_Image constructor.
*
* @param string $schema_id The string to use in an image's `@id`.
*/
public function __construct( $schema_id ) {
$this->schema_id = $schema_id;
$this->generate_object();
}
/**
* Find an image based on its URL and generate a Schema object for it.
*
* @param string $url The image URL to base our object on.
* @param string $caption An optional caption.
*
* @return array Schema ImageObject array.
*/
public function generate_from_url( $url, $caption = '' ) {
$attachment_id = WPSEO_Image_Utils::get_attachment_by_url( $url );
if ( $attachment_id > 0 ) {
return $this->generate_from_attachment_id( $attachment_id, $caption );
}
return $this->simple_image_object( $url, $caption );
}
/**
* Retrieve data about an image from the database and use it to generate a Schema object.
*
* @param int $attachment_id The attachment to retrieve data from.
* @param string $caption The caption string, if there is one.
*
* @return array Schema ImageObject array.
*/
public function generate_from_attachment_id( $attachment_id, $caption = '' ) {
$this->attachment_id = $attachment_id;
$this->data['url'] = wp_get_attachment_image_url( $this->attachment_id, 'full' );
$this->add_image_size();
$this->add_caption( $caption );
return $this->data;
}
/**
* If we can't find $url in our database, we output a simple ImageObject.
*
* @param string $url The image URL.
* @param string $caption A caption, if set.
*
* @return array $data Schema ImageObject array.
*/
public function simple_image_object( $url, $caption = '' ) {
$this->data['url'] = $url;
if ( ! empty( $caption ) ) {
$this->data['caption'] = $caption;
}
return $this->data;
}
/**
* Retrieves an image's caption if set, or uses the alt tag if that's set.
*
* @param string $caption The caption string, if there is one.
*
* @return void
*/
private function add_caption( $caption = '' ) {
if ( ! empty( $caption ) ) {
$this->data['caption'] = $caption;
return;
}
$caption = wp_get_attachment_caption();
if ( ! empty( $caption ) ) {
$this->data['caption'] = $caption;
return;
}
$caption = get_post_meta( $this->attachment_id, '_wp_attachment_image_alt', true );
if ( ! empty( $caption ) ) {
$this->data['caption'] = $caption;
}
}
/**
* Generates our bare bone ImageObject.
*
* @return void
*/
private function generate_object() {
$this->data = [
'@type' => 'ImageObject',
'@id' => $this->schema_id,
];
}
/**
* Adds image's width and height.
*
* @return void
*/
private function add_image_size() {
$image_meta = wp_get_attachment_metadata( $this->attachment_id );
if ( empty( $image_meta['width'] ) || empty( $image_meta['height'] ) ) {
return;
}
$this->data['width'] = $image_meta['width'];
$this->data['height'] = $image_meta['height'];
}
}

View File

@@ -0,0 +1,146 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns ImageObject schema data.
*
* @since 11.5
*/
class WPSEO_Schema_MainImage implements WPSEO_Graph_Piece {
/**
* A value object with context variables.
*
* @var WPSEO_Schema_Context
*/
private $context;
/**
* WPSEO_Schema_WebPage constructor.
*
* @codeCoverageIgnore
*
* @param WPSEO_Schema_Context $context A value object with context variables.
*/
public function __construct( WPSEO_Schema_Context $context ) {
$this->context = $context;
}
/**
* Determines whether or not a piece should be added to the graph.
*
* @return bool
*/
public function is_needed() {
if ( is_singular() ) {
return true;
}
return false;
}
/**
* Adds a main image for the current URL to the schema if there is one.
*
* This can be either the featured image, or fall back to the first image in the content of the page.
*
* @return false|array $data Image Schema.
*/
public function generate() {
$image_id = $this->context->canonical . WPSEO_Schema_IDs::PRIMARY_IMAGE_HASH;
$image_schema = $this->get_featured_image( $this->context->id, $image_id );
if ( $image_schema === null ) {
$image_schema = $this->get_first_content_image( $this->context->id, $image_id );
}
if ( $image_schema === null ) {
return false;
}
$this->context->has_image = true;
return $image_schema;
}
/**
* Gets the image schema for the web page based on the featured image.
*
* @param int $post_id The post id.
* @param string $image_id The image schema id.
*
* @return array|null The image schema object or null if there is no featured image.
*/
private function get_featured_image( $post_id, $image_id ) {
if ( ! has_post_thumbnail( $post_id ) ) {
return null;
}
return $this->generate_image_schema_from_attachment_id( $image_id );
}
/**
* Gets the image schema for the web page based on the first content image image.
*
* @param int $post_id The post id.
* @param string $image_id The image schema id.
*
* @return array|null The image schema object or null if there is no image in the content.
*/
private function get_first_content_image( $post_id, $image_id ) {
$image_url = $this->get_first_usable_content_image_for_post( $post_id );
if ( $image_url === null ) {
return null;
}
return $this->generate_image_schema_from_url( $image_id, $image_url );
}
/**
* Gets the post's first usable content image. Null if none is available.
*
* @codeCoverageIgnore
*
* @param int $post_id The post id.
*
* @return string|null The image URL or null if there is no image.
*/
protected function get_first_usable_content_image_for_post( $post_id ) {
return WPSEO_Image_Utils::get_first_usable_content_image_for_post( $post_id );
}
/**
* Generates image schema from the attachment id.
*
* @codeCoverageIgnore
*
* @param string $image_id The image schema id.
*
* @return array Schema ImageObject array.
*/
protected function generate_image_schema_from_attachment_id( $image_id ) {
$schema_image = new WPSEO_Schema_Image( $image_id );
return $schema_image->generate_from_attachment_id( get_post_thumbnail_id() );
}
/**
* Generates image schema from the url.
*
* @codeCoverageIgnore
*
* @param string $image_id The image schema id.
* @param string $image_url The image URL.
*
* @return array Schema ImageObject array.
*/
protected function generate_image_schema_from_url( $image_id, $image_url ) {
$schema_image = new WPSEO_Schema_Image( $image_id );
return $schema_image->generate_from_url( $image_url );
}
}

View File

@@ -0,0 +1,114 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns schema Organization data.
*
* @since 10.2
*/
class WPSEO_Schema_Organization implements WPSEO_Graph_Piece {
/**
* A value object with context variables.
*
* @var WPSEO_Schema_Context
*/
private $context;
/**
* WPSEO_Schema_Organization constructor.
*
* @param WPSEO_Schema_Context $context A value object with context variables.
*/
public function __construct( WPSEO_Schema_Context $context ) {
$this->context = $context;
}
/**
* 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 $data The Organization schema.
*/
public function generate() {
$data = [
'@type' => 'Organization',
'@id' => $this->context->site_url . WPSEO_Schema_IDs::ORGANIZATION_HASH,
'name' => $this->context->company_name,
'url' => $this->context->site_url,
'sameAs' => $this->fetch_social_profiles(),
];
$data = $this->add_logo( $data );
return $data;
}
/**
* Adds a site's logo.
*
* @param array $data The Organization schema.
*
* @return array $data The Organization schema.
*/
private function add_logo( $data ) {
$schema_id = $this->context->site_url . WPSEO_Schema_IDs::ORGANIZATION_LOGO_HASH;
$schema_image = new WPSEO_Schema_Image( $schema_id );
$data['logo'] = $schema_image->generate_from_attachment_id( $this->context->company_logo_id, $this->context->company_name );
$data['image'] = [ '@id' => $schema_id ];
return $data;
}
/**
* Retrieve the social profiles to display in the organization schema.
*
* @since 1.8
*
* @link https://developers.google.com/webmasters/structured-data/customize/social-profiles
*
* @return array $profiles An array of social profiles.
*/
private function fetch_social_profiles() {
$profiles = [];
$social_profiles = [
'facebook_site',
'instagram_url',
'linkedin_url',
'myspace_url',
'youtube_url',
'pinterest_url',
'wikipedia_url',
];
foreach ( $social_profiles as $profile ) {
if ( WPSEO_Options::get( $profile, '' ) !== '' ) {
$profiles[] = WPSEO_Options::get( $profile );
}
}
if ( WPSEO_Options::get( 'twitter_site', '' ) !== '' ) {
$profiles[] = 'https://twitter.com/' . WPSEO_Options::get( 'twitter_site' );
}
/**
* 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;
}
}

View File

@@ -0,0 +1,262 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns schema Person data.
*
* @since 10.2
*/
class WPSEO_Schema_Person implements WPSEO_Graph_Piece {
/**
* A value object with context variables.
*
* @var WPSEO_Schema_Context
*/
private $context;
/**
* 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',
];
/**
* The hash used for images.
*
* @var string
*/
protected $image_hash;
/**
* WPSEO_Schema_Person constructor.
*
* @param WPSEO_Schema_Context $context A value object with context variables.
*/
public function __construct( WPSEO_Schema_Context $context ) {
$this->image_hash = WPSEO_Schema_IDs::PERSON_LOGO_HASH;
$this->context = $context;
}
/**
* Determine whether we should return Person schema.
*
* @return bool
*/
public function is_needed() {
if ( ( $this->context->site_represents === 'person' ) || is_author() ) {
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 );
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 = $this->context->site_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.
*/
return apply_filters( 'wpseo_schema_person_user_id', $user_id );
}
/**
* Retrieve a list of social profile URLs for Person.
*
* @param int $user_id User ID.
*
* @return string[] $output A list of social profiles.
*/
protected function get_social_profiles( $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 );
$output = [];
foreach ( $social_profiles as $profile ) {
$social_url = $this->url_social_site( $profile, $user_id );
if ( $social_url ) {
$output[] = $social_url;
}
}
return $output;
}
/**
* Builds our array of Schema Person data for a given user ID.
*
* @param int $user_id The user ID to use.
*
* @return array An array of Schema Person data.
*/
protected function build_person_data( $user_id ) {
$user_data = get_userdata( $user_id );
$data = [
'@type' => $this->type,
'@id' => WPSEO_Schema_Utils::get_user_schema_id( $user_id, $this->context ),
'name' => $user_data->display_name,
];
$data = $this->add_image( $data, $user_data );
if ( ! empty( $user_data->description ) ) {
$data['description'] = $user_data->description;
}
$social_profiles = $this->get_social_profiles( $user_id );
if ( is_array( $social_profiles ) ) {
$data['sameAs'] = $social_profiles;
}
return $data;
}
/**
* Returns an ImageObject for the persons avatar.
*
* @param array $data The Person schema.
* @param \WP_User $user_data User data.
*
* @return array $data The Person schema.
*/
protected function add_image( $data, $user_data ) {
$schema_id = $this->context->site_url . $this->image_hash;
$data = $this->set_image_from_options( $data, $schema_id );
if ( ! isset( $data['image'] ) ) {
$data = $this->set_image_from_avatar( $data, $user_data, $schema_id );
}
if ( is_array( $this->type ) && in_array( 'Organization', $this->type ) ) {
$data['logo'] = [ '@id' => $schema_id ];
}
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.
*
* @return array $data The Person schema.
*/
private function set_image_from_options( $data, $schema_id ) {
if ( $this->context->site_represents !== 'person' ) {
return $data;
}
$person_logo_id = WPSEO_Image_Utils::get_attachment_id_from_settings( 'person_logo' );
if ( $person_logo_id ) {
$image = new WPSEO_Schema_Image( $schema_id );
$data['image'] = $image->generate_from_attachment_id( $person_logo_id, $data['name'] );
}
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.
*
* @return array $data The Person schema.
*/
private function set_image_from_avatar( $data, $user_data, $schema_id ) {
// 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;
}
$schema_image = new WPSEO_Schema_Image( $schema_id );
$data['image'] = $schema_image->simple_image_object( $url, $user_data->display_name );
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 ) ) {
switch ( $social_site ) {
case 'twitter':
$url = 'https://twitter.com/' . $url;
break;
}
}
return $url;
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Schema utility functions.
*
* @since 11.6
*/
class WPSEO_Schema_Utils {
/**
* Retrieve a users Schema ID.
*
* @param int $user_id The ID of the User you need a Schema ID for.
* @param WPSEO_Schema_Context $context A value object with context variables.
*
* @return string The user's schema ID.
*/
public static function get_user_schema_id( $user_id, $context ) {
$user = get_userdata( $user_id );
if ( is_object( $user ) && isset( $user->user_login ) ) {
return $context->site_url . WPSEO_Schema_IDs::PERSON_HASH . wp_hash( $user->user_login . $user_id );
}
return $context->site_url . WPSEO_Schema_IDs::PERSON_HASH;
}
}

View File

@@ -0,0 +1,172 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns schema WebPage data.
*
* @since 10.2
*/
class WPSEO_Schema_WebPage implements WPSEO_Graph_Piece {
/**
* The date helper.
*
* @var WPSEO_Date_Helper
*/
protected $date;
/**
* A value object with context variables.
*
* @var WPSEO_Schema_Context
*/
private $context;
/**
* WPSEO_Schema_WebPage constructor.
*
* @param WPSEO_Schema_Context $context A value object with context variables.
*/
public function __construct( WPSEO_Schema_Context $context ) {
$this->context = $context;
$this->date = new WPSEO_Date_Helper();
}
/**
* Determines whether or not a piece should be added to the graph.
*
* @return bool
*/
public function is_needed() {
if ( is_404() ) {
return false;
}
return true;
}
/**
* Returns WebPage schema data.
*
* @return array WebPage schema data.
*/
public function generate() {
$data = [
'@type' => $this->determine_page_type(),
'@id' => $this->context->canonical . WPSEO_Schema_IDs::WEBPAGE_HASH,
'url' => $this->context->canonical,
'inLanguage' => get_bloginfo( 'language' ),
'name' => $this->context->title,
'isPartOf' => [
'@id' => $this->context->site_url . WPSEO_Schema_IDs::WEBSITE_HASH,
],
];
if ( is_front_page() ) {
if ( $this->context->site_represents_reference ) {
$data['about'] = $this->context->site_represents_reference;
}
}
if ( is_singular() ) {
$this->add_image( $data );
$post = get_post( $this->context->id );
$data['datePublished'] = $this->date->format( $post->post_date_gmt );
$data['dateModified'] = $this->date->format( $post->post_modified_gmt );
if ( get_post_type( $post ) === 'post' ) {
$data = $this->add_author( $data, $post );
}
}
if ( ! empty( $this->context->description ) ) {
$data['description'] = strip_tags( $this->context->description, '<h1><h2><h3><h4><h5><h6><br><ol><ul><li><a><p><b><strong><i><em>' );
}
if ( $this->add_breadcrumbs() ) {
$data['breadcrumb'] = [
'@id' => $this->context->canonical . WPSEO_Schema_IDs::BREADCRUMB_HASH,
];
}
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' => WPSEO_Schema_Utils::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 . WPSEO_Schema_IDs::PRIMARY_IMAGE_HASH ];
}
}
/**
* Determine if we should add a breadcrumb attribute.
*
* @return bool
*/
private function add_breadcrumbs() {
if ( is_front_page() ) {
return false;
}
if ( $this->context->breadcrumbs_enabled ) {
return true;
}
return false;
}
/**
* Determine the page type for the current page.
*
* @return string
*/
private function determine_page_type() {
switch ( true ) {
case is_search():
$type = 'SearchResultsPage';
break;
case is_author():
$type = 'ProfilePage';
break;
case WPSEO_Frontend_Page_Type::is_posts_page():
case WPSEO_Frontend_Page_Type::is_home_posts_page():
case is_archive():
$type = 'CollectionPage';
break;
default:
$type = 'WebPage';
}
/**
* Filter: 'wpseo_schema_webpage_type' - Allow changing the WebPage type.
*
* @api string $type The WebPage type.
*/
return apply_filters( 'wpseo_schema_webpage_type', $type );
}
}

View File

@@ -0,0 +1,118 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Returns schema Website data.
*
* @since 10.2
*/
class WPSEO_Schema_Website implements WPSEO_Graph_Piece {
/**
* A value object with context variables.
*
* @var WPSEO_Schema_Context
*/
private $context;
/**
* WPSEO_Schema_Website constructor.
*
* @param WPSEO_Schema_Context $context A value object with context variables.
*/
public function __construct( WPSEO_Schema_Context $context ) {
$this->context = $context;
}
/**
* 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.
*
* @since 1.5.7
*
* @link https://developers.google.com/structured-data/site-name
*
* @return array Website data blob.
*/
public function generate() {
$data = [
'@type' => 'WebSite',
'@id' => $this->context->site_url . WPSEO_Schema_IDs::WEBSITE_HASH,
'url' => $this->context->site_url,
'name' => $this->context->site_name,
];
if ( $this->context->site_description ) {
$data['description'] = $this->context->site_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 );
return $data;
}
/**
* Returns an alternate name if one was specified in the Yoast SEO settings.
*
* @param array $data The website data array.
*
* @return array $data
*/
private function add_alternate_name( $data ) {
if ( WPSEO_Options::get( 'alternate_website_name', '' ) !== '' ) {
$data['alternateName'] = WPSEO_Options::get( 'alternate_website_name' );
}
return $data;
}
/**
* Adds the internal search JSON LD code to the homepage if it's not disabled.
*
* @link https://developers.google.com/structured-data/slsb-overview
*
* @param array $data The website data array.
*
* @return array $data
*/
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 ) ) {
/**
* 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' => $search_url,
'query-input' => 'required name=search_term_string',
];
}
return $data;
}
}

View File

@@ -0,0 +1,198 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
/**
* Class WPSEO_Schema
*
* Outputs schema code specific for Google's JSON LD stuff.
*
* @since 1.8
*/
class WPSEO_Schema implements WPSEO_WordPress_Integration {
/**
* Holds the parsed blocks for the current page.
*
* @var array
*/
private $parsed_blocks = [];
/**
* Holds context variables about the current page and site.
*
* @var WPSEO_Schema_Context
*/
private $context;
/**
* Registers the hooks.
*/
public function register_hooks() {
add_action( 'wpseo_head', [ $this, 'json_ld' ], 91 );
add_action( 'wpseo_json_ld', [ $this, 'generate' ], 1 );
// This AMP hook is only used in Reader (formerly Classic) mode.
add_action( 'amp_post_template_head', [ $this, 'json_ld' ], 9 );
}
/**
* JSON LD output function that the functions for specific code can hook into.
*
* @since 1.8
*/
public function json_ld() {
$deprecated_data = [
'_deprecated' => 'Please use the "wpseo_schema_*" filters to extend the Yoast SEO schema data - see the WPSEO_Schema class.',
];
/**
* Filter: 'wpseo_json_ld_output' - Allows disabling Yoast's schema output entirely.
*
* @api mixed If false or an empty array is returned, disable our output.
*/
$return = apply_filters( 'wpseo_json_ld_output', $deprecated_data, '' );
if ( $return === [] || $return === false ) {
return;
}
// Remove the AMP hook that also outputs Schema metadata on AMP pages.
remove_action( 'amp_post_template_head', 'amp_print_schemaorg_metadata' );
do_action( 'wpseo_json_ld' );
}
/**
* Outputs the JSON LD code in a valid JSON+LD wrapper.
*
* @since 10.2
*
* @return void
*/
public function generate() {
$graph = [];
$this->context = new WPSEO_Schema_Context();
$pieces = $this->get_graph_pieces();
// Parse the Gutenberg blocks so we know whether to show pieces for those.
$this->parse_blocks();
foreach ( $pieces as $piece ) {
$identifier = str_replace( 'wpseo_schema_', '', strtolower( get_class( $piece ) ) );
if ( property_exists( $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;
}
$graph_piece = $piece->generate();
/**
* Filter: 'wpseo_schema_<identifier>' - Allows changing graph piece output.
*
* @api array $graph_piece The graph piece to filter.
*/
$graph_piece = apply_filters( 'wpseo_schema_' . $identifier, $graph_piece );
if ( is_array( $graph_piece ) ) {
$graph[] = $graph_piece;
}
}
foreach ( $this->parsed_blocks as $block_type => $blocks ) {
foreach ( $blocks as $block ) {
/**
* Filter: 'wpseo_schema_block_<block-type>' - Allows filtering graph output per block.
*
* @param WP_Block_Parser_Block $block The block.
* @param WPSEO_Schema_Context $context A value object with context variables.
*
* @api array $graph Our Schema output.
*/
$block_type = strtolower( $block['blockName'] );
$graph = apply_filters( 'wpseo_schema_block_' . $block_type, $graph, $block, $this->context );
}
}
WPSEO_Utils::schema_output( $graph, 'yoast-schema-graph yoast-schema-graph--main' );
}
/**
* Gets all the graph pieces we need.
*
* @return array A filtered array of graph pieces.
*/
private function get_graph_pieces() {
$pieces = [
new WPSEO_Schema_Organization( $this->context ),
new WPSEO_Schema_Person( $this->context ),
new WPSEO_Schema_Website( $this->context ),
new WPSEO_Schema_MainImage( $this->context ),
new WPSEO_Schema_WebPage( $this->context ),
new WPSEO_Schema_Breadcrumb( $this->context ),
new WPSEO_Schema_Article( $this->context ),
new WPSEO_Schema_Author( $this->context ),
new WPSEO_Schema_FAQ( $this->context ),
new WPSEO_Schema_HowTo( $this->context ),
];
/**
* Filter: 'wpseo_schema_graph_pieces' - Allows adding pieces to the graph.
*
* @param WPSEO_Schema_Context $context An object with context variables.
*
* @api array $pieces The schema pieces.
*/
return apply_filters( 'wpseo_schema_graph_pieces', $pieces, $this->context );
}
/**
* Parse the blocks and pass them on to our head.
*/
private function parse_blocks() {
if ( ! function_exists( 'parse_blocks' ) ) {
return;
}
if ( ! is_singular() ) {
return;
}
$this->get_parsed_blocks();
foreach ( array_keys( $this->parsed_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 array $blocks All the blocks of this block type.
* @param WPSEO_Schema_Context $context A value object with context variables.
*/
do_action( 'wpseo_pre_schema_block_type_' . $block_type, $this->parsed_blocks[ $block_type ], $this->context );
}
}
/**
* Parse the blocks and loop through them.
*/
private function get_parsed_blocks() {
$post = get_post();
$parsed_blocks = parse_blocks( $post->post_content );
foreach ( $parsed_blocks as $block ) {
if ( ! isset( $this->parsed_blocks[ $block['blockName'] ] ) || ! is_array( $this->parsed_blocks[ $block['blockName'] ] ) ) {
$this->parsed_blocks[ $block['blockName'] ] = [];
}
$this->parsed_blocks[ $block['blockName'] ][] = $block;
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Frontend\Schema
*/
if ( ! interface_exists( 'WPSEO_Graph_Piece' ) ) {
/**
* An interface for registering Schema Graph Pieces.
*
* @since 10.2
*/
interface WPSEO_Graph_Piece {
/**
* Add your piece of the graph.
*
* @return array|bool $graph A graph piece on success, false on failure.
*/
public function generate();
/**
* Determines whether or not a piece should be added to the graph.
*
* @return bool
*/
public function is_needed();
}
}