wordpress-seo.php

I’ll build this as a strong inc/integrations/wordpress-seo.php integration file for an RX medical theme: safe guards, Yoast SEO support, schema, breadcrumbs, meta cleanup, sitemap controls, Open Graph/Twitter helpers, and performance-friendly hooks.

Yes friend — create this file:

inc/integrations/wordpress-seo.php

Then include it from your theme functions.php:

require_once get_template_directory() . '/inc/integrations/wordpress-seo.php';

This file adds advanced Yoast SEO integration for your RX medical theme: breadcrumbs, schema graph, medical article schema, Open Graph, Twitter image, sitemap image support, canonical cleanup, robots control, title/meta fallback, admin notices, body classes, and shortcode helpers. Yoast officially supports schema customization through filters like wpseo_schema_graph, breadcrumb implementation through yoast_breadcrumb(), and sitemap image filters such as wpseo_sitemap_urlimages. WordPress themes should also support title-tag via add_theme_support().

<?php
/**
 * RX Theme - WordPress SEO / Yoast SEO Integration
 *
 * File: inc/integrations/wordpress-seo.php
 *
 * Purpose:
 * - Deep Yoast SEO compatibility
 * - Medical website schema improvements
 * - Breadcrumb rendering helper
 * - Open Graph / Twitter fallback images
 * - XML sitemap image support
 * - Canonical and robots control
 * - SEO-safe archive / search / author handling
 * - Medical Article / FAQ / HowTo schema helpers
 *
 * @package RX_Theme
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'RX_Theme_WordPress_SEO' ) ) :

final class RX_Theme_WordPress_SEO {

	/**
	 * Singleton instance.
	 *
	 * @var RX_Theme_WordPress_SEO|null
	 */
	private static $instance = null;

	/**
	 * Theme prefix.
	 *
	 * @var string
	 */
	private $prefix = 'rx_theme';

	/**
	 * Get instance.
	 *
	 * @return RX_Theme_WordPress_SEO
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	private function __construct() {
		$this->hooks();
	}

	/**
	 * Register hooks.
	 *
	 * @return void
	 */
	private function hooks() {

		/**
		 * Theme support.
		 */
		add_action( 'after_setup_theme', array( $this, 'add_theme_supports' ), 5 );

		/**
		 * Yoast presence classes / admin notice.
		 */
		add_filter( 'body_class', array( $this, 'body_classes' ) );
		add_action( 'admin_notices', array( $this, 'yoast_missing_notice' ) );

		/**
		 * Breadcrumbs.
		 */
		add_filter( 'wpseo_breadcrumb_links', array( $this, 'breadcrumb_links' ) );
		add_filter( 'wpseo_breadcrumb_single_link', array( $this, 'breadcrumb_single_link' ), 10, 2 );
		add_filter( 'wpseo_breadcrumb_output_wrapper', array( $this, 'breadcrumb_wrapper' ) );
		add_filter( 'wpseo_breadcrumb_output_class', array( $this, 'breadcrumb_class' ) );

		/**
		 * Titles and meta.
		 */
		add_filter( 'wpseo_title', array( $this, 'seo_title' ) );
		add_filter( 'wpseo_metadesc', array( $this, 'meta_description' ) );
		add_filter( 'wpseo_canonical', array( $this, 'canonical_url' ) );
		add_filter( 'wpseo_robots', array( $this, 'robots_rules' ) );

		/**
		 * Open Graph.
		 */
		add_filter( 'wpseo_opengraph_title', array( $this, 'seo_title' ) );
		add_filter( 'wpseo_opengraph_desc', array( $this, 'meta_description' ) );
		add_filter( 'wpseo_opengraph_image', array( $this, 'opengraph_image' ) );
		add_filter( 'wpseo_opengraph_url', array( $this, 'canonical_url' ) );
		add_filter( 'wpseo_og_locale', array( $this, 'og_locale' ) );

		/**
		 * Twitter.
		 */
		add_filter( 'wpseo_twitter_title', array( $this, 'seo_title' ) );
		add_filter( 'wpseo_twitter_description', array( $this, 'meta_description' ) );
		add_filter( 'wpseo_twitter_image', array( $this, 'twitter_image' ) );
		add_filter( 'wpseo_twitter_card_type', array( $this, 'twitter_card_type' ) );

		/**
		 * Schema.
		 */
		add_filter( 'wpseo_schema_graph', array( $this, 'schema_graph' ), 20, 2 );
		add_filter( 'wpseo_schema_webpage', array( $this, 'schema_webpage' ) );
		add_filter( 'wpseo_schema_article', array( $this, 'schema_article' ) );
		add_filter( 'wpseo_schema_organization', array( $this, 'schema_organization' ) );
		add_filter( 'wpseo_schema_person', array( $this, 'schema_person' ) );
		add_filter( 'wpseo_schema_needs_breadcrumb', '__return_true' );

		/**
		 * XML sitemap.
		 */
		add_filter( 'wpseo_sitemap_urlimages', array( $this, 'sitemap_post_images' ), 10, 2 );
		add_filter( 'wpseo_sitemap_urlimages_term', array( $this, 'sitemap_term_images' ), 10, 2 );
		add_filter( 'wpseo_exclude_from_sitemap_by_post_ids', array( $this, 'exclude_posts_from_sitemap' ) );
		add_filter( 'wpseo_sitemap_exclude_taxonomy', array( $this, 'exclude_taxonomy_from_sitemap' ), 10, 2 );
		add_filter( 'wpseo_sitemap_exclude_post_type', array( $this, 'exclude_post_type_from_sitemap' ), 10, 2 );

		/**
		 * Head cleanup / duplicate prevention.
		 */
		add_action( 'wp', array( $this, 'remove_duplicate_theme_meta' ), 20 );

		/**
		 * Shortcodes.
		 */
		add_shortcode( 'rx_breadcrumbs', array( $this, 'breadcrumb_shortcode' ) );
		add_shortcode( 'rx_seo_title', array( $this, 'seo_title_shortcode' ) );
		add_shortcode( 'rx_meta_description', array( $this, 'meta_description_shortcode' ) );
	}

	/**
	 * Add theme supports.
	 *
	 * @return void
	 */
	public function add_theme_supports() {
		add_theme_support( 'title-tag' );
		add_theme_support( 'custom-logo' );
		add_theme_support( 'post-thumbnails' );

		/**
		 * Tell Yoast that theme supports breadcrumbs.
		 */
		add_theme_support( 'yoast-seo-breadcrumbs' );
	}

	/**
	 * Check if Yoast SEO is active.
	 *
	 * @return bool
	 */
	public function is_yoast_active() {
		return defined( 'WPSEO_VERSION' ) || class_exists( 'WPSEO_Options' ) || function_exists( 'yoast_breadcrumb' );
	}

	/**
	 * Add body classes.
	 *
	 * @param array $classes Body classes.
	 * @return array
	 */
	public function body_classes( $classes ) {
		$classes[] = $this->is_yoast_active() ? 'rx-yoast-active' : 'rx-yoast-inactive';

		if ( is_singular() ) {
			$classes[] = 'rx-seo-singular';
		}

		if ( is_singular( 'post' ) ) {
			$classes[] = 'rx-seo-article';
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$classes[] = 'rx-seo-taxonomy-archive';
		}

		return array_unique( $classes );
	}

	/**
	 * Admin notice if Yoast is missing.
	 *
	 * @return void
	 */
	public function yoast_missing_notice() {
		if ( ! current_user_can( 'activate_plugins' ) ) {
			return;
		}

		if ( $this->is_yoast_active() ) {
			return;
		}

		$screen = get_current_screen();

		if ( ! $screen || 'themes' !== $screen->base ) {
			return;
		}

		echo '<div class="notice notice-info is-dismissible">';
		echo '<p><strong>RX Theme SEO:</strong> For full SEO, breadcrumb, schema, Open Graph, and sitemap features, install and activate Yoast SEO.</p>';
		echo '</div>';
	}

	/**
	 * Remove duplicate theme meta when Yoast controls SEO.
	 *
	 * @return void
	 */
	public function remove_duplicate_theme_meta() {
		if ( ! $this->is_yoast_active() ) {
			return;
		}

		remove_action( 'wp_head', 'rel_canonical' );
		remove_action( 'wp_head', 'wp_generator' );
		remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head' );
	}

	/**
	 * Render breadcrumbs.
	 *
	 * Use in template:
	 * RX_Theme_WordPress_SEO::instance()->breadcrumbs();
	 *
	 * @param bool $echo Echo output.
	 * @return string|null
	 */
	public function breadcrumbs( $echo = true ) {
		if ( ! function_exists( 'yoast_breadcrumb' ) ) {
			return null;
		}

		$output = yoast_breadcrumb(
			'<nav class="rx-breadcrumbs" aria-label="' . esc_attr__( 'Breadcrumb', 'rx-theme' ) . '">',
			'</nav>',
			false
		);

		if ( $echo ) {
			echo wp_kses_post( $output );
			return null;
		}

		return $output;
	}

	/**
	 * Breadcrumb shortcode.
	 *
	 * @return string
	 */
	public function breadcrumb_shortcode() {
		return (string) $this->breadcrumbs( false );
	}

	/**
	 * Customize breadcrumb links.
	 *
	 * @param array $links Breadcrumb links.
	 * @return array
	 */
	public function breadcrumb_links( $links ) {
		if ( empty( $links ) || ! is_array( $links ) ) {
			return $links;
		}

		/**
		 * Add Blog crumb for single posts.
		 */
		if ( is_singular( 'post' ) ) {
			$blog_page_id = (int) get_option( 'page_for_posts' );

			if ( $blog_page_id ) {
				$blog_crumb = array(
					'url'  => get_permalink( $blog_page_id ),
					'text' => get_the_title( $blog_page_id ),
				);

				array_splice( $links, 1, 0, array( $blog_crumb ) );
			}
		}

		/**
		 * Add medical library crumb for health articles.
		 */
		if ( is_singular( 'post' ) && has_category( array( 'medical', 'health', 'disease', 'treatment' ) ) ) {
			$medical_url = home_url( '/medical-library/' );

			$medical_crumb = array(
				'url'  => $medical_url,
				'text' => __( 'Medical Library', 'rx-theme' ),
			);

			array_splice( $links, 1, 0, array( $medical_crumb ) );
		}

		return $links;
	}

	/**
	 * Customize a single breadcrumb link HTML.
	 *
	 * @param string $link_output Link HTML.
	 * @param array  $link Link data.
	 * @return string
	 */
	public function breadcrumb_single_link( $link_output, $link ) {
		if ( empty( $link_output ) ) {
			return $link_output;
		}

		$link_output = str_replace( '<a ', '<a class="rx-breadcrumb-link" ', $link_output );
		$link_output = str_replace( '<span ', '<span class="rx-breadcrumb-current" ', $link_output );

		return $link_output;
	}

	/**
	 * Breadcrumb wrapper element.
	 *
	 * @return string
	 */
	public function breadcrumb_wrapper() {
		return 'nav';
	}

	/**
	 * Breadcrumb class.
	 *
	 * @return string
	 */
	public function breadcrumb_class() {
		return 'rx-breadcrumbs yoast-breadcrumbs';
	}

	/**
	 * SEO title fallback / enhancement.
	 *
	 * @param string $title SEO title.
	 * @return string
	 */
	public function seo_title( $title ) {
		if ( ! empty( $title ) ) {
			return $title;
		}

		if ( is_singular() ) {
			$title = single_post_title( '', false );
		} elseif ( is_category() || is_tag() || is_tax() ) {
			$title = single_term_title( '', false );
		} elseif ( is_search() ) {
			$title = sprintf(
				/* translators: %s search query */
				__( 'Search results for %s', 'rx-theme' ),
				get_search_query()
			);
		} elseif ( is_404() ) {
			$title = __( 'Page Not Found', 'rx-theme' );
		} else {
			$title = get_bloginfo( 'name' );
		}

		$site_name = get_bloginfo( 'name' );

		if ( $site_name && false === stripos( $title, $site_name ) ) {
			$title .= ' - ' . $site_name;
		}

		return wp_strip_all_tags( $title );
	}

	/**
	 * SEO title shortcode.
	 *
	 * @return string
	 */
	public function seo_title_shortcode() {
		return esc_html( $this->seo_title( '' ) );
	}

	/**
	 * Meta description fallback.
	 *
	 * @param string $description Meta description.
	 * @return string
	 */
	public function meta_description( $description ) {
		if ( ! empty( $description ) ) {
			return $description;
		}

		if ( is_singular() ) {
			$post_id = get_the_ID();

			$manual = get_post_meta( $post_id, '_rx_meta_description', true );

			if ( $manual ) {
				return $this->trim_description( $manual );
			}

			$excerpt = get_the_excerpt( $post_id );

			if ( $excerpt ) {
				return $this->trim_description( $excerpt );
			}

			$content = get_post_field( 'post_content', $post_id );

			return $this->trim_description( $content );
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$term = get_queried_object();

			if ( ! empty( $term->description ) ) {
				return $this->trim_description( $term->description );
			}
		}

		if ( is_search() ) {
			return sprintf(
				/* translators: %s search query */
				__( 'Find helpful medical and health information about %s on %s.', 'rx-theme' ),
				get_search_query(),
				get_bloginfo( 'name' )
			);
		}

		return $this->trim_description( get_bloginfo( 'description' ) );
	}

	/**
	 * Meta description shortcode.
	 *
	 * @return string
	 */
	public function meta_description_shortcode() {
		return esc_html( $this->meta_description( '' ) );
	}

	/**
	 * Trim description.
	 *
	 * @param string $text Text.
	 * @param int    $limit Character limit.
	 * @return string
	 */
	private function trim_description( $text, $limit = 160 ) {
		$text = wp_strip_all_tags( strip_shortcodes( (string) $text ) );
		$text = preg_replace( '/\s+/', ' ', $text );
		$text = trim( $text );

		if ( mb_strlen( $text ) > $limit ) {
			$text = mb_substr( $text, 0, $limit - 3 ) . '...';
		}

		return $text;
	}

	/**
	 * Canonical URL.
	 *
	 * @param string $canonical Canonical URL.
	 * @return string
	 */
	public function canonical_url( $canonical ) {
		if ( is_singular() ) {
			$custom = get_post_meta( get_the_ID(), '_rx_canonical_url', true );

			if ( $custom && wp_http_validate_url( $custom ) ) {
				return esc_url_raw( $custom );
			}
		}

		if ( is_paged() ) {
			return $canonical;
		}

		if ( is_search() ) {
			return home_url( '/' );
		}

		return $canonical;
	}

	/**
	 * Robots rules.
	 *
	 * @param string $robots Robots string.
	 * @return string
	 */
	public function robots_rules( $robots ) {
		if ( is_search() || is_404() ) {
			return 'noindex,follow';
		}

		if ( is_author() && ! $this->author_has_posts() ) {
			return 'noindex,follow';
		}

		if ( is_paged() && apply_filters( 'rx_theme_noindex_paged_archives', false ) ) {
			return 'noindex,follow';
		}

		if ( is_singular() ) {
			$noindex = get_post_meta( get_the_ID(), '_rx_noindex', true );

			if ( '1' === (string) $noindex ) {
				return 'noindex,follow';
			}
		}

		return $robots;
	}

	/**
	 * Check author posts.
	 *
	 * @return bool
	 */
	private function author_has_posts() {
		$author = get_queried_object();

		if ( empty( $author->ID ) ) {
			return false;
		}

		return count_user_posts( (int) $author->ID, 'post', true ) > 0;
	}

	/**
	 * Open Graph image.
	 *
	 * @param string $image Image URL.
	 * @return string
	 */
	public function opengraph_image( $image ) {
		if ( ! empty( $image ) ) {
			return $image;
		}

		return $this->get_social_image();
	}

	/**
	 * Twitter image.
	 *
	 * @param string $image Image URL.
	 * @return string
	 */
	public function twitter_image( $image ) {
		if ( ! empty( $image ) ) {
			return $image;
		}

		return $this->get_social_image();
	}

	/**
	 * Twitter card type.
	 *
	 * @param string $type Card type.
	 * @return string
	 */
	public function twitter_card_type( $type ) {
		return 'summary_large_image';
	}

	/**
	 * OG locale.
	 *
	 * @param string $locale Locale.
	 * @return string
	 */
	public function og_locale( $locale ) {
		$wp_locale = get_locale();

		if ( $wp_locale ) {
			return str_replace( '-', '_', $wp_locale );
		}

		return $locale;
	}

	/**
	 * Get social image fallback.
	 *
	 * @return string
	 */
	private function get_social_image() {
		if ( is_singular() ) {
			$post_id = get_the_ID();

			$custom = get_post_meta( $post_id, '_rx_social_image', true );

			if ( $custom && wp_http_validate_url( $custom ) ) {
				return esc_url_raw( $custom );
			}

			if ( has_post_thumbnail( $post_id ) ) {
				$image = wp_get_attachment_image_url( get_post_thumbnail_id( $post_id ), 'full' );

				if ( $image ) {
					return esc_url_raw( $image );
				}
			}
		}

		$custom_logo_id = get_theme_mod( 'custom_logo' );

		if ( $custom_logo_id ) {
			$logo = wp_get_attachment_image_url( $custom_logo_id, 'full' );

			if ( $logo ) {
				return esc_url_raw( $logo );
			}
		}

		$fallback = get_template_directory_uri() . '/assets/images/social-default.jpg';

		return esc_url_raw( apply_filters( 'rx_theme_default_social_image', $fallback ) );
	}

	/**
	 * Modify Yoast schema graph.
	 *
	 * @param array $graph Schema graph.
	 * @param mixed $context Yoast context.
	 * @return array
	 */
	public function schema_graph( $graph, $context ) {
		if ( ! is_array( $graph ) ) {
			return $graph;
		}

		$graph[] = $this->website_search_schema();

		if ( is_singular( 'post' ) ) {
			$graph[] = $this->medical_article_schema();
			$graph[] = $this->speakable_schema();
		}

		if ( is_front_page() || is_home() ) {
			$graph[] = $this->medical_organization_schema();
		}

		if ( is_author() ) {
			$graph[] = $this->author_profile_schema();
		}

		$faq_schema = $this->faq_schema_from_post();

		if ( $faq_schema ) {
			$graph[] = $faq_schema;
		}

		$howto_schema = $this->howto_schema_from_post();

		if ( $howto_schema ) {
			$graph[] = $howto_schema;
		}

		return $graph;
	}

	/**
	 * WebPage schema.
	 *
	 * @param array $data Schema data.
	 * @return array
	 */
	public function schema_webpage( $data ) {
		if ( ! is_array( $data ) ) {
			return $data;
		}

		if ( is_singular() ) {
			$data['dateModified'] = get_the_modified_date( DATE_W3C );
			$data['datePublished'] = get_the_date( DATE_W3C );
		}

		if ( is_search() ) {
			$data['@type'] = 'SearchResultsPage';
		}

		if ( is_404() ) {
			$data['@type'] = 'WebPage';
			$data['name']  = __( 'Page Not Found', 'rx-theme' );
		}

		return $data;
	}

	/**
	 * Article schema.
	 *
	 * @param array $data Article schema.
	 * @return array
	 */
	public function schema_article( $data ) {
		if ( ! is_singular( 'post' ) || ! is_array( $data ) ) {
			return $data;
		}

		$post_id = get_the_ID();

		$is_medical = has_category( array( 'medical', 'health', 'disease', 'treatment', 'medicine' ), $post_id );

		if ( $is_medical ) {
			$data['@type'] = array( 'Article', 'MedicalWebPage', 'MedicalScholarlyArticle' );
		}

		$data['headline']      = get_the_title( $post_id );
		$data['datePublished'] = get_the_date( DATE_W3C, $post_id );
		$data['dateModified']  = get_the_modified_date( DATE_W3C, $post_id );
		$data['wordCount']     = str_word_count( wp_strip_all_tags( get_post_field( 'post_content', $post_id ) ) );

		$reviewed_by = get_post_meta( $post_id, '_rx_medical_reviewed_by', true );

		if ( $reviewed_by ) {
			$data['reviewedBy'] = array(
				'@type' => 'Person',
				'name'  => sanitize_text_field( $reviewed_by ),
			);
		}

		$specialty = get_post_meta( $post_id, '_rx_medical_specialty', true );

		if ( $specialty ) {
			$data['medicalSpecialty'] = sanitize_text_field( $specialty );
		}

		return $data;
	}

	/**
	 * Organization schema.
	 *
	 * @param array $data Organization schema.
	 * @return array
	 */
	public function schema_organization( $data ) {
		if ( ! is_array( $data ) ) {
			return $data;
		}

		$data['@type'] = array( 'Organization', 'MedicalOrganization' );
		$data['name']  = get_bloginfo( 'name' );
		$data['url']   = home_url( '/' );

		$logo = $this->get_social_image();

		if ( $logo ) {
			$data['logo'] = array(
				'@type' => 'ImageObject',
				'url'   => $logo,
			);
		}

		$same_as = apply_filters(
			'rx_theme_organization_same_as',
			array()
		);

		if ( ! empty( $same_as ) && is_array( $same_as ) ) {
			$data['sameAs'] = array_values( array_filter( array_map( 'esc_url_raw', $same_as ) ) );
		}

		return $data;
	}

	/**
	 * Person schema.
	 *
	 * @param array $data Person schema.
	 * @return array
	 */
	public function schema_person( $data ) {
		if ( ! is_array( $data ) ) {
			return $data;
		}

		if ( is_author() ) {
			$author = get_queried_object();

			if ( ! empty( $author->ID ) ) {
				$data['name']        = get_the_author_meta( 'display_name', $author->ID );
				$data['description'] = get_the_author_meta( 'description', $author->ID );
				$data['url']         = get_author_posts_url( $author->ID );
			}
		}

		return $data;
	}

	/**
	 * Website search schema.
	 *
	 * @return array
	 */
	private function website_search_schema() {
		return array(
			'@type'           => 'WebSite',
			'@id'             => home_url( '/#website-search' ),
			'url'             => home_url( '/' ),
			'name'            => get_bloginfo( 'name' ),
			'potentialAction' => array(
				'@type'       => 'SearchAction',
				'target'      => home_url( '/?s={search_term_string}' ),
				'query-input' => 'required name=search_term_string',
			),
		);
	}

	/**
	 * Medical organization schema.
	 *
	 * @return array
	 */
	private function medical_organization_schema() {
		return array(
			'@type' => 'MedicalOrganization',
			'@id'   => home_url( '/#medical-organization' ),
			'name'  => get_bloginfo( 'name' ),
			'url'   => home_url( '/' ),
			'logo'  => array(
				'@type' => 'ImageObject',
				'url'   => $this->get_social_image(),
			),
		);
	}

	/**
	 * Medical article schema.
	 *
	 * @return array
	 */
	private function medical_article_schema() {
		$post_id = get_the_ID();

		$schema = array(
			'@type'            => 'MedicalWebPage',
			'@id'              => get_permalink( $post_id ) . '#medical-webpage',
			'url'              => get_permalink( $post_id ),
			'name'             => get_the_title( $post_id ),
			'description'      => $this->meta_description( '' ),
			'datePublished'    => get_the_date( DATE_W3C, $post_id ),
			'dateModified'     => get_the_modified_date( DATE_W3C, $post_id ),
			'lastReviewed'     => get_the_modified_date( DATE_W3C, $post_id ),
			'medicalAudience'  => array(
				'@type' => 'MedicalAudience',
				'name'  => 'Patient',
			),
			'about'            => array(),
		);

		$condition = get_post_meta( $post_id, '_rx_medical_condition', true );

		if ( $condition ) {
			$schema['about'][] = array(
				'@type' => 'MedicalCondition',
				'name'  => sanitize_text_field( $condition ),
			);
		}

		$specialty = get_post_meta( $post_id, '_rx_medical_specialty', true );

		if ( $specialty ) {
			$schema['specialty'] = sanitize_text_field( $specialty );
		}

		return $schema;
	}

	/**
	 * Speakable schema.
	 *
	 * @return array
	 */
	private function speakable_schema() {
		$post_id = get_the_ID();

		return array(
			'@type' => 'SpeakableSpecification',
			'@id'   => get_permalink( $post_id ) . '#speakable',
			'cssSelector' => array(
				'.entry-title',
				'.entry-content p:first-of-type',
			),
		);
	}

	/**
	 * Author profile schema.
	 *
	 * @return array
	 */
	private function author_profile_schema() {
		$author = get_queried_object();

		if ( empty( $author->ID ) ) {
			return array();
		}

		return array(
			'@type'       => 'ProfilePage',
			'@id'         => get_author_posts_url( $author->ID ) . '#profile',
			'name'        => get_the_author_meta( 'display_name', $author->ID ),
			'description' => get_the_author_meta( 'description', $author->ID ),
			'url'         => get_author_posts_url( $author->ID ),
		);
	}

	/**
	 * FAQ schema from post meta.
	 *
	 * Expected meta:
	 * _rx_faq_items = array(
	 *   array( 'question' => '...', 'answer' => '...' )
	 * )
	 *
	 * @return array|null
	 */
	private function faq_schema_from_post() {
		if ( ! is_singular() ) {
			return null;
		}

		$post_id = get_the_ID();
		$items   = get_post_meta( $post_id, '_rx_faq_items', true );

		if ( empty( $items ) || ! is_array( $items ) ) {
			return null;
		}

		$main_entity = array();

		foreach ( $items as $item ) {
			if ( empty( $item['question'] ) || empty( $item['answer'] ) ) {
				continue;
			}

			$main_entity[] = array(
				'@type'          => 'Question',
				'name'           => wp_strip_all_tags( $item['question'] ),
				'acceptedAnswer' => array(
					'@type' => 'Answer',
					'text'  => wp_kses_post( $item['answer'] ),
				),
			);
		}

		if ( empty( $main_entity ) ) {
			return null;
		}

		return array(
			'@type'      => 'FAQPage',
			'@id'        => get_permalink( $post_id ) . '#faq',
			'mainEntity' => $main_entity,
		);
	}

	/**
	 * HowTo schema from post meta.
	 *
	 * Expected meta:
	 * _rx_howto_steps = array(
	 *   array( 'name' => 'Step title', 'text' => 'Step detail' )
	 * )
	 *
	 * @return array|null
	 */
	private function howto_schema_from_post() {
		if ( ! is_singular() ) {
			return null;
		}

		$post_id = get_the_ID();
		$steps   = get_post_meta( $post_id, '_rx_howto_steps', true );

		if ( empty( $steps ) || ! is_array( $steps ) ) {
			return null;
		}

		$schema_steps = array();

		foreach ( $steps as $step ) {
			if ( empty( $step['name'] ) || empty( $step['text'] ) ) {
				continue;
			}

			$schema_steps[] = array(
				'@type' => 'HowToStep',
				'name'  => wp_strip_all_tags( $step['name'] ),
				'text'  => wp_strip_all_tags( $step['text'] ),
			);
		}

		if ( empty( $schema_steps ) ) {
			return null;
		}

		return array(
			'@type' => 'HowTo',
			'@id'   => get_permalink( $post_id ) . '#howto',
			'name'  => get_the_title( $post_id ),
			'step'  => $schema_steps,
		);
	}

	/**
	 * Add images to Yoast post sitemap.
	 *
	 * @param array $images Existing images.
	 * @param int   $post_id Post ID.
	 * @return array
	 */
	public function sitemap_post_images( $images, $post_id ) {
		$post_id = absint( $post_id );

		if ( ! $post_id ) {
			return $images;
		}

		if ( has_post_thumbnail( $post_id ) ) {
			$image_url = wp_get_attachment_image_url( get_post_thumbnail_id( $post_id ), 'full' );

			if ( $image_url ) {
				$images[] = array(
					'src'   => esc_url_raw( $image_url ),
					'title' => get_the_title( $post_id ),
					'alt'   => get_post_meta( get_post_thumbnail_id( $post_id ), '_wp_attachment_image_alt', true ),
				);
			}
		}

		$content = get_post_field( 'post_content', $post_id );

		if ( $content ) {
			preg_match_all( '/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $content, $matches );

			if ( ! empty( $matches[1] ) ) {
				foreach ( $matches[1] as $src ) {
					$images[] = array(
						'src' => esc_url_raw( $src ),
					);
				}
			}
		}

		return $this->unique_sitemap_images( $images );
	}

	/**
	 * Add images to Yoast term sitemap.
	 *
	 * @param array $images Existing images.
	 * @param object $term Term object.
	 * @return array
	 */
	public function sitemap_term_images( $images, $term ) {
		if ( empty( $term->term_id ) ) {
			return $images;
		}

		$image_id = get_term_meta( $term->term_id, 'thumbnail_id', true );

		if ( $image_id ) {
			$image_url = wp_get_attachment_image_url( $image_id, 'full' );

			if ( $image_url ) {
				$images[] = array(
					'src'   => esc_url_raw( $image_url ),
					'title' => single_term_title( '', false ),
				);
			}
		}

		$rx_term_image = get_term_meta( $term->term_id, '_rx_term_image', true );

		if ( $rx_term_image && wp_http_validate_url( $rx_term_image ) ) {
			$images[] = array(
				'src'   => esc_url_raw( $rx_term_image ),
				'title' => single_term_title( '', false ),
			);
		}

		return $this->unique_sitemap_images( $images );
	}

	/**
	 * Remove duplicate sitemap images.
	 *
	 * @param array $images Images.
	 * @return array
	 */
	private function unique_sitemap_images( $images ) {
		if ( empty( $images ) || ! is_array( $images ) ) {
			return array();
		}

		$seen   = array();
		$unique = array();

		foreach ( $images as $image ) {
			if ( empty( $image['src'] ) ) {
				continue;
			}

			$key = md5( $image['src'] );

			if ( isset( $seen[ $key ] ) ) {
				continue;
			}

			$seen[ $key ] = true;
			$unique[]     = $image;
		}

		return $unique;
	}

	/**
	 * Exclude posts from sitemap by post meta.
	 *
	 * @param array $post_ids Post IDs.
	 * @return array
	 */
	public function exclude_posts_from_sitemap( $post_ids ) {
		$query = new WP_Query(
			array(
				'post_type'      => 'any',
				'post_status'    => 'publish',
				'fields'         => 'ids',
				'posts_per_page' => 500,
				'meta_query'     => array(
					array(
						'key'   => '_rx_exclude_from_sitemap',
						'value' => '1',
					),
				),
				'no_found_rows'  => true,
			)
		);

		if ( ! empty( $query->posts ) ) {
			$post_ids = array_merge( $post_ids, $query->posts );
		}

		return array_unique( array_map( 'absint', $post_ids ) );
	}

	/**
	 * Exclude taxonomies from sitemap.
	 *
	 * @param bool   $exclude Whether to exclude.
	 * @param string $taxonomy Taxonomy name.
	 * @return bool
	 */
	public function exclude_taxonomy_from_sitemap( $exclude, $taxonomy ) {
		$excluded = apply_filters(
			'rx_theme_excluded_sitemap_taxonomies',
			array( 'post_format' )
		);

		if ( in_array( $taxonomy, $excluded, true ) ) {
			return true;
		}

		return $exclude;
	}

	/**
	 * Exclude post types from sitemap.
	 *
	 * @param bool   $exclude Whether to exclude.
	 * @param string $post_type Post type.
	 * @return bool
	 */
	public function exclude_post_type_from_sitemap( $exclude, $post_type ) {
		$excluded = apply_filters(
			'rx_theme_excluded_sitemap_post_types',
			array( 'attachment' )
		);

		if ( in_array( $post_type, $excluded, true ) ) {
			return true;
		}

		return $exclude;
	}
}

endif;

/**
 * Start integration.
 */
add_action(
	'after_setup_theme',
	function() {
		RX_Theme_WordPress_SEO::instance();
	},
	1
);

/**
 * Template helper function.
 *
 * Use:
 * rx_theme_breadcrumbs();
 *
 * @return void
 */
if ( ! function_exists( 'rx_theme_breadcrumbs' ) ) {
	function rx_theme_breadcrumbs() {
		RX_Theme_WordPress_SEO::instance()->breadcrumbs( true );
	}
}

/**
 * Template helper: get SEO title.
 *
 * @return string
 */
if ( ! function_exists( 'rx_theme_get_seo_title' ) ) {
	function rx_theme_get_seo_title() {
		return RX_Theme_WordPress_SEO::instance()->seo_title( '' );
	}
}

/**
 * Template helper: get meta description.
 *
 * @return string
 */
if ( ! function_exists( 'rx_theme_get_meta_description' ) ) {
	function rx_theme_get_meta_description() {
		return RX_Theme_WordPress_SEO::instance()->meta_description( '' );
	}
}

To show breadcrumbs in your theme template, add this where you want breadcrumb output, for example in header.php, single.php, or page.php:

<?php
if ( function_exists( 'rx_theme_breadcrumbs' ) ) {
	rx_theme_breadcrumbs();
}
?>

You can also use this shortcode inside post/page content:

[rx_breadcrumbs]

This is a strong “integration file.” For even bigger RX SEO power, the next best companion file would be:

inc/integrations/schema-medical.php for custom disease, drug, diagnostic test, FAQ, treatment, and author-review schema.

Leave a Reply

Your email address will not be published. Required fields are marked *