organization(); if ( $org ) { $graph[] = $org; } $graph[] = $this->website(); if ( is_singular( array( 'post' ) ) ) { $article = $this->article( get_post() ); if ( $article ) { $graph[] = $article; } $breadcrumb = $this->breadcrumb( get_post() ); if ( $breadcrumb ) { $graph[] = $breadcrumb; } } // Enhancements recorded by the Pro bulk-fixer (FAQ / HowTo) on any singular. if ( is_singular() ) { foreach ( $this->enhancement_nodes( get_post() ) as $node ) { $graph[] = $node; } } $graph = apply_filters( 'evoka_schema_graph', array_filter( $graph ) ); if ( empty( $graph ) ) { return; } $payload = array( '@context' => 'https://schema.org', '@graph' => array_values( $graph ), ); echo "\n\n"; echo '\n"; } /** * Organization node. * * @return array|null */ private function organization() { $name = evoka_plain_text( get_bloginfo( 'name' ) ); if ( '' === trim( (string) $name ) ) { return null; } $node = array( '@type' => 'Organization', '@id' => home_url( '/#organization' ), 'name' => $name, 'url' => home_url( '/' ), ); $logo_id = (int) get_theme_mod( 'custom_logo' ); if ( $logo_id ) { $src = wp_get_attachment_image_url( $logo_id, 'full' ); if ( $src ) { $node['logo'] = array( '@type' => 'ImageObject', 'url' => $src, ); } } // Entity signals: "who else identifies this as you" (sameAs) and "what // this org is known for" (knowsAbout) - both strengthen the entity // AI engines build for a brand, independent of any one page. $same_as = self::parse_lines( get_option( 'evoka_org_sameas', '' ), 'esc_url_raw' ); if ( ! empty( $same_as ) ) { $node['sameAs'] = $same_as; } $knows_about = self::parse_lines( get_option( 'evoka_org_knows_about', '' ), 'evoka_plain_text' ); if ( ! empty( $knows_about ) ) { $node['knowsAbout'] = $knows_about; } return $node; } /** * WebSite node (with SearchAction). * * @return array */ private function website() { $node = array( '@type' => 'WebSite', '@id' => home_url( '/#website' ), 'name' => evoka_plain_text( get_bloginfo( 'name' ) ), 'url' => home_url( '/' ), 'potentialAction' => array( '@type' => 'SearchAction', 'target' => array( '@type' => 'EntryPoint', 'urlTemplate' => home_url( '/?s={search_term_string}' ), ), 'query-input' => 'required name=search_term_string', ), ); // Omit an empty description rather than emitting "". $description = evoka_plain_text( get_bloginfo( 'description' ) ); if ( '' !== $description ) { $node['description'] = $description; } return $node; } /** * Article node for a single post, including author Person (E-E-A-T) and * Speakable hints. * * @param WP_Post|null $post Post object. * @return array|null */ private function article( $post ) { if ( ! $post instanceof WP_Post ) { return null; } $node = array( '@type' => 'Article', '@id' => get_permalink( $post ) . '#article', 'headline' => evoka_plain_text( get_the_title( $post ) ), 'datePublished' => get_post_time( 'c', true, $post ), 'dateModified' => get_post_modified_time( 'c', true, $post ), 'mainEntityOfPage' => get_permalink( $post ), 'author' => $this->author( (int) $post->post_author ), 'publisher' => array( '@id' => home_url( '/#organization' ) ), 'speakable' => array( '@type' => 'SpeakableSpecification', 'cssSelector' => array( 'h1', '.entry-content p:first-of-type' ), ), ); $excerpt = has_excerpt( $post ) ? evoka_plain_text( get_the_excerpt( $post ) ) : evoka_trim_words( evoka_plain_text( strip_shortcodes( $post->post_content ) ), 40 ); if ( $excerpt ) { $node['description'] = $excerpt; } if ( has_post_thumbnail( $post ) ) { $img = wp_get_attachment_image_url( get_post_thumbnail_id( $post ), 'full' ); if ( $img ) { $node['image'] = $img; } } return $node; } /** * Author Person node with credential hints for E-E-A-T. * * @param int $author_id Author user ID. * @return array */ private function author( $author_id ) { $node = array( '@type' => 'Person', 'name' => evoka_plain_text( get_the_author_meta( 'display_name', $author_id ) ), 'url' => get_author_posts_url( $author_id ), ); $bio = get_the_author_meta( 'description', $author_id ); if ( $bio ) { $node['description'] = evoka_plain_text( $bio ); } // Credential / job title give answer engines an expertise signal. $job = get_the_author_meta( 'evoka_job_title', $author_id ); if ( $job ) { $node['jobTitle'] = evoka_plain_text( $job ); } $same_as = array_filter( array( get_the_author_meta( 'url', $author_id ), get_the_author_meta( 'evoka_linkedin', $author_id ), ) ); if ( ! empty( $same_as ) ) { $node['sameAs'] = array_values( $same_as ); } return $node; } /** * BreadcrumbList node for a single post. * * @param WP_Post|null $post Post object. * @return array|null */ private function breadcrumb( $post ) { if ( ! $post instanceof WP_Post ) { return null; } $items = array( array( '@type' => 'ListItem', 'position' => 1, 'name' => __( 'Home', 'evoka' ), 'item' => home_url( '/' ), ), array( '@type' => 'ListItem', 'position' => 2, 'name' => evoka_plain_text( get_the_title( $post ) ), 'item' => get_permalink( $post ), ), ); return array( '@type' => 'BreadcrumbList', 'itemListElement' => $items, ); } /** * Build FAQ / HowTo nodes from enhancements stored by the Pro bulk-fixer. * * @param WP_Post|null $post Post object. * @return array List of JSON-LD nodes (possibly empty). */ private function enhancement_nodes( $post ) { $nodes = array(); if ( ! $post instanceof WP_Post ) { return $nodes; } $enh = get_post_meta( $post->ID, '_evoka_enhancements', true ); if ( ! is_array( $enh ) ) { return $nodes; } // FAQPage. if ( ! empty( $enh['faq'] ) && is_array( $enh['faq'] ) ) { $items = array(); foreach ( $enh['faq'] as $pair ) { if ( empty( $pair['question'] ) || empty( $pair['answer'] ) ) { continue; } $items[] = array( '@type' => 'Question', 'name' => evoka_plain_text( $pair['question'] ), 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => evoka_plain_text( $pair['answer'] ), ), ); } if ( ! empty( $items ) ) { $nodes[] = array( '@type' => 'FAQPage', '@id' => get_permalink( $post ) . '#faq', 'mainEntity' => $items, ); } } // HowTo - only with real steps (schema.org/Google require "step"). // Older (v0.2) meta stored `true` with no steps; that is ignored until // the bulk fix is re-run. if ( ! empty( $enh['howto']['steps'] ) && is_array( $enh['howto']['steps'] ) ) { $steps = array(); $position = 0; foreach ( $enh['howto']['steps'] as $step ) { $text = isset( $step['text'] ) ? evoka_plain_text( $step['text'] ) : ''; if ( '' === $text ) { continue; } $node = array( '@type' => 'HowToStep', 'position' => ++$position, 'text' => $text, ); if ( ! empty( $step['name'] ) ) { $node['name'] = evoka_plain_text( $step['name'] ); } $steps[] = $node; } if ( count( $steps ) >= 2 ) { $nodes[] = array( '@type' => 'HowTo', '@id' => get_permalink( $post ) . '#howto', 'name' => evoka_plain_text( get_the_title( $post ) ), 'step' => $steps, ); } } return $nodes; } /** * Split a textarea setting into clean, unique, non-empty lines. * * @param string $value Raw newline-separated setting value. * @param callable $sanitize Per-line sanitizer, e.g. 'esc_url_raw'. * @return array */ private static function parse_lines( $value, $sanitize ) { $lines = preg_split( '/[\r\n]+/', (string) $value ); $lines = array_map( 'trim', (array) $lines ); $lines = array_map( $sanitize, $lines ); $lines = array_filter( $lines, function ( $l ) { return '' !== $l; } ); return array_values( array_unique( $lines ) ); } }