output_website_schema();
$this->output_organization_schema();
return;
}
if (is_singular('post')) {
$this->output_article_schema();
return;
}
if (is_singular('page')) {
$this->output_webpage_schema();
return;
}
}
/**
* The schema type chosen in the meta box, validated against the list.
*/
public static function get_type($post_id, $post_type) {
if ($post_type === 'post') {
$type = get_post_meta($post_id, '_shadowsync_seo_article_type', true);
return in_array($type, self::ARTICLE_TYPES, true) ? $type : 'Article';
}
$type = get_post_meta($post_id, '_shadowsync_seo_page_type', true);
return in_array($type, self::PAGE_TYPES, true) ? $type : 'WebPage';
}
private function output_website_schema() {
$schema = [
'@context' => 'https://schema.org',
'@type' => 'WebSite',
'name' => get_bloginfo('name'),
'url' => home_url('/'),
];
$this->print_schema($schema);
}
private function output_organization_schema() {
$schema = [
'@context' => 'https://schema.org',
'@type' => 'Organization',
'name' => get_bloginfo('name'),
'url' => home_url('/'),
];
// Add logo if set
$custom_logo_id = get_theme_mod('custom_logo');
if ($custom_logo_id) {
$logo_url = wp_get_attachment_image_url($custom_logo_id, 'full');
if ($logo_url) {
$schema['logo'] = [
'@type' => 'ImageObject',
'url' => $logo_url,
];
}
}
$this->print_schema($schema);
}
/**
* Posts: Article, BlogPosting or NewsArticle, or HowTo when the post has
* a numbered list to take the steps from.
*/
private function output_article_schema() {
$post_id = get_queried_object_id();
$post = get_post($post_id);
$type = self::get_type($post_id, 'post');
if ($type === 'HowTo') {
$steps = $this->extract_howto_steps($post->post_content);
if (count($steps) >= 2) {
$this->output_howto_schema($post, $steps);
return;
}
// A HowTo without steps is invalid, so describe it as an article.
$type = 'Article';
}
$author = get_the_author_meta('display_name', $post->post_author);
$schema = [
'@context' => 'https://schema.org',
'@type' => $type,
'headline' => get_the_title($post_id),
'url' => get_permalink($post_id),
'datePublished' => get_post_time('c', true, $post_id),
'dateModified' => get_post_modified_time('c', true, $post_id),
'author' => [
'@type' => 'Person',
'name' => $author,
],
'publisher' => [
'@type' => 'Organization',
'name' => get_bloginfo('name'),
'url' => home_url('/'),
],
'description' => $this->get_description($post),
];
$image = $this->get_image($post_id);
if ($image) {
$schema['image'] = $image;
}
$this->print_schema($schema);
}
private function output_howto_schema($post, $steps) {
$schema = [
'@context' => 'https://schema.org',
'@type' => 'HowTo',
'name' => get_the_title($post->ID),
'url' => get_permalink($post->ID),
'description' => $this->get_description($post),
'step' => [],
];
foreach ($steps as $i => $text) {
$schema['step'][] = [
'@type' => 'HowToStep',
'position' => $i + 1,
'text' => $text,
];
}
$image = $this->get_image($post->ID);
if ($image) {
$schema['image'] = $image;
}
$this->print_schema($schema);
}
/**
* Pages: WebPage or one of its subtypes. FAQPage carries the questions
* found in the page, and falls back to WebPage when there are none.
*/
private function output_webpage_schema() {
$post_id = get_queried_object_id();
$post = get_post($post_id);
$type = self::get_type($post_id, 'page');
$schema = [
'@context' => 'https://schema.org',
'@type' => $type,
'name' => get_the_title($post_id),
'url' => get_permalink($post_id),
'description' => $this->get_description($post),
'isPartOf' => [
'@type' => 'WebSite',
'name' => get_bloginfo('name'),
'url' => home_url('/'),
],
];
if ($type === 'FAQPage') {
/**
* Filters the question and answer pairs used for FAQPage schema.
*
* @param array $questions Each item has 'question' and 'answer' keys.
* @param WP_Post $post The page being output.
*/
$questions = apply_filters('shadowsync_seo_faq_questions', $this->extract_faq($post->post_content), $post);
$schema['mainEntity'] = [];
foreach ((array) $questions as $qa) {
if (empty($qa['question']) || empty($qa['answer'])) {
continue;
}
$schema['mainEntity'][] = [
'@type' => 'Question',
'name' => $qa['question'],
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => $qa['answer'],
],
];
}
// An FAQPage with no questions is invalid, so describe it as a
// plain page instead.
if (empty($schema['mainEntity'])) {
unset($schema['mainEntity']);
$schema['@type'] = 'WebPage';
}
}
$this->print_schema($schema);
}
/**
* Question and answer pairs from the content: every h2 to h4 heading that
* ends in a question mark, answered by the text up to the next heading.
*/
private function extract_faq($content) {
$content = strip_shortcodes($content);
$items = [];
if (!preg_match_all('#