$org_type, '@id' => $org_id, 'name' => ! empty( $settings['organization_name'] ) ? $settings['organization_name'] : get_bloginfo( 'name' ), 'url' => $site_url, ); if ( ! empty( $settings['organization_logo'] ) ) { $org_node['logo'] = array( '@type' => 'ImageObject', 'url' => $settings['organization_logo'], ); } if ( ! empty( $settings['local_phone'] ) ) { $org_node['telephone'] = $settings['local_phone']; $org_node['priceRange'] = $settings['local_price_range'] ?? '$$'; } if ( ! empty( $settings['local_street'] ) || ! empty( $settings['local_city'] ) ) { $org_node['address'] = array( '@type' => 'PostalAddress', 'streetAddress' => $settings['local_street'] ?? '', 'addressLocality' => $settings['local_city'] ?? '', 'addressRegion' => $settings['local_region'] ?? '', 'postalCode' => $settings['local_postal'] ?? '', 'addressCountry' => $settings['local_country'] ?? '', ); } if ( ! empty( $settings['local_lat'] ) && ! empty( $settings['local_lng'] ) ) { $org_node['geo'] = array( '@type' => 'GeoCoordinates', 'latitude' => $settings['local_lat'], 'longitude' => $settings['local_lng'], ); } if ( ! empty( $settings['local_opening_hours'] ) && $has_local ) { $org_node['openingHours'] = $settings['local_opening_hours']; } $graph[] = $org_node; $graph[] = array( '@type' => 'WebSite', '@id' => $web_id, 'url' => $site_url, 'name' => get_bloginfo( 'name' ), 'publisher' => array( '@id' => $org_id ), 'potentialAction' => array( '@type' => 'SearchAction', 'target' => array( '@type' => 'EntryPoint', 'urlTemplate' => $site_url . '?s={search_term_string}', ), 'query-input' => 'required name=search_term_string', ), ); $crumbs = Frontend_Head::get_breadcrumb_items(); if ( count( $crumbs ) > 1 ) { $item_list = array(); foreach ( $crumbs as $idx => $crumb ) { $item_list[] = array( '@type' => 'ListItem', 'position' => $idx + 1, 'name' => $crumb['name'], 'item' => $crumb['url'], ); } $graph[] = array( '@type' => 'BreadcrumbList', '@id' => get_permalink() . '#breadcrumb', 'itemListElement' => $item_list, ); } if ( is_singular() ) { $post = get_queried_object(); if ( $post instanceof \WP_Post ) { $meta = Core::get_meta( (int) $post->ID, 'post' ); $schema_type = $meta && ! empty( $meta->schema_type ) ? $meta->schema_type : 'Article'; $permalink = (string) get_permalink( $post->ID ); $author_id = (int) $post->post_author; $person_id = $site_url . '#/schema/person/' . $author_id; $graph[] = $this->build_author_eeat_node( $author_id, $person_id ); $entity_node = $this->build_singular_entity_node( $post, $meta, $schema_type, $permalink, $person_id, $org_id, $web_id ); $graph[] = $entity_node; $auto_faq = $this->extract_faq_node_from_content( $post->post_content, $permalink ); if ( ! empty( $auto_faq ) && 'FAQPage' !== $schema_type ) { $graph[] = $auto_faq; } } } $payload = array( '@context' => 'https://schema.org', '@graph' => $graph, ); $encoded_json = wp_json_encode( $payload, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); if ( is_string( $encoded_json ) && '' !== $encoded_json ) { wp_print_inline_script_tag( $encoded_json, array( 'type' => 'application/ld+json', 'class' => 'modulustools-schema-graph', ) ); } } /** * Builds the Author `Person` Schema node enriched with E-E-A-T user metadata. * * @param int $user_id WordPress User ID. * @param string $person_id Schema `@id` URI. * @return array */ private function build_author_eeat_node( int $user_id, string $person_id ): array { $user = get_userdata( $user_id ); $node = array( '@type' => 'Person', '@id' => $person_id, 'name' => $user ? $user->display_name : get_bloginfo( 'name' ), 'url' => get_author_posts_url( $user_id ), ); if ( $user && ! empty( $user->description ) ) { $node['description'] = $user->description; } $job_title = get_user_meta( $user_id, 'modulustools_eeat_job_title', true ); $credentials = get_user_meta( $user_id, 'modulustools_eeat_credentials', true ); $alumni_of = get_user_meta( $user_id, 'modulustools_eeat_alumni_of', true ); $knows_about = get_user_meta( $user_id, 'modulustools_eeat_knows_about', true ); $same_as_raw = get_user_meta( $user_id, 'modulustools_eeat_same_as', true ); if ( ! empty( $job_title ) ) { $node['jobTitle'] = $job_title; } if ( ! empty( $credentials ) ) { $node['hasCredential'] = array( '@type' => 'EducationalOccupationalCredential', 'credentialCategory' => $credentials, ); } if ( ! empty( $alumni_of ) ) { $node['alumniOf'] = array( '@type' => 'CollegeOrUniversity', 'name' => $alumni_of, ); } if ( ! empty( $knows_about ) ) { $topics = array_filter( array_map( 'trim', explode( ',', (string) $knows_about ) ) ); if ( ! empty( $topics ) ) { $node['knowsAbout'] = array_values( $topics ); } } if ( ! empty( $same_as_raw ) ) { $links = array_filter( array_map( 'esc_url_raw', array_map( 'trim', explode( "\n", (string) $same_as_raw ) ) ) ); if ( ! empty( $links ) ) { $node['sameAs'] = array_values( $links ); } } return $node; } /** * Builds the primary singular Schema node. * * @param \WP_Post $post Current post object. * @param object|null $meta Custom table row. * @param string $schema_type Entity type. * @param string $permalink Canonical URL. * @param string $person_id Author `@id`. * @param string $org_id Organization `@id`. * @param string $web_id WebSite `@id`. * @return array */ private function build_singular_entity_node( \WP_Post $post, ?object $meta, string $schema_type, string $permalink, string $person_id, string $org_id, string $web_id ): array { $headline = $meta && ! empty( $meta->title ) ? $meta->title : get_the_title( $post ); $description = $meta && ! empty( $meta->description ) ? $meta->description : wp_trim_words( wp_strip_all_tags( $post->post_content ), 28 ); $settings = get_option( 'modulustools_seo_settings', array() ); if ( 'Product' === $schema_type ) { $brand_name = $meta && ! empty( $meta->product_brand ) ? $meta->product_brand : ( $settings['organization_name'] ?? get_bloginfo( 'name' ) ); $return_days = absint( $settings['merchant_return_days'] ?? 30 ); $product_node = array( '@type' => 'Product', '@id' => $permalink . '#product', 'name' => $headline, 'description' => $description, 'url' => $permalink, 'brand' => array( '@type' => 'Brand', 'name' => $brand_name, ), ); if ( $meta && ! empty( $meta->product_gtin ) ) { $product_node['gtin'] = $meta->product_gtin; } if ( $meta && ! empty( $meta->product_mpn ) ) { $product_node['mpn'] = $meta->product_mpn; } if ( has_post_thumbnail( $post->ID ) ) { $product_node['image'] = (string) get_the_post_thumbnail_url( $post->ID, 'full' ); } $offer = array( '@type' => 'Offer', 'url' => $permalink, 'priceCurrency' => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'USD', 'price' => '0.00', 'availability' => 'https://schema.org/InStock', 'hasMerchantReturnPolicy' => array( '@type' => 'MerchantReturnPolicy', 'returnPolicyCategory' => 'https://schema.org/MerchantReturnFiniteReturnWindow', 'merchantReturnDays' => $return_days, 'returnMethod' => 'https://schema.org/ReturnByMail', ), ); if ( function_exists( 'wc_get_product' ) ) { $wc_product = wc_get_product( $post->ID ); if ( $wc_product ) { $product_node['sku'] = $wc_product->get_sku(); $offer['price'] = $wc_product->get_price(); $offer['availability'] = $wc_product->is_in_stock() ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock'; } } $product_node['offers'] = $offer; return $product_node; } $node = array( '@type' => $schema_type, '@id' => $permalink . '#primaryentity', 'headline' => $headline, 'description' => $description, 'url' => $permalink, 'datePublished' => get_the_date( DATE_W3C, $post ), 'dateModified' => get_the_modified_date( DATE_W3C, $post ), 'author' => array( '@id' => $person_id ), 'publisher' => array( '@id' => $org_id ), 'isPartOf' => array( '@id' => $web_id ), 'mainEntityOfPage' => $permalink, ); if ( has_post_thumbnail( $post->ID ) ) { $node['image'] = (string) get_the_post_thumbnail_url( $post->ID, 'full' ); } if ( $meta && ! empty( $meta->eeat_reviewed_by ) ) { $node['reviewedBy'] = array( '@type' => 'Person', 'name' => $meta->eeat_reviewed_by, ); } if ( $meta && ! empty( $meta->eeat_citations ) ) { $citations = array_filter( array_map( 'esc_url_raw', array_map( 'trim', explode( "\n", (string) $meta->eeat_citations ) ) ) ); if ( ! empty( $citations ) ) { $node['citation'] = array_values( $citations ); } } return $node; } /** * Automatically extracts Question-and-Answer H2/H3 blocks into a `FAQPage` graph node. * * @param string $content Post HTML content. * @param string $permalink Post URL. * @return array */ private function extract_faq_node_from_content( string $content, string $permalink ): array { if ( ! preg_match_all( '/]*>(.*?\?)<\/h[23]>\s*

(.*?)<\/p>/is', $content, $matches, PREG_SET_ORDER ) ) { return array(); } $questions = array(); foreach ( $matches as $match ) { $q_text = trim( wp_strip_all_tags( $match[1] ) ); $a_text = trim( wp_strip_all_tags( $match[2] ) ); if ( '' !== $q_text && '' !== $a_text ) { $questions[] = array( '@type' => 'Question', 'name' => $q_text, 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => $a_text, ), ); } } if ( empty( $questions ) ) { return array(); } return array( '@type' => 'FAQPage', '@id' => $permalink . '#faq', 'mainEntity' => $questions, ); } /** * Renders E-E-A-T Author Credential fields on the WordPress User Profile screen. * * @param \WP_User $user User object. * @return void */ public function render_user_eeat_fields( \WP_User $user ): void { wp_nonce_field( 'modulustools_save_user_eeat', 'modulustools_user_eeat_nonce' ); ?>