*/ private static $base_matrix = array( // Homepage: LocalBusiness covers all local business sites; the builder // resolves the precise subtype (Plumber, Dentist, etc.) from settings. 'homepage' => array( 'LocalBusiness', 'WebSite', 'WebPage', ), 'about_page' => array( 'AboutPage', 'BreadcrumbList', ), 'contact_page' => array( 'ContactPage', 'BreadcrumbList', ), // Each location gets its own full LocalBusiness entity. 'location_page' => array( 'LocalBusiness', 'WebPage', 'BreadcrumbList', ), // Service listing overview. 'service_hub' => array( 'WebPage', 'BreadcrumbList', ), // Individual service detail. 'service_page' => array( 'Service', 'WebPage', 'BreadcrumbList', ), 'blog_post' => array( 'Article', 'WebPage', 'BreadcrumbList', ), 'blog_archive' => array( 'WebPage', ), 'product_page' => array( 'Product', 'WebPage', 'BreadcrumbList', ), 'dedicated_faq_page' => array( 'FAQPage', 'WebPage', 'BreadcrumbList', ), // Legal pages produce no schema output. 'legal_page' => array(), // Event schema is valid regardless of site type. 'event_page' => array( 'Event', 'WebPage', 'BreadcrumbList', ), // Course page — local businesses default to generic WebPage. // Overridden by course_education group. 'course_page' => array( 'WebPage', 'BreadcrumbList', ), // Recipe — valid for restaurant/food local businesses. 'recipe_page' => array( 'Recipe', 'WebPage', 'BreadcrumbList', ), // Podcast episode — local businesses fall back to generic WebPage. // Overridden by podcast group. 'podcast_episode' => array( 'WebPage', 'BreadcrumbList', ), // Software product page — falls back to generic WebPage unless overridden. 'software_product_page' => array( 'WebPage', 'BreadcrumbList', ), // Portfolio item — falls back to generic WebPage unless overridden by // portfolio_agency group. Local businesses with case-study pages get WebPage // not CreativeWork, so their pages aren't mis-tagged as agency portfolios. 'portfolio_item' => array( 'WebPage', 'BreadcrumbList', ), 'generic_page' => array( 'WebPage', 'BreadcrumbList', ), ); /** * Per-group overrides. * * Each entry replaces the base_matrix value for the keyed page types. * Only page types that actually differ need to be listed here. * * @var array> */ private static $group_overrides = array( // Online Store / Nonprofit / Community — Organization homepage, no local pages. 'non_local' => array( 'homepage' => array( 'Organization', 'WebSite', 'WebPage', ), // Location pages don't make sense for non-local sites. 'location_page' => array( 'WebPage', 'BreadcrumbList', ), ), // Blog / Content Site. 'blog_content' => array( 'homepage' => array( 'Organization', 'WebSite', 'WebPage', ), 'location_page' => array( 'WebPage', 'BreadcrumbList', ), // Content sites don't have service pages. 'service_page' => array( 'WebPage', 'BreadcrumbList', ), 'service_hub' => array( 'WebPage', 'BreadcrumbList', ), ), // News / Magazine / Media — NewsArticle for posts, org on homepage. 'news_media' => array( 'homepage' => array( // Organization builder reads site type → outputs NewsMediaOrganization. 'Organization', 'WebSite', 'WebPage', ), 'location_page' => array( 'WebPage', 'BreadcrumbList', ), 'blog_post' => array( 'NewsArticle', 'WebPage', 'BreadcrumbList', ), 'service_page' => array( 'WebPage', 'BreadcrumbList', ), 'service_hub' => array( 'WebPage', 'BreadcrumbList', ), ), // Course / Online Education — Course schema on course pages. 'course_education' => array( 'homepage' => array( // Organization builder → EducationalOrganization. 'Organization', 'WebSite', 'WebPage', ), 'location_page' => array( 'WebPage', 'BreadcrumbList', ), 'course_page' => array( 'Course', 'WebPage', 'BreadcrumbList', ), ), // Software / SaaS / App — SoftwareApplication on feature/product pages. 'software_saas' => array( 'homepage' => array( 'Organization', 'WebSite', 'WebPage', ), 'location_page' => array( 'WebPage', 'BreadcrumbList', ), 'software_product_page' => array( 'SoftwareApplication', 'WebPage', 'BreadcrumbList', ), ), // Podcast — PodcastEpisode on episode pages. 'podcast' => array( 'homepage' => array( 'Organization', 'WebSite', 'WebPage', ), 'location_page' => array( 'WebPage', 'BreadcrumbList', ), 'podcast_episode' => array( 'PodcastEpisode', 'WebPage', 'BreadcrumbList', ), 'service_page' => array( 'WebPage', 'BreadcrumbList', ), 'service_hub' => array( 'WebPage', 'BreadcrumbList', ), ), // Portfolio / Creative Agency — CreativeWork on portfolio items. 'portfolio_agency' => array( 'homepage' => array( 'Organization', 'WebSite', 'WebPage', ), 'location_page' => array( 'WebPage', 'BreadcrumbList', ), 'portfolio_item' => array( 'CreativeWork', 'WebPage', 'BreadcrumbList', ), ), // Government. 'government' => array( 'homepage' => array( // Organization builder → GovernmentOrganization. 'Organization', 'WebSite', 'WebPage', ), 'location_page' => array( 'WebPage', 'BreadcrumbList', ), ), ); /** * Hard exclusion rules — schema types that must never appear on certain page types. * * Applied after the matrix lookup regardless of what the matrix says. * * @var array */ private static $exclusions = array( // FAQPage restricted to genuine dedicated FAQ pages (post-March 2026 Google). 'FAQPage' => array( 'service_page', 'product_page', 'blog_post', 'homepage', 'about_page', 'contact_page', 'location_page', 'service_hub', 'blog_archive', 'legal_page', 'generic_page', 'event_page', 'course_page', 'recipe_page', 'podcast_episode', 'software_product_page', 'portfolio_item', ), // Service schema is meaningful only on a dedicated service page. 'Service' => array( 'homepage', 'about_page', 'contact_page', 'location_page', 'service_hub', 'blog_post', 'blog_archive', 'product_page', 'dedicated_faq_page', 'legal_page', 'generic_page', 'event_page', 'course_page', 'recipe_page', 'podcast_episode', 'software_product_page', 'portfolio_item', ), // Organization is defined on the homepage only. 'Organization' => array( 'about_page', 'contact_page', 'location_page', 'service_hub', 'service_page', 'blog_post', 'blog_archive', 'product_page', 'dedicated_faq_page', 'legal_page', 'generic_page', 'event_page', 'course_page', 'recipe_page', 'podcast_episode', 'software_product_page', 'portfolio_item', ), // LocalBusiness is only appropriate on homepage / location pages. 'LocalBusiness' => array( 'about_page', 'contact_page', 'service_hub', 'service_page', 'blog_post', 'blog_archive', 'product_page', 'dedicated_faq_page', 'legal_page', 'generic_page', 'event_page', 'course_page', 'recipe_page', 'podcast_episode', 'software_product_page', 'portfolio_item', ), // CreativeWork only on portfolio_item pages. 'CreativeWork' => array( 'homepage', 'about_page', 'contact_page', 'location_page', 'service_hub', 'service_page', 'blog_post', 'blog_archive', 'product_page', 'dedicated_faq_page', 'legal_page', 'generic_page', 'event_page', 'course_page', 'recipe_page', 'podcast_episode', 'software_product_page', ), // PodcastEpisode only on podcast_episode pages. 'PodcastEpisode' => array( 'homepage', 'about_page', 'contact_page', 'location_page', 'service_hub', 'service_page', 'blog_post', 'blog_archive', 'product_page', 'dedicated_faq_page', 'legal_page', 'generic_page', 'event_page', 'course_page', 'recipe_page', 'software_product_page', 'portfolio_item', ), // SoftwareApplication only on software_product_page. 'SoftwareApplication' => array( 'homepage', 'about_page', 'contact_page', 'location_page', 'service_hub', 'service_page', 'blog_post', 'blog_archive', 'product_page', 'dedicated_faq_page', 'legal_page', 'generic_page', 'event_page', 'course_page', 'recipe_page', 'podcast_episode', 'portfolio_item', ), ); /** * Get the ordered list of schema types to generate for a given page type. * * Reads the current site type group, applies group overrides on top of the * base matrix, then enforces hard exclusions. * * Returns an empty array for legal pages (caller should skip output entirely). * * @param string $page_type One of Schema_AI_Page_Analyzer::$page_types. * @return string[] Ordered schema.org type names. */ public static function get_schema_types( $page_type ) { $matrix = self::get_resolved_matrix(); $types = isset( $matrix[ $page_type ] ) ? $matrix[ $page_type ] : $matrix['generic_page']; return self::apply_exclusions( $page_type, $types ); } /** * Check whether a schema type is allowed on a page type. * * @param string $schema_type Schema.org type string. * @param string $page_type Page type slug. * @return bool */ public static function is_allowed( $schema_type, $page_type ) { if ( ! isset( self::$exclusions[ $schema_type ] ) ) { return true; } return ! in_array( $page_type, self::$exclusions[ $schema_type ], true ); } /** * Get the full resolved matrix for the current site type group. * * Merges the base matrix with the active group's overrides. * * @return array */ public static function get_matrix() { return self::get_resolved_matrix(); } // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- /** * Merge base matrix with overrides for the active decision group. * * @return array */ private static function get_resolved_matrix() { $group = class_exists( 'Schema_AI_Site_Type' ) ? Schema_AI_Site_Type::get_decision_group() : 'local_business'; if ( ! isset( self::$group_overrides[ $group ] ) ) { return self::$base_matrix; } return array_merge( self::$base_matrix, self::$group_overrides[ $group ] ); } /** * Remove schema types that are excluded for the given page type. * * @param string $page_type Page type slug. * @param string[] $types Proposed schema type list. * @return string[] */ private static function apply_exclusions( $page_type, $types ) { return array_values( array_filter( $types, function ( $type ) use ( $page_type ) { return self::is_allowed( $type, $page_type ); } ) ); } }