'Yoast SEO', 'seo-by-rank-math/rank-math.php' => 'Rank Math', 'all-in-one-seo-pack/all_in_one_seo_pack.php' => 'All in One SEO', 'wp-seopress/seopress.php' => 'SEOPress', 'autodescription/autodescription.php' => 'The SEO Framework', 'slim-seo/slim-seo.php' => 'Slim SEO', ]; public function __construct() { add_action('wp_head', [$this, 'output'], 2); } /** * Active plugins that also emit social/meta tags. * * @return array plugin file => display name */ public static function detectConflicts() { if (!function_exists('is_plugin_active')) { $lib = path_join(ABSPATH, 'wp-admin/includes/plugin.php'); if (!file_exists($lib)) { return []; } include_once $lib; } $found = []; foreach (self::CONFLICTING_PLUGINS as $file => $name) { if (is_plugin_active($file)) { $found[$file] = $name; } } return $found; } public function output() { $context = Context::current(); if (!$this->shouldOutput($context)) { return; } if ('on' === Utils::getSetting('openGraph')) { $this->printOpenGraph($context); } if ('on' === Utils::getSetting('twitterCards')) { $this->printTwitterCard($context); } } protected function printOpenGraph(Context $context) { $title = $this->socialValue($context, 'og_title', Resolver::title($context)); $description = $this->socialValue($context, 'og_description', Resolver::description($context)); $url = Resolver::canonical($context) ?: Utils::getCurrentUrl(true); $this->tag('og:locale', get_locale()); $this->tag('og:site_name', get_bloginfo('name')); $this->tag('og:type', $this->openGraphType($context)); $this->tag('og:title', $title); $this->tag('og:description', $description); $this->tag('og:url', $url); $image = Resolver::image($context); if ('' !== $image['url']) { $this->tag('og:image', $image['url']); // Dimensions let a scraper lay the card out before it has fetched the // file, which is the difference between a card rendering immediately and // rendering blank on first share. if ($image['width'] && $image['height']) { $this->tag('og:image:width', (string) $image['width']); $this->tag('og:image:height', (string) $image['height']); } if ('' !== $image['type']) { $this->tag('og:image:type', $image['type']); } if ('' !== $image['alt']) { $this->tag('og:image:alt', $image['alt']); } } if (Context::TYPE_SINGULAR === $context->type && $context->hasPost()) { $this->printArticleTags($context); } } protected function printArticleTags(Context $context) { $post = $context->object; $this->tag('article:published_time', get_post_time('c', true, $post)); $this->tag('article:modified_time', get_post_modified_time('c', true, $post)); $author = get_the_author_meta('display_name', (int) $post->post_author); if ($author) { $this->tag('article:author', $author); } $terms = get_the_terms($post, 'category'); if (!empty($terms) && !is_wp_error($terms)) { $this->tag('article:section', $terms[0]->name); } $tags = get_the_terms($post, 'post_tag'); if (!empty($tags) && !is_wp_error($tags)) { foreach ($tags as $tag) { $this->tag('article:tag', $tag->name); } } } protected function printTwitterCard(Context $context) { $image = Resolver::image($context); // summary_large_image needs an image to be large about. Without one X // renders the small card regardless, so declaring it would be a lie. $card = '' !== $image['url'] ? 'summary_large_image' : 'summary'; $this->nameTag('twitter:card', $card); $site = Utils::getSetting('socialTwitterSite'); if (is_string($site) && '' !== trim($site)) { $this->nameTag('twitter:site', $this->handle($site)); } // Only emitted when it differs from the Open Graph value; X falls back to // og:title and og:description on its own. $title = $this->socialValue($context, 'twitter_title', ''); if ('' !== $title) { $this->nameTag('twitter:title', $title); } $description = $this->socialValue($context, 'twitter_description', ''); if ('' !== $description) { $this->nameTag('twitter:description', $description); } $twitterImage = Resolver::override($context, 'twitter_image'); if ('' !== $twitterImage) { $this->nameTag('twitter:image', $twitterImage); } } /** * Open Graph type for this view. Only 'article' and 'website' are used — * the richer types require properties we do not collect, and an incomplete * og:type is treated as invalid rather than ignored. */ protected function openGraphType(Context $context) { if (Context::TYPE_SINGULAR === $context->type && $context->hasPost() && 'page' !== $context->postType) { return 'article'; } if (Context::TYPE_AUTHOR === $context->type) { return 'profile'; } return 'website'; } /** * A social-specific override, falling back to the shared value. */ protected function socialValue(Context $context, $field, $fallback) { $override = Resolver::override($context, $field); if ('' !== $override) { return Variables::resolve($override, $context); } return $fallback; } protected function handle($value) { $value = trim($value); // Accept a full profile URL, an @handle or a bare handle. The pattern is // delimited with #, so the fragment marker inside the class is escaped. // An unescaped # ends the pattern early and PHP warns "Unknown modifier ']'". if (preg_match('#(?:twitter|x)\.com/([^/?\#]+)#i', $value, $matches)) { $value = $matches[1]; } return '@' . ltrim($value, '@'); } protected function tag($property, $content) { if ('' === (string) $content) { return; } printf('' . "\n", esc_attr($property), esc_attr($content)); } /** * X reads name= rather than property=. Both work in practice, but the * documented form is name= and validators flag the other. */ protected function nameTag($name, $content) { if ('' === (string) $content) { return; } printf('' . "\n", esc_attr($name), esc_attr($content)); } protected function shouldOutput(Context $context) { if (is_feed() || is_admin() || is_embed()) { return false; } if (defined('REST_REQUEST') && REST_REQUEST) { return false; } // A noindexed page has no business generating a share card either. $robots = Resolver::robots($context); if (isset($robots['index']) && 'noindex' === $robots['index']) { return false; } return (bool) apply_filters('rankify_output_social_tags', true); } }