post_status ) { return null; } // @phan-suppress-next-line PhanUndeclaredClassMethod -- Jetpack_SEO_Posts lives in plugins/jetpack; Schema_Builder::emit() guards on class_exists. $override = Jetpack_SEO_Posts::get_post_schema_type( $post ); $type = '' !== $override ? $override : self::default_schema_for_post( $post ); switch ( $type ) { case 'faq': return self::build_faq( $post ); case 'article': return self::build_article( $post ); default: return null; } } /** * Default Schema type for a post when the user has not set an override: * Article for standard posts, none for pages, attachments, or custom types. * * @param WP_Post $post The post. * @return string */ private static function default_schema_for_post( WP_Post $post ) { // Only standard posts get Article schema by default; everything else // (pages, attachments, custom post types) requires an explicit override. return 'post' === $post->post_type ? 'article' : ''; } /** * Article JSON-LD. * * @param WP_Post $post The post. * @return array */ private static function build_article( WP_Post $post ) { $node = array( '@type' => 'Article', 'headline' => wp_strip_all_tags( get_the_title( $post ) ), 'datePublished' => get_post_time( 'c', true, $post ), 'dateModified' => get_post_modified_time( 'c', true, $post ), 'mainEntityOfPage' => array( '@type' => 'WebPage', '@id' => get_permalink( $post ), ), ); $image = get_the_post_thumbnail_url( $post, 'full' ); if ( $image ) { $node['image'] = $image; } // @phan-suppress-next-line PhanUndeclaredClassMethod -- Jetpack_SEO_Posts lives in plugins/jetpack; Schema_Builder::emit() guards on class_exists. $description = Jetpack_SEO_Posts::get_post_description( $post ); if ( $description ) { // Cap it: get_post_description() falls back to full post_content, which // would otherwise dump the whole body into the markup. $node['description'] = wp_trim_words( wp_strip_all_tags( $description ), self::DESCRIPTION_MAX_WORDS, '' ); } return $node; } /** * FAQPage JSON-LD, parsed from `core/details` blocks (summary = question, * rendered content = answer). Returns null when the post has none, so we * never emit an empty/invalid FAQPage. * * @param WP_Post $post The post. * @return array|null */ private static function build_faq( WP_Post $post ) { if ( ! function_exists( 'parse_blocks' ) ) { return null; } // render_block() skips the `the_content` chain, so the paywall never sees this text. if ( Content_Gate::is_gated( $post ) ) { return null; } $items = array(); foreach ( parse_blocks( $post->post_content ) as $block ) { if ( 'core/details' !== ( $block['blockName'] ?? '' ) ) { continue; } $question = trim( html_entity_decode( wp_strip_all_tags( (string) ( $block['innerHTML'] ?? '' ) ), ENT_QUOTES, 'UTF-8' ) ); // Render only the inner blocks for the answer. Rendering the whole // core/details block would re-include the (the question). $answer_html = ''; foreach ( $block['innerBlocks'] ?? array() as $inner_block ) { $answer_html .= render_block( $inner_block ); } $answer = trim( html_entity_decode( wp_strip_all_tags( $answer_html ), ENT_QUOTES, 'UTF-8' ) ); if ( '' === $question || '' === $answer ) { continue; } $items[] = array( '@type' => 'Question', 'name' => $question, 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => $answer, ), ); } if ( empty( $items ) ) { return null; } return array( '@type' => 'FAQPage', 'mainEntity' => $items, ); } }