Commit realizado el 12:13:52 08-04-2024

This commit is contained in:
Pagina Web Monito
2024-04-08 12:13:55 -04:00
commit 0c33094de9
7815 changed files with 1365694 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,56 @@
<?php
/**
* The Block Admin
*
* @since 1.0.104
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema\Blocks;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Helpers\Arr;
defined( 'ABSPATH' ) || exit;
/**
* Block Admin class.
*/
class Admin {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->filter( 'rank_math/settings/general', 'add_general_settings' );
}
/**
* Add block settings into general optional panel.
*
* @param array $tabs Array of option panel tabs.
*
* @return array
*/
public function add_general_settings( $tabs ) {
Arr::insert(
$tabs,
[
'blocks' => [
'icon' => 'rm-icon rm-icon-stories',
'title' => esc_html__( 'Blocks', 'rank-math' ),
'desc' => esc_html__( 'Take control over the default settings available for Rank Math Blocks.', 'rank-math' ),
'file' => dirname( __FILE__ ) . '/views/options-general.php',
],
],
7
);
return $tabs;
}
}

View File

@@ -0,0 +1,238 @@
<?php
/**
* The FAQ Block
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use WP_Block_Type_Registry;
defined( 'ABSPATH' ) || exit;
/**
* Block_FAQ class.
*/
class Block_FAQ extends Block {
/**
* Block type name.
*
* @var string
*/
private $block_type = 'rank-math/faq-block';
/**
* The single instance of the class.
*
* @var Block_FAQ
*/
protected static $instance = null;
/**
* Retrieve main Block_FAQ instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Block_FAQ
*/
public static function get() {
if ( is_null( self::$instance ) && ! ( self::$instance instanceof Block_FAQ ) ) {
self::$instance = new Block_FAQ();
}
return self::$instance;
}
/**
* The Constructor.
*/
public function __construct() {
if ( WP_Block_Type_Registry::get_instance()->is_registered( $this->block_type ) ) {
return;
}
register_block_type(
$this->block_type,
[
'render_callback' => [ $this, 'render' ],
'editor_style' => 'rank-math-block-admin',
'attributes' => [
'listStyle' => [
'type' => 'string',
'default' => '',
],
'titleWrapper' => [
'type' => 'string',
'default' => 'h3',
],
'sizeSlug' => [
'type' => 'string',
'default' => 'thumbnail',
],
'questions' => [
'type' => 'array',
'default' => [],
'items' => [ 'type' => 'object' ],
],
'listCssClasses' => [
'type' => 'string',
'default' => '',
],
'titleCssClasses' => [
'type' => 'string',
'default' => '',
],
'contentCssClasses' => [
'type' => 'string',
'default' => '',
],
'textAlign' => [
'type' => 'string',
'default' => 'left',
],
],
]
);
add_filter( 'rank_math/schema/block/faq-block', [ $this, 'add_graph' ], 10, 2 );
}
/**
* Add FAQ schema data in JSON-LD array.
*
* @param array $data Array of JSON-LD data.
* @param array $block JsonLD Instance.
*
* @return array
*/
public function add_graph( $data, $block ) {
// Early bail.
if ( ! $this->has_questions( $block['attrs'] ) ) {
return $data;
}
if ( ! isset( $data['faqs'] ) ) {
$data['faqs'] = [
'@type' => 'FAQPage',
'mainEntity' => [],
];
}
$permalink = get_permalink() . '#';
foreach ( $block['attrs']['questions'] as $question ) {
if ( empty( $question['title'] ) || empty( $question['content'] ) || empty( $question['visible'] ) ) {
continue;
}
$question['title'] = do_shortcode( $question['title'] );
$question['content'] = do_shortcode( $question['content'] );
if ( empty( $question['id'] ) ) {
$question['id'] = 'rm-faq-' . md5( $question['title'] );
}
$data['faqs']['mainEntity'][] = [
'@type' => 'Question',
'url' => $permalink . $question['id'],
'name' => wp_strip_all_tags( $question['title'] ),
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => $this->clean_text( $question['content'] ),
],
];
}
return $data;
}
/**
* Render block content.
*
* @param array $attributes Array of atributes.
* @return string
*/
public static function markup( $attributes = [] ) {
$list_tag = self::get()->get_list_style( $attributes['listStyle'] );
$item_tag = self::get()->get_list_item_style( $attributes['listStyle'] );
$class = 'rank-math-block';
if ( ! empty( $attributes['className'] ) ) {
$class .= ' ' . esc_attr( $attributes['className'] );
}
// HTML.
$out = [];
$out[] = sprintf( '<div id="rank-math-faq" class="%1$s"%2$s>', $class, self::get()->get_styles( $attributes ) );
$out[] = sprintf( '<%1$s class="rank-math-list %2$s">', $list_tag, esc_attr( $attributes['listCssClasses'] ) );
// Questions.
foreach ( $attributes['questions'] as $question ) {
if ( empty( $question['title'] ) || empty( $question['content'] ) || empty( $question['visible'] ) ) {
continue;
}
if ( empty( $question['id'] ) ) {
$question['id'] = 'rm-faq-' . md5( $question['title'] );
}
$out[] = sprintf( '<%1$s id="%2$s" class="rank-math-list-item">', $item_tag, $question['id'] );
$out[] = sprintf(
'<%1$s class="rank-math-question %2$s">%3$s</%1$s>',
apply_filters( 'rank_math/blocks/faq/title_wrapper', esc_attr( $attributes['titleWrapper'] ) ),
esc_attr( $attributes['titleCssClasses'] ),
wp_kses_post( $question['title'] )
);
$out[] = '<div class="rank-math-answer ' . esc_attr( $attributes['contentCssClasses'] ) . '">';
if ( ! empty( $question['imageUrl'] ) ) {
$out[] = '<img src="' . esc_url( $question['imageUrl'] ) . '" />';
} else {
$out[] = self::get()->get_image( $question, $attributes['sizeSlug'] );
}
$out[] = self::get()->normalize_text( $question['content'], 'faq' );
$out[] = '</div>';
$out[] = sprintf( '</%1$s>', $item_tag );
}
$out[] = sprintf( '</%1$s>', $list_tag );
$out[] = '</div>';
return join( "\n", $out );
}
/**
* Render block content
*
* @param array $attributes Array of atributes.
*
* @return string
*/
public function render( $attributes ) {
// Early bail.
if ( ! $this->has_questions( $attributes ) ) {
return '';
}
return self::markup( $attributes );
}
/**
* Check if FAQ block has questions data.
*
* @param array $attributes Array of attributes.
*
* @return boolean
*/
private function has_questions( $attributes ) {
return ! isset( $attributes['questions'] ) || empty( $attributes['questions'] ) ? false : true;
}
}

View File

@@ -0,0 +1,513 @@
<?php
/**
* The HowTo Block
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Paper\Paper;
use RankMath\Helpers\Str;
use RankMath\Helpers\Attachment;
use WP_Block_Type_Registry;
defined( 'ABSPATH' ) || exit;
/**
* HowTo Block class.
*/
class Block_HowTo extends Block {
/**
* Block type name.
*
* @var string
*/
private $block_type = 'rank-math/howto-block';
/**
* The single instance of the class.
*
* @var Block_HowTo
*/
protected static $instance = null;
/**
* Retrieve main Block_HowTo instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Block_HowTo
*/
public static function get() {
if ( is_null( self::$instance ) && ! ( self::$instance instanceof Block_HowTo ) ) {
self::$instance = new Block_HowTo();
}
return self::$instance;
}
/**
* The Constructor.
*/
public function __construct() {
if ( WP_Block_Type_Registry::get_instance()->is_registered( $this->block_type ) ) {
return;
}
register_block_type(
$this->block_type,
[
'render_callback' => [ $this, 'render' ],
'editor_style' => 'rank-math-block-admin',
'attributes' => [
'hasDuration' => [
'type' => 'boolean',
'default' => false,
],
'days' => [
'type' => 'string',
'default' => '',
],
'hours' => [
'type' => 'string',
'default' => '',
],
'minutes' => [
'type' => 'string',
'default' => '',
],
'description' => [
'type' => 'string',
'default' => '',
],
'steps' => [
'type' => 'array',
'default' => [],
'items' => [ 'type' => 'object' ],
],
'sizeSlug' => [
'type' => 'string',
'default' => 'full',
],
'imageID' => [
'type' => 'integer',
],
'mainSizeSlug' => [
'type' => 'string',
'default' => 'full',
],
'listStyle' => [
'type' => 'string',
'default' => '',
],
'timeLabel' => [
'type' => 'string',
'default' => '',
],
'titleWrapper' => [
'type' => 'string',
'default' => 'h3',
],
'listCssClasses' => [
'type' => 'string',
'default' => '',
],
'titleCssClasses' => [
'type' => 'string',
'default' => '',
],
'contentCssClasses' => [
'type' => 'string',
'default' => '',
],
'textAlign' => [
'type' => 'string',
'default' => 'left',
],
],
]
);
add_filter( 'rank_math/schema/block/howto-block', [ $this, 'add_graph' ], 10, 2 );
}
/**
* Add HowTO schema data in JSON-LD array..
*
* @param array $data Array of JSON-LD data.
* @param array $block JsonLD Instance.
*
* @return array
*/
public function add_graph( $data, $block ) {
// Early bail.
if ( ! $this->has_steps( $block['attrs'] ) ) {
return $data;
}
$attrs = $block['attrs'];
if ( ! isset( $data['howto'] ) ) {
$data['howto'] = [
'@type' => 'HowTo',
'name' => Paper::get()->get_title(),
'description' => isset( $attrs['description'] ) ? $this->clean_text( do_shortcode( $attrs['description'] ) ) : '',
'totalTime' => '',
'step' => [],
];
}
$this->add_step_image( $data['howto'], $attrs );
$this->add_duration( $data['howto'], $attrs );
$permalink = get_permalink() . '#';
foreach ( $attrs['steps'] as $index => $step ) {
if ( empty( $step['visible'] ) ) {
continue;
}
$schema_step = $this->add_step( $step, $permalink . $step['id'] );
if ( $schema_step ) {
$data['howto']['step'][] = $schema_step;
}
}
return $data;
}
/**
* Render block content.
*
* @param array $attributes Array of atributes.
* @return string
*/
public static function markup( $attributes = [] ) {
$list_style = isset( $attributes['listStyle'] ) ? esc_attr( $attributes['listStyle'] ) : '';
$list_css_classes = isset( $attributes['listCssClasses'] ) ? esc_attr( $attributes['listCssClasses'] ) : '';
$title_wrapper = isset( $attributes['titleWrapper'] ) ? esc_attr( $attributes['titleWrapper'] ) : 'h2';
$title_css_classes = isset( $attributes['titleCssClasses'] ) ? esc_attr( $attributes['titleCssClasses'] ) : '';
$content_css_classes = isset( $attributes['contentCssClasses'] ) ? esc_attr( $attributes['contentCssClasses'] ) : '';
$size_slug = isset( $attributes['sizeSlug'] ) ? esc_attr( $attributes['sizeSlug'] ) : '';
$list_tag = self::get()->get_list_style( $list_style );
$item_tag = self::get()->get_list_item_style( $list_style );
$class = 'rank-math-block';
if ( ! empty( $attributes['className'] ) ) {
$class .= ' ' . esc_attr( $attributes['className'] );
}
// HTML.
$out = [];
$out[] = sprintf( '<div id="rank-math-howto" class="%1$s" %2$s>', $class, self::get()->get_styles( $attributes ) );
// HeaderContent.
$out[] = '<div class="rank-math-howto-description">';
if ( ! empty( $attributes['imageUrl'] ) ) {
$out[] = '<img src="' . esc_url( $attributes['imageUrl'] ) . '" />';
} elseif ( ! empty( $attributes['mainSizeSlug'] ) ) {
$out[] = self::get()->get_image( $attributes, $attributes['mainSizeSlug'], '' );
}
if ( ! empty( $attributes['description'] ) ) {
$out[] = self::get()->normalize_text( $attributes['description'], 'howto' );
}
$out[] = '</div>';
$out[] = self::get()->build_duration( $attributes );
$out[] = sprintf( '<%1$s class="rank-math-steps %2$s">', $list_tag, $list_css_classes );
// Steps.
foreach ( $attributes['steps'] as $index => $step ) {
if ( empty( $step['visible'] ) ) {
continue;
}
$step_id = isset( $step['id'] ) ? esc_attr( $step['id'] ) : '';
$out[] = sprintf( '<%1$s id="%2$s" class="rank-math-step">', $item_tag, $step_id );
if ( ! empty( $step['title'] ) ) {
$out[] = sprintf(
'<%1$s class="rank-math-step-title %2$s">%3$s</%1$s>',
$title_wrapper,
$title_css_classes,
$step['title']
);
}
$step_content = ! empty( $step['content'] ) ? self::get()->normalize_text( $step['content'], 'howto' ) : '';
$step_image = ! empty( $step['imageUrl'] ) ? '<img src="' . esc_url( $step['imageUrl'] ) . '" />' : self::get()->get_image( $step, $size_slug, '' );
$out[] = sprintf(
'<div class="rank-math-step-content %2$s">%4$s%3$s</div>',
$title_wrapper,
$content_css_classes,
$step_content,
$step_image
);
$out[] = sprintf( '</%1$s>', $item_tag );
}
$out[] = sprintf( '</%1$s>', $list_tag );
$out[] = '</div>';
return apply_filters( 'rank_math/schema/block/howto/content', join( "\n", $out ), $out, $attributes );
}
/**
* Render block content.
*
* @param array $attributes Array of atributes.
*
* @return string
*/
public function render( $attributes ) {
// Early bail.
if ( ! $this->has_steps( $attributes ) ) {
return '';
}
return self::markup( $attributes );
}
/**
* Add Step
*
* @param array $step Step.
* @param string $permalink Permalink.
*/
private function add_step( $step, $permalink ) {
$name = wp_strip_all_tags( do_shortcode( $step['title'] ) );
$text = $this->clean_text( do_shortcode( $step['content'] ) );
if ( empty( $name ) && empty( $text ) ) {
return false;
}
$schema_step = [
'@type' => 'HowToStep',
'url' => '' . $permalink,
];
if ( empty( $name ) ) {
$schema_step['text'] = '';
if ( empty( $text ) && empty( $schema_step['image'] ) ) {
return false;
}
if ( ! empty( $text ) ) {
$schema_step['text'] = $text;
}
} elseif ( empty( $text ) ) {
$schema_step['text'] = $name;
} else {
$schema_step['name'] = $name;
if ( ! empty( $text ) ) {
$schema_step['itemListElement'] = [
[
'@type' => 'HowToDirection',
'text' => $text,
],
];
}
}
if ( false === $this->add_step_image( $schema_step, $step ) ) {
$this->add_step_image_from_content( $schema_step, $step );
}
return $schema_step;
}
/**
* Checks if we have an inline image and add it.
*
* @param array $schema_step Our Schema output for the Step.
* @param array $step The step block data.
*/
private function add_step_image_from_content( &$schema_step, $step ) {
// Early Bail.
if ( empty( $step['content'] ) || ! Str::contains( '<img', $step['content'] ) ) {
return;
}
// Search for image.
preg_match_all( '/<img.+?src=[\'"]([^\'"]+)[\'"].*?>/i', $step['content'], $matches );
if ( ! isset( $matches[1][0] ) || empty( $matches[1][0] ) ) {
return;
}
$schema_image = [
'@type' => 'ImageObject',
'url' => $matches[1][0],
];
$image_id = Attachment::get_by_url( $schema_image['url'] );
if ( $image_id > 0 ) {
$this->add_caption( $schema_image, $image_id );
$this->add_image_size( $schema_image, $image_id );
}
$schema_step['image'] = $schema_image;
}
/**
* Checks if we have a step image and add it.
*
* @copyright Copyright (C) 2008-2019, Yoast BV
* The following code is a derivative work of the code from the Yoast(https://github.com/Yoast/wordpress-seo/), which is licensed under GPL v3.
*
* @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 ( empty( $step['imageID'] ) ) {
return false;
}
$image_id = absint( $step['imageID'] );
if ( ! ( $image_id > 0 ) ) {
return false;
}
$image_url = wp_get_attachment_image_url( $image_id, 'full' );
if ( ! $image_url ) {
return false;
}
$schema_image = [
'@type' => 'ImageObject',
'url' => $image_url,
];
$this->add_caption( $schema_image, $image_id );
$this->add_image_size( $schema_image, $image_id );
$schema_step['image'] = $schema_image;
return true;
}
/**
* Add caption to schema.
*
* @param array $schema_image Our Schema output for the Image.
* @param int $image_id The image ID.
*/
private function add_caption( &$schema_image, $image_id ) {
$caption = wp_get_attachment_caption( $image_id );
if ( ! empty( $caption ) ) {
$schema_image['caption'] = $caption;
return;
}
$caption = Attachment::get_alt_tag( $image_id );
if ( ! empty( $caption ) ) {
$schema_image['caption'] = $caption;
}
}
/**
* Add image size to schema.
*
* @param array $schema_image Our Schema output for the Image.
* @param int $image_id The image ID.
*/
private function add_image_size( &$schema_image, $image_id ) {
$image_meta = wp_get_attachment_metadata( $image_id );
if ( empty( $image_meta['width'] ) || empty( $image_meta['height'] ) ) {
return;
}
$schema_image['width'] = $image_meta['width'];
$schema_image['height'] = $image_meta['height'];
}
/**
* Add duration to schema.
*
* @param array $data Our Schema output.
* @param array $attrs The block attributes.
*/
private function add_duration( &$data, $attrs ) {
if ( ! empty( $attrs['hasDuration'] ) ) {
$days = absint( $attrs['days'] ?? 0 );
$hours = absint( $attrs['hours'] ?? 0 );
$minutes = absint( $attrs['minutes'] ?? 0 );
if ( ( $days + $hours + $minutes ) > 0 ) {
$data['totalTime'] = esc_attr( 'P' . $days . 'DT' . $hours . 'H' . $minutes . 'M' );
}
}
}
/**
* Generate HowTo duration property.
*
* @param array $attrs The block attributes.
*
* @return string
*/
private function build_duration( $attrs ) {
if ( empty( $attrs['hasDuration'] ) ) {
return '';
}
$days = isset( $attrs['days'] ) ? absint( $attrs['days'] ) : 0;
$hours = isset( $attrs['hours'] ) ? absint( $attrs['hours'] ) : 0;
$minutes = isset( $attrs['minutes'] ) ? absint( $attrs['minutes'] ) : 0;
$elements = [];
if ( $days > 0 ) {
/* translators: %d is the number of days. */
$elements[] = sprintf( _n( '%d day', '%d days', $days, 'rank-math' ), $days );
}
if ( $hours > 0 ) {
/* translators: %d is the number of hours. */
$elements[] = sprintf( _n( '%d hour', '%d hours', $hours, 'rank-math' ), $hours );
}
if ( $minutes > 0 ) {
/* translators: %d is the number of minutes. */
$elements[] = sprintf( _n( '%d minute', '%d minutes', $minutes, 'rank-math' ), $minutes );
}
$count = count( $elements );
$formats = [
1 => '%1$s',
/* translators: placeholders are units of time, e.g. '1 hour and 30 minutes' */
2 => __( '%1$s and %2$s', 'rank-math' ),
/* translators: placeholders are units of time, e.g. '1 day, 8 hours and 30 minutes' */
3 => __( '%1$s, %2$s and %3$s', 'rank-math' ),
];
return sprintf(
'<p class="rank-math-howto-duration"><strong>%2$s</strong> <span>%1$s</span></p>',
isset( $formats[ $count ] ) ? vsprintf( $formats[ $count ], $elements ) : '',
empty( $attrs['timeLabel'] ) ? __( 'Total Time:', 'rank-math' ) : esc_html( $attrs['timeLabel'] )
);
}
/**
* Function to check the HowTo block have steps data.
*
* @param array $attributes Array of attributes.
*
* @return boolean
*/
private function has_steps( $attributes ) {
return ! isset( $attributes['steps'] ) || empty( $attributes['steps'] ) ? false : true;
}
}

View File

@@ -0,0 +1,152 @@
<?php
/**
* The Block Parser
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Helpers\Str;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Block_Parser class.
*/
class Block_Parser {
use Hooker;
/**
* Holds the parsed blocks.
*
* @var array
*/
private $blocks = [];
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'rank_math/json_ld', 'parse', 8 );
}
/**
* Filter function to add Blocks data in schema.
*
* @param array $data Array of JSON-LD data.
*
* @return array
*/
public function parse( $data ) {
if ( ! function_exists( 'parse_blocks' ) || ! is_singular() ) {
return $data;
}
$this->get_parsed_blocks();
foreach ( $this->blocks as $block_type => $blocks ) {
foreach ( $blocks as $block ) {
/**
* Filter: 'rank_math/schema/block/<block-type>' - Allows filtering graph output per block.
*
* @param array $data Array of JSON-LD data.
* @param array $block The block.
*/
$data = $this->do_filter( 'schema/block/' . $block_type, $data, $block );
}
}
return $data;
}
/**
* Parse the blocks and loop through them.
*/
private function get_parsed_blocks() {
$post = get_post();
$parsed_blocks = parse_blocks( $post->post_content );
/**
* Filter: 'rank_math/schema/block/before_filter'
*
* @param array $parsed_blocks Array of parsed blocks.
*/
$parsed_blocks = $this->do_filter( 'schema/block/before_filter', $parsed_blocks );
$this->filter_blocks( $parsed_blocks );
}
/**
* Filter blocks.
*
* @param array $blocks Blocks to filter.
*/
private function filter_blocks( $blocks ) {
foreach ( $blocks as $block ) {
if ( $this->is_nested_block( $block ) || ! $this->is_valid_block( $block ) ) {
continue;
}
$name = \str_replace( 'rank-math/', '', $block['blockName'] );
$name = strtolower( $name );
if ( ! isset( $this->blocks[ $name ] ) || ! is_array( $this->blocks[ $name ] ) ) {
$this->blocks[ $name ] = [];
}
$this->blocks[ $name ][] = $block;
}
}
/**
* Is nested block.
*
* @param array $block Block.
*
* @return boolean
*/
private function is_nested_block( $block ) {
if ( empty( $block['blockName'] ) ) {
return false;
}
/**
* Filter: 'rank_math/schema/nested_blocks' - Allows filtering for nested blocks.
*
* @param array $data Array of JSON-LD data.
* @param array $block The block.
*/
$nested = $this->do_filter(
'schema/nested_blocks',
[
'core/group',
'core/columns',
'core/column',
]
);
if ( ! in_array( $block['blockName'], $nested, true ) ) {
return false;
}
$this->filter_blocks( $block['innerBlocks'] );
return true;
}
/**
* Is block valid.
*
* @param array $block Block.
*
* @return boolean
*/
private function is_valid_block( $block ) {
return ! empty( $block['blockName'] ) && Str::starts_with( 'rank-math', $block['blockName'] );
}
}

View File

@@ -0,0 +1,110 @@
<?php
/**
* The Block Base
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Block class.
*/
class Block {
/**
* Function to certain tags from the text.
*
* @param string $text Block content.
*
* @return string
*/
protected function clean_text( $text ) {
return strip_tags( $text, '<h1><h2><h3><h4><h5><h6><br><ol><ul><li><a><p><b><strong><i><em>' );
}
/**
* Function to get the block image.
*
* @param array $attrs Block attributes data.
* @param string $size Image size.
* @param string $class Attachment image class.
*
* @return string The HTML image element.
*/
protected function get_image( $attrs, $size = 'thumbnail', $class = 'class=alignright' ) {
$image_id = empty( $attrs['imageID'] ) ? '' : absint( $attrs['imageID'] );
if ( ! $image_id ) {
return '';
}
$html = wp_get_attachment_image( $image_id, $size, false, $class );
return $html ? $html : wp_get_attachment_image( $image_id, 'full', false, $class );
}
/**
* Get styles
*
* @param array $attributes Array of attributes.
*
* @return string
*/
protected function get_styles( $attributes ) {
return empty( $attributes['textAlign'] ) || 'left' === $attributes['textAlign']
? ''
: ' style="' . join( ';', [ 'text-align:' . $attributes['textAlign'] ] ) . '"';
}
/**
* Get list style
*
* @param string $style Style.
*
* @return string
*/
protected function get_list_style( $style ) {
if ( 'numbered' === $style ) {
return 'ol';
}
return 'unordered' === $style ? 'ul' : 'div';
}
/**
* Get list item style
*
* @param string $style Style.
*
* @return string
*/
protected function get_list_item_style( $style ) {
return in_array( $style, [ 'numbered', 'unordered' ], true ) ? 'li' : 'div';
}
/**
* Normalize the block text.
*
* @param string $text Text.
* @param string $block Block name.
*
* @return string
*/
protected function normalize_text( $text, $block ) {
/**
* Filter: Allow developers to preserve line breaks.
*
* @param bool $return If set, this will convert all remaining line breaks after paragraphing.
* @param string $block Block name.
*/
return wpautop( $text, apply_filters( 'rank_math/block/preserve_line_breaks', true, $block ) );
}
}

View File

@@ -0,0 +1 @@
.wp-block-rank-math-toc-block nav li,.wp-block-rank-math-toc-block nav div{position:relative;min-height:28px;margin-bottom:0}.wp-block-rank-math-toc-block nav li.disabled,.wp-block-rank-math-toc-block nav div.disabled{display:block !important;opacity:0.5}.wp-block-rank-math-toc-block nav li .components-base-control,.wp-block-rank-math-toc-block nav div .components-base-control{position:absolute;top:2px;left:-4px;right:-3px}.wp-block-rank-math-toc-block nav li .rank-math-block-actions,.wp-block-rank-math-toc-block nav div .rank-math-block-actions{position:absolute;top:1px;right:0;display:none;line-height:1}.wp-block-rank-math-toc-block nav li .rank-math-block-actions button.components-button,.wp-block-rank-math-toc-block nav div .rank-math-block-actions button.components-button{min-width:24px;width:24px;height:24px;line-height:34px}.wp-block-rank-math-toc-block nav li:hover .rank-math-block-actions,.wp-block-rank-math-toc-block nav li:focus .rank-math-block-actions,.wp-block-rank-math-toc-block nav li .components-base-control+.rank-math-block-actions .rank-math-block-actions,.wp-block-rank-math-toc-block nav div:hover .rank-math-block-actions,.wp-block-rank-math-toc-block nav div:focus .rank-math-block-actions,.wp-block-rank-math-toc-block nav div .components-base-control+.rank-math-block-actions .rank-math-block-actions{display:block}.rank-math-toc-exclude-headings{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.rank-math-toc-exclude-headings>div{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;margin-bottom:10px !important}

View File

@@ -0,0 +1 @@
.wp-block-rank-math-toc-block nav ol{counter-reset:item}.wp-block-rank-math-toc-block nav ol li{display:block}.wp-block-rank-math-toc-block nav ol li:before{content:counters(item, ".") ". ";counter-increment:item}

View File

@@ -0,0 +1,25 @@
<?php
/**
* Block script dependencies.
*
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
return [
'dependencies' => [
'wp-blocks',
'wp-element',
'wp-components',
'wp-block-editor',
'wp-data',
'wp-dom',
'wp-url',
'wp-i18n',
'lodash',
'wp-primitives',
'wp-reusable-blocks',
],
'version' => rank_math()->version,
];

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,52 @@
.wp-block-rank-math-toc-block {
nav {
li, div {
position: relative;
min-height: 28px;
margin-bottom: 0;
&.disabled {
display: block !important;
opacity: 0.5;
}
.components-base-control {
position: absolute;
top: 2px;
left: -4px;
right: -3px;
}
.rank-math-block-actions {
position: absolute;
top: 1px;
right: 0;
display: none;
line-height: 1;
button.components-button {
min-width: 24px;
width: 24px;
height: 24px;
line-height: 34px;
}
}
&:hover, &:focus, .components-base-control + .rank-math-block-actions {
.rank-math-block-actions {
display: block;
}
}
}
}
}
.rank-math-toc-exclude-headings {
display: flex;
flex-wrap: wrap;
> div {
flex: 0 0 50%;
margin-bottom: 10px!important;
}
}

View File

@@ -0,0 +1,17 @@
.wp-block-rank-math-toc-block {
nav {
ol {
counter-reset: item;
li {
display: block;
}
li:before {
content: counters(item, ".") ". ";
counter-increment: item;
}
}
}
}

View File

@@ -0,0 +1,55 @@
import { linearToNestedHeadingList } from './utils'
import { useBlockProps } from '@wordpress/block-editor'
import List from './list'
const attributes = {
title: {
type: 'text',
},
headings: {
type: 'array',
items: {
type: 'object',
},
},
listStyle: {
type: 'text',
},
titleWrapper: {
type: 'text',
default: 'h2',
},
excludeHeadings: {
type: 'array',
},
}
const v1 = {
attributes,
save( { attributes } ) {
if ( attributes.headings.length === 0 ) {
return null
}
const TitleWrapper = attributes.titleWrapper
const headings = linearToNestedHeadingList( attributes.headings )
const ListStyle = attributes.listStyle
return (
<div { ...useBlockProps.save() }>
{ attributes.title && <TitleWrapper dangerouslySetInnerHTML={ { __html: attributes.title } }></TitleWrapper> }
<nav>
<ListStyle>
<List
headings={ headings }
ListStyle={ ListStyle }
isSave={ true }
/>
</ListStyle>
</nav>
</div>
)
},
}
export default [ v1 ]

View File

@@ -0,0 +1,138 @@
/**
* External dependencies
*/
import { isUndefined, map, includes, remove } from 'lodash'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import {
useBlockProps,
RichText,
store as blockEditorStore,
} from '@wordpress/block-editor'
import { useDispatch } from '@wordpress/data'
import { Placeholder } from '@wordpress/components'
import { useEffect, useState } from '@wordpress/element'
/**
* Internal dependencies
*/
import { GetLatestHeadings, linearToNestedHeadingList } from './utils'
import List from './list'
import InspectControls from './inspectControls'
import Toolbar from './toolbar'
export default ( {
attributes,
setAttributes,
} ) => {
const blockProps = useBlockProps()
// State to monitor edit heading links.
const [ edit, toggleEdit ] = useState( false )
const [ excludeHeading, toggleExcludeHeading ] = useState( {} )
if ( ! attributes.listStyle ) {
setAttributes( { listStyle: rankMath.listStyle } )
}
const ListStyle = attributes.listStyle
const tocTitle = attributes.title ?? rankMath.tocTitle
const excludeHeadings = ! isUndefined( attributes.excludeHeadings ) ? attributes.excludeHeadings : rankMath.tocExcludeHeadings
// Function to hide certain heading.
const hideHeading = ( value, key ) => {
const headings = map( attributes.headings, ( heading ) => {
if ( heading.key === key ) {
heading.disable = value
}
return heading
} )
setAttributes( { headings } )
}
// Function to update Heading link.
const onHeadingUpdate = ( value, key, isContent = false ) => {
const headings = map( attributes.headings, ( heading ) => {
if ( heading.key === key ) {
if ( isContent ) {
heading.content = value
heading.isUpdated = true
} else {
heading.isGeneratedLink = false
heading.link = value
}
}
return heading
} )
setAttributes( { headings } )
}
const setExcludeHeadings = ( headingLevel ) => {
if ( includes( excludeHeadings, headingLevel ) ) {
remove( excludeHeadings, ( heading ) => {
return heading === headingLevel
} )
} else {
excludeHeadings.push( headingLevel )
}
setAttributes( { excludeHeadings } )
toggleExcludeHeading( ! excludeHeading )
}
const { __unstableMarkNextChangeAsNotPersistent } = useDispatch( blockEditorStore )
// Get Latest headings from the content.
const latestHeadings = GetLatestHeadings( attributes.headings, excludeHeadings )
useEffect( () => {
if ( latestHeadings !== null ) {
__unstableMarkNextChangeAsNotPersistent();
setAttributes( { headings: latestHeadings } )
}
}, [ latestHeadings ] )
const headingTree = linearToNestedHeadingList( attributes.headings )
if ( isUndefined( attributes.headings ) || attributes.headings.length === 0 ) {
return (
<div { ...blockProps }>
<Placeholder
label={ __( 'Table of Contents', 'rank-math' ) }
instructions={ __( 'Add Heading blocks to this page to generate the Table of Contents.', 'rank-math' ) }
/>
<InspectControls attributes={ attributes } setAttributes={ setAttributes } excludeHeadings={ excludeHeadings } setExcludeHeadings={ setExcludeHeadings } />
</div>
)
}
return (
<div { ...blockProps }>
<RichText
tagName={ attributes.titleWrapper }
value={ tocTitle }
onChange={ ( newTitle ) => {
setAttributes( { title: newTitle } )
} }
placeholder={ __( 'Enter a title', 'rank-math' ) }
/>
<nav>
<ListStyle>
<List
headings={ headingTree }
onHeadingUpdate={ onHeadingUpdate }
edit={ edit }
toggleEdit={ toggleEdit }
hideHeading={ hideHeading }
ListStyle={ ListStyle }
/>
</ListStyle>
</nav>
<Toolbar setAttributes={ setAttributes } />
<InspectControls attributes={ attributes } setAttributes={ setAttributes } excludeHeadings={ excludeHeadings } setExcludeHeadings={ setExcludeHeadings } />
</div>
)
}

View File

@@ -0,0 +1,23 @@
/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks'
/**
* Internal dependencies
*/
import edit from './edit'
import save from './save'
import deprecated from './deprecated'
/**
* Register TOC block
*/
registerBlockType(
'rank-math/toc-block',
{
edit,
save,
deprecated,
}
)

View File

@@ -0,0 +1,58 @@
/**
* External dependencies
*/
import { map, includes, toUpper } from 'lodash'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { InspectorControls } from '@wordpress/block-editor'
import {
PanelBody,
SelectControl,
CheckboxControl,
} from '@wordpress/components'
export default ( { attributes, setAttributes, excludeHeadings, setExcludeHeadings } ) => {
return (
<InspectorControls>
<PanelBody title={ __( 'Settings', 'rank-math' ) }>
<SelectControl
label={ __( 'Title Wrapper', 'rank-math' ) }
value={ attributes.titleWrapper }
options={ [
{ value: 'h2', label: __( 'H2', 'rank-math' ) },
{ value: 'h3', label: __( 'H3', 'rank-math' ) },
{ value: 'h4', label: __( 'H4', 'rank-math' ) },
{ value: 'h5', label: __( 'H5', 'rank-math' ) },
{ value: 'h6', label: __( 'H6', 'rank-math' ) },
{ value: 'p', label: __( 'P', 'rank-math' ) },
{ value: 'div', label: __( 'DIV', 'rank-math' ) },
] }
onChange={ ( titleWrapper ) => {
setAttributes( { titleWrapper } )
} }
/>
<br />
<h3>{ __( 'Exclude Headings', 'rank-math' ) }</h3>
<div className="rank-math-toc-exclude-headings">
{
map( [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ], ( value ) => {
return (
<CheckboxControl
key={ value }
label={ __( 'Heading ', 'rank-math' ) + toUpper( value ) }
checked={ includes( excludeHeadings, value ) }
onChange={ ( newVlaue ) => setExcludeHeadings( value, newVlaue ) }
/>
)
} )
}
</div>
</PanelBody>
</InspectorControls>
)
}

View File

@@ -0,0 +1,90 @@
/**
* External dependencies
*/
import { isEmpty } from 'lodash'
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { Button, TextControl } from '@wordpress/components'
import { RichText } from '@wordpress/block-editor'
export default function List( { headings = {}, onHeadingUpdate = {}, edit = {}, toggleEdit = {}, hideHeading = {}, ListStyle = 'ul', isSave = false } ) {
if ( isEmpty( headings ) ) {
return null
}
return (
<>
{ headings.map( ( heading ) => {
if ( isSave && heading.heading.disable ) {
return false
}
const { content, link, disable, key } = heading.heading
const TagName = 'div' === ListStyle ? 'div' : 'li'
return (
<TagName key={ key } className={ disable ? 'disabled' : '' }>
{
isSave &&
<a href={ link }>
{ content }
</a>
}
{
! isSave &&
<RichText
tagName="a"
value={ content }
allowedFormats={ [] }
onChange={ ( newContent ) => onHeadingUpdate( newContent, key, true ) }
placeholder={ __( 'Heading text', 'rank-math' ) }
/>
}
{
heading.children &&
<ListStyle>
<List
headings={ heading.children }
onHeadingUpdate={ onHeadingUpdate }
edit={ edit }
toggleEdit={ toggleEdit }
hideHeading={ hideHeading }
ListStyle={ ListStyle }
isSave={ isSave }
/>
</ListStyle>
}
{
key === edit &&
<TextControl
placeholder={ __( 'Heading Link', 'rank-math' ) }
value={ link }
onChange={ ( newLink ) => onHeadingUpdate( newLink, key ) }
/>
}
{
! isSave &&
<span className="rank-math-block-actions">
<Button
icon={ edit === key ? 'saved' : 'admin-links' }
className="rank-math-item-visbility"
onClick={ () => toggleEdit( edit === key ? false : key ) }
title={ __( 'Edit Link', 'rank-math' ) }
/>
<Button
className="rank-math-item-delete"
icon={ ! disable ? 'visibility' : 'hidden' }
onClick={ () => hideHeading( ! disable, key ) }
title={ __( 'Hide', 'rank-math' ) }
/>
</span>
}
</TagName>
)
} ) }
</>
)
}

View File

@@ -0,0 +1,40 @@
/**
* External dependencies
*/
import { isUndefined } from 'lodash'
/**
* WordPress dependencies
*/
import { useBlockProps } from '@wordpress/block-editor'
/**
* Internal dependencies
*/
import { linearToNestedHeadingList } from './utils'
import List from './list'
export default function save( { attributes } ) {
if ( isUndefined( attributes.headings ) || attributes.headings.length === 0 ) {
return null
}
const TitleWrapper = attributes.titleWrapper
const headings = linearToNestedHeadingList( attributes.headings )
const ListStyle = attributes.listStyle
return (
<div { ...useBlockProps.save() } id="rank-math-toc">
{ attributes.title && <TitleWrapper dangerouslySetInnerHTML={ { __html: attributes.title } }></TitleWrapper> }
<nav>
<ListStyle>
<List
headings={ headings }
ListStyle={ ListStyle }
isSave={ true }
/>
</ListStyle>
</nav>
</div>
)
}

View File

@@ -0,0 +1,34 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n'
import { BlockControls } from '@wordpress/block-editor'
import {
Toolbar,
ToolbarButton,
} from '@wordpress/components'
import { formatListBullets, formatListNumbered, alignLeft } from '@wordpress/icons'
export default ( { setAttributes } ) => {
return (
<BlockControls>
<Toolbar label={ __( 'Table of Content Options', 'rank-math' ) }>
<ToolbarButton
icon={ formatListBullets }
label={ __( 'Unordered List', 'rank-math' ) }
onClick={ () => setAttributes( { listStyle: 'ul' } ) }
/>
<ToolbarButton
icon={ formatListNumbered }
label={ __( 'Ordered List', 'rank-math' ) }
onClick={ () => setAttributes( { listStyle: 'ol' } ) }
/>
<ToolbarButton
icon={ alignLeft }
label={ __( 'None', 'rank-math' ) }
onClick={ () => setAttributes( { listStyle: 'div' } ) }
/>
</Toolbar>
</BlockControls>
)
}

View File

@@ -0,0 +1,196 @@
/**
* External dependencies
*/
import { isEmpty, isUndefined, isString, kebabCase, includes, forEach, isEqual, map, isNull } from 'lodash'
/**
* WordPress dependencies
*/
import { store as blockEditorStore } from '@wordpress/block-editor'
import { __unstableStripHTML as stripHTML } from '@wordpress/dom'
import { useSelect, useDispatch } from '@wordpress/data'
/**
* Get the headings from the content.
*
* @param {Array} headings Array of headings data
* @param {Array} excludeHeadings Heading levels to exclude
*/
export function GetLatestHeadings( headings, excludeHeadings ) {
return useSelect(
( select ) => {
const {
getBlockAttributes,
getBlockName,
getClientIdsWithDescendants,
} = select( blockEditorStore )
const { __experimentalConvertBlockToStatic: convertBlockToStatic } = useDispatch( 'core/reusable-blocks' )
// Get the client ids of all blocks in the editor.
const allBlockClientIds = getClientIdsWithDescendants()
const _latestHeadings = []
let i = 0
const anchors = []
for ( const blockClientId of allBlockClientIds ) {
const blockName = getBlockName( blockClientId )
if ( blockName === 'core/block' ) {
const attrs = getBlockAttributes( blockClientId )
if ( ! isNull( attrs.ref ) ) {
const reusableBlock = wp.data.select( 'core' ).getEditedEntityRecord( 'postType', 'wp_block', attrs.ref )
const blocks = map( reusableBlock.blocks, ( block ) => {
return block.name
} )
if ( includes( blocks, 'rank-math/toc-block' ) && ! isNull( getBlockAttributes( blockClientId ) ) ) {
convertBlockToStatic( blockClientId )
}
}
continue
}
if ( ! includes( [ 'rank-math/faq-block', 'rank-math/howto-block', 'core/heading' ], blockName ) ) {
continue
}
const headingAttributes = getBlockAttributes( blockClientId )
if ( blockName === 'rank-math/faq-block' || blockName === 'rank-math/howto-block' ) {
const titleWrapper = headingAttributes.titleWrapper
if (
includes( excludeHeadings, titleWrapper ) ||
includes( [ 'div', 'p' ], titleWrapper )
) {
continue
}
const data = blockName === 'rank-math/howto-block' ? headingAttributes.steps : headingAttributes.questions
if ( isEmpty( data ) ) {
continue
}
forEach( data, ( value ) => {
const currentHeading = ! isUndefined( headings ) && ! isEmpty( headings[ _latestHeadings.length ] ) ? headings[ _latestHeadings.length ] : {
content: '',
level: '',
disable: false,
isUpdated: false,
isGeneratedLink: true,
}
const isGeneratedLink = ! isUndefined( currentHeading.isGeneratedLink ) && currentHeading.isGeneratedLink
const content = ! isUndefined( currentHeading.isUpdated ) && currentHeading.isUpdated ? currentHeading.content : value.title
_latestHeadings.push( {
key: value.id,
content: stripHTML( content ),
level: parseInt( headingAttributes.titleWrapper.replace( 'h', '' ) ),
link: ! isGeneratedLink ? currentHeading.link : `#${ value.id }`,
disable: currentHeading.disable ? currentHeading.disable : false,
isUpdated: ! isUndefined( currentHeading.isUpdated ) ? currentHeading.isUpdated : false,
isGeneratedLink,
} )
} )
continue
}
if ( blockName === 'core/heading' ) {
if ( includes( excludeHeadings, 'h' + headingAttributes.level ) ) {
continue
}
const currentHeading = ! isUndefined( headings ) && ! isEmpty( headings[ _latestHeadings.length ] ) ? headings[ _latestHeadings.length ] : {
content: '',
level: '',
disable: false,
isUpdated: false,
isGeneratedLink: true,
}
const isGeneratedLink = ! isUndefined( currentHeading.isGeneratedLink ) && currentHeading.isGeneratedLink
let anchor = headingAttributes.anchor
const headingText = ! isEmpty( headingAttributes.content.text ) ? headingAttributes.content.text : headingAttributes.content
if ( isEmpty( headingAttributes.anchor ) || isGeneratedLink ) {
anchor = kebabCase( stripHTML( headingText ) )
}
if ( includes( anchors, anchor ) ) {
i += 1
anchor = anchor + '-' + i
}
anchors.push( anchor )
headingAttributes.anchor = anchor
const headingContent = isString( headingText ) ? stripHTML(
headingText.replace(
/(<br *\/?>)+/g,
' '
)
) : ''
const content = ! isUndefined( currentHeading.isUpdated ) && currentHeading.isUpdated ? currentHeading.content : headingContent
_latestHeadings.push( {
key: blockClientId,
content: stripHTML( content ),
level: headingAttributes.level,
link: ! isGeneratedLink ? currentHeading.link : `#${ headingAttributes.anchor }`,
disable: currentHeading.disable ? currentHeading.disable : false,
isUpdated: ! isUndefined( currentHeading.isUpdated ) ? currentHeading.isUpdated : false,
isGeneratedLink,
} )
}
}
if ( isEqual( headings, _latestHeadings ) ) {
return null
}
return _latestHeadings
}
)
}
/**
* Nest heading based on the Heading level.
*
* @param {Array} headingList The flat list of headings to nest.
*
* @return {Array} The nested list of headings.
*/
export function linearToNestedHeadingList( headingList = [] ) {
const nestedHeadingList = []
forEach( headingList, ( heading, key ) => {
if ( isEmpty( heading.content ) ) {
return
}
// Make sure we are only working with the same level as the first iteration in our set.
if ( heading.level === headingList[ 0 ].level ) {
if ( headingList[ key + 1 ]?.level > heading.level ) {
let endOfSlice = headingList.length
for ( let i = key + 1; i < headingList.length; i++ ) {
if ( headingList[ i ].level === heading.level ) {
endOfSlice = i
break
}
}
nestedHeadingList.push( {
heading,
children: linearToNestedHeadingList(
headingList.slice( key + 1, endOfSlice )
),
} )
} else {
nestedHeadingList.push( {
heading,
children: null,
} )
}
}
} )
return nestedHeadingList
}

View File

@@ -0,0 +1,52 @@
{
"apiVersion": 2,
"title": "Table of Contents by Rank Math",
"description": "Automatically generate the Table of Contents from the Headings added to this page.",
"name": "rank-math/toc-block",
"category": "rank-math-blocks",
"icon": "rm-icon rm-icon-stories",
"textdomain": "rank-math",
"editorScript": "file:./assets/js/index.js",
"editorStyle": "file:./assets/css/toc.css",
"style": "file:./assets/css/toc_list_style.css",
"attributes": {
"title": {
"type": "string"
},
"headings": {
"type": "array",
"items": {
"type": "object"
}
},
"listStyle": {
"type": "string"
},
"titleWrapper": {
"type": "string",
"default": "h2"
},
"excludeHeadings": {
"type": "array"
}
},
"supports": {
"color": {
"link": true,
"gradients": true
},
"multiple": false,
"spacing": {
"margin": true,
"padding": true
},
"typography": {
"fontSize": true,
"lineHeight": true,
"__experimentalDefaultControls": {
"fontSize": true
}
},
"align": true
}
}

View File

@@ -0,0 +1,159 @@
<?php
/**
* The TOC Block
*
* @since 1.0.104
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use WP_Block_Type_Registry;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* HowTo Block class.
*/
class Block_TOC extends Block {
use Hooker;
/**
* Block type name.
*
* @var string
*/
private $block_type = 'rank-math/toc-block';
/**
* The single instance of the class.
*
* @var Block_TOC
*/
protected static $instance = null;
/**
* Retrieve main Block_TOC instance.
*
* Ensure only one instance is loaded or can be loaded.
*
* @return Block_TOC
*/
public static function get() {
if ( is_null( self::$instance ) && ! ( self::$instance instanceof Block_TOC ) ) {
self::$instance = new Block_TOC();
}
return self::$instance;
}
/**
* The Constructor.
*/
public function __construct() {
if ( WP_Block_Type_Registry::get_instance()->is_registered( $this->block_type ) ) {
return;
}
$this->filter( 'rank_math/schema/block/toc-block', 'add_graph', 10, 2 );
$this->filter( 'render_block_rank-math/toc-block', 'render_toc_block_content', 10, 2 );
$this->filter( 'rank_math/metabox/post/values', 'block_settings_metadata' );
register_block_type( RANK_MATH_PATH . 'includes/modules/schema/blocks/toc/block.json' );
}
/**
* Add meta data to use in the TOC block.
*
* @param array $values Aray of tabs.
*
* @return array
*/
public function block_settings_metadata( $values ) {
$values['tocTitle'] = Helper::get_settings( 'general.toc_block_title' );
$values['tocExcludeHeadings'] = Helper::get_settings( 'general.toc_block_exclude_headings', [] );
$values['listStyle'] = Helper::get_settings( 'general.toc_block_list_style', 'ul' );
return $values;
}
/**
* Add default TOC title.
*
* @param string $block_content Block content.
* @param array $parsed_block The full block, including name and attributes.
*
* @return string
*/
public function render_toc_block_content( $block_content, $parsed_block ) {
if ( isset( $parsed_block['attrs']['title'] ) ) {
return $block_content;
}
$title = Helper::get_settings( 'general.toc_block_title' );
if ( ! $title ) {
return $block_content;
}
$title_wrapper = $parsed_block['attrs']['titleWrapper'] ?? 'h2';
$block_content = preg_replace_callback(
'/(<div class=".*?wp-block-rank-math-toc-block.*?"\>)/i',
function( $value ) use ( $title, $block_content, $title_wrapper ) {
if ( ! isset( $value[0] ) ) {
return $block_content;
}
$value[0] = str_replace( '>', ' id="rank-math-toc">', $value[0] );
return $value[0] . '<' . tag_escape( $title_wrapper ) . '>' . esc_html( $title ) . '</' . tag_escape( $title_wrapper ) . '>';
},
$block_content
);
return str_replace( 'class=""', '', $block_content );
}
/**
* Add TOC schema data in JSON-LD array.
*
* @param array $data Array of JSON-LD data.
* @param array $block JsonLD Instance.
*
* @return array
*/
public function add_graph( $data, $block ) {
$attributes = $block['attrs'];
// Early bail.
if ( empty( $attributes['headings'] ) ) {
return $data;
}
if ( ! isset( $data['toc'] ) ) {
$data['toc'] = [];
}
foreach ( $attributes['headings'] as $heading ) {
if ( ! empty( $heading['disable'] ) ) {
continue;
}
$data['toc'][] = [
'@context' => 'https://schema.org',
'@type' => 'SiteNavigationElement',
'@id' => '#rank-math-toc',
'name' => $heading['content'],
'url' => get_permalink() . $heading['link'],
];
}
if ( empty( $data['toc'] ) ) {
unset( $data['toc'] );
}
return $data;
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Blocks general settings.
*
* @package RankMath
* @subpackage RankMath\Schema
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$cmb->add_field(
[
'id' => 'toc_block_title',
'type' => 'text',
'name' => esc_html__( 'Table of Contents Title', 'rank-math' ),
'desc' => esc_html__( 'Enter the default title to use for the Table of Contents block.', 'rank-math' ),
'classes' => 'rank-math-advanced-option',
'default' => esc_html__( 'Table of Contents', 'rank-math' ),
]
);
$cmb->add_field(
[
'id' => 'toc_block_list_style',
'type' => 'select',
'name' => esc_html__( 'Table of Contents List style', 'rank-math' ),
'desc' => esc_html__( 'Select the default list style for the Table of Contents block.', 'rank-math' ),
'options' => [
'div' => esc_html__( 'None', 'rank-math' ),
'ol' => esc_html__( 'Numbered', 'rank-math' ),
'ul' => esc_html__( 'Unordered', 'rank-math' ),
],
'default' => 'unordered',
]
);
$cmb->add_field(
[
'id' => 'toc_block_exclude_headings',
'name' => esc_html__( 'Table of Contents Exclude Headings', 'rank-math' ),
'desc' => esc_html__( 'Choose the headings to exclude from the Table of Contents block.', 'rank-math' ),
'type' => 'multicheck',
'options' => [
'h1' => esc_html__( 'Heading H1', 'rank-math' ),
'h2' => esc_html__( 'Heading H2', 'rank-math' ),
'h3' => esc_html__( 'Heading H3', 'rank-math' ),
'h4' => esc_html__( 'Heading H4', 'rank-math' ),
'h5' => esc_html__( 'Heading H5', 'rank-math' ),
'h6' => esc_html__( 'Heading H6', 'rank-math' ),
],
]
);

View File

@@ -0,0 +1,306 @@
<?php
/**
* The admin-side code of the Schema module.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Module\Base;
use RankMath\Admin\Admin_Helper;
use RankMath\Helpers\Str;
defined( 'ABSPATH' ) || exit;
/**
* Admin class.
*/
class Admin extends Base {
/**
* Module ID.
*
* @var string
*/
public $id = '';
/**
* Module directory.
*
* @var string
*/
public $directory = '';
/**
* The Constructor.
*/
public function __construct() {
$directory = dirname( __FILE__ );
$this->config(
[
'id' => 'rich-snippet',
'directory' => $directory,
]
);
parent::__construct();
$this->action( 'cmb2_admin_init', 'add_kb_links', 50 );
$this->action( 'rank_math/admin/editor_scripts', 'enqueue' );
$this->action( 'rank_math/post/column/seo_details', 'display_schema_type', 10, 2 );
}
/**
* Display schema type in the `seo_details` column on the posts.
*
* @param int $post_id The current post ID.
* @param array $data SEO data of current post.
*/
public function display_schema_type( $post_id, $data ) {
$schema = absint( get_option( 'page_for_posts' ) ) !== $post_id ? $this->get_schema_types( $data, $post_id ) : 'CollectionPage';
$schema = ! empty( $schema ) ? $schema : $this->get_schema_name( Helper::get_default_schema_type( $post_id, true ) );
$schema = $schema ? $schema : esc_html__( 'Off', 'rank-math' );
?>
<span class="rank-math-column-display schema-type">
<strong><?php esc_html_e( 'Schema', 'rank-math' ); ?>:</strong>
<?php echo esc_html( Helper::sanitize_schema_title( $schema ) ); ?>
</span>
<?php
}
/**
* Enqueue Styles and Scripts required for the metabox on the post screen.
*/
public function enqueue() {
if ( ! Helper::has_cap( 'onpage_snippet' ) || Admin_Helper::is_posts_page() ) {
return;
}
$values = [];
$cmb = $this->get_metabox();
if ( false === $cmb ) {
return;
}
Helper::add_json( 'schemas', $this->get_schema_data( $cmb->object_id() ) );
Helper::add_json( 'customSchemaImage', esc_url( rank_math()->plugin_url() . 'includes/modules/schema/assets/img/custom-schema-builder.jpg' ) );
wp_enqueue_style( 'rank-math-schema', rank_math()->plugin_url() . 'includes/modules/schema/assets/css/schema.css', [ 'wp-components', 'rank-math-editor' ], rank_math()->version );
$this->enqueue_translation();
$screen = get_current_screen();
if ( 'rank_math_schema' !== $screen->post_type ) {
wp_enqueue_script( 'rank-math-schema', rank_math()->plugin_url() . 'includes/modules/schema/assets/js/schema-gutenberg.js', [ 'rank-math-editor' ], rank_math()->version, true );
wp_set_script_translations( 'rank-math-schema', 'rank-math' );
}
}
/**
* KB Links for gutenberg
*/
public function add_kb_links() {
Helper::add_json(
'assessor',
[
'reviewConverterLink' => Helper::get_admin_url( 'status', 'view=tools' ),
]
);
}
/**
* Get Schema Data.
*
* @param int $post_id Post ID.
*
* @return array $schemas Schema Data.
*/
private function get_schema_data( $post_id ) {
$schemas = DB::get_schemas( $post_id );
if ( ! empty( $schemas ) ) {
return $schemas;
}
$default_type = $this->get_default_schema_type( $post_id );
if ( ! $default_type ) {
return [];
}
$schemas['new-9999'] = [
'@type' => $default_type,
'metadata' => [
'title' => Helper::sanitize_schema_title( $default_type ),
'type' => 'template',
'shortcode' => uniqid( 's-' ),
'isPrimary' => true,
],
];
if ( ! in_array( $default_type, [ 'Article', 'NewsArticle', 'BlogPosting' ], true ) ) {
return $schemas;
}
$post_type = get_post_type( $post_id );
$name = Helper::get_settings( "titles.pt_{$post_type}_default_snippet_name" );
$description = Helper::get_settings( "titles.pt_{$post_type}_default_snippet_desc" );
$schemas['new-9999']['headline'] = $name ? $name : '';
$schemas['new-9999']['description'] = $description ? $description : '';
$schemas['new-9999']['keywords'] = '%keywords%';
$schemas['new-9999']['author'] = [
'@type' => 'Person',
'name' => '%name%',
];
return $schemas;
}
/**
* Get default schema type.
*
* @param int $post_id Post ID.
*
* @return string|bool Schema type.
*/
private function get_default_schema_type( $post_id ) {
$default_type = ucfirst( Helper::get_default_schema_type( $post_id ) );
if ( ! $default_type ) {
return false;
}
if ( 'Video' === $default_type ) {
return 'VideoObject';
}
if ( 'Software' === $default_type ) {
return 'SoftwareApplication';
}
if ( 'Jobposting' === $default_type ) {
return 'JobPosting';
}
return $default_type;
}
/**
* Enqueue translation.
*/
private function enqueue_translation() {
if ( function_exists( 'wp_set_script_translations' ) ) {
wp_set_script_translations( 'rank-math-schema', 'rank-math', rank_math()->plugin_dir() . 'languages/' );
}
}
/**
* Get schema types for current post.
*
* @param array $data Current post SEO data.
* @param int $post_id Current post ID.
*
* @return string Comma separated schema types.
*/
private function get_schema_types( $data, $post_id ) {
if ( empty( $data ) ) {
return false;
}
$types = [];
foreach ( $data as $key => $value ) {
if ( ! Str::starts_with( 'rank_math_schema_', $key ) ) {
continue;
}
$schema = maybe_unserialize( $value );
if ( empty( $schema['@type'] ) ) {
continue;
}
if ( ! is_array( $schema['@type'] ) ) {
$types[] = $this->get_schema_name( $schema['@type'] );
continue;
}
$types = array_merge(
$types,
array_map(
function( $type ) {
return $this->get_schema_name( $type );
},
$schema['@type']
)
);
}
if ( empty( $types ) && Helper::get_default_schema_type( $post_id ) ) {
$types[] = $this->get_schema_name( Helper::get_default_schema_type( $post_id ) );
}
if ( has_block( 'rank-math/faq-block', $post_id ) ) {
$types[] = 'FAQPage';
}
if ( has_block( 'rank-math/howto-block', $post_id ) ) {
$types[] = 'HowTo';
}
return empty( $types ) ? false : implode( ', ', $types );
}
/**
* Function to get Sanitized schema name with sub-schema.
*
* @param string $schema Selected schema type.
*
* @return string Schema name with sub-schema.
*/
private function get_schema_name( $schema ) {
$subtitle = in_array( $schema, [ 'BlogPosting', 'NewsArticle' ], true ) ? " ($schema)" : '';
return Helper::sanitize_schema_title( $schema ) . $subtitle;
}
/**
* Get a CMB2 instance by the metabox ID.
*
* @return bool|CMB2
*/
private function get_metabox() {
if ( Admin_Helper::is_term_profile_page() ) {
return false;
}
return cmb2_get_metabox( 'rank_math_metabox' );
}
/**
* Can exclude field.
*
* @param string $id Field id.
* @param string $type Field type.
*
* @return bool
*/
private function can_exclude( $id, $type ) {
$exclude = [ 'meta_tab_container_open', 'tab_container_open', 'tab_container_close', 'tab', 'raw', 'notice' ];
return in_array( $type, $exclude, true ) || ! Str::starts_with( 'rank_math_snippet_', $id );
}
/**
* Convert string to camel case.
*
* @param string $str String to convert.
*
* @return string
*/
private function camelize( $str ) {
$sep = '_';
$str = str_replace( 'rank_math_snippet_', '', $str );
$str = str_replace( $sep, '', ucwords( $str, $sep ) );
return lcfirst( $str );
}
}

View File

@@ -0,0 +1,119 @@
<?php
/**
* The Schema Blocks
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Blocks class.
*/
class Blocks {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'init', 'init' );
$filter = version_compare( get_bloginfo( 'version' ), '5.8', '>=' ) ? 'block_categories_all' : 'block_categories';
$this->filter( $filter, 'block_categories' );
$this->action( 'enqueue_block_editor_assets', 'editor_assets' ); // Backend.
}
/**
* Init blocks.
*/
public function init() {
if ( ! function_exists( 'register_block_type' ) ) {
return;
}
wp_register_style(
'rank-math-block-admin',
rank_math()->plugin_url() . 'assets/admin/css/blocks.css',
null,
rank_math()->version
);
new Blocks\Admin();
new Block_FAQ();
new Block_HowTo();
new Block_TOC();
}
/**
* Create a new (Rank Math) block category.
*
* @param array $categories Array of block categories.
*
* @return array
*/
public function block_categories( $categories ) {
return array_merge(
$categories,
[
[
'slug' => 'rank-math-blocks',
'title' => __( 'Rank Math', 'rank-math' ),
'icon' => 'wordpress',
],
]
);
}
/**
* Enqueue Styles and Scripts required for blocks at backend.
*/
public function editor_assets() {
if ( ! $this->is_block_faq() && ! $this->is_block_howto() ) {
return;
}
Helper::add_json(
'blocks',
[
'faq' => $this->is_block_faq(),
'howTo' => $this->is_block_howto(),
]
);
wp_enqueue_script(
'rank-math-block-faq',
rank_math()->plugin_url() . 'assets/admin/js/blocks.js',
[],
rank_math()->version,
true
);
}
/**
* Is FAQ Block enabled.
*
* @return boolean
*/
private function is_block_faq() {
return true;
}
/**
* Is HowTo Block enabled.
*
* @return boolean
*/
private function is_block_howto() {
return true;
}
}

View File

@@ -0,0 +1,285 @@
<?php
/**
* The Schema module database operations.
*
* @since 1.4.3
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Admin\Database\Database;
defined( 'ABSPATH' ) || exit;
/**
* DB class.
*/
class DB {
/**
* Get query builder object.
*
* @param string $table Meta table name.
*
* @return Query_Builder
*/
private static function table( $table = 'postmeta' ) {
return Database::table( $table );
}
/**
* Get all schemas by Object ID.
*
* @param int $object_id Object ID.
* @param string $table Meta table name.
* @param bool $from_db If set to true, the schema will be retrieved from the database.
*
* @return array
*/
public static function get_schemas( $object_id, $table = 'postmeta', $from_db = false ) {
static $schema_cache = [];
// Add exception handler.
if ( is_null( $object_id ) ) {
return [];
}
// Get from cache.
if ( ! $from_db && isset( $schema_cache[ $table . '_' . $object_id ] ) ) {
return $schema_cache[ $table . '_' . $object_id ];
}
$key = 'termmeta' === $table ? 'term_id' : 'post_id';
$data = self::table( $table )
->select( 'meta_id' )
->select( 'meta_value' )
->where( $key, $object_id )
->whereLike( 'meta_key', 'rank_math_schema', '' )
->get();
$schemas = [];
foreach ( $data as $schema ) {
$value = maybe_unserialize( $schema->meta_value );
if ( empty( $value ) ) {
continue;
}
$id = 'schema-' . $schema->meta_id;
$schemas[ $id ] = maybe_unserialize( $schema->meta_value );
}
// Add to cache.
$schema_cache[ $table . '_' . $object_id ] = $schemas;
return $schemas;
}
/**
* Get Schema types by Object ID.
*
* @param int $object_id Object ID.
* @param bool $sanitize Sanitize schema types.
* @param bool $translate Whether to get the schema name.
*
* @return array
*/
public static function get_schema_types( $object_id, $sanitize = false, $translate = true ) {
$schemas = self::get_schemas( $object_id );
if ( empty( $schemas ) && Helper::get_default_schema_type( $object_id ) ) {
$schemas[] = [ '@type' => ucfirst( Helper::get_default_schema_type( $object_id ) ) ];
}
if ( has_block( 'rank-math/faq-block', $object_id ) ) {
$schemas[] = [ '@type' => 'FAQPage' ];
}
if ( has_block( 'rank-math/howto-block', $object_id ) ) {
$schemas[] = [ '@type' => 'HowTo' ];
}
if ( empty( $schemas ) ) {
return false;
}
$types = array_reduce(
wp_list_pluck( $schemas, '@type' ),
function( $carry, $type ) {
if ( is_array( $type ) ) {
return array_merge( $carry, $type );
}
$carry[] = $type;
return $carry;
},
[]
);
$types = array_unique( $types );
if ( $sanitize ) {
$types = array_map(
function ( $type ) use ( $translate ) {
return Helper::sanitize_schema_title( $type, $translate );
},
$types
);
}
return implode( ', ', $types );
}
/**
* Get schema by shortcode id.
*
* @param string $id Shortcode unique id.
* @param bool $from_db If set to true, the schema will be retrieved from the database.
* @return array
*/
public static function get_schema_by_shortcode_id( $id, $from_db = false ) {
/**
* Keep Schema data in memory after querying by shortcode ID, to avoid
* unnecessary queries.
*
* @var array
*/
static $cached_schema_shortcodes = [];
if ( ! $from_db && isset( $cached_schema_shortcodes[ $id ] ) ) {
return $cached_schema_shortcodes[ $id ];
}
// First, check for meta_key matches for a "shortcut" to the schema.
$shortcut = false;
if ( strpos( self::table()->table, 'post' ) !== false ) {
// Only check for shortcuts if we're querying for a post.
$shortcut = self::table()
->select( 'meta_value' )
->where( 'meta_key', 'rank_math_shortcode_schema_' . $id )
->one();
}
if ( ! empty( $shortcut ) ) {
$data = self::table()
->select( 'post_id' )
->select( 'meta_value' )
->where( 'meta_id', $shortcut->meta_value )
->one();
if ( ! empty( $data ) ) {
$schema = [
'post_id' => $data->post_id,
'schema' => maybe_unserialize( $data->meta_value ),
];
// Cache the schema for future use.
$cached_schema_shortcodes[ $id ] = $schema;
return $schema;
}
}
$data = self::table()
->select( 'post_id' )
->select( 'meta_value' )
->whereLike( 'meta_value', $id, '%:"' )
->one();
if ( ! empty( $data ) ) {
$schema = [
'post_id' => $data->post_id,
'schema' => maybe_unserialize( $data->meta_value ),
];
// Cache the schema for future use.
$cached_schema_shortcodes[ $id ] = $schema;
return $schema;
}
return false;
}
/**
* Get schema type for template.
*
* @param int $post_id Post id.
*
* @return string
*/
public static function get_template_type( $post_id ) {
$data = self::table()
->select( 'meta_value' )
->where( 'post_id', $post_id )
->whereLike( 'meta_key', 'rank_math_schema', '' )
->one();
if ( empty( $data ) ) {
return '';
}
$schema = maybe_unserialize( $data->meta_value );
return [
'type' => isset( $schema['@type'] ) ? $schema['@type'] : '',
'schema' => $schema,
];
}
/**
* Delete Schema data using Post ID.
*
* @param int $post_id Post ID.
*
* @return string
*/
public static function delete_schema_data( $post_id ) {
return self::table()->where( 'post_id', $post_id )->whereLike( 'meta_key', 'rank_math_schema_' )->delete();
}
/**
* Unpublish job posting when expired.
*
* @param JsonLD $jsonld JsonLD Instance.
* @param array $schemas Array of JSON-LD entity.
*/
public static function unpublish_jobposting_post( $jsonld, $schemas ) {
if ( ! is_singular() ) {
return;
}
$job_postings = array_map(
function( $schema ) {
return isset( $schema['@type'] ) && 'JobPosting' === $schema['@type'] ? $schema : false;
},
$schemas
);
if ( empty( $job_postings ) ) {
return;
}
foreach ( $job_postings as $job_posting ) {
if (
empty( $job_posting['metadata']['unpublish'] ) ||
'on' !== $job_posting['metadata']['unpublish'] ||
empty( $job_posting['validThrough'] ) ||
date_create( 'now' )->getTimestamp() < strtotime( $job_posting['validThrough'] )
) {
continue;
}
wp_update_post(
[
'ID' => $jsonld->post_id,
'post_status' => 'draft',
]
);
break;
}
}
}

View File

@@ -0,0 +1,318 @@
<?php
/**
* The frontend code of the Schema module.
*
* @since 1.4.3
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Helpers\Str;
defined( 'ABSPATH' ) || exit;
/**
* Frontend class.
*/
class Frontend {
use Hooker;
/**
* Hold post object.
*
* @var WP_Post
*/
public $post = null;
/**
* Hold post ID.
*
* @var ID
*/
public $post_id = 0;
/**
* The Constructor.
*/
public function __construct() {
if ( Helper::is_divi_frontend_editor() ) {
return;
}
$this->action( 'rank_math/json_ld', 'add_schema', 10, 2 );
$this->action( 'rank_math/json_ld', 'connect_schema_entities', 99, 2 );
$this->filter( 'rank_math/snippet/rich_snippet_event_entity', 'validate_event_schema', 11, 2 );
$this->filter( 'rank_math/snippet/rich_snippet_article_entity', 'add_name_property', 11, 2 );
$this->action( 'rank_math/schema/validated_data', 'remove_person_entity', 999 );
new Opengraph();
}
/**
* Output schema data for a post.
*
* @param array $data Array of json-ld data.
* @param JsonLD $jsonld Instance of jsonld.
*
* @return array
*/
public function add_schema( $data, $jsonld ) {
if ( ! is_singular() || post_password_required() ) {
return $data;
}
global $post;
$schemas = array_filter(
DB::get_schemas( $post->ID ),
function( $schema ) {
return ! in_array( $schema['@type'], [ 'WooCommerceProduct', 'EDDProduct' ], true );
}
);
// Check & Unpublish the JobPosting post.
DB::unpublish_jobposting_post( $jsonld, $schemas );
$schemas = $jsonld->replace_variables( $schemas, [], $data );
$schemas = $jsonld->filter( $schemas, $jsonld, $data );
return array_merge( $data, $schemas );
}
/**
* Connect different schema entities using isPartOf & publisher properties.
*
* @param array $schemas Array of json-ld data.
* @param JsonLD $jsonld Instance of jsonld.
*
* @return array
*/
public function connect_schema_entities( $schemas, $jsonld ) {
if ( empty( $schemas ) ) {
return $schemas;
}
$jsonld->parts['canonical'] = ! empty( $jsonld->parts['canonical'] ) ? $jsonld->parts['canonical'] : \RankMath\Paper\Paper::get()->get_canonical();
$schema_types = [];
foreach ( $schemas as $id => $schema ) {
if ( ! Str::starts_with( 'schema-', $id ) && 'richSnippet' !== $id ) {
continue;
}
$schema_types[] = $schema['@type'];
$this->connect_properties( $schema, $id, $jsonld, $schemas );
$this->add_main_entity_of_page( $schema, $jsonld );
$schemas[ $id ] = $schema;
}
return $this->change_webpage_entity( $schemas, $schema_types );
}
/**
* Add name property to the Article schema.
*
* @since 1.0.61
*
* @param array $schema Snippet Data.
* @return array
*/
public function add_name_property( $schema ) {
if ( empty( $schema['headline'] ) ) {
return $schema;
}
$schema['name'] = $schema['headline'];
if ( ! isset( $schema['articleSection'] ) ) {
global $post;
$schema['articleSection'] = Helper::replace_vars( '%primary_taxonomy_terms%', $post );
}
return $schema;
}
/**
* Add timezone to startDate field.
*
* @param array $schema Event schema Data.
* @return array
*/
public function validate_event_schema( $schema ) {
if ( ! empty( $schema['startDate'] ) ) {
$start_date = date_i18n( 'Y-m-d H:i:sP', strtotime( $schema['startDate'] ) );
$schema['startDate'] = str_replace( ' ', 'T', $start_date );
}
if ( ! empty( $schema['endDate'] ) ) {
$end_date = date_i18n( 'Y-m-d H:i:sP', strtotime( $schema['endDate'] ) );
$schema['endDate'] = str_replace( ' ', 'T', $end_date );
}
return $schema;
}
/**
* Connect schema properties.
*
* @param array $schema Schema Entity.
* @param string $id Schema Entity ID.
* @param JsonLD $jsonld JsonLD Instance.
* @param array $schemas Array of json-ld data.
*/
private function connect_properties( &$schema, $id, $jsonld, $schemas ) {
if ( isset( $schema['isCustom'] ) ) {
unset( $schema['isCustom'] );
return;
}
// Remove empty ImageObject.
if ( isset( $schema['image'] ) && empty( $schema['image']['url'] ) && ! is_array( $schema['image'] ) ) {
unset( $schema['image'] );
}
$jsonld->parts['canonical'] = ! empty( $jsonld->parts['canonical'] ) ? $jsonld->parts['canonical'] : \RankMath\Paper\Paper::get()->get_canonical();
$schema['@id'] = $jsonld->parts['canonical'] . '#' . $id;
$types = array_map( 'strtolower', (array) $schema['@type'] );
foreach ( $types as $type ) {
$is_event = Str::contains( 'event', $type );
// Add publisher entity @id in the organizer property of Event schema.
if ( $is_event ) {
$jsonld->add_prop( 'publisher', $schema, 'organizer', $schemas );
}
$props = [
'is_part_of' => [
'key' => 'webpage',
'value' => ! in_array( $type, [ 'jobposting', 'musicgroup', 'person', 'product', 'productgroup', 'restaurant', 'service' ], true ) && ! $is_event,
],
'publisher' => [
'key' => 'publisher',
'value' => ! in_array( $type, [ 'jobposting', 'musicgroup', 'person', 'product', 'productgroup', 'restaurant', 'service' ], true ) && ! $is_event,
],
'thumbnail' => [
'key' => 'image',
'value' => ! in_array( $type, [ 'videoobject' ], true ) || isset( $schema['image'] ),
],
'language' => [
'key' => 'inLanguage',
'value' => ! in_array( $type, [ 'person', 'service', 'restaurant', 'product', 'productgroup', 'musicgroup', 'musicalbum', 'jobposting' ], true ),
],
];
if ( isset( $schema['image'] ) && 'product' === $type && is_array( $schema['image'] ) ) {
$props['thumbnail']['value'] = false;
}
foreach ( $props as $prop => $data ) {
if ( ! $data['value'] ) {
continue;
}
$jsonld->add_prop( $prop, $schema, $data['key'], $schemas );
}
}
}
/**
* Add mainEntityOfPage property to Primary schema entity.
*
* @param array $schema Schema Entity.
* @param JsonLD $jsonld JsonLD Instance.
*/
private function add_main_entity_of_page( &$schema, $jsonld ) {
if ( ! isset( $schema['isPrimary'] ) ) {
return;
}
if ( ! empty( $schema['isPrimary'] ) ) {
$schema['mainEntityOfPage'] = [ '@id' => $jsonld->parts['canonical'] . '#webpage' ];
}
unset( $schema['isPrimary'] );
}
/**
* Remove Person entity if it is not referenced in any other entities.
*
* @param array $data Array of json-ld data.
*
* @return array
*/
public function remove_person_entity( $data ) {
if ( empty( $data['ProfilePage'] ) || empty( $data['ProfilePage']['@id'] ) || ! is_singular() ) {
return $data;
}
if ( function_exists( 'bp_is_user' ) && bp_is_user() ) {
return $data;
}
$temp_data = $data;
$id = $temp_data['ProfilePage']['@id'];
$ids = [];
unset( $temp_data['ProfilePage'] );
array_walk_recursive(
$temp_data,
function( $value, $key ) use ( &$ids, $id ) {
if ( '@id' === $key && $value === $id ) {
$ids[] = $value;
}
}
);
if ( empty( $ids ) ) {
unset( $data['ProfilePage'] );
}
return $data;
}
/**
* Change WebPage entity type depending on the schemas on the page.
*
* @param array $schemas Schema data.
* @param array $types Schema types.
*
* @return array
*/
private function change_webpage_entity( $schemas, $types ) {
if ( in_array( 'Product', $types, true ) ) {
$schemas['WebPage']['@type'] = 'ItemPage';
}
if ( isset( $schemas['howto'] ) && ! empty( $schemas['WebPage'] ) ) {
$schemas['howto']['mainEntityOfPage'] = [ '@id' => $schemas['WebPage']['@id'] ];
}
$faq_data = array_map(
function( $schema ) {
return isset( $schema['@type'] ) && 'FAQPage' === $schema['@type'];
},
$schemas
);
$faq_key = is_array( $faq_data ) && ! empty( $faq_data ) ? key( array_filter( $faq_data ) ) : '';
if ( ! $faq_key ) {
return $schemas;
}
if ( in_array( $faq_key, array_keys( $schemas ), true ) ) {
$schemas['WebPage']['@type'] =
! empty( $types )
? array_merge( (array) $schemas['WebPage']['@type'], [ 'FAQPage' ] )
: 'FAQPage';
$schemas['WebPage']['mainEntity'] = $schemas[ $faq_key ]['mainEntity'];
unset( $schemas[ $faq_key ] );
}
return $schemas;
}
}

View File

@@ -0,0 +1,794 @@
<?php
/**
* Output the Schema.org markup in JSON-LD format.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Helpers\Url;
use RankMath\Helpers\Str;
use RankMath\Helpers\Arr;
use RankMath\Paper\Paper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* JsonLD class.
*/
class JsonLD {
use Hooker;
/**
* Hold post object.
*
* @var WP_Post
*/
public $post = null;
/**
* Hold post ID.
*
* @var ID
*/
public $post_id = 0;
/**
* Hold post parts.
*
* @var array
*/
public $parts = [];
/**
* The Constructor.
*/
public function setup() {
$this->action( 'rank_math/head', 'json_ld', 90 );
$this->action( 'rank_math/json_ld', 'add_context_data' );
$this->action( 'rank_math/json_ld/preview', 'generate_preview' );
new Block_Parser();
new Frontend();
}
/**
* Get Schema Preview. Used in the Code Validation in Pro.
*/
public function generate_preview() {
global $post;
if ( is_singular() ) {
$this->post = $post;
$this->post_id = $post->ID;
$this->get_parts();
}
$data = $this->do_filter( 'json_ld', [], $this );
unset( $data['BreadcrumbList'] );
// Preview schema.
$schema = \json_decode( file_get_contents( 'php://input' ), true );
$schema_id = $schema['schemaID'];
if ( isset( $data[ $schema_id ] ) ) {
$current_data = $data[ $schema_id ];
unset( $data[ $schema_id ] );
} else {
$current_data = array_pop( $data );
}
unset( $schema['schemaID'] );
$schema = $this->replace_variables( $schema );
$schema = $this->filter( $schema, $this, $data );
$schema = wp_parse_args( $schema['schema'], $current_data );
if ( ! empty( $schema['@type'] ) && in_array( $schema['@type'], [ 'WooCommerceProduct', 'EDDProduct' ], true ) ) {
$schema['@type'] = 'Product';
}
// Merge.
$data = array_merge( $data, [ $schema_id => $schema ] );
/**
* Filter to change the Code validation data..
*
* @param array $unsigned An array of data to output in JSON-LD.
*/
$data = $this->do_filter( 'schema/preview/validate', $this->do_filter( 'schema/validated_data', $this->validate_schema( $data ) ) );
echo wp_json_encode( array_values( $data ) );
}
/**
* Function to get Old schema data. This code is used in the Schema_Converter to convert old schema data.
*
* @param int $post_id Post id for conversion.
* @param mixed $class Class instance of snippet.
* @return array
*/
public function get_old_schema( $post_id, $class ) {
global $post;
$post = get_post( $post_id ); // phpcs:ignore
$this->post = $post;
$this->post_id = $post_id;
setup_postdata( $post );
$this->get_parts();
/**
* Collect data to output in JSON-LD.
*
* @param array $unsigned An array of data to output in json-ld.
* @param JsonLD $unsigned JsonLD instance.
*/
return $class->process( [], $this );
}
/**
* Output the ld+json tag with the generated schema data.
*/
public function json_ld() {
global $post;
if ( is_singular() ) {
$this->post = $post;
$this->post_id = $post->ID;
$this->get_parts();
}
/**
* Filter to collect data to output in JSON-LD.
*
* @param array $unsigned An array of data to output in JSON-LD.
* @param JsonLD $unsigned JsonLD instance.
*/
$data = $this->do_filter( 'json_ld', [], $this );
$data = $this->do_filter( 'schema/validated_data', $this->validate_schema( $data ) );
if ( is_array( $data ) && ! empty( $data ) ) {
$class = defined( 'RANK_MATH_PRO_FILE' ) ? 'schema-pro' : 'schema';
$json = [
'@context' => 'https://schema.org',
'@graph' => array_values( $data ),
];
$options = defined( 'RANKMATH_DEBUG' ) && RANKMATH_DEBUG ? JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES : JSON_UNESCAPED_SLASHES;
echo '<script type="application/ld+json" class="rank-math-' . esc_attr( $class ) . '">' . wp_json_encode( wp_kses_post_deep( $json ), $options ) . '</script>' . "\n";
}
}
/**
* Validate schema data. Removes invalid and empty values from the schema data.
*
* @param array $data Array of JSON-LD data.
*
* @return array
*/
private function validate_schema( $data ) {
if ( ! is_array( $data ) || empty( $data ) ) {
return $data;
}
foreach ( $data as $id => $value ) {
if ( is_array( $value ) ) {
// Remove aline @type.
if ( isset( $value['@type'] ) && 1 === count( $value ) ) {
unset( $data[ $id ] );
continue;
}
// Remove empty review.
if ( 'review' === $id && isset( $value['@type'] ) ) {
if ( ! isset( $value['reviewRating'] ) || ! isset( $value['reviewRating']['ratingValue'] ) ) {
unset( $data[ $id ] );
continue;
}
}
// Recursive.
$data[ $id ] = $this->validate_schema( $value );
}
// Remove empty values.
// Remove need of array_filter as this will go recursive.
if ( '' === $value ) {
unset( $data[ $id ] );
continue;
}
}
return $data;
}
/**
* Get Default Schema Data.
*
* @param array $data Array of JSON-LD data.
*
* @return array
*/
public function add_schema( $data ) {
global $post;
$schema = DB::get_schemas( $post->ID );
return array_merge( $data, $schema );
}
/**
* Get Default Schema Data.
*
* @param array $data Array of json-ld data.
*
* @return array
*/
public function add_context_data( $data ) {
$is_product_archive = $this->is_product_archive_page();
$can_add_global = $this->can_add_global_entities( $data, $is_product_archive );
$snippets = [
'\\RankMath\\Schema\\Publisher' => ! isset( $data['publisher'] ) && $can_add_global,
'\\RankMath\\Schema\\Website' => $can_add_global,
'\\RankMath\\Schema\\PrimaryImage' => is_singular() && ! post_password_required() && $can_add_global,
'\\RankMath\\Schema\\Breadcrumbs' => $this->can_add_breadcrumb(),
'\\RankMath\\Schema\\Webpage' => $can_add_global,
'\\RankMath\\Schema\\Author' => is_author() || ( is_singular() && $can_add_global ),
'\\RankMath\\Schema\\Products_Page' => $is_product_archive,
'\\RankMath\\Schema\\Singular' => ! post_password_required() && is_singular(),
];
foreach ( $snippets as $class => $can_run ) {
if ( $can_run ) {
$class = new $class();
$data = $class->process( $data, $this );
}
}
return $data;
}
/**
* Function to replace variables used in Schema fields.
*
* @param array $schemas Schema to replace.
* @param object $object Current Object.
* @param array $data Array of json-ld data.
*
* @return array
*/
public function replace_variables( $schemas, $object = [], $data = [] ) {
$new_schemas = [];
$object = empty( $object ) ? get_queried_object() : $object;
foreach ( $schemas as $key => $schema ) {
if ( 'metadata' === $key ) {
$new_schemas['isPrimary'] = ! empty( $schema['isPrimary'] );
if ( ! empty( $schema['type'] ) && 'custom' === $schema['type'] ) {
$new_schemas['isCustom'] = true;
}
continue;
}
$this->replace_author( $schema, $data );
if ( is_array( $schema ) ) {
$new_schemas[ $key ] = $this->replace_variables( $schema, $object, $data );
continue;
}
// Need this conditions to convert date to valid ISO 8601 format.
if ( in_array( $key, [ 'datePublished', 'uploadDate' ], true ) && '%date(Y-m-dTH:i:sP)%' === $schema ) {
$schema = '%date(Y-m-d\TH:i:sP)%';
}
if ( 'dateModified' === $key && '%modified(Y-m-dTH:i:sP)%' === $schema ) {
$schema = '%modified(Y-m-d\TH:i:sP)%';
}
$new_schemas[ $key ] = is_string( $schema ) && Str::contains( '%', $schema ) && ! filter_var( $schema, FILTER_VALIDATE_URL )
? Helper::replace_vars( $schema, $object ) : $schema;
if ( '' === $new_schemas[ $key ] ) {
unset( $new_schemas[ $key ] );
}
}
return $new_schemas;
}
/**
* Function to replace %author% with Author entity @id.
*
* @param array $schema Schema to replace.
* @param array $data Array of json-ld data.
*
* @return array
*/
private function replace_author( &$schema, $data ) {
if ( empty( $data['ProfilePage'] ) ) {
return;
}
if ( empty( $schema['author'] ) || ! isset( $schema['author']['name'] ) || ! in_array( $schema['author']['name'], [ '%name%', '%post_author%' ], true ) ) {
return;
}
$schema['author'] = [
'@id' => $data['ProfilePage']['@id'],
'name' => get_the_author(),
];
}
/**
* Filter schema data before adding it to ld+json.
*
* @param array $schemas Schema to replace.
* @param JsonLD $jsonld Instance of jsonld.
* @param array $data Array of json-ld data.
*
* @return array
*/
public function filter( $schemas, $jsonld, $data ) {
$new_schemas = [];
foreach ( $schemas as $key => $schema ) {
$type = is_array( $schema['@type'] ) ? $schema['@type'][0] : $schema['@type'];
$type = strtolower( $type );
$type = in_array( $type, [ 'musicgroup', 'musicalbum' ], true )
? 'music'
: ( in_array( $type, [ 'blogposting', 'newsarticle' ], true ) ? 'article' : $type );
$type = Str::contains( 'event', $type ) ? 'event' : $type;
$hook = 'snippet/rich_snippet_' . $type;
/**
* Short-circuit if 3rd party is interested generating his own data.
*/
$pre = $this->do_filter( $hook, false, $jsonld->parts, $data );
if ( false !== $pre ) {
$new_schemas[ $key ] = $this->do_filter( $hook . '_entity', $pre );
$new_schemas[ $key ] = $this->do_filter( 'snippet/rich_snippet_entity', $new_schemas[ $key ] );
continue;
}
$new_schemas[ $key ] = $this->do_filter( $hook . '_entity', $schema );
$new_schemas[ $key ] = $this->do_filter( 'snippet/rich_snippet_entity', $new_schemas[ $key ] );
}
return $new_schemas;
}
/**
* Whether to add global schema entities.
*
* @param array $data Array of json-ld data.
* @param bool $is_product_archive Whether the current page is a Product archive.
* @return bool
*/
public function can_add_global_entities( $data = [], $is_product_archive = false ) {
if ( ! $is_product_archive && ( is_category() || is_tag() || is_tax() ) ) {
$queried_object = get_queried_object();
return ! Helper::get_settings( 'titles.remove_' . $queried_object->taxonomy . '_snippet_data' ) && ! $this->do_filter( 'snippet/remove_taxonomy_data', false, $queried_object->taxonomy );
}
if ( is_front_page() || ! is_singular() || ! Helper::can_use_default_schema( $this->post_id ) || ! empty( $data ) ) {
return true;
}
$schemas = DB::get_schemas( $this->post_id );
if ( ! empty( $schemas ) ) {
return true;
}
/**
* Allow developer to remove global schema entities.
*
* @param bool $can_add
* @param JsonLD $unsigned JsonLD instance.
*/
return $this->do_filter( 'schema/add_global_entities', Helper::get_default_schema_type( $this->post_id, true ), $this );
}
/**
* Can add breadcrumb schema.
*
* @return bool
*/
private function can_add_breadcrumb() {
/**
* Allow developer to disable the breadcrumb JSON-LD output.
*
* @param bool $unsigned Default: true
*/
return ! is_front_page() && Helper::is_breadcrumbs_enabled() && $this->do_filter( 'json_ld/breadcrumbs_enabled', true );
}
/**
* Check if current page is a WooCommerce archive page.
*
* @return bool
*/
private function is_product_archive_page() {
return Helper::is_woocommerce_active() && ( ( is_tax() && in_array( get_query_var( 'taxonomy' ), get_object_taxonomies( 'product' ), true ) ) || is_shop() );
}
/**
* Add property to entity.
*
* @param string $prop Name of the property to add into entity.
* @param array $entity Array of json-ld entity.
* @param string $key Property key to add into entity.
* @param array $data Array of json-ld data.
*/
public function add_prop( $prop, &$entity, $key = '', $data = [] ) {
if ( empty( $prop ) ) {
return;
}
$hash = [
'email' => [ 'titles.email', 'email' ],
'phone' => [ 'titles.phone', 'telephone' ],
];
if ( isset( $hash[ $prop ] ) && $value = Helper::get_settings( $hash[ $prop ][0] ) ) { // phpcs:ignore
$entity[ $hash[ $prop ][1] ] = $value;
return;
}
$perform = "add_prop_{$prop}";
if ( method_exists( $this, $perform ) ) {
$this->$perform( $entity, $key, $data );
}
}
/**
* Add logo property to the entity.
*
* @param array $entity Array of JSON-LD entity.
*/
private function add_prop_image( &$entity ) {
$logo = Helper::get_settings( 'titles.knowledgegraph_logo' );
if ( ! $logo ) {
$logo_id = \get_option( 'site_logo' );
$logo = $logo_id ? wp_get_attachment_image_url( $logo_id ) : '';
}
if ( ! $logo ) {
return;
}
$entity['logo'] = [
'@type' => 'ImageObject',
'@id' => home_url( '/#logo' ),
'url' => $logo,
'contentUrl' => $logo,
'caption' => $this->get_website_name(),
];
$this->add_prop_language( $entity['logo'] );
$attachment = wp_get_attachment_metadata( Helper::get_settings( 'titles.knowledgegraph_logo_id' ), true );
if ( ! $attachment ) {
return;
}
$entity['logo']['width'] = $attachment['width'];
$entity['logo']['height'] = $attachment['height'];
}
/**
* Add Language property to the entity.
*
* @param array $entity Array of JSON-LD entity.
*/
private function add_prop_language( &$entity ) {
$entity['inLanguage'] = $this->do_filter( 'schema/language', get_bloginfo( 'language' ) );
}
/**
* Add Image property to entity.
*
* @param array $entity Array of json-ld entity.
* @param string $key Entity Key.
* @param array $data Schema Data.
*/
private function add_prop_thumbnail( &$entity, $key, $data ) {
if ( ! empty( $data['primaryImage'] ) ) {
$entity[ $key ] = [ '@id' => $data['primaryImage']['@id'] ];
}
}
/**
* Add isPartOf property to entity.
*
* @param array $entity Array of json-ld entity.
* @param string $key Entity Key.
*/
private function add_prop_is_part_of( &$entity, $key ) {
$hash = [
'website' => home_url( '/#website' ),
'webpage' => Paper::get()->get_canonical() . '#webpage',
];
if ( ! empty( $hash[ $key ] ) ) {
$entity['isPartOf'] = [ '@id' => $hash[ $key ] ];
}
}
/**
* Add publisher property to entity
*
* @param array $entity Entity.
* @param string $key Entity Key.
* @param array $data Schema Data.
*/
public function add_prop_publisher( &$entity, $key, $data ) {
if ( empty( $data['publisher'] ) ) {
return;
}
$entity[ $key ] = [ '@id' => $data['publisher']['@id'] ];
}
/**
* Add url property to entity.
*
* @param array $entity Array of JSON-LD entity.
*/
private function add_prop_url( &$entity ) {
if ( $url = Helper::get_settings( 'titles.url' ) ) { // phpcs:ignore
$entity['url'] = ! Url::is_relative( $url ) ? $url : 'http://' . $url;
}
}
/**
* Add address property to entity.
*
* @param array $entity Array of JSON-LD entity.
*/
private function add_prop_address( &$entity ) {
if ( $address = Helper::get_settings( 'titles.local_address' ) ) { // phpcs:ignore
$entity['address'] = [ '@type' => 'PostalAddress' ] + $address;
}
}
/**
* Add aggregateratings to entity.
*
* @param string $schema Schema to get data for.
* @param array $entity Array of JSON-LD entity to attach data to.
*/
public function add_ratings( $schema, &$entity ) {
$rating = Helper::get_post_meta( "snippet_{$schema}_rating" );
// Early Bail!
if ( ! $rating ) {
return;
}
$entity['review'] = [
'author' => [
'@type' => 'Person',
'name' => get_the_author_meta( 'display_name' ),
],
'datePublished' => get_post_time( 'Y-m-d\TH:i:sP', false ),
'dateModified' => get_post_modified_time( 'Y-m-d\TH:i:sP', false ),
'reviewRating' => [
'@type' => 'Rating',
'ratingValue' => $rating,
'bestRating' => Helper::get_post_meta( "snippet_{$schema}_rating_max" ) ? Helper::get_post_meta( "snippet_{$schema}_rating_max" ) : 5,
'worstRating' => Helper::get_post_meta( "snippet_{$schema}_rating_min" ) ? Helper::get_post_meta( "snippet_{$schema}_rating_min" ) : 1,
],
];
}
/**
* Get website name with a fallback to bloginfo( 'name' ).
*
* @return string
*/
public function get_website_name() {
return Helper::get_settings( 'titles.website_name', $this->get_organization_name() );
}
/**
* Get website name with a fallback to bloginfo( 'name' ).
*
* @return string
*/
public function get_organization_name() {
$name = Helper::get_settings( 'titles.knowledgegraph_name' );
return $name ? $name : get_bloginfo( 'name' );
}
/**
* Set publisher/provider data for JSON-LD.
*
* @param array $entity Array of JSON-LD entity.
* @param array $organization Organization data.
* @param string $type Type data set to. Default: 'publisher'.
*/
public function set_publisher( &$entity, $organization, $type = 'publisher' ) {
$keys = [ '@context', '@type', 'url', 'name', 'logo', 'image', 'contactPoint', 'sameAs' ];
foreach ( $keys as $key ) {
if ( ! isset( $organization[ $key ] ) ) {
continue;
}
$entity[ $type ][ $key ] = 'logo' !== $key ? $organization[ $key ] : [
'@type' => 'ImageObject',
'url' => $organization[ $key ],
];
}
}
/**
* Set address for JSON-LD.
*
* @param string $schema Schema to get data for.
* @param array $entity Array of JSON-LD entity to attach data to.
*/
public function set_address( $schema, &$entity ) {
$address = Helper::get_post_meta( "snippet_{$schema}_address" );
// Early Bail!
if ( ! is_array( $address ) || empty( $address ) ) {
return;
}
$entity['address'] = [ '@type' => 'PostalAddress' ];
foreach ( $address as $key => $value ) {
$entity['address'][ $key ] = $value;
}
}
/**
* Set data to entity.
*
* Loop through post meta value grab data and attache it to the entity.
*
* @param array $hash Key to get data and Value to save as.
* @param array $entity Array of JSON-LD entity to attach data to.
*/
public function set_data( $hash, &$entity ) {
foreach ( $hash as $metakey => $dest ) {
$entity[ $dest ] = Helper::get_post_meta( $metakey, $this->post_id );
}
}
/**
* Get post title.
*
* Retrieves the title in this order.
* 1. Custom post meta set in rich snippet
* 2. Headline template set in Titles & Meta
*
* @param int $post_id Post ID to get title for.
*
* @return string
*/
public function get_post_title( $post_id = 0 ) {
$title = Helper::get_post_meta( 'snippet_name', $post_id );
if ( ! $title && ! empty( $this->post ) ) {
$title = Helper::replace_vars( Helper::get_settings( "titles.pt_{$this->post->post_type}_default_snippet_name", '%seo_title%' ), $this->post );
}
$title = $title ? $title : Paper::get()->get_title();
return Str::truncate( $title );
}
/**
* Get post url.
*
* @param int $post_id Post ID to get URL for.
* @return string
*/
public function get_post_url( $post_id = 0 ) {
$url = Helper::get_post_meta( 'snippet_url', $post_id );
return $url ? $url : ( 0 === $post_id ? Paper::get()->get_canonical() : get_the_permalink( $post_id ) );
}
/**
* Get product description.
*
* @param object $product Product Object.
* @return string
*/
public function get_product_desc( $product = [] ) {
if ( empty( $product ) ) {
return;
}
if ( $description = Helper::get_post_meta( 'description', $product->get_id() ) ) { //phpcs:ignore
return $description;
}
$product_object = get_post( $product->get_id() );
$description = Paper::get_from_options( 'pt_product_description', $product_object, '%excerpt%' );
if ( ! $description ) {
$description = $product->get_short_description() ? $product->get_short_description() : $product->get_description();
}
$description = $this->do_filter( 'product_description/apply_shortcode', false ) ? do_shortcode( $description ) : Helper::strip_shortcodes( $description );
return wp_strip_all_tags( $description, true );
}
/**
* Get product title.
*
* @param object $product Product Object.
* @return string
*/
public function get_product_title( $product = [] ) {
if ( empty( $product ) ) {
return '';
}
if ( $title = Helper::get_post_meta( 'title', $product->get_id() ) ) { //phpcs:ignore
return $title;
}
$product_object = get_post( $product->get_id() );
$title = Paper::get_from_options( 'pt_product_title', $product_object, '%title% %sep% %sitename%' );
return $title ? $title : $product->get_name();
}
/**
* Get post parts.
*/
private function get_parts() {
$parts = [
'title' => $this->get_post_title(),
'url' => $this->get_post_url(),
'canonical' => Paper::get()->get_canonical(),
'modified' => mysql2date( DATE_W3C, $this->post->post_modified, false ),
'published' => mysql2date( DATE_W3C, $this->post->post_date, false ),
'excerpt' => Helper::replace_vars( '%excerpt%', $this->post ),
];
// Description.
$desc = Helper::get_post_meta( 'snippet_desc' );
if ( ! $desc ) {
$desc = Helper::replace_vars( Helper::get_settings( "titles.pt_{$this->post->post_type}_default_snippet_desc" ), $this->post );
}
$parts['desc'] = $desc ? $desc : ( Helper::get_post_meta( 'description' ) ? Helper::get_post_meta( 'description' ) : $parts['excerpt'] );
// Author.
$author = Helper::get_post_meta( 'snippet_author' );
$parts['author'] = $author ? $author : get_the_author_meta( 'display_name', $this->post->post_author );
// Modified date cannot be before publish date.
if ( strtotime( $this->post->post_modified ) < strtotime( $this->post->post_date ) ) {
$parts['modified'] = $parts['published'];
}
$this->parts = $parts;
}
/**
* Get global social profile URLs, to use in the `sameAs` property.
*
* @link https://developers.google.com/webmasters/structured-data/customize/social-profiles
*/
public function get_social_profiles() {
$profiles = [ Helper::get_settings( 'titles.social_url_facebook' ) ];
$twitter = Helper::get_settings( 'titles.twitter_author_names' );
if ( $twitter ) {
$profiles[] = "https://twitter.com/$twitter";
}
$addional_profiles = Helper::get_settings( 'titles.social_additional_profiles' );
if ( ! empty( $addional_profiles ) ) {
$profiles = array_merge( $profiles, Arr::from_string( $addional_profiles, "\n" ) );
}
return array_values( array_filter( $profiles ) );
}
}

View File

@@ -0,0 +1,153 @@
<?php
/**
* Output Opengraph tags for specific schema types.
*
* @since 1.0.56
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Opengraph class.
*/
class Opengraph {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'rank_math/opengraph/facebook', 'add_schema_tags', 90 );
}
/**
* Output the schema opengraph tags.
*
* @param OpenGraph $opengraph The current opengraph network object.
*/
public function add_schema_tags( $opengraph ) {
if ( ! is_singular() ) {
return;
}
$schemas = $this->get_schemas();
if ( empty( $schemas ) ) {
return;
}
$hash = [
'VideoObject' => 'video',
'Product' => 'product',
'Article' => 'article',
'NewsArticle' => 'article',
'BlogPosting' => 'article',
];
foreach ( $schemas as $schema ) {
$method = $hash[ $schema['@type'] ];
$this->$method( $schema, $opengraph );
}
}
/**
* Function to get schema data.
*/
private function get_schemas() {
global $post;
$schemas = array_filter(
DB::get_schemas( $post->ID ),
function( $schema ) {
return ! empty( $schema['@type'] ) && in_array( $schema['@type'], [ 'Article', 'NewsArticle', 'BlogPosting', 'Product', 'VideoObject' ], true );
}
);
if ( ! empty( $schemas ) ) {
return $schemas;
}
$default_schema = Helper::get_default_schema_type( $post->ID, true, false );
if ( ! in_array( $default_schema, [ 'Article', 'BlogPosting', 'NewsArticle' ], true ) ) {
return false;
}
return [
[
'@type' => $default_schema,
'datePublished' => '%date(Y-m-d\TH:i:sP)%',
'dateModified' => '%modified(Y-m-d\TH:i:sP)%',
],
];
}
/**
* Output Video Schema tags.
*
* @param array $schema Schema Data.
* @param OpenGraph $opengraph The current opengraph network object.
*/
private function video( $schema, $opengraph ) {
$video_url = ! empty( $schema['contentUrl'] ) ? $schema['contentUrl'] : ( ! empty( $schema['embedUrl'] ) ? $schema['embedUrl'] : '' );
if ( ! $video_url ) {
return;
}
$opengraph->tag( 'og:video', $video_url );
if ( ! empty( $schema['duration'] ) ) {
global $post;
$opengraph->tag( 'video:duration', Helper::duration_to_seconds( Helper::replace_vars( $schema['duration'], $post ) ) );
}
}
/**
* Output Product Schema tags.
*
* @param array $schema Schema Data.
* @param OpenGraph $opengraph The current opengraph network object.
*/
public function product( $schema, $opengraph ) {
if ( isset( $schema['brand'], $schema['brand']['name'] ) ) {
$opengraph->tag( 'product:brand', $schema['brand']['name'] );
}
$tags = [
'product:price:amount' => ! empty( $schema['offers']['price'] ) ? $schema['offers']['price'] : '',
'product:price:currency' => ! empty( $schema['offers']['priceCurrency'] ) ? $schema['offers']['priceCurrency'] : '',
'product:availability' => ! empty( $schema['offers']['availability'] ) && 'instock' === $schema['offers']['availability'] ? 'instock' : '',
];
foreach ( $tags as $tag => $value ) {
$opengraph->tag( $tag, $value );
}
}
/**
* Output Article Schema tags.
*
* @param array $schema Schema Data.
* @param OpenGraph $opengraph The current opengraph network object.
*/
private function article( $schema, $opengraph ) {
if ( empty( $schema['datePublished'] ) || empty( $schema['dateModified'] ) ) {
return;
}
global $post;
$pub = '%date(Y-m-dTH:i:sP)%' === $schema['datePublished'] ? '%date(Y-m-d\TH:i:sP)%' : $schema['datePublished'];
$mod = '%modified(Y-m-dTH:i:sP)%' === $schema['dateModified'] ? '%modified(Y-m-d\TH:i:sP)%' : $schema['dateModified'];
$pub = Helper::replace_vars( $pub, $post );
$mod = Helper::replace_vars( $mod, $post );
$opengraph->tag( 'article:published_time', $pub );
if ( $mod !== $pub ) {
$opengraph->tag( 'article:modified_time', $mod );
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* The Schema Module
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Schema class.
*/
class Schema {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
if ( is_admin() ) {
new Admin();
}
$this->action( 'wp', 'integrations' );
$this->filter( 'rank_math/elementor/dark_styles', 'add_dark_style' );
new Blocks();
new Snippet_Shortcode();
}
/**
* Add dark style
*
* @param array $styles The dark mode styles.
*/
public function add_dark_style( $styles = [] ) {
$styles['rank-math-schema-dark'] = rank_math()->plugin_url() . 'includes/modules/schema/assets/css/schema-dark.css';
return $styles;
}
/**
* Initialize integrations.
*/
public function integrations() {
$type = get_query_var( 'sitemap' );
if ( ! empty( $type ) ) {
return;
}
( new JsonLD() )->setup();
}
}

View File

@@ -0,0 +1,516 @@
<?php
/**
* The Schema Shortcode
*
* @since 1.0.24
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Traits\Shortcode;
use RankMath\Helpers\Str;
use RankMath\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* Snippet_Shortcode class.
*/
class Snippet_Shortcode {
use Hooker, Shortcode;
/**
* Post object.
*
* @var object
*/
private $post;
/**
* Schema data.
*
* @var array
*/
private $schema;
/**
* The Constructor.
*/
public function __construct() {
$this->add_shortcode( 'rank_math_rich_snippet', 'rich_snippet' );
$this->add_shortcode( 'rank_math_review_snippet', 'rich_snippet' );
if ( ! is_admin() ) {
$this->filter( 'the_content', 'output_schema_in_content', 11 );
}
if ( ! function_exists( 'register_block_type' ) ) {
return;
}
register_block_type(
'rank-math/rich-snippet',
[
'render_callback' => [ $this, 'rich_snippet' ],
'attributes' => [
'id' => [
'default' => '',
'type' => 'string',
],
'post_id' => [
'default' => '',
'type' => 'integer',
],
],
]
);
}
/**
* Schema shortcode.
*
* @param array $atts Optional. Shortcode arguments - currently only 'show'
* parameter, which is a comma-separated list of elements to show.
*
* @return string Shortcode output.
*/
public function rich_snippet( $atts ) {
$atts = shortcode_atts(
[
'id' => false,
'post_id' => Param::get( 'post_id' ) ? Param::get( 'post_id' ) : get_the_ID(),
'className' => '',
],
$atts,
'rank_math_rich_snippet'
);
if ( 'edit' === Param::get( 'context' ) ) {
rank_math()->variables->setup();
}
$data = $this->get_schema_data( $atts['id'], $atts['post_id'] );
if ( empty( $data ) || empty( $data['schema'] ) ) {
return esc_html__( 'No schema found.', 'rank-math' );
}
$post = get_post( $data['post_id'] );
$schemas = ! empty( $atts['id'] ) ? [ $data['schema'] ] : $data['schema'];
$html = '';
foreach ( $schemas as $schema ) {
$schema = $this->replace_variables( $schema, $post );
$schema = $this->do_filter( 'schema/shortcode/filter_attributes', $schema, $atts );
/**
* Change the Schema HTML output.
*
* @param string $unsigned HTML output.
* @param array $schema Schema data.
* @param WP_Post $post The post instance.
* @param Snippet_Shortcode $this Snippet_Shortcode instance.
*/
$html .= $this->do_filter( 'snippet/html', $this->get_snippet_content( $schema, $post, $atts ), $schema, $post, $this );
}
return $html;
}
/**
* Get Snippet content.
*
* @param array $schema Schema to replace.
* @param WP_Post $post Post schema attached to.
* @param array $atts Optional. Shortcode arguments - currently only 'show'
* parameter, which is a comma-separated list of elements to show.
*
* @return string Shortcode output.
*/
public function get_snippet_content( $schema, $post, $atts ) {
wp_enqueue_style( 'rank-math-review-snippet', rank_math()->assets() . 'css/rank-math-snippet.css', null, rank_math()->version );
$type = \strtolower( $schema['@type'] );
$type = preg_replace( '/[^a-z0-9_-]+/i', '', $type );
$this->post = $post;
$this->schema = $schema;
if ( in_array( $type, [ 'article', 'blogposting', 'newsarticle' ], true ) ) {
return;
}
if ( Str::ends_with( 'event', $type ) ) {
$type = 'event';
}
if ( 'resturant' === $type ) {
$type = 'restaurant';
}
$class = ! empty( $atts['className'] ) ? $atts['className'] : '';
ob_start();
?>
<div id="rank-math-rich-snippet-wrapper" class="<?php echo esc_attr( $class ); ?>">
<?php
$type = sanitize_file_name( $type );
$file = rank_math()->plugin_dir() . "includes/modules/schema/shortcode/$type.php";
if ( file_exists( $file ) ) {
include $file;
}
$this->do_action( 'snippet/after_schema_content', $this );
?>
</div>
<?php
return ob_get_clean();
}
/**
* Get field value.
*
* @param string $field_id Field id.
* @param mixed $default Default value.
* @return mixed
*/
public function get_field_value( $field_id, $default = null ) {
$array = $this->schema;
if ( isset( $array[ $field_id ] ) ) {
if ( isset( $array[ $field_id ]['@type'] ) ) {
unset( $array[ $field_id ]['@type'] );
}
return $array[ $field_id ];
}
foreach ( explode( '.', $field_id ) as $segment ) {
if ( ! is_array( $array ) || ! array_key_exists( $segment, $array ) ) {
return $default;
}
$array = $array[ $segment ];
}
return $array;
}
/**
* Get field.
*
* @param string $title Field title.
* @param string $field_id Field id to get value.
* @param string $convert_date Convert date value to proper format.
* @param mixed $default Default value.
*/
public function get_field( $title, $field_id, $convert_date = false, $default = null ) {
$value = $this->get_field_value( $field_id, $default );
if ( empty( $value ) ) {
return;
}
if ( $convert_date ) {
$value = Helper::convert_date( $value );
}
$this->output_field( $title, $value );
}
/**
* Get Opening hours data.
*
* @param string $field_id Field id to get value.
*/
public function get_opening_hours( $field_id ) {
$opening_hours = $this->get_field_value( $field_id );
if ( empty( $opening_hours ) ) {
return;
}
if ( count( array_filter( array_keys( $opening_hours ), 'is_string' ) ) > 0 ) {
$this->get_opening_hour( $opening_hours );
return;
}
echo '<div>';
echo '<strong>' . esc_html__( 'Opening Hours', 'rank-math' ) . '</strong><br />';
foreach ( $opening_hours as $opening_hour ) {
$this->get_opening_hour( $opening_hour );
}
echo '</div>';
}
/**
* Get Opening hours.
*
* @param array $opening_hour Opening hours data.
*/
public function get_opening_hour( $opening_hour ) {
$labels = [
'dayOfWeek' => esc_html__( 'Days', 'rank-math' ),
'opens' => esc_html__( 'Opening Time', 'rank-math' ),
'closes' => esc_html__( 'Closing Time', 'rank-math' ),
];
foreach ( $labels as $key => $label ) {
if ( empty( $opening_hour[ $key ] ) ) {
continue;
}
$this->output_field( $label, $opening_hour[ $key ] );
}
}
/**
* Get field.
*
* @param string $title Field title.
* @param mixed $value Field value.
*/
public function output_field( $title, $value ) {
?>
<p>
<strong><?php echo esc_html( $title ); // phpcs:ignore ?>: </strong>
<?php echo is_array( $value ) ? implode( ', ', $value ) : wp_kses_post( $value ); // phpcs:ignore ?>
</p>
<?php
}
/**
* Get title.
*/
public function get_title() {
if ( ! isset( $this->schema['name'] ) && ! isset( $this->schema['title'] ) ) {
return;
}
$title = isset( $this->schema['title'] ) ? $this->schema['title'] : $this->schema['name'];
$title = $title && '' !== $title ? $title : Helper::replace_vars( '%title%', $this->post );
?>
<h5 class="rank-math-title"><?php echo esc_html( $title ); // phpcs:ignore ?></h5>
<?php
}
/**
* Get description.
*
* @param string $description Schema description field.
*/
public function get_description( $description = 'description' ) {
$excerpt = Helper::replace_vars( '%excerpt%', $this->post );
if ( $description && '' !== $description ) {
$description = $this->get_field_value( $description );
}
$description = $description && '' !== $description ? $description : ( $excerpt ? $excerpt : Helper::get_post_meta( 'description', $this->post->ID ) );
?>
<p><?php echo wp_kses_post( do_shortcode( $description ) ); ?></p>
<?php
}
/**
* Get image.
*/
public function get_image() {
if ( ! isset( $this->schema['image'] ) ) {
return;
}
$image = Helper::get_thumbnail_with_fallback( $this->post->ID, 'medium' );
if ( empty( $image ) ) {
return;
}
?>
<div class="rank-math-review-image">
<img src="<?php echo esc_url( $image[0] ); ?>" />
</div>
<?php
}
/**
* Display nicely formatted reviews.
*
* @param string $field_id Field id to get value.
*/
public function show_ratings( $field_id = 'review.reviewRating.ratingValue' ) {
$rating = (float) $this->get_field_value( $field_id );
if ( empty( $rating ) ) {
return;
}
$best_rating = (int) $this->get_field_value( 'review.reviewRating.bestRating', 5 );
?>
<div class="rank-math-total-wrapper">
<strong><?php echo $this->do_filter( 'review/text', esc_html__( 'Editor\'s Rating:', 'rank-math' ) ); // phpcs:ignore ?></strong><br />
<span class="rank-math-total"><?php echo $rating; // phpcs:ignore ?></span>
<div class="rank-math-review-star">
<div class="rank-math-review-result-wrapper">
<?php echo \str_repeat( '<i class="rank-math-star"></i>', $best_rating ); // phpcs:ignore ?>
<div class="rank-math-review-result" style="width:<?php echo ( $rating * ( 100 / $best_rating ) ); // phpcs:ignore ?>%;">
<?php echo \str_repeat( '<i class="rank-math-star"></i>', $best_rating ); // phpcs:ignore ?>
</div>
</div>
</div>
</div>
<?php
}
/**
* Add schema data in the content.
*
* @param string $content Post content.
* @return string
*
* @since 1.0.12
*/
public function output_schema_in_content( $content ) {
if ( ! is_singular() ) {
return $content;
}
$schemas = $this->get_schemas();
if ( empty( $schemas ) ) {
return $content;
}
foreach ( $schemas as $schema ) {
$location = $this->get_content_location( $schema );
if ( false === $location || 'custom' === $location ) {
continue;
}
$review = do_shortcode( '[rank_math_rich_snippet id="' . $schema['metadata']['shortcode'] . '"]' );
if ( in_array( $location, [ 'top', 'both' ], true ) ) {
$content = $review . $content;
}
if ( in_array( $location, [ 'bottom', 'both' ], true ) && $this->can_add_multi_page() ) {
$content .= $review;
}
}
return $content;
}
/**
* Get schema data by shortcode/post ID.
*
* @param string $shortcode_id Schema shortcode ID.
* @param string $post_id Post ID.
* @return array
*/
private function get_schema_data( $shortcode_id, $post_id = false ) {
if ( ! empty( $shortcode_id ) && is_string( $shortcode_id ) ) {
return DB::get_schema_by_shortcode_id( $shortcode_id );
}
if ( ! $post_id ) {
$post_id = Param::get( 'post_id' ) ? Param::get( 'post_id' ) : get_the_ID();
}
$data = DB::get_schemas( $post_id );
return empty( $data ) ? false : [
'post_id' => $post_id,
'schema' => $data,
];
}
/**
* Function to replace variables used in Schema fields.
*
* @param array $schemas Schema to replace.
* @param WP_Post $post Post schema attached to.
* @return array
*/
private function replace_variables( $schemas, $post ) {
if ( ! is_array( $schemas ) && ! is_object( $schemas ) ) {
return [];
}
$new_schemas = [];
foreach ( $schemas as $key => $schema ) {
if ( 'metadata' === $key ) {
continue;
}
if ( is_array( $schema ) ) {
$new_schemas[ $key ] = $this->replace_variables( $schema, $post );
continue;
}
$new_schemas[ $key ] = Str::contains( '%', $schema ) ? Helper::replace_seo_fields( $schema, $post ) : $schema;
}
return $new_schemas;
}
/**
* Check if we can inject the review in the content.
*
* @param array $schema Schema Data.
*
* @return boolean|string
*/
private function get_content_location( $schema ) {
$location = ! empty( $schema['metadata']['shortcode'] ) && isset( $schema['metadata']['reviewLocation'] ) ? $schema['metadata']['reviewLocation'] : false;
return $this->do_filter( 'snippet/review/location', $location );
}
/**
* Get schema data to show in the content.
*
* @return boolean|array
*
* @since 1.0.59
*/
private function get_schemas() {
/**
* Filter: Allow disabling the review display.
*
* @param bool $return True to disable.
*/
if ( ! is_main_query() || ! in_the_loop() || $this->do_filter( 'snippet/review/hide_data', false ) ) {
return false;
}
$schemas = $this->get_schema_data( false );
if ( empty( $schemas ) ) {
return false;
}
return array_filter(
$schemas['schema'],
function( $schema ) {
return ! empty( $schema['metadata']['reviewLocation'] );
}
);
}
/**
* Check if we can add content if multipage.
*
* @return bool
*/
private function can_add_multi_page() {
global $multipage, $numpages, $page;
return ( ! $multipage || $page === $numpages );
}
}

View File

@@ -0,0 +1 @@
<?php // Silence is golden.

View File

@@ -0,0 +1,29 @@
<?php
/**
* The Schema Interface
*
* @since 1.0.13
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
defined( 'ABSPATH' ) || exit;
/**
* Schema interface.
*/
interface Snippet {
/**
* Process schema data
*
* @param array $data Array of JSON-LD data.
* @param JsonLD $jsonld Instance of JsonLD.
*
* @return array
*/
public function process( $data, $jsonld );
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Shortcode - Book
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$this->get_title();
$this->get_image();
?>
<div class="rank-math-review-data">
<?php $this->get_description(); ?>
<?php
$this->get_field(
esc_html__( 'URL', 'rank-math' ),
'url'
);
?>
<?php
$this->get_field(
esc_html__( 'Author', 'rank-math' ),
'author.name'
);
?>
<?php
if ( ! empty( $schema['hasPart'] ) ) {
$hash = [
'edition' => __( 'Edition', 'rank-math' ),
'name' => __( 'Name', 'rank-math' ),
'url' => __( 'Url', 'rank-math' ),
'author' => __( 'Author', 'rank-math' ),
'isbn' => __( 'ISBN', 'rank-math' ),
'datePublished' => __( 'Date Published', 'rank-math' ),
'bookFormat' => __( 'Format', 'rank-math' ),
];
foreach ( $schema['hasPart'] as $edition ) {
$this->schema = $edition;
foreach ( $hash as $id => $label ) {
$this->get_field( $label, $id );
}
}
$this->schema = $schema;
}
?>
<?php $this->show_ratings(); ?>
</div>

View File

@@ -0,0 +1,111 @@
<?php
/**
* Shortcode - Course
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$this->get_title();
$this->get_image();
?>
<div class="rank-math-review-data">
<?php $this->get_description(); ?>
<?php
$this->get_field(
esc_html__( 'Course Provider', 'rank-math' ),
'provider.@type'
);
?>
<?php
$this->get_field(
esc_html__( 'Course Provider Name', 'rank-math' ),
'provider.name'
);
?>
<?php
$this->get_field(
esc_html__( 'Course Provider URL', 'rank-math' ),
'provider.sameAs'
);
?>
<?php
$this->get_field(
esc_html__( 'Course Mode', 'rank-math' ),
'hasCourseInstance.courseMode'
);
?>
<?php
$this->get_field(
esc_html__( 'Course Workload', 'rank-math' ),
'hasCourseInstance.courseWorkload',
);
?>
<?php
$this->get_field(
esc_html__( 'Start Date', 'rank-math' ),
'hasCourseInstance.courseSchedule.startDate',
);
?>
<?php
$this->get_field(
esc_html__( 'End Date', 'rank-math' ),
'hasCourseInstance.courseSchedule.endDate',
);
?>
<?php
$this->get_field(
esc_html__( 'Duration', 'rank-math' ),
'hasCourseInstance.courseSchedule.duration',
);
?>
<?php
$this->get_field(
esc_html__( 'Repeat Count', 'rank-math' ),
'hasCourseInstance.courseSchedule.repeatCount',
);
?>
<?php
$this->get_field(
esc_html__( 'Repeat Frequency', 'rank-math' ),
'hasCourseInstance.courseSchedule.repeatFrequency',
);
?>
<?php
$this->get_field(
esc_html__( 'Course Type', 'rank-math' ),
'offers.category'
);
?>
<?php
$this->get_field(
esc_html__( 'Course Currency', 'rank-math' ),
'offers.priceCurrency'
);
?>
<?php
$this->get_field(
esc_html__( 'Course Price', 'rank-math' ),
'offers.price'
);
?>
<?php $this->show_ratings(); ?>
</div>

View File

@@ -0,0 +1,158 @@
<?php
/**
* Shortcode - Event
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$this->get_title();
$this->get_image();
$value = $this->get_field_value( 'eventAttendanceMode' );
$is_online = 'Online' === $value;
$is_offline = 'Offline' === $value;
if ( 'MixedEventAttendanceMode' === $value ) {
$is_online = true;
$is_offline = true;
$value = esc_html__( 'Online + Offline', 'rank-math' );
}
?>
<div class="rank-math-review-data">
<?php $this->get_description(); ?>
<?php
$this->get_field(
esc_html__( 'Event Type', 'rank-math' ),
'@type'
);
?>
<?php
$this->output_field(
esc_html__( 'Event Attendance Mode', 'rank-math' ),
$value
);
?>
<?php
$this->get_field(
esc_html__( 'Event Status', 'rank-math' ),
'eventStatus'
);
?>
<?php
if ( $is_offline ) {
$this->get_field(
esc_html__( 'Venue Name', 'rank-math' ),
'location.name'
);
$this->get_field(
esc_html__( 'Venue URL', 'rank-math' ),
'location.url'
);
$this->get_field(
esc_html__( 'Address', 'rank-math' ),
'location.address'
);
}
?>
<?php
if ( $is_online ) {
$this->get_field(
esc_html__( 'Online Event URL', 'rank-math' ),
'VirtualLocation.url'
);
}
?>
<?php
$this->get_field(
esc_html__( 'Performer', 'rank-math' ),
'performer.@type'
);
?>
<?php
$this->get_field(
esc_html__( 'Performer Name', 'rank-math' ),
'performer.name'
);
?>
<?php
$this->get_field(
esc_html__( 'Performer URL', 'rank-math' ),
'performer.sameAs'
);
?>
<?php
$this->get_field(
esc_html__( 'Start Date', 'rank-math' ),
'startDate',
true
);
?>
<?php
$this->get_field(
esc_html__( 'End Date', 'rank-math' ),
'endDate',
true
);
?>
<?php
$this->get_field(
esc_html__( 'Ticket URL', 'rank-math' ),
'offers.url'
);
?>
<?php
$this->get_field(
esc_html__( 'Entry Price', 'rank-math' ),
'offers.price'
);
?>
<?php
$this->get_field(
esc_html__( 'Currency', 'rank-math' ),
'offers.priceCurrency'
);
?>
<?php
$this->get_field(
esc_html__( 'Availability', 'rank-math' ),
'offers.availability'
);
?>
<?php
$this->get_field(
esc_html__( 'Availability Starts', 'rank-math' ),
'startDate'
);
?>
<?php
$this->get_field(
esc_html__( 'Stock Inventory', 'rank-math' ),
'offers.inventoryLevel'
);
?>
<?php $this->show_ratings(); ?>
</div>

View File

@@ -0,0 +1 @@
<?php // Silence is golden.

View File

@@ -0,0 +1,95 @@
<?php
/**
* Shortcode - Job Posting
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$this->get_title();
$this->get_image();
?>
<div class="rank-math-review-data">
<?php $this->get_description(); ?>
<?php
$this->get_field(
esc_html__( 'Salary', 'rank-math' ),
'baseSalary.value.value'
);
?>
<?php
$this->get_field(
esc_html__( 'Salary Currency', 'rank-math' ),
'baseSalary.currency'
);
?>
<?php
$this->get_field(
esc_html__( 'Payroll', 'rank-math' ),
'baseSalary.value.unitText'
);
?>
<?php
$this->get_field(
esc_html__( 'Date Posted', 'rank-math' ),
'datePosted'
);
?>
<?php
$this->get_field(
esc_html__( 'Expiry Posted', 'rank-math' ),
'validThrough'
);
?>
<?php
$this->get_field(
esc_html__( 'Unpublish when expired', 'rank-math' ),
'unpublish'
);
?>
<?php
$this->get_field(
esc_html__( 'Employment Type ', 'rank-math' ),
'employmentType'
);
?>
<?php
$this->get_field(
esc_html__( 'Hiring Organization ', 'rank-math' ),
'hiringOrganization.name'
);
?>
<?php
$this->get_field(
esc_html__( 'Organization URL', 'rank-math' ),
'hiringOrganization.sameAs'
);
?>
<?php
$this->get_field(
esc_html__( 'Organization Logo', 'rank-math' ),
'hiringOrganization.logo'
);
?>
<?php
$this->get_field(
esc_html__( 'Location', 'rank-math' ),
'jobLocation.address'
);
?>
</div>

View File

@@ -0,0 +1,32 @@
<?php
/**
* Shortcode - Music
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$this->get_title();
$this->get_image();
?>
<div class="rank-math-review-data">
<?php $this->get_description(); ?>
<?php
$this->get_field(
esc_html__( 'URL', 'rank-math' ),
'url'
);
?>
<?php
$this->get_field(
esc_html__( 'Type', 'rank-math' ),
'@type'
);
?>
</div>

View File

@@ -0,0 +1,46 @@
<?php
/**
* Shortcode - Person
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$this->get_title();
$this->get_image();
?>
<div class="rank-math-review-data">
<?php $this->get_description(); ?>
<?php
$this->get_field(
esc_html__( 'Email', 'rank-math' ),
'email'
);
?>
<?php
$this->get_field(
esc_html__( 'Address', 'rank-math' ),
'address'
);
?>
<?php
$this->get_field(
esc_html__( 'Gender', 'rank-math' ),
'gender'
);
?>
<?php
$this->get_field(
esc_html__( 'Job Title', 'rank-math' ),
'jobTitle'
);
?>
</div>

View File

@@ -0,0 +1,62 @@
<?php
/**
* Shortcode - Product
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$this->get_title();
$this->get_image();
?>
<div class="rank-math-review-data">
<?php $this->get_description(); ?>
<?php
$this->get_field(
esc_html__( 'Product SKU', 'rank-math' ),
'sku'
);
?>
<?php
$this->get_field(
esc_html__( 'Product Brand', 'rank-math' ),
'brand.name'
);
?>
<?php
$this->get_field(
esc_html__( 'Product Currency', 'rank-math' ),
'offers.priceCurrency'
);
?>
<?php
$this->get_field(
esc_html__( 'Product Price', 'rank-math' ),
'offers.price'
);
?>
<?php
$this->get_field(
esc_html__( 'Price Valid Until', 'rank-math' ),
'offers.priceValidUntil'
);
?>
<?php
$this->get_field(
esc_html__( 'Product In-Stock', 'rank-math' ),
'offers.availability'
);
?>
<?php $this->show_ratings(); ?>
</div>

View File

@@ -0,0 +1,167 @@
<?php
/**
* Shortcode - Recipe
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$this->get_title();
$this->get_image();
?>
<div class="rank-math-review-data">
<?php $this->get_description(); ?>
<?php
$this->get_field(
esc_html__( 'Type', 'rank-math' ),
'recipeCategory'
);
?>
<?php
$this->get_field(
esc_html__( 'Cuisine', 'rank-math' ),
'recipeCuisine'
);
?>
<?php
$this->get_field(
esc_html__( 'Keywords', 'rank-math' ),
'keywords'
);
?>
<?php
$this->get_field(
esc_html__( 'Recipe Yield', 'rank-math' ),
'recipeYield'
);
?>
<?php
$this->get_field(
esc_html__( 'Calories', 'rank-math' ),
'nutrition.calories'
);
?>
<?php
$this->get_field(
esc_html__( 'Preparation Time', 'rank-math' ),
'prepTime'
);
?>
<?php
$this->get_field(
esc_html__( 'Cooking Time', 'rank-math' ),
'cookTime'
);
?>
<?php
$this->get_field(
esc_html__( 'Total Time', 'rank-math' ),
'totalTime'
);
?>
<?php
$this->get_field(
esc_html__( 'Recipe Video Name', 'rank-math' ),
'video.name'
);
?>
<?php
$this->get_field(
esc_html__( 'Recipe Video Description', 'rank-math' ),
'video.description'
);
?>
<?php
$this->get_field(
esc_html__( 'Recipe Video Thumbnail', 'rank-math' ),
'video.thumbnailUrl'
);
?>
<?php
global $wp_embed;
if ( ! empty( $this->schema['video'] ) ) {
if ( ! empty( $this->schema['video']['embedUrl'] ) ) {
echo do_shortcode( $wp_embed->autoembed( $this->schema['video']['embedUrl'] ) );
} elseif ( ! empty( $this->schema['video']['contentUrl'] ) ) {
echo do_shortcode( $wp_embed->autoembed( $this->schema['video']['contentUrl'] ) );
}
}
?>
<?php
$ingredient = $this->get_field_value( 'recipeIngredient' );
$this->output_field(
esc_html__( 'Recipe Ingredients', 'rank-math' ),
'<ul><li>' . join( '</li><li>', $ingredient ) . '</li></ul>'
);
?>
<?php
$instructions = $this->get_field_value( 'recipeInstructions' );
if ( is_string( $instructions ) ) {
$this->get_field(
esc_html__( 'Recipe Instructions', 'rank-math' ),
'recipeInstructions'
);
} else {
// HowTo Array.
if ( isset( $instructions[0]['@type'] ) && 'HowtoStep' === $instructions[0]['@type'] ) {
$instructions = wp_list_pluck( $instructions, 'text' );
$this->output_field(
esc_html__( 'Recipe Instructions', 'rank-math' ),
'<ul><li>' . join( '</li><li>', $instructions ) . '</li></ul>'
);
}
// Single HowToSection data.
if ( ! empty( $instructions['itemListElement'] ) ) {
$this->output_field(
esc_html__( 'Recipe Instructions', 'rank-math' ),
''
);
$this->output_field(
$instructions['name'],
'<ul><li>' . join( '</li><li>', wp_list_pluck( $instructions['itemListElement'], 'text' ) ) . '</li></ul>'
);
}
// Multiple HowToSection data.
if ( isset( $instructions[0]['@type'] ) && 'HowToSection' === $instructions[0]['@type'] ) {
$this->output_field(
esc_html__( 'Recipe Instructions', 'rank-math' ),
''
);
foreach ( $instructions as $section ) {
if ( empty( $section['itemListElement'] ) ) {
continue;
}
$this->output_field(
$section['name'],
'<ul><li>' . join( '</li><li>', wp_list_pluck( $section['itemListElement'], 'text' ) ) . '</li></ul>'
);
}
}
}
?>
<?php $this->show_ratings(); ?>
</div>

View File

@@ -0,0 +1,62 @@
<?php
/**
* Shortcode - Restaurant
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$this->get_title();
$this->get_image();
?>
<div class="rank-math-review-data">
<?php $this->get_description(); ?>
<?php
$this->get_field(
esc_html__( 'Address', 'rank-math' ),
'address'
);
?>
<?php
$this->get_field(
esc_html__( 'Geo Coordinates', 'rank-math' ),
'geo'
);
?>
<?php
$this->get_field(
esc_html__( 'Phone Number', 'rank-math' ),
'telephone'
);
?>
<?php
$this->get_field(
esc_html__( 'Price Range', 'rank-math' ),
'priceRange'
);
?>
<?php $this->get_opening_hours( 'openingHoursSpecification' ); ?>
<?php
$this->get_field(
esc_html__( 'Serves Cuisine', 'rank-math' ),
'servesCuisine'
);
?>
<?php
$this->get_field(
esc_html__( 'Menu URL', 'rank-math' ),
'hasMenu'
);
?>
</div>

View File

@@ -0,0 +1,39 @@
<?php
/**
* Shortcode - Service
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$this->get_title();
$this->get_image();
?>
<div class="rank-math-review-data">
<?php $this->get_description(); ?>
<?php
$this->get_field(
esc_html__( 'Service Type', 'rank-math' ),
'serviceType'
);
?>
<?php
$this->get_field(
esc_html__( 'Price', 'rank-math' ),
'offers.price'
);
?>
<?php
$this->get_field(
esc_html__( 'Currency', 'rank-math' ),
'offers.priceCurrency'
);
?>
</div>

View File

@@ -0,0 +1,48 @@
<?php
/**
* Shortcode - Software Application
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
$this->get_title();
$this->get_image();
?>
<div class="rank-math-review-data">
<?php $this->get_description(); ?>
<?php
$this->get_field(
esc_html__( 'Price', 'rank-math' ),
'offers.price'
);
?>
<?php
$this->get_field(
esc_html__( 'Price Currency', 'rank-math' ),
'offers.priceCurrency'
);
?>
<?php
$this->get_field(
esc_html__( 'Operating System', 'rank-math' ),
'operatingSystem'
);
?>
<?php
$this->get_field(
esc_html__( 'Application Category', 'rank-math' ),
'applicationCategory'
);
?>
<?php $this->show_ratings(); ?>
</div>

View File

@@ -0,0 +1,26 @@
<?php
/**
* Shortcode - Video
*
* @package RankMath
* @subpackage RankMath\Schema
*/
defined( 'ABSPATH' ) || exit;
global $wp_embed;
$this->get_title();
?>
<div class="rank-math-review-data">
<?php $this->get_description(); ?>
<?php
if ( ! empty( $this->schema['embedUrl'] ) ) {
echo do_shortcode( $wp_embed->autoembed( $this->schema['embedUrl'] ) );
} elseif ( ! empty( $this->schema['contentUrl'] ) ) {
echo do_shortcode( $wp_embed->autoembed( $this->schema['contentUrl'] ) );
}
?>
</div>

View File

@@ -0,0 +1,60 @@
<?php
/**
* The Article Class.
*
* @since 1.0.13
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
defined( 'ABSPATH' ) || exit;
/**
* Article class.
*/
class Article implements Snippet {
use Hooker;
/**
* Article rich snippet.
*
* @param array $data Array of JSON-LD data.
* @param JsonLD $jsonld JsonLD Instance.
*
* @return array
*/
public function process( $data, $jsonld ) {
$entity = [
'@type' => Helper::get_default_schema_type( $jsonld->post->ID ),
'headline' => $jsonld->parts['title'],
'keywords' => Helper::replace_vars( '%keywords%', $jsonld->post ),
'datePublished' => $jsonld->parts['published'],
'dateModified' => $jsonld->parts['modified'],
'isPrimary' => true,
'articleSection' => Helper::replace_vars( '%primary_taxonomy_terms%', $jsonld->post ),
'author' => ! empty( $data['ProfilePage'] ) ?
[
'@id' => $data['ProfilePage']['@id'],
'name' => $jsonld->parts['author'],
] :
[
'@type' => 'Person',
'name' => $jsonld->parts['author'],
],
];
$jsonld->add_prop( 'publisher', $entity, 'publisher', $data );
if ( ! empty( $jsonld->parts['desc'] ) ) {
$entity['description'] = $jsonld->parts['desc'];
}
return $entity;
}
}

View File

@@ -0,0 +1,133 @@
<?php
/**
* The Author Class.
*
* @since 1.0.13
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\User;
use RankMath\Paper\Paper;
defined( 'ABSPATH' ) || exit;
/**
* Author class.
*/
class Author implements Snippet {
/**
* Add Author entity in JSON-LD data.
*
* @link https://schema.org/Person
*
* @param array $data Array of JSON-LD data.
* @param JsonLD $jsonld JsonLD Instance.
*
* @return array
*/
public function process( $data, $jsonld ) {
$is_archive_disabled = Helper::get_settings( 'titles.disable_author_archives' );
$author_id = is_singular() ? $jsonld->post->post_author : get_the_author_meta( 'ID' );
$author_name = get_the_author();
$author_url = get_author_posts_url( $author_id );
$data['ProfilePage'] = [
'@type' => 'Person',
'@id' => ! $is_archive_disabled ? $author_url : $jsonld->parts['url'] . '#author',
'name' => $author_name,
'description' => wp_strip_all_tags( stripslashes( $this->get_description( $author_id ) ), true ),
'url' => $is_archive_disabled ? '' : $author_url,
];
$this->add_image( $data['ProfilePage'], $author_id, $jsonld );
$this->add_same_as( $data['ProfilePage'], $author_id );
$this->add_works_for( $data['ProfilePage'], $data );
if ( is_author() && ! empty( $data['WebPage'] ) ) {
$data['ProfilePage']['mainEntityOfPage'] = [
'@id' => $data['WebPage']['@id'],
];
}
return $data;
}
/**
* Add sameAs property to the Person entity.
*
* @param array $entity Author schema data.
* @param int $author_id Author ID.
*/
private function add_same_as( &$entity, $author_id ) {
$same_as = [
get_the_author_meta( 'user_url', $author_id ),
get_user_meta( $author_id, 'facebook', true ),
];
if ( $twitter = get_user_meta( $author_id, 'twitter', true ) ) { // phpcs:ignore
$same_as[] = 'https://twitter.com/' . $twitter;
}
$addional_urls = get_user_meta( $author_id, 'additional_profile_urls', true );
if ( $addional_urls ) {
$same_as = array_merge( $same_as, explode( ' ', $addional_urls ) );
}
$same_as = array_filter( $same_as );
if ( empty( $same_as ) ) {
return;
}
$entity['sameAs'] = array_values( $same_as );
}
/**
* Add image property to the Person entity.
*
* @param array $entity Author schema data.
* @param int $author_id Author ID.
* @param JsonLD $jsonld JsonLD Instance.
*/
private function add_image( &$entity, $author_id, $jsonld ) {
$entity['image'] = [
'@type' => 'ImageObject',
'@id' => get_avatar_url( $author_id ),
'url' => get_avatar_url( $author_id ),
'caption' => get_the_author(),
];
$jsonld->add_prop( 'language', $entity['image'] );
}
/**
* Add worksFor property referencing it to the publisher entity.
*
* @param array $entity Author schema data.
* @param array $data Schema Data.
*/
private function add_works_for( &$entity, $data ) {
if (
empty( $data['publisher'] ) ||
in_array( 'Person', (array) $data['publisher']['@type'], true )
) {
return;
}
$entity['worksFor'] = [ '@id' => $data['publisher']['@id'] ];
}
/**
* Get author description.
*
* @param int $author_id Author ID.
*/
private function get_description( $author_id ) {
$description = User::get_meta( 'description', $author_id );
return $description ? $description : Paper::get_from_options( 'author_archive_description' );
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* The Breadcrumbs Class.
*
* @since 1.0.13
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Frontend\Breadcrumbs as BreadcrumbTrail;
use RankMath\Paper\Paper;
defined( 'ABSPATH' ) || exit;
/**
* Breadcrumbs class.
*/
class Breadcrumbs implements Snippet {
/**
* Generate breadcrumbs JSON-LD.
*
* @link https://schema.org/BreadcrumbList
*
* @param array $data Array of JSON-LD data.
* @param JsonLD $jsonld JsonLD Instance.
*
* @return array
*/
public function process( $data, $jsonld ) {
$crumbs = BreadcrumbTrail::get() ? BreadcrumbTrail::get()->get_crumbs() : false;
if ( empty( $crumbs ) ) {
return $data;
}
$entity = [
'@type' => 'BreadcrumbList',
'@id' => Paper::get()->get_canonical() . '#breadcrumb',
'itemListElement' => [],
];
$position = 1;
foreach ( $crumbs as $crumb ) {
if ( ! empty( $crumb['hide_in_schema'] ) || empty( $crumb[1] ) ) {
continue;
}
$entity['itemListElement'][] = [
'@type' => 'ListItem',
'position' => $position,
'item' => [
'@id' => $crumb[1],
'name' => $crumb[0],
],
];
$position++;
}
$entity = apply_filters( 'rank_math/snippet/breadcrumb', $entity );
if ( empty( $entity['itemListElement'] ) ) {
return $data;
}
$data['BreadcrumbList'] = $entity;
return $data;
}
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* The Primary Image Class.
*
* @since 1.0.43
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* PrimaryImage class.
*/
class PrimaryImage implements Snippet {
/**
* Add primaryImage entity in JSON-LD data.
*
* @param array $data Array of JSON-LD data.
* @param JsonLD $jsonld JsonLD Instance.
*
* @return array
*/
public function process( $data, $jsonld ) {
$image = Helper::get_thumbnail_with_fallback( get_the_ID(), 'full' );
if ( empty( $image ) ) {
return $data;
}
$data['primaryImage'] = [
'@type' => 'ImageObject',
'@id' => $image[0],
'url' => $image[0],
'width' => $image[1],
'height' => $image[2],
'caption' => isset( $image['caption'] ) ? $image['caption'] : '',
];
$jsonld->add_prop( 'language', $data['primaryImage'] );
return $data;
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* The Easy Digital Downloads Product Class.
*
* @since 1.0.13
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use EDD_Download;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Product_Edd class.
*/
class Product_Edd {
/**
* Set product data for rich snippet.
*
* @param array $entity Array of JSON-LD entity.
* @param JsonLD $jsonld JsonLD Instance.
*/
public function set_product( &$entity, $jsonld ) {
$product_id = get_the_ID();
$permalink = get_permalink();
$product = new EDD_Download( $product_id );
$entity['url'] = $permalink;
$entity['name'] = $jsonld->post->post_title;
$entity['description'] = Helper::replace_vars( '%seo_description%' );
$entity['category'] = Product::get_category( $product_id, 'download_category' );
$entity['mainEntityOfPage'] = [ '@id' => $jsonld->parts['canonical'] . '#webpage' ];
// SKU.
if ( $product->get_sku() ) {
$entity['sku'] = $product->get_sku();
}
// Offers.
$seller = Product::get_seller( $jsonld );
$variations = $this->has_variations( $product );
if ( false !== $variations ) {
$entity['offers'] = [];
foreach ( $variations as $variation ) {
$offer = [
'@type' => 'Offer',
'description' => $variation['name'],
'price' => $variation['amount'],
'priceCurrency' => edd_get_currency(),
'priceValidUntil' => date( 'Y-12-31', time() + YEAR_IN_SECONDS ),
'url' => $permalink,
'seller' => $seller,
];
// Set Price Specification.
$this->set_price_specification( $variation['amount'], $offer );
$entity['offers'][] = $offer;
}
return;
}
// Single offer.
$entity['offers'] = [
'@type' => 'Offer',
'price' => $product->get_price() ? $product->get_price() : '0',
'priceCurrency' => edd_get_currency(),
'priceValidUntil' => date( 'Y-12-31', time() + YEAR_IN_SECONDS ),
'seller' => $seller,
'url' => $permalink,
];
// Set Price Specification.
$this->set_price_specification( $product->get_price(), $entity['offers'] );
}
/**
* Set price specification.
*
* @param object $price Product price.
* @param array $entity Array of offer entity.
*/
private function set_price_specification( $price, &$entity ) {
if ( ! edd_use_taxes() ) {
return;
}
$entity['priceSpecification'] = [
'price' => $price ? $price : '0',
'priceCurrency' => edd_get_currency(),
'valueAddedTaxIncluded' => edd_prices_include_tax() ? 'true' : 'false',
];
}
/**
* If product is variable, set variations.
*
* @param EDD_Download $product Current product.
*
* @return array|boolean
*/
private function has_variations( $product ) {
if ( ! $product->has_variable_prices() ) {
return false;
}
$variations = $product->get_prices();
return ! empty( $variations ) ? $variations : false;
}
}

View File

@@ -0,0 +1,373 @@
<?php
/**
* The WooCommerce Product Class.
*
* @since 1.0.13
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Product_WooCommerce class.
*/
class Product_WooCommerce {
/**
* Attribute assigner.
*
* @var WC_Attributes
*/
private $attributes;
/**
* Set product data for rich snippet.
*
* @param array $entity Array of JSON-LD entity.
* @param JsonLD $jsonld JsonLD Instance.
*/
public function set_product( &$entity, $jsonld ) {
$product = wc_get_product( get_the_ID() );
$this->attributes = new WC_Attributes( $product );
if ( Helper::is_module_active( 'woocommerce' ) ) {
$brand = \RankMath\WooCommerce\Woocommerce::get_brands( $product->get_id() );
// Brand.
if ( ! empty( $brand ) ) {
$entity['brand'] = [
'@type' => 'Brand',
'name' => $brand,
];
}
}
$entity['name'] = $jsonld->get_product_title( $product );
$entity['description'] = $jsonld->get_product_desc( $product );
$entity['sku'] = $product->get_sku() ? $product->get_sku() : '';
$entity['category'] = Product::get_category( $product->get_id(), 'product_cat' );
$entity['mainEntityOfPage'] = [ '@id' => $jsonld->parts['canonical'] . '#webpage' ];
$this->set_weight( $product, $entity );
$this->set_dimensions( $product, $entity );
$this->set_images( $product, $entity );
$this->set_ratings( $product, $entity );
$this->set_offers( $product, $entity, Product::get_seller( $jsonld ) );
// GTIN numbers need product attributes.
$this->attributes->assign_property( $entity, 'gtin8' );
$this->attributes->assign_property( $entity, 'gtin12' );
$this->attributes->assign_property( $entity, 'gtin13' );
$this->attributes->assign_property( $entity, 'gtin14' );
// Color.
$this->attributes->assign_property( $entity, 'color' );
// Remaining Attributes.
$this->attributes->assign_remaining( $entity );
}
/**
* Set product weight.
*
* @param object $product Product instance.
* @param array $entity Array of JSON-LD entity.
*/
private function set_weight( $product, &$entity ) {
if ( ! $product->has_weight() ) {
return;
}
$hash = [
'lbs' => 'LBR',
'kg' => 'KGM',
'g' => 'GRM',
'oz' => 'ONZ',
];
$unit = get_option( 'woocommerce_weight_unit' );
$entity['weight'] = [
'@type' => 'QuantitativeValue',
'unitCode' => isset( $hash[ $unit ] ) ? $hash[ $unit ] : 'LBR',
'value' => $product->get_weight(),
];
}
/**
* Set product dimension.
*
* @param object $product Product instance.
* @param array $entity Array of JSON-LD entity.
*/
private function set_dimensions( $product, &$entity ) {
if ( ! $product->has_dimensions() ) {
return;
}
$hash = [
'in' => 'INH',
'm' => 'MTR',
'cm' => 'CMT',
'mm' => 'MMT',
'yd' => 'YRD',
];
$unit = get_option( 'woocommerce_dimension_unit' );
$code = isset( $hash[ $unit ] ) ? $hash[ $unit ] : '';
$entity['height'] = [
'@type' => 'QuantitativeValue',
'unitCode' => $code,
'value' => $product->get_height(),
];
$entity['width'] = [
'@type' => 'QuantitativeValue',
'unitCode' => $code,
'value' => $product->get_width(),
];
$entity['depth'] = [
'@type' => 'QuantitativeValue',
'unitCode' => $code,
'value' => $product->get_length(),
];
}
/**
* Set product images.
*
* @param object $product Product instance.
* @param array $entity Array of JSON-LD entity.
*/
private function set_images( $product, &$entity ) {
if ( ! $product->get_image_id() ) {
return;
}
$image = wp_get_attachment_image_src( $product->get_image_id(), 'single-post-thumbnail' );
if ( ! empty( $image ) ) {
$entity['image'][] = [
'@type' => 'ImageObject',
'url' => $image[0],
'height' => $image[2],
'width' => $image[1],
];
}
$gallery = $product->get_gallery_image_ids();
foreach ( $gallery as $image_id ) {
$image = wp_get_attachment_image_src( $image_id, 'single-post-thumbnail' );
if ( empty( $image ) ) {
continue;
}
$entity['image'][] = [
'@type' => 'ImageObject',
'url' => $image[0],
'height' => $image[2],
'width' => $image[1],
];
}
}
/**
* Set product ratings.
*
* @param object $product Product instance.
* @param array $entity Array of JSON-LD entity.
*/
private function set_ratings( $product, &$entity ) {
if ( $product->get_rating_count() < 1 ) {
return;
}
// Aggregate Rating.
$entity['aggregateRating'] = [
'@type' => 'AggregateRating',
'ratingValue' => $product->get_average_rating(),
'bestRating' => '5',
'ratingCount' => $product->get_rating_count(),
'reviewCount' => $product->get_review_count(),
];
// Reviews.
$comments = get_comments(
[
'post_type' => 'product',
'post_id' => get_the_ID(),
'status' => 'approve',
'parent' => 0,
]
);
$permalink = $product->get_permalink();
foreach ( $comments as $comment ) {
$rating = intval( get_comment_meta( $comment->comment_ID, 'rating', true ) );
if ( ! $rating ) {
continue;
}
$entity['review'][] = [
'@type' => 'Review',
'@id' => $permalink . '#li-comment-' . $comment->comment_ID,
'description' => $comment->comment_content,
'datePublished' => $comment->comment_date,
'reviewRating' => [
'@type' => 'Rating',
'ratingValue' => $rating,
'bestRating' => '5',
'worstRating' => '1',
],
'author' => [
'@type' => 'Person',
'name' => $comment->comment_author,
'url' => $comment->comment_author_url,
],
];
}
}
/**
* Set product offers.
*
* @param object $product Product instance.
* @param array $entity Array of JSON-LD entity.
* @param array $seller Seller info.
*/
private function set_offers( $product, &$entity, $seller ) {
if ( '' === $product->get_price() ) {
return;
}
if ( true === $this->set_offers_variable( $product, $entity, $seller ) ) {
return;
}
$offer = [
'@type' => 'Offer',
'price' => $product->get_price() ? wc_format_decimal( $product->get_price(), wc_get_price_decimals() ) : '0',
'priceCurrency' => get_woocommerce_currency(),
'priceValidUntil' => $product->is_on_sale() && ! empty( $product->get_date_on_sale_to() ) ? date_i18n( 'Y-m-d', strtotime( $product->get_date_on_sale_to() ) ) : date( 'Y-12-31', time() + YEAR_IN_SECONDS ),
'availability' => $product->is_in_stock() ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock',
'itemCondition' => 'NewCondition',
'url' => $product->get_permalink(),
'seller' => $seller,
];
// Set Price Specification.
$this->set_price_specification( $product->get_price(), $offer );
$this->attributes->assign_property( $offer, 'itemCondition' );
$entity['offers'] = $offer;
}
/**
* Set product variable offers.
*
* @param object $product Product instance.
* @param array $entity Array of JSON-LD entity.
* @param array $seller Seller info.
*/
private function set_offers_variable( $product, &$entity, $seller ) {
if ( ! $product->is_type( 'variable' ) ) {
return false;
}
if ( $this->set_single_variable_offer( $product, $entity, $seller ) ) {
return true;
}
$permalink = $product->get_permalink();
$lowest = wc_format_decimal( $product->get_variation_price( 'min', false ), wc_get_price_decimals() );
$highest = wc_format_decimal( $product->get_variation_price( 'max', false ), wc_get_price_decimals() );
if ( $lowest === $highest ) {
$offer = [
'@type' => 'Offer',
'price' => $lowest,
'priceValidUntil' => date( 'Y-12-31', time() + YEAR_IN_SECONDS ),
];
// Set Price Specification.
$this->set_price_specification( $lowest, $offer );
} else {
$offer = [
'@type' => 'AggregateOffer',
'lowPrice' => $lowest,
'highPrice' => $highest,
'offerCount' => count( $product->get_children() ),
];
}
$offer += [
'priceCurrency' => get_woocommerce_currency(),
'availability' => 'http://schema.org/' . ( $product->is_in_stock() ? 'InStock' : 'OutOfStock' ),
'seller' => $seller,
'url' => $permalink,
];
$entity['offers'] = $offer;
return true;
}
/**
* Set Single Variable Product offer.
*
* Credit @leewillis77: https://github.com/leewillis77/wc-structured-data-option-4
*
* @param object $product Product instance.
* @param array $entity Array of JSON-LD entity.
* @param array $seller Seller info.
*/
private function set_single_variable_offer( $product, &$entity, $seller ) {
$data_store = \WC_Data_Store::load( 'product' );
$variation_id = $data_store->find_matching_product_variation( $product, wp_unslash( $_GET ) );
$variation = $variation_id ? wc_get_product( $variation_id ) : false;
if ( empty( $variation ) ) {
return false;
}
$price_valid_until = date( 'Y-12-31', current_time( 'timestamp', true ) + YEAR_IN_SECONDS );
if ( $variation->is_on_sale() && $variation->get_date_on_sale_to() ) {
$price_valid_until = date( 'Y-m-d', $variation->get_date_on_sale_to()->getTimestamp() );
}
$entity['offers'] = [
'@type' => 'Offer',
'url' => $variation->get_permalink(),
'sku' => $variation->get_sku(),
'price' => wc_format_decimal( $variation->get_price(), wc_get_price_decimals() ),
'priceCurrency' => get_woocommerce_currency(),
'priceValidUntil' => $price_valid_until,
'seller' => $seller,
'availability' => 'http://schema.org/' . ( $variation->is_in_stock() ? 'InStock' : 'OutOfStock' ),
];
return true;
}
/**
* Set price specification.
*
* @param object $price Product price.
* @param array $entity Array of offer entity.
*/
private function set_price_specification( $price, &$entity ) {
if ( ! wc_tax_enabled() ) {
return;
}
$entity['priceSpecification'] = [
'price' => $price ? $price : '0',
'priceCurrency' => get_woocommerce_currency(),
'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false',
];
}
}

View File

@@ -0,0 +1,110 @@
<?php
/**
* The Product Class.
*
* @since 1.0.13
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Schema\Product_Edd;
use RankMath\Schema\Product_WooCommerce;
defined( 'ABSPATH' ) || exit;
/**
* Product class.
*/
class Product implements Snippet {
/**
* Hold JsonLD Instance.
*
* @var JsonLD
*/
private $json = '';
/**
* Product rich snippet.
*
* @param array $data Array of JSON-LD data.
* @param JsonLD $jsonld JsonLD Instance.
*
* @return array
*/
public function process( $data, $jsonld ) {
$entity = [
'@type' => 'Product',
];
if ( Helper::is_woocommerce_active() && is_product() ) {
remove_action( 'wp_footer', [ WC()->structured_data, 'output_structured_data' ], 10 );
remove_action( 'woocommerce_email_order_details', [ WC()->structured_data, 'output_email_structured_data' ], 30 );
$product = new Product_WooCommerce();
$product->set_product( $entity, $jsonld );
}
if ( Helper::is_edd_active() && is_singular( 'download' ) ) {
remove_filter( 'wp_footer', [ \EDD()->structured_data, 'output_structured_data' ] );
remove_action( 'edd_purchase_link_top', 'edd_purchase_link_single_pricing_schema', 10 );
remove_action( 'loop_start', 'edd_microdata_wrapper_open', 10 );
$product = new Product_Edd();
$product->set_product( $entity, $jsonld );
}
return $entity;
}
/**
* Get seller.
*
* @param JsonLD $jsonld JsonLD Instance.
*
* @return array
*/
public static function get_seller( $jsonld ) {
$site_url = home_url();
$type = Helper::get_settings( 'titles.knowledgegraph_type' );
$seller = [
'@type' => 'person' === $type ? 'Person' : 'Organization',
'@id' => trailingslashit( $site_url ),
'name' => $jsonld->get_website_name(),
'url' => $site_url,
];
if ( 'company' === $type ) {
$seller['logo'] = Helper::get_settings( 'titles.knowledgegraph_logo' );
}
return $seller;
}
/**
* Set product categories.
*
* @param int $product_id Product ID.
* @param string $taxonomy Taxonomy.
*/
public static function get_category( $product_id, $taxonomy ) {
$categories = get_the_terms( $product_id, $taxonomy );
if ( is_wp_error( $categories ) || empty( $categories ) ) {
return;
}
if ( 0 === $categories[0]->parent ) {
return $categories[0]->name;
}
$ancestors = array_reverse( get_ancestors( $categories[0]->term_id, $taxonomy ) );
foreach ( $ancestors as $parent ) {
$term = get_term( $parent, $taxonomy );
$category[] = $term->name;
}
$category[] = $categories[0]->name;
return join( ' > ', $category );
}
}

View File

@@ -0,0 +1,113 @@
<?php
/**
* The Products Page Class.
*
* @since 1.0.13
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Products_Page class.
*/
class Products_Page implements Snippet {
/**
* Sets the Schema structured data on the Products Archive page.
*
* @param array $data Array of JSON-LD data.
* @param JsonLD $jsonld JsonLD Instance.
*
* @return array
*/
public function process( $data, $jsonld ) {
if ( ! $this->can_add_snippet_taxonomy() || ! $this->can_add_snippet_shop() ) {
return $data;
}
$data['ProductsPage'] = [
'@context' => 'https://schema.org/',
'@graph' => [],
];
while ( have_posts() ) {
the_post();
$post_id = get_the_ID();
$product = wc_get_product( $post_id );
$url = $jsonld->get_post_url( $post_id );
$part = [
'@type' => 'Product',
'name' => $jsonld->get_product_title( $product ),
'url' => $url,
'@id' => $url,
'description' => $jsonld->get_product_desc( $product ),
];
$data['ProductsPage']['@graph'][] = $part;
}
wp_reset_postdata();
return $data;
}
/**
* Check if structured data can be added on the current Product taxonomy.
*
* @return boolean|string
*/
private function can_add_snippet_taxonomy() {
$queried_object = get_queried_object();
/**
* Allow developer to remove snippet data.
*
* @param bool $unsigned Default: false
* @param string $unsigned Taxonomy Name
*/
if (
! is_shop() &&
(
true === Helper::get_settings( 'titles.remove_' . $queried_object->taxonomy . '_snippet_data' ) ||
true === apply_filters( 'rank_math/snippet/remove_taxonomy_data', false, $queried_object->taxonomy )
)
) {
return false;
}
return true;
}
/**
* Check if structured data can be added on the Shop page.
*
* @return boolean|string
*/
private function can_add_snippet_shop() {
/**
* Allow developer to remove snippet data from Shop page.
*
* @param bool $unsigned Default: false
*/
if (
is_shop() &&
(
true === Helper::get_settings( 'general.remove_shop_snippet_data' ) ||
true === apply_filters( 'rank_math/snippet/remove_shop_data', false )
)
) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* The Publisher Class.
*
* @since 1.0.43
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Publisher class.
*/
class Publisher implements Snippet {
/**
* Generate Organization JSON-LD.
*
* @param array $data Array of JSON-LD data.
* @param JsonLD $jsonld JsonLD Instance.
*
* @return array
*/
public function process( $data, $jsonld ) {
$type = Helper::get_settings( 'titles.knowledgegraph_type' );
$id = 'company' === $type ? 'organization' : 'person';
$data['publisher'] = [
'@type' => $this->get_publisher_type( $type ),
'@id' => home_url( "/#{$id}" ),
'name' => $jsonld->get_organization_name(),
];
$social_profiles = $jsonld->get_social_profiles();
if ( ! empty( $social_profiles ) ) {
$data['publisher']['sameAs'] = $social_profiles;
}
$jsonld->add_prop( 'image', $data['publisher'] );
if ( empty( $data['publisher']['logo'] ) ) {
return $data;
}
if ( 'person' === $type ) {
$data['publisher']['image'] = $data['publisher']['logo'];
}
if ( ! is_singular() ) {
unset( $data['publisher']['logo'] );
}
return $data;
}
/**
* Get Publisher Type.
*
* @param string $type Knowledgegraph type.
*
* @return string|array
*/
private function get_publisher_type( $type ) {
if ( 'company' === $type ) {
return 'Organization';
}
if ( ! is_singular() ) {
return 'Person';
}
return [ 'Person', 'Organization' ];
}
}

View File

@@ -0,0 +1,141 @@
<?php
/**
* The Singular Class.
*
* @since 1.0.13
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Schema\DB;
defined( 'ABSPATH' ) || exit;
/**
* Singular class.
*/
class Singular implements Snippet {
use Hooker;
/**
* Generate rich snippet.
*
* @param array $data Array of JSON-LD data.
* @param JsonLD $jsonld JsonLD Instance.
*
* @return array
*/
public function process( $data, $jsonld ) {
$schema = $this->can_add_schema( $jsonld );
if ( false === $schema ) {
return $data;
}
$hook = 'snippet/rich_snippet_' . $schema;
/**
* Short-circuit if 3rd party is interested generating his own data.
*/
$pre = $this->do_filter( $hook, false, $jsonld->parts, $data );
if ( false !== $pre ) {
$data['richSnippet'] = $this->do_filter( $hook . '_entity', $pre );
return $data;
}
$object = $this->get_schema_class( $schema );
if ( false === $object ) {
return $data;
}
$entity = $object->process( $data, $jsonld );
$data['richSnippet'] = $this->do_filter( $hook . '_entity', $entity );
return $data;
}
/**
* Get Schema type.
*
* @param JsonLD $jsonld JsonLD Instance.
*
* @return boolean|string
*/
private function can_add_schema( $jsonld ) {
if ( empty( $jsonld->post_id ) ) {
return false;
}
$schemas = DB::get_schemas( $jsonld->post_id );
if ( ! empty( $schemas ) ) {
$has_product = array_filter(
$schemas,
function( $schema ) {
return ! empty( $schema['@type'] ) && in_array( $schema['@type'], [ 'WooCommerceProduct', 'EDDProduct' ], true );
}
);
return ! empty( $has_product ) ? 'product' : false;
}
if ( metadata_exists( 'post', $jsonld->post_id, 'rank_math_rich_snippet' ) ) {
return Helper::get_post_meta( 'rich_snippet' );
}
if ( ! Helper::can_use_default_schema( $jsonld->post_id ) ) {
return false;
}
return $this->get_default_schema( $jsonld );
}
/**
* Get Default Rich Snippet type from Settings.
*
* @param JsonLD $jsonld JsonLD Instance.
*
* @return string
*/
private function get_default_schema( $jsonld ) {
$schema = Helper::get_default_schema_type( $jsonld->post_id, true );
if ( ! $schema ) {
return false;
}
if ( in_array( $schema, [ 'BlogPosting', 'NewsArticle', 'Article' ], true ) ) {
return 'article';
}
if (
( Helper::is_woocommerce_active() && is_singular( 'product' ) ) ||
( Helper::is_edd_active() && is_singular( 'download' ) )
) {
return 'product';
}
return false;
}
/**
* Get appropriate Schema Class.
*
* @param string $schema Schema type.
* @return bool|Class
*/
private function get_schema_class( $schema ) {
$data = [
'article' => '\\RankMath\\Schema\\Article',
'product' => '\\RankMath\\Schema\\Product',
];
if ( isset( $data[ $schema ] ) && class_exists( $data[ $schema ] ) ) {
return new $data[ $schema ]();
}
return false;
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* Helper class for handling WooComerce product attributes for rich snippets.
*
* @since 0.9.0
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
defined( 'ABSPATH' ) || exit;
/**
* WC_Attributes class.
*/
class WC_Attributes {
/**
* Hold product attributes.
*
* @var array
*/
private $_attributes;
/**
* Hold product object.
*
* @var WC_Product
*/
private $_product;
/**
* The Constructor.
*
* @param WC_Product $product The Product.
*/
public function __construct( $product ) {
$this->_product = $product;
$this->_attributes = $product->get_attributes();
}
/**
* Find attribute for property.
*
* @param array $entity Entity to attach data to.
* @param string $needle Assign this property.
*/
public function assign_property( &$entity, $needle ) {
foreach ( $this->_attributes as $key => $attrib ) {
if ( stristr( $key, $needle ) ) {
$entity[ $needle ] = $this->_product->get_attribute( $key );
unset( $this->_attributes[ $key ] );
return;
}
}
}
/**
* Map remaining attributes as PropertyValue.
*
* @param array $entity Entity to attach data to.
*/
public function assign_remaining( &$entity ) {
foreach ( $this->_attributes as $key => $attrib ) {
if ( $attrib['is_visible'] && ! $attrib['is_variation'] ) {
$entity['additionalProperty'][] = [
'@type' => 'PropertyValue',
'name' => $key,
'value' => $this->_product->get_attribute( $key ),
];
}
}
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* The Webpage Class.
*
* @since 1.0.13
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
use RankMath\Paper\Paper;
defined( 'ABSPATH' ) || exit;
/**
* Webpage class.
*/
class Webpage implements Snippet {
/**
* Generate WebPage JSON-LD.
*
* @link https://schema.org/WebPage
*
* @param array $data Array of JSON-LD data.
* @param JsonLD $jsonld JsonLD Instance.
*
* @return array
*/
public function process( $data, $jsonld ) {
$entity = [
'@type' => $this->get_type(),
'@id' => Paper::get()->get_canonical() . '#webpage',
'url' => Paper::get()->get_canonical(),
'name' => Paper::get()->get_title(),
];
if ( is_singular() ) {
$entity['datePublished'] = $jsonld->parts['published'];
$entity['dateModified'] = $jsonld->parts['modified'];
}
if ( is_front_page() ) {
$jsonld->add_prop( 'publisher', $entity, 'about', $data );
}
$jsonld->add_prop( 'is_part_of', $entity, 'website' );
$jsonld->add_prop( 'thumbnail', $entity, 'primaryImageOfPage', $data );
$jsonld->add_prop( 'language', $entity );
if ( isset( $data['BreadcrumbList'] ) ) {
$entity['breadcrumb'] = [ '@id' => $data['BreadcrumbList']['@id'] ];
}
$data['WebPage'] = $entity;
return $data;
}
/**
* Get WebPage type depending on the current page.
*
* @return string
*/
private function get_type() {
$about_page = Helper::get_settings( 'titles.local_seo_about_page' );
$contact_page = Helper::get_settings( 'titles.local_seo_contact_page' );
$hash = [
'SearchResultsPage' => is_search(),
'ProfilePage' => is_author(),
'CollectionPage' => is_home() || is_archive(),
'AboutPage' => $about_page && is_page( $about_page ),
'ContactPage' => $contact_page && is_page( $contact_page ),
];
return ! empty( array_filter( $hash ) ) ? key( array_filter( $hash ) ) : 'WebPage';
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* The Website Class.
*
* @since 1.0.13
* @package RankMath
* @subpackage RankMath\Schema
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Schema;
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
/**
* Website class.
*/
class Website implements Snippet {
/**
* Generate WebSite JSON-LD.
*
* @link https://schema.org/WebSite
*
* @param array $data Array of JSON-LD data.
* @param JsonLD $jsonld JsonLD Instance.
*
* @return array
*/
public function process( $data, $jsonld ) {
$data['WebSite'] = [
'@type' => 'WebSite',
'@id' => home_url( '/#website' ),
'url' => get_home_url(),
'name' => $jsonld->get_website_name(),
];
$alternate_name = Helper::get_settings( 'titles.website_alternate_name' );
if ( $alternate_name ) {
$data['WebSite']['alternateName'] = $alternate_name;
}
$jsonld->add_prop( 'publisher', $data['WebSite'], 'publisher', $data );
$jsonld->add_prop( 'language', $data['WebSite'] );
/**
* Disable the JSON-LD output for the Sitelinks Searchbox.
*
* @param boolean Display or not the JSON-LD for the Sitelinks Searchbox.
*/
if ( apply_filters( 'rank_math/json_ld/disable_search', ! is_front_page() || is_paged() ) ) {
return $data;
}
/**
* Change the search URL in the JSON-LD.
*
* @param string $search_url The search URL with `{search_term_string}` placeholder.
*/
$search_url = apply_filters( 'rank_math/json_ld/search_url', home_url( '/?s={search_term_string}' ) );
$data['WebSite']['potentialAction'] = [
'@type' => 'SearchAction',
'target' => $search_url,
'query-input' => 'required name=search_term_string',
];
return $data;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Metabox - Schema Tab
*
* @package RankMath
* @subpackage RankMath\Schema
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
if ( ! Helper::has_cap( 'onpage_snippet' ) ) {
return;
}
$cmb->add_field(
[
'id' => 'rank_math_schema_generator',
'type' => 'raw',
'content' => '<div id="rank-math-schema-generator"></div>',
'save_field' => false,
]
);
$cmb->add_field(
[
'id' => 'rank-math-schemas',
'type' => 'textarea',
'classes' => 'hidden',
'save_field' => false,
]
);
$cmb->add_field(
[
'id' => 'rank-math-schemas-delete',
'type' => 'textarea',
'default' => '[]',
'classes' => 'hidden',
'save_field' => false,
]
);