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('#]*>(.*?)#is', $content, $headings, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { return $items; } foreach ($headings as $i => $heading) { $level = (int) $heading[1][0]; $question = $this->to_plain_text($heading[2][0]); if ($level < 2 || $level > 4 || !preg_match('/[??]$/u', $question)) { continue; } $start = $heading[0][1] + strlen($heading[0][0]); $end = isset($headings[$i + 1]) ? $headings[$i + 1][0][1] : strlen($content); $answer = $this->to_plain_text(substr($content, $start, $end - $start)); if ($answer !== '') { $items[] = ['question' => $question, 'answer' => $answer]; } } return $items; } /** * HowTo steps from the items of the first numbered list in the content. */ private function extract_howto_steps($content) { $content = strip_shortcodes($content); $steps = []; if (!preg_match('#]*>(.*?)#is', $content, $list)) { return $steps; } preg_match_all('#]*>(.*?)#is', $list[1], $items); foreach ($items[1] as $item) { $text = $this->to_plain_text($item); if ($text !== '') { $steps[] = $text; } } return $steps; } private function to_plain_text($html) { $text = html_entity_decode(wp_strip_all_tags($html), ENT_QUOTES, 'UTF-8'); return trim(preg_replace('/\s+/u', ' ', $text)); } private function get_description($post) { return get_post_meta($post->ID, '_shadowsync_seo_meta_description', true) ?: wp_trim_words(wp_strip_all_tags($post->post_content), 25); } private function get_image($post_id) { if (!has_post_thumbnail($post_id)) { return null; } $thumb = wp_get_attachment_image_src(get_post_thumbnail_id($post_id), 'large'); if (!$thumb) { return null; } return [ '@type' => 'ImageObject', 'url' => $thumb[0], 'width' => $thumb[1], 'height' => $thumb[2], ]; } private function print_schema($schema) { echo '' . "\n"; } }