self::is_protocol_support_enabled(), 'public_listing_consent' => self::has_protocol_public_listing_consent(), 'site_url' => $site_url, 'site_name' => $name, 'industry' => $industry, 'schema_type' => 'Person' === $type ? 'Person' : 'Organization', 'public_id' => $public_id, 'protocol_url' => AISH_REST::get_protocol_support_target_url(), 'protocol_name' => AISH_REST::get_protocol_support_target_name(), ); } /** * Get active partners as Schema.org entities. * * @return array */ private function get_partners_data() { $partners = AISH_Partner_Store::get_all(); $active_partners = array(); if ( ! empty( $partners ) ) { foreach ( $partners as $partner ) { if ( isset( $partner['status'] ) && 'active' === $partner['status'] ) { $relation_type = isset( $partner['my_relation_to_partner'] ) ? AISH_REST::sanitize_relation_type( $partner['my_relation_to_partner'] ) : 'collaboration'; $relation_prop = $this->map_relation_to_schema_property( $relation_type ); $entry_type = ( ! empty( $partner['partner_type'] ) && 'Person' === $partner['partner_type'] ) ? 'Person' : 'Organization'; $entry = array( '@type' => $entry_type, 'url' => esc_url( $partner['url'] ), ); if ( ! empty( $partner['partner_name'] ) ) { $entry['name'] = sanitize_text_field( $partner['partner_name'] ); } if ( ! isset( $active_partners[ $relation_prop ] ) ) { $active_partners[ $relation_prop ] = array(); } $active_partners[ $relation_prop ][] = $entry; } } } return $active_partners; } /** * Get my identity data as a Schema.org entity. * * @return array */ private function get_my_identity() { $name = trim( (string) carbon_get_theme_option( 'aish_my_business_name' ) ); if ( '' === $name ) { $name = get_bloginfo( 'name' ); } $identity = array( '@context' => 'https://schema.org', '@type' => $this->get_schema_type(), 'name' => $name, 'url' => get_site_url(), ); $industry = carbon_get_theme_option( 'aish_my_industry' ); if ( ! empty( $industry ) ) { $identity['description'] = $industry; } // Generate dynamic proofs as identifiers for active partners only. $proofs = $this->get_identity_proofs(); if ( ! empty( $proofs ) ) { $identity['identifier'] = $proofs; } return $identity; } /** * Inject standalone Schema into wp_head. * Falls back to standalone output when SEO plugin filters didn't inject partner data. */ public function inject_schema() { if ( ! $this->should_output() ) { return; } $seo_active = defined( 'WPSEO_VERSION' ) || defined( 'RANK_MATH_VERSION' ); if ( ! ( $seo_active && $this->seo_filter_injected ) ) { $identity = $this->get_my_identity(); $partners = $this->get_partners_data(); if ( ! empty( $partners ) ) { foreach ( $partners as $relation_prop => $entries ) { $identity[ $relation_prop ] = $entries; } } $this->render_json_ld_script( $identity ); } $support_schema = $this->get_protocol_support_schema(); if ( ! empty( $support_schema ) ) { $this->render_json_ld_script( $support_schema ); } } /** * Build the independent protocol support Schema node. * * @return array */ private function get_protocol_support_schema() { if ( ! self::is_protocol_support_enabled() ) { return array(); } $payload = self::get_protocol_support_payload(); return array( '@context' => 'https://schema.org', '@type' => 'CreativeWork', '@id' => trailingslashit( $payload['site_url'] ) . '#aish-protocol-support', 'name' => __( 'AI Semantic Handshake Protocol Support', 'ai-semantic-handshake' ), 'publisher' => array( '@type' => $payload['schema_type'], 'name' => $payload['site_name'], 'url' => $payload['site_url'], ), 'isBasedOn' => array( '@type' => 'CreativeWork', 'name' => __( 'AI Semantic Handshake Protocol', 'ai-semantic-handshake' ), 'url' => $payload['protocol_url'], ), 'mentions' => array( array( '@type' => 'WebSite', 'name' => $payload['protocol_name'], 'url' => $payload['protocol_url'], ), ), ); } /** * Render a JSON-LD script block. * * @param array $data JSON-LD payload. * @return void */ private function render_json_ld_script( $data ) { if ( empty( $data ) || ! is_array( $data ) ) { return; } $json = wp_json_encode( $data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); if ( ! $json ) { return; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Safe JSON generated for application/ld+json script context. echo "\n\n"; } /** * Filter Yoast SEO Schema graph. * * @param array $data Schema graph pieces. * @return array */ public function filter_yoast_schema( $data ) { if ( ! $this->should_output() ) { return $data; } $partners = $this->get_partners_data(); $proofs = $this->get_identity_proofs(); foreach ( $data as &$node ) { if ( ! isset( $node['@type'] ) ) { continue; } $type = $node['@type']; $is_org = ( is_array( $type ) && ( in_array( 'Organization', $type, true ) || in_array( 'Person', $type, true ) ) ) || 'Organization' === $type || 'Person' === $type; if ( $is_org ) { // Inject identifier proofs. if ( ! empty( $proofs ) ) { $node['identifier'] = $proofs; } // Inject partner relationships. if ( ! empty( $partners ) ) { foreach ( $partners as $relation_prop => $entries ) { $node[ $relation_prop ] = $entries; } } $this->seo_filter_injected = true; break; } } return $data; } /** * Filter Rank Math Schema. * * @param array $data Schema data. * @return array */ public function filter_rank_math_schema( $data ) { if ( ! $this->should_output() ) { return $data; } $partners = $this->get_partners_data(); $proofs = $this->get_identity_proofs(); // Rank Math may use 'Organization' or 'Person' as top-level keys. $entity_types = array( 'Organization', 'Person' ); foreach ( $entity_types as $type ) { if ( isset( $data[ $type ] ) ) { if ( ! empty( $proofs ) ) { $data[ $type ]['identifier'] = $proofs; } if ( ! empty( $partners ) ) { foreach ( $partners as $relation_prop => $entries ) { $data[ $type ][ $relation_prop ] = $entries; } } $this->seo_filter_injected = true; break; } } return $data; } /** * Map relation type to schema property. * * @param string $relation_type Internal relation type. * @return string */ private function map_relation_to_schema_property( $relation_type ) { $map = array( 'sister_site' => 'sameAs', 'media_mention' => 'mentions', 'friendly_recommendation' => 'mentions', 'collaboration' => 'knowsAbout', 'client' => 'knowsAbout', 'vendor' => 'knowsAbout', 'strategic_partner' => 'knowsAbout', 'community_peer' => 'knowsAbout', ); $relation_type = AISH_REST::sanitize_relation_type( $relation_type ); return isset( $map[ $relation_type ] ) ? $map[ $relation_type ] : 'knowsAbout'; } /** * Get identity proofs for active partners only. * * @return array Array of SHA-256 proof hashes. */ private function get_identity_proofs() { $partners = AISH_Partner_Store::get_all(); $proofs = array(); if ( ! empty( $partners ) ) { foreach ( $partners as $partner ) { if ( ! empty( $partner['url'] ) && isset( $partner['status'] ) && 'active' === $partner['status'] ) { $proofs[] = AISH_REST::generate_proof( $partner['url'] ); } } } return $proofs; } }