schema.php

I’ll build this as a reusable inc/helpers/schema.php module for your RX WordPress theme, with JSON-LD output, medical article support, breadcrumbs, organization, website, author, FAQ, HowTo, and safety checks.

Yes my friend. Below is an advanced schema.php file for your rx-theme/inc/helpers/schema.php.

It includes many possible schema features for RX theme:

  • Website schema
  • Organization schema
  • Medical organization schema
  • WebPage schema
  • Article / BlogPosting / MedicalWebPage schema
  • Breadcrumb schema
  • Person / Author schema
  • Publisher schema
  • ImageObject schema
  • FAQ schema from post meta
  • HowTo schema from post meta
  • MedicalCondition schema
  • MedicalSignOrSymptom schema
  • MedicalTest schema
  • Speakable schema
  • SearchAction schema
  • Logo schema
  • Social profile support
  • Archive / Category / Tag schema
  • Single post schema
  • Page schema
  • Homepage schema
  • Attachment image schema
  • Safe JSON-LD output
  • Developer filters for future extension

inc/helpers/schema.php

<?php
/**
 * RX Theme Advanced Schema Markup
 *
 * File: inc/helpers/schema.php
 *
 * Adds advanced JSON-LD schema markup for:
 * - Website
 * - Organization
 * - MedicalOrganization
 * - WebPage
 * - BlogPosting
 * - Article
 * - MedicalWebPage
 * - MedicalCondition
 * - BreadcrumbList
 * - Person / Author
 * - FAQPage
 * - HowTo
 * - MedicalTest
 * - MedicalSignOrSymptom
 * - ImageObject
 * - SearchAction
 * - SpeakableSpecification
 *
 * @package RX_Theme
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Do not redeclare.
 */
if ( ! class_exists( 'RX_Advanced_Schema' ) ) :

final class RX_Advanced_Schema {

	/**
	 * Schema context.
	 *
	 * @var string
	 */
	private static $context = 'https://schema.org';

	/**
	 * Init hooks.
	 */
	public static function init() {
		add_action( 'wp_head', array( __CLASS__, 'output_schema' ), 20 );
	}

	/**
	 * Main schema output.
	 */
	public static function output_schema() {

		if ( is_admin() || is_feed() || is_robots() || is_trackback() ) {
			return;
		}

		$schemas = array();

		$schemas[] = self::website_schema();
		$schemas[] = self::organization_schema();

		if ( self::is_medical_site_enabled() ) {
			$schemas[] = self::medical_organization_schema();
		}

		$schemas[] = self::webpage_schema();
		$schemas[] = self::breadcrumb_schema();

		if ( is_front_page() || is_home() ) {
			$schemas[] = self::homepage_schema();
		}

		if ( is_singular( 'post' ) ) {
			$schemas[] = self::article_schema();

			if ( self::is_medical_article() ) {
				$schemas[] = self::medical_webpage_schema();
				$schemas[] = self::medical_condition_schema();
				$schemas[] = self::medical_symptom_schema();
				$schemas[] = self::medical_test_schema();
			}

			$schemas[] = self::faq_schema();
			$schemas[] = self::howto_schema();
		}

		if ( is_page() && ! is_front_page() ) {
			$schemas[] = self::page_schema();
			$schemas[] = self::faq_schema();
			$schemas[] = self::howto_schema();
		}

		if ( is_author() ) {
			$schemas[] = self::author_archive_schema();
		}

		if ( is_category() || is_tag() || is_tax() ) {
			$schemas[] = self::collection_page_schema();
		}

		if ( is_attachment() ) {
			$schemas[] = self::attachment_schema();
		}

		$schemas = array_filter( $schemas );

		/**
		 * Allow child theme or plugin to modify all schema pieces.
		 *
		 * Example:
		 * add_filter( 'rx_schema_graph', function( $schemas ) {
		 *     $schemas[] = array(
		 *         '@type' => 'Thing',
		 *         'name'  => 'Custom Schema',
		 *     );
		 *     return $schemas;
		 * });
		 */
		$schemas = apply_filters( 'rx_schema_graph', $schemas );

		if ( empty( $schemas ) ) {
			return;
		}

		$output = array(
			'@context' => self::$context,
			'@graph'   => array_values( $schemas ),
		);

		echo "\n" . '<script type="application/ld+json" class="rx-schema-jsonld">' . "\n";
		echo wp_json_encode(
			$output,
			JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
		);
		echo "\n" . '</script>' . "\n";
	}

	/**
	 * Website schema.
	 */
	private static function website_schema() {
		$site_name = get_bloginfo( 'name' );
		$site_url  = home_url( '/' );

		$schema = array(
			'@type'       => 'WebSite',
			'@id'         => trailingslashit( $site_url ) . '#website',
			'url'         => $site_url,
			'name'        => $site_name,
			'description' => get_bloginfo( 'description' ),
			'publisher'   => array(
				'@id' => trailingslashit( $site_url ) . '#organization',
			),
			'potentialAction' => array(
				'@type'       => 'SearchAction',
				'target'      => add_query_arg( 's', '{search_term_string}', home_url( '/' ) ),
				'query-input' => 'required name=search_term_string',
			),
			'inLanguage' => self::language(),
		);

		return apply_filters( 'rx_schema_website', $schema );
	}

	/**
	 * Organization schema.
	 */
	private static function organization_schema() {
		$site_url  = home_url( '/' );
		$site_name = get_bloginfo( 'name' );
		$logo      = self::site_logo_url();

		$schema = array(
			'@type'       => 'Organization',
			'@id'         => trailingslashit( $site_url ) . '#organization',
			'name'        => $site_name,
			'url'         => $site_url,
			'description' => get_bloginfo( 'description' ),
			'logo'        => $logo ? array(
				'@type' => 'ImageObject',
				'url'   => $logo,
			) : null,
			'image'       => $logo ? $logo : null,
			'sameAs'      => self::social_profiles(),
		);

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_organization', $schema );
	}

	/**
	 * Medical organization schema.
	 */
	private static function medical_organization_schema() {
		$site_url  = home_url( '/' );
		$site_name = get_bloginfo( 'name' );
		$logo      = self::site_logo_url();

		$schema = array(
			'@type'       => 'MedicalOrganization',
			'@id'         => trailingslashit( $site_url ) . '#medical-organization',
			'name'        => $site_name,
			'url'         => $site_url,
			'description' => get_bloginfo( 'description' ),
			'logo'        => $logo ? array(
				'@type' => 'ImageObject',
				'url'   => $logo,
			) : null,
			'image'       => $logo ? $logo : null,
			'medicalSpecialty' => self::medical_specialties(),
			'sameAs'      => self::social_profiles(),
		);

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_medical_organization', $schema );
	}

	/**
	 * HomePage schema.
	 */
	private static function homepage_schema() {
		$schema = array(
			'@type'       => 'WebPage',
			'@id'         => home_url( '/' ) . '#homepage',
			'url'         => home_url( '/' ),
			'name'        => get_bloginfo( 'name' ),
			'description' => get_bloginfo( 'description' ),
			'isPartOf'    => array(
				'@id' => home_url( '/' ) . '#website',
			),
			'about'       => array(
				'@id' => home_url( '/' ) . '#organization',
			),
			'inLanguage'  => self::language(),
		);

		return apply_filters( 'rx_schema_homepage', $schema );
	}

	/**
	 * Generic WebPage schema.
	 */
	private static function webpage_schema() {
		$url   = self::current_url();
		$title = self::current_title();

		$schema = array(
			'@type'      => self::webpage_type(),
			'@id'        => trailingslashit( $url ) . '#webpage',
			'url'        => $url,
			'name'       => $title,
			'isPartOf'   => array(
				'@id' => home_url( '/' ) . '#website',
			),
			'about'      => array(
				'@id' => home_url( '/' ) . '#organization',
			),
			'inLanguage' => self::language(),
		);

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

			$schema['datePublished'] = get_the_date( DATE_W3C, $post_id );
			$schema['dateModified']  = get_the_modified_date( DATE_W3C, $post_id );

			$excerpt = self::post_excerpt( $post_id );
			if ( $excerpt ) {
				$schema['description'] = $excerpt;
			}

			$image = self::featured_image_schema( $post_id );
			if ( $image ) {
				$schema['primaryImageOfPage'] = $image;
			}

			$schema['breadcrumb'] = array(
				'@id' => trailingslashit( $url ) . '#breadcrumb',
			);

			$schema['speakable'] = self::speakable_schema();
		}

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_webpage', $schema );
	}

	/**
	 * Detect webpage type.
	 */
	private static function webpage_type() {
		if ( is_front_page() ) {
			return 'WebPage';
		}

		if ( is_search() ) {
			return 'SearchResultsPage';
		}

		if ( is_category() || is_tag() || is_tax() || is_archive() ) {
			return 'CollectionPage';
		}

		if ( is_singular( 'post' ) && self::is_medical_article() ) {
			return 'MedicalWebPage';
		}

		if ( is_singular() ) {
			return 'WebPage';
		}

		return 'WebPage';
	}

	/**
	 * Article schema.
	 */
	private static function article_schema() {
		$post_id = get_queried_object_id();

		if ( ! $post_id ) {
			return null;
		}

		$url       = get_permalink( $post_id );
		$title     = get_the_title( $post_id );
		$excerpt   = self::post_excerpt( $post_id );
		$image     = self::featured_image_schema( $post_id );
		$word_count = self::word_count( $post_id );

		$schema = array(
			'@type'            => self::article_type(),
			'@id'              => trailingslashit( $url ) . '#article',
			'mainEntityOfPage' => array(
				'@id' => trailingslashit( $url ) . '#webpage',
			),
			'headline'         => wp_strip_all_tags( $title ),
			'description'      => $excerpt,
			'image'            => $image,
			'datePublished'    => get_the_date( DATE_W3C, $post_id ),
			'dateModified'     => get_the_modified_date( DATE_W3C, $post_id ),
			'author'           => self::author_schema( (int) get_post_field( 'post_author', $post_id ) ),
			'publisher'        => array(
				'@id' => home_url( '/' ) . '#organization',
			),
			'articleSection'   => self::post_categories( $post_id ),
			'keywords'         => self::post_keywords( $post_id ),
			'wordCount'        => $word_count,
			'inLanguage'       => self::language(),
		);

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_article', $schema, $post_id );
	}

	/**
	 * Article type.
	 */
	private static function article_type() {
		if ( self::is_medical_article() ) {
			return 'MedicalScholarlyArticle';
		}

		return 'BlogPosting';
	}

	/**
	 * Medical WebPage schema.
	 */
	private static function medical_webpage_schema() {
		$post_id = get_queried_object_id();

		if ( ! $post_id ) {
			return null;
		}

		$url = get_permalink( $post_id );

		$schema = array(
			'@type'       => 'MedicalWebPage',
			'@id'         => trailingslashit( $url ) . '#medical-webpage',
			'url'         => $url,
			'name'        => get_the_title( $post_id ),
			'description' => self::post_excerpt( $post_id ),
			'isPartOf'    => array(
				'@id' => home_url( '/' ) . '#website',
			),
			'publisher'   => array(
				'@id' => home_url( '/' ) . '#medical-organization',
			),
			'author'      => self::author_schema( (int) get_post_field( 'post_author', $post_id ) ),
			'reviewedBy'  => self::reviewed_by_schema( $post_id ),
			'lastReviewed' => self::post_meta( $post_id, '_rx_last_reviewed' ),
			'medicalAudience' => array(
				'@type' => 'MedicalAudience',
				'audienceType' => 'Patient',
			),
			'specialty'   => self::medical_specialties(),
			'inLanguage'  => self::language(),
		);

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_medical_webpage', $schema, $post_id );
	}

	/**
	 * Medical condition schema.
	 *
	 * Recommended post meta fields:
	 * _rx_medical_condition_name
	 * _rx_medical_condition_description
	 * _rx_medical_condition_causes
	 * _rx_medical_condition_symptoms
	 * _rx_medical_condition_tests
	 * _rx_medical_condition_treatments
	 */
	private static function medical_condition_schema() {
		$post_id = get_queried_object_id();

		if ( ! $post_id || ! self::is_medical_article() ) {
			return null;
		}

		$name = self::post_meta( $post_id, '_rx_medical_condition_name' );

		if ( ! $name ) {
			$name = get_the_title( $post_id );
		}

		$description = self::post_meta( $post_id, '_rx_medical_condition_description' );
		if ( ! $description ) {
			$description = self::post_excerpt( $post_id );
		}

		$schema = array(
			'@type'       => 'MedicalCondition',
			'@id'         => trailingslashit( get_permalink( $post_id ) ) . '#medical-condition',
			'name'        => wp_strip_all_tags( $name ),
			'description' => wp_strip_all_tags( $description ),
			'url'         => get_permalink( $post_id ),
			'possibleTreatment' => self::medical_treatments_from_meta( $post_id ),
			'signOrSymptom'    => self::medical_symptoms_from_meta( $post_id ),
			'cause'            => self::medical_causes_from_meta( $post_id ),
			'diagnosis'        => self::medical_tests_from_meta( $post_id ),
			'associatedAnatomy' => self::medical_anatomy_from_meta( $post_id ),
		);

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_medical_condition', $schema, $post_id );
	}

	/**
	 * Medical symptom schema.
	 */
	private static function medical_symptom_schema() {
		$post_id = get_queried_object_id();

		$symptoms = self::array_meta( $post_id, '_rx_medical_condition_symptoms' );

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

		$items = array();

		foreach ( $symptoms as $symptom ) {
			$items[] = array(
				'@type' => 'MedicalSignOrSymptom',
				'name'  => wp_strip_all_tags( $symptom ),
			);
		}

		$schema = array(
			'@type' => 'ItemList',
			'@id'   => trailingslashit( get_permalink( $post_id ) ) . '#medical-symptoms',
			'name'  => 'Signs and symptoms of ' . get_the_title( $post_id ),
			'itemListElement' => self::item_list_elements( $items ),
		);

		return apply_filters( 'rx_schema_medical_symptoms', $schema, $post_id );
	}

	/**
	 * Medical test schema.
	 */
	private static function medical_test_schema() {
		$post_id = get_queried_object_id();

		$tests = self::array_meta( $post_id, '_rx_medical_condition_tests' );

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

		$items = array();

		foreach ( $tests as $test ) {
			$items[] = array(
				'@type' => 'MedicalTest',
				'name'  => wp_strip_all_tags( $test ),
			);
		}

		$schema = array(
			'@type' => 'ItemList',
			'@id'   => trailingslashit( get_permalink( $post_id ) ) . '#medical-tests',
			'name'  => 'Diagnostic tests for ' . get_the_title( $post_id ),
			'itemListElement' => self::item_list_elements( $items ),
		);

		return apply_filters( 'rx_schema_medical_tests', $schema, $post_id );
	}

	/**
	 * Page schema.
	 */
	private static function page_schema() {
		$post_id = get_queried_object_id();

		if ( ! $post_id ) {
			return null;
		}

		$schema = array(
			'@type'         => 'WebPage',
			'@id'           => trailingslashit( get_permalink( $post_id ) ) . '#page',
			'url'           => get_permalink( $post_id ),
			'name'          => get_the_title( $post_id ),
			'description'   => self::post_excerpt( $post_id ),
			'datePublished' => get_the_date( DATE_W3C, $post_id ),
			'dateModified'  => get_the_modified_date( DATE_W3C, $post_id ),
			'isPartOf'      => array(
				'@id' => home_url( '/' ) . '#website',
			),
			'inLanguage'    => self::language(),
		);

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_page', $schema, $post_id );
	}

	/**
	 * Breadcrumb schema.
	 */
	private static function breadcrumb_schema() {
		if ( is_front_page() ) {
			return null;
		}

		$items = array();
		$pos   = 1;

		$items[] = array(
			'@type'    => 'ListItem',
			'position' => $pos++,
			'name'     => __( 'Home', 'rx-theme' ),
			'item'     => home_url( '/' ),
		);

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

			if ( is_singular( 'post' ) ) {
				$categories = get_the_category( $post_id );

				if ( ! empty( $categories ) && ! is_wp_error( $categories ) ) {
					$cat = $categories[0];

					$items[] = array(
						'@type'    => 'ListItem',
						'position' => $pos++,
						'name'     => $cat->name,
						'item'     => get_category_link( $cat->term_id ),
					);
				}
			}

			$items[] = array(
				'@type'    => 'ListItem',
				'position' => $pos++,
				'name'     => get_the_title( $post_id ),
				'item'     => get_permalink( $post_id ),
			);
		} elseif ( is_category() || is_tag() || is_tax() ) {
			$term = get_queried_object();

			if ( $term && ! is_wp_error( $term ) ) {
				$items[] = array(
					'@type'    => 'ListItem',
					'position' => $pos++,
					'name'     => single_term_title( '', false ),
					'item'     => get_term_link( $term ),
				);
			}
		} elseif ( is_search() ) {
			$items[] = array(
				'@type'    => 'ListItem',
				'position' => $pos++,
				'name'     => sprintf(
					/* translators: %s search query */
					__( 'Search results for %s', 'rx-theme' ),
					get_search_query()
				),
				'item'     => self::current_url(),
			);
		} elseif ( is_author() ) {
			$items[] = array(
				'@type'    => 'ListItem',
				'position' => $pos++,
				'name'     => get_the_author_meta( 'display_name', get_queried_object_id() ),
				'item'     => self::current_url(),
			);
		} elseif ( is_archive() ) {
			$items[] = array(
				'@type'    => 'ListItem',
				'position' => $pos++,
				'name'     => get_the_archive_title(),
				'item'     => self::current_url(),
			);
		}

		$schema = array(
			'@type'           => 'BreadcrumbList',
			'@id'             => trailingslashit( self::current_url() ) . '#breadcrumb',
			'itemListElement' => $items,
		);

		return apply_filters( 'rx_schema_breadcrumb', $schema );
	}

	/**
	 * Author schema.
	 */
	private static function author_schema( $author_id ) {
		if ( ! $author_id ) {
			return null;
		}

		$name        = get_the_author_meta( 'display_name', $author_id );
		$description = get_the_author_meta( 'description', $author_id );
		$url         = get_author_posts_url( $author_id );
		$avatar      = get_avatar_url( $author_id, array( 'size' => 256 ) );

		$schema = array(
			'@type'       => 'Person',
			'@id'         => trailingslashit( $url ) . '#author',
			'name'        => $name,
			'url'         => $url,
			'description' => $description,
			'image'       => $avatar ? array(
				'@type' => 'ImageObject',
				'url'   => $avatar,
			) : null,
			'worksFor'    => array(
				'@id' => home_url( '/' ) . '#organization',
			),
		);

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_author', $schema, $author_id );
	}

	/**
	 * Reviewed by schema.
	 *
	 * Optional post meta:
	 * _rx_reviewed_by_name
	 * _rx_reviewed_by_url
	 */
	private static function reviewed_by_schema( $post_id ) {
		$name = self::post_meta( $post_id, '_rx_reviewed_by_name' );
		$url  = self::post_meta( $post_id, '_rx_reviewed_by_url' );

		if ( ! $name ) {
			return null;
		}

		$schema = array(
			'@type' => 'Person',
			'name'  => wp_strip_all_tags( $name ),
			'url'   => esc_url_raw( $url ),
		);

		return self::remove_empty_items( $schema );
	}

	/**
	 * Author archive schema.
	 */
	private static function author_archive_schema() {
		$author_id = get_queried_object_id();

		if ( ! $author_id ) {
			return null;
		}

		$schema = array(
			'@type'       => 'ProfilePage',
			'@id'         => trailingslashit( get_author_posts_url( $author_id ) ) . '#profile',
			'url'         => get_author_posts_url( $author_id ),
			'name'        => get_the_author_meta( 'display_name', $author_id ),
			'description' => get_the_author_meta( 'description', $author_id ),
			'mainEntity'  => self::author_schema( $author_id ),
			'inLanguage'  => self::language(),
		);

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_author_archive', $schema, $author_id );
	}

	/**
	 * Collection page schema.
	 */
	private static function collection_page_schema() {
		$title = wp_strip_all_tags( get_the_archive_title() );
		$desc  = wp_strip_all_tags( get_the_archive_description() );

		$schema = array(
			'@type'       => 'CollectionPage',
			'@id'         => trailingslashit( self::current_url() ) . '#collection',
			'url'         => self::current_url(),
			'name'        => $title,
			'description' => $desc,
			'isPartOf'    => array(
				'@id' => home_url( '/' ) . '#website',
			),
			'inLanguage'  => self::language(),
		);

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_collection_page', $schema );
	}

	/**
	 * Attachment schema.
	 */
	private static function attachment_schema() {
		$post_id = get_queried_object_id();

		if ( ! $post_id ) {
			return null;
		}

		$mime = get_post_mime_type( $post_id );

		if ( ! $mime || false === strpos( $mime, 'image' ) ) {
			return null;
		}

		$image = wp_get_attachment_image_src( $post_id, 'full' );

		if ( ! $image ) {
			return null;
		}

		$schema = array(
			'@type'       => 'ImageObject',
			'@id'         => trailingslashit( get_attachment_link( $post_id ) ) . '#image',
			'url'         => wp_get_attachment_url( $post_id ),
			'contentUrl'  => wp_get_attachment_url( $post_id ),
			'name'        => get_the_title( $post_id ),
			'description' => wp_get_attachment_caption( $post_id ),
			'width'       => isset( $image[1] ) ? absint( $image[1] ) : null,
			'height'      => isset( $image[2] ) ? absint( $image[2] ) : null,
			'inLanguage'  => self::language(),
		);

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_attachment', $schema, $post_id );
	}

	/**
	 * FAQ schema.
	 *
	 * Supported post meta format:
	 * _rx_faqs = array(
	 *   array(
	 *     'question' => 'What is anemia?',
	 *     'answer'   => 'Anemia is...'
	 *   )
	 * )
	 *
	 * Also supports JSON string.
	 */
	private static function faq_schema() {
		$post_id = get_queried_object_id();

		if ( ! $post_id ) {
			return null;
		}

		$faqs = get_post_meta( $post_id, '_rx_faqs', true );
		$faqs = self::normalize_repeater_meta( $faqs );

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

		$main_entity = array();

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

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

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

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

		return apply_filters( 'rx_schema_faq', $schema, $post_id );
	}

	/**
	 * HowTo schema.
	 *
	 * Supported post meta:
	 * _rx_howto_name
	 * _rx_howto_description
	 * _rx_howto_steps = array(
	 *   array(
	 *     'name' => 'Step title',
	 *     'text' => 'Step description'
	 *   )
	 * )
	 */
	private static function howto_schema() {
		$post_id = get_queried_object_id();

		if ( ! $post_id ) {
			return null;
		}

		$name        = self::post_meta( $post_id, '_rx_howto_name' );
		$description = self::post_meta( $post_id, '_rx_howto_description' );
		$steps       = get_post_meta( $post_id, '_rx_howto_steps', true );
		$steps       = self::normalize_repeater_meta( $steps );

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

		$step_items = array();
		$position   = 1;

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

			$step_items[] = array(
				'@type'    => 'HowToStep',
				'position' => $position++,
				'name'     => ! empty( $step['name'] ) ? wp_strip_all_tags( $step['name'] ) : '',
				'text'     => wp_strip_all_tags( $step['text'] ),
			);
		}

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

		$schema = array(
			'@type'       => 'HowTo',
			'@id'         => trailingslashit( get_permalink( $post_id ) ) . '#howto',
			'name'        => wp_strip_all_tags( $name ),
			'description' => wp_strip_all_tags( $description ),
			'step'        => $step_items,
			'inLanguage'  => self::language(),
		);

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_howto', $schema, $post_id );
	}

	/**
	 * Speakable schema.
	 */
	private static function speakable_schema() {
		if ( ! is_singular() ) {
			return null;
		}

		$schema = array(
			'@type' => 'SpeakableSpecification',
			'cssSelector' => array(
				'h1',
				'.entry-title',
				'.entry-content p:first-of-type',
			),
		);

		return apply_filters( 'rx_schema_speakable', $schema );
	}

	/**
	 * Featured image schema.
	 */
	private static function featured_image_schema( $post_id ) {
		if ( ! has_post_thumbnail( $post_id ) ) {
			return null;
		}

		$image_id = get_post_thumbnail_id( $post_id );
		$image    = wp_get_attachment_image_src( $image_id, 'full' );

		if ( ! $image ) {
			return null;
		}

		$schema = array(
			'@type'  => 'ImageObject',
			'@id'    => trailingslashit( get_permalink( $post_id ) ) . '#primaryimage',
			'url'    => esc_url_raw( $image[0] ),
			'width'  => isset( $image[1] ) ? absint( $image[1] ) : null,
			'height' => isset( $image[2] ) ? absint( $image[2] ) : null,
			'caption' => wp_get_attachment_caption( $image_id ),
		);

		$schema = self::remove_empty_items( $schema );

		return apply_filters( 'rx_schema_featured_image', $schema, $post_id );
	}

	/**
	 * Medical treatments from meta.
	 */
	private static function medical_treatments_from_meta( $post_id ) {
		$treatments = self::array_meta( $post_id, '_rx_medical_condition_treatments' );

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

		$output = array();

		foreach ( $treatments as $treatment ) {
			$output[] = array(
				'@type' => 'MedicalTherapy',
				'name'  => wp_strip_all_tags( $treatment ),
			);
		}

		return $output;
	}

	/**
	 * Medical symptoms from meta.
	 */
	private static function medical_symptoms_from_meta( $post_id ) {
		$symptoms = self::array_meta( $post_id, '_rx_medical_condition_symptoms' );

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

		$output = array();

		foreach ( $symptoms as $symptom ) {
			$output[] = array(
				'@type' => 'MedicalSignOrSymptom',
				'name'  => wp_strip_all_tags( $symptom ),
			);
		}

		return $output;
	}

	/**
	 * Medical causes from meta.
	 */
	private static function medical_causes_from_meta( $post_id ) {
		$causes = self::array_meta( $post_id, '_rx_medical_condition_causes' );

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

		$output = array();

		foreach ( $causes as $cause ) {
			$output[] = array(
				'@type' => 'MedicalCause',
				'name'  => wp_strip_all_tags( $cause ),
			);
		}

		return $output;
	}

	/**
	 * Medical tests from meta.
	 */
	private static function medical_tests_from_meta( $post_id ) {
		$tests = self::array_meta( $post_id, '_rx_medical_condition_tests' );

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

		$output = array();

		foreach ( $tests as $test ) {
			$output[] = array(
				'@type' => 'MedicalTest',
				'name'  => wp_strip_all_tags( $test ),
			);
		}

		return $output;
	}

	/**
	 * Medical anatomy from meta.
	 */
	private static function medical_anatomy_from_meta( $post_id ) {
		$items = self::array_meta( $post_id, '_rx_medical_anatomy' );

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

		$output = array();

		foreach ( $items as $item ) {
			$output[] = array(
				'@type' => 'AnatomicalStructure',
				'name'  => wp_strip_all_tags( $item ),
			);
		}

		return $output;
	}

	/**
	 * ItemList helper.
	 */
	private static function item_list_elements( $items ) {
		$output   = array();
		$position = 1;

		foreach ( $items as $item ) {
			$output[] = array(
				'@type'    => 'ListItem',
				'position' => $position++,
				'item'     => $item,
			);
		}

		return $output;
	}

	/**
	 * Check if medical schema is globally enabled.
	 */
	private static function is_medical_site_enabled() {
		/**
		 * Default true for RX theme because your site is medical focused.
		 */
		return (bool) apply_filters( 'rx_schema_enable_medical_site', true );
	}

	/**
	 * Detect medical article.
	 */
	private static function is_medical_article() {
		if ( ! is_singular( 'post' ) ) {
			return false;
		}

		$post_id = get_queried_object_id();

		$enabled = get_post_meta( $post_id, '_rx_is_medical_article', true );

		if ( 'no' === $enabled ) {
			return false;
		}

		if ( 'yes' === $enabled ) {
			return true;
		}

		$categories = wp_get_post_categories( $post_id, array( 'fields' => 'names' ) );
		$tags       = wp_get_post_tags( $post_id, array( 'fields' => 'names' ) );

		$all_terms = array_merge( (array) $categories, (array) $tags );
		$keywords  = array(
			'medical',
			'health',
			'disease',
			'condition',
			'symptom',
			'diagnosis',
			'treatment',
			'medicine',
			'doctor',
			'anatomy',
			'pathology',
			'therapy',
			'surgery',
			'drug',
			'clinical',
		);

		foreach ( $all_terms as $term ) {
			foreach ( $keywords as $keyword ) {
				if ( false !== stripos( $term, $keyword ) ) {
					return true;
				}
			}
		}

		return (bool) apply_filters( 'rx_schema_is_medical_article', true, $post_id );
	}

	/**
	 * Medical specialties.
	 */
	private static function medical_specialties() {
		$specialties = array(
			'PrimaryCare',
			'InternalMedicine',
			'Cardiovascular',
			'Dermatologic',
			'Endocrine',
			'Gastroenterologic',
			'Genetic',
			'Geriatric',
			'Gynecologic',
			'Hematologic',
			'Infectious',
			'Neurologic',
			'Oncologic',
			'Ophthalmologic',
			'Orthopedic',
			'Otolaryngologic',
			'Pediatric',
			'Psychiatric',
			'Pulmonary',
			'Renal',
			'Rheumatologic',
		);

		return apply_filters( 'rx_schema_medical_specialties', $specialties );
	}

	/**
	 * Social profiles.
	 *
	 * You may edit these links manually.
	 */
	private static function social_profiles() {
		$profiles = array(
			// 'https://www.facebook.com/your-page',
			// 'https://twitter.com/your-handle',
			// 'https://www.linkedin.com/company/your-company',
			// 'https://www.youtube.com/@yourchannel',
			// 'https://www.pinterest.com/yourprofile',
		);

		return apply_filters( 'rx_schema_social_profiles', array_filter( $profiles ) );
	}

	/**
	 * Site logo URL.
	 */
	private static function site_logo_url() {
		$custom_logo_id = get_theme_mod( 'custom_logo' );

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

			if ( ! empty( $logo[0] ) ) {
				return esc_url_raw( $logo[0] );
			}
		}

		$site_icon = get_site_icon_url( 512 );

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

		return null;
	}

	/**
	 * Current URL.
	 */
	private static function current_url() {
		global $wp;

		if ( is_singular() ) {
			return get_permalink();
		}

		if ( is_home() && ! is_front_page() ) {
			$page_for_posts = get_option( 'page_for_posts' );

			if ( $page_for_posts ) {
				return get_permalink( $page_for_posts );
			}
		}

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

			if ( $term && ! is_wp_error( $term ) ) {
				return get_term_link( $term );
			}
		}

		return home_url( add_query_arg( array(), $wp->request ) );
	}

	/**
	 * Current title.
	 */
	private static function current_title() {
		if ( is_singular() ) {
			return get_the_title();
		}

		if ( is_search() ) {
			return sprintf(
				/* translators: %s search query */
				__( 'Search results for %s', 'rx-theme' ),
				get_search_query()
			);
		}

		if ( is_archive() ) {
			return wp_strip_all_tags( get_the_archive_title() );
		}

		return get_bloginfo( 'name' );
	}

	/**
	 * Post excerpt.
	 */
	private static function post_excerpt( $post_id ) {
		$excerpt = get_the_excerpt( $post_id );

		if ( ! $excerpt ) {
			$content = get_post_field( 'post_content', $post_id );
			$excerpt = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $content ) ), 35 );
		}

		return wp_strip_all_tags( $excerpt );
	}

	/**
	 * Post categories.
	 */
	private static function post_categories( $post_id ) {
		$categories = get_the_category( $post_id );

		if ( empty( $categories ) || is_wp_error( $categories ) ) {
			return null;
		}

		return wp_list_pluck( $categories, 'name' );
	}

	/**
	 * Post keywords from tags.
	 */
	private static function post_keywords( $post_id ) {
		$tags = get_the_tags( $post_id );

		if ( empty( $tags ) || is_wp_error( $tags ) ) {
			return null;
		}

		return implode( ', ', wp_list_pluck( $tags, 'name' ) );
	}

	/**
	 * Word count.
	 */
	private static function word_count( $post_id ) {
		$content = get_post_field( 'post_content', $post_id );
		$content = wp_strip_all_tags( strip_shortcodes( $content ) );

		return str_word_count( $content );
	}

	/**
	 * Language.
	 */
	private static function language() {
		return str_replace( '_', '-', get_locale() );
	}

	/**
	 * Safe post meta.
	 */
	private static function post_meta( $post_id, $key ) {
		$value = get_post_meta( $post_id, $key, true );

		if ( is_array( $value ) || is_object( $value ) ) {
			return null;
		}

		return $value ? wp_strip_all_tags( $value ) : null;
	}

	/**
	 * Array meta helper.
	 *
	 * Supports:
	 * - Serialized array
	 * - JSON array
	 * - Comma-separated string
	 * - New-line-separated string
	 */
	private static function array_meta( $post_id, $key ) {
		$value = get_post_meta( $post_id, $key, true );

		if ( empty( $value ) ) {
			return array();
		}

		if ( is_array( $value ) ) {
			return array_filter( array_map( 'sanitize_text_field', $value ) );
		}

		if ( is_string( $value ) ) {
			$decoded = json_decode( $value, true );

			if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
				return array_filter( array_map( 'sanitize_text_field', $decoded ) );
			}

			if ( false !== strpos( $value, "\n" ) ) {
				return array_filter( array_map( 'sanitize_text_field', preg_split( '/\r\n|\r|\n/', $value ) ) );
			}

			if ( false !== strpos( $value, ',' ) ) {
				return array_filter( array_map( 'sanitize_text_field', explode( ',', $value ) ) );
			}

			return array( sanitize_text_field( $value ) );
		}

		return array();
	}

	/**
	 * Normalize repeater meta.
	 */
	private static function normalize_repeater_meta( $value ) {
		if ( empty( $value ) ) {
			return array();
		}

		if ( is_string( $value ) ) {
			$decoded = json_decode( $value, true );

			if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
				$value = $decoded;
			}
		}

		if ( ! is_array( $value ) ) {
			return array();
		}

		$clean = array();

		foreach ( $value as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}

			$clean_row = array();

			foreach ( $row as $key => $item ) {
				if ( is_scalar( $item ) ) {
					$clean_row[ sanitize_key( $key ) ] = wp_kses_post( $item );
				}
			}

			if ( ! empty( $clean_row ) ) {
				$clean[] = $clean_row;
			}
		}

		return $clean;
	}

	/**
	 * Remove empty values recursively.
	 */
	private static function remove_empty_items( $data ) {
		if ( ! is_array( $data ) ) {
			return $data;
		}

		foreach ( $data as $key => $value ) {
			if ( is_array( $value ) ) {
				$value = self::remove_empty_items( $value );
			}

			if (
				null === $value ||
				'' === $value ||
				array() === $value
			) {
				unset( $data[ $key ] );
			} else {
				$data[ $key ] = $value;
			}
		}

		return $data;
	}
}

endif;

RX_Advanced_Schema::init();

Add this line in your main functions.php

require_once get_template_directory() . '/inc/helpers/schema.php';

Optional post meta fields this file supports

You can later add these custom fields by ACF, Meta Box plugin, or your own admin panel:

_rx_is_medical_article
_rx_last_reviewed
_rx_reviewed_by_name
_rx_reviewed_by_url
_rx_medical_condition_name
_rx_medical_condition_description
_rx_medical_condition_causes
_rx_medical_condition_symptoms
_rx_medical_condition_tests
_rx_medical_condition_treatments
_rx_medical_anatomy
_rx_faqs
_rx_howto_name
_rx_howto_description
_rx_howto_steps

For example, _rx_medical_condition_symptoms can be:

Pain
Fever
Swelling
Fatigue
Headache

Or JSON:

["Pain", "Fever", "Swelling", "Fatigue", "Headache"]

This file is safe for your RX medical theme and gives you a strong SEO schema foundation.

Leave a Reply

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