render_main_sitemap();\n exit;\n }\n\n if ( get_query_var( 'pnseo_news_sitemap' ) ) {\n $this->render_news_sitemap();\n exit;\n }\n }\n\n public function force_news_article_type( $type, $indexable ) {\n if ( is_singular( 'post' ) ) {\n return 'NewsArticle';\n }\n\n if ( $indexable && isset( $indexable->object_type, $indexable->object_sub_type ) && 'post' === $indexable->object_type && 'post' === $indexable->object_sub_type ) {\n return 'NewsArticle';\n }\n\n return $type;\n }\n\n public function enhance_article_schema( $data, $context ) {\n if ( ! is_singular( 'post' ) ) {\n return $data;\n }\n\n $post_id = get_queried_object_id();\n if ( ! $post_id ) {\n return $data;\n }\n\n $description = trim( get_the_excerpt( $post_id ) );\n if ( '' === $description ) {\n $description = $this->build_excerpt( $post_id );\n }\n if ( '' !== $description ) {\n $data['description'] = wp_strip_all_tags( $description );\n }\n\n if ( empty( $data['image'] ) && has_post_thumbnail( $post_id ) ) {\n $data['image'] = [ '@id' => get_the_post_thumbnail_url( $post_id, 'full' ) ];\n }\n\n if ( empty( $data['articleSection'] ) ) {\n $terms = wp_get_post_terms( $post_id, 'category', [ 'fields' => 'names' ] );\n if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {\n $data['articleSection'] = array_values( array_filter( $terms ) );\n }\n }\n\n $data['publisher'] = [ '@id' => home_url( '/#newsmediaorganization' ) ];\n\n return $data;\n }\n\n public function enhance_organization_schema( $data ) {\n if ( ! is_array( $data ) ) {\n return $data;\n }\n\n $data['@type'] = 'NewsMediaOrganization';\n $data['name'] = 'Peack News';\n $data['url'] = home_url( '/' );\n\n if ( empty( $data['email'] ) ) {\n $data['email'] = 'editorial@peacknews.com';\n }\n\n return $data;\n }\n public function enhance_webpage_schema( $data, $context, $graph_piece, $graph ) {\n if ( is_author() ) {\n $data['description'] = 'Author archive and newsroom profile for Peack News.';\n }\n elseif ( is_category() ) {\n $term = get_queried_object();\n if ( $term && ! empty( $term->description ) ) {\n $data['description'] = wp_strip_all_tags( $term->description );\n }\n }\n elseif ( is_front_page() ) {\n $data['description'] = 'Peack News covers AI, cybersecurity, technology and world affairs with source-based reporting, explainers and accountable updates.';\n }\n\n return $data;\n }\n\n public function filter_webpage_type( $type ) {\n if ( is_author() ) {\n return [ 'CollectionPage', 'ProfilePage' ];\n }\n\n return $type;\n }\n\n public function disable_post_comments( $open, $post_id ) {\n if ( 'post' === get_post_type( $post_id ) ) {\n return false;\n }\n\n return $open;\n }\n\n public function render_breadcrumbs() {\n if ( is_front_page() || ! function_exists( 'yoast_breadcrumb' ) ) {\n return;\n }\n\n if ( ! is_singular() && ! is_category() && ! is_author() ) {\n return;\n }\n\n echo '
';\n yoast_breadcrumb( '' );\n echo '
';\n echo '

Peack News author

';\n echo '

' . esc_html( $author->display_name ) . '

';\n echo '

' . esc_html( $bio ) . '

';\n if ( '' !== $focus ) {\n echo '

Expertise focus: ' . esc_html( $focus ) . '

';\n }\n if ( '' !== $areas ) {\n echo '

Areas covered: ' . esc_html( $areas ) . '

';\n }\n if ( ! empty( $profiles ) ) {\n echo '

Profiles: ' . $this->build_profile_links_html( $profiles ) . '

';\n }\n echo '

Contact: editorial@peacknews.com

';\n echo '
';\n echo '

Coverage hub

';\n echo '

' . esc_html( single_cat_title( '', false ) ) . '

';\n echo '

' . esc_html( $description ) . '

';\n\n $hubs = $this->get_hub_configuration();\n if ( isset( $hubs[ $term->slug ] ) ) {\n $config = $hubs[ $term->slug ];\n $featured = get_posts(\n [\n 'post_type' => 'post',\n 'post_status' => 'publish',\n 'posts_per_page' => 1,\n 'orderby' => 'date',\n 'order' => 'DESC',\n 'ignore_sticky_posts' => true,\n 'category__in' => [ (int) $term->term_id ],\n ]\n );\n $pillar = ! empty( $config['pillar_slug'] ) ? get_page_by_path( $config['pillar_slug'], OBJECT, 'page' ) : null;\n $tags = [];\n if ( ! empty( $config['tag_slugs'] ) ) {\n $tags = get_terms(\n [\n 'taxonomy' => 'post_tag',\n 'hide_empty' => true,\n 'slug' => $config['tag_slugs'],\n ]\n );\n }\n\n echo '
';\n if ( ! empty( $config['subtopics'] ) ) {\n echo '

Subtopics: ' . esc_html( implode( ', ', $config['subtopics'] ) ) . '

';\n }\n if ( $pillar ) {\n echo '

Start here: ' . esc_html( get_the_title( $pillar ) ) . '

';\n }\n if ( ! empty( $tags ) && ! is_wp_error( $tags ) ) {\n echo '
Related topics:' . $this->build_term_chips( $tags ) . '
';\n }\n if ( ! empty( $featured ) ) {\n echo '';\n }\n echo '
';\n }\n\n echo '

Editor's note

" . esc_html( $editor_note ) . "

';\n echo '

Story details

';\n echo '';\n echo '

Key developments

Why this matters

' . esc_html( $why ) . '

Impact and next steps

' . esc_html( $impact ) . '

Background

' . esc_html( $background ) . '

Timeline

    ';\n foreach ( $timeline as $item ) {\n echo '
  1. ' . esc_html( $item ) . '
  2. ';\n }\n echo '

Source

This article is based on reporting from ' . esc_html( $host ? $host : $source ) . '.

';\n echo '

About the author

';\n echo '

' . esc_html( get_the_author_meta( 'display_name', $author_id ) ) . '

';\n if ( '' !== $author_bio ) {\n echo '

' . esc_html( $author_bio ) . '

';\n }\n if ( '' !== $focus ) {\n echo '

Expertise focus: ' . esc_html( $focus ) . '

';\n }\n if ( '' !== $areas ) {\n echo '

Areas covered: ' . esc_html( $areas ) . '

';\n }\n if ( ! empty( $profiles ) ) {\n echo '

Profiles: ' . $this->build_profile_links_html( $profiles ) . '

';\n }\n echo '

editorial@peacknews.com

';\n echo '

Draft trigger: ' . esc_html( implode( ', ', $report['failures'] ) ) . '

This article was moved to draft because these checks failed: ' . esc_html( $message ) . '.

' . PHP_EOL;\n foreach ( $items as $item ) {\n echo '' . $this->xml_escape( $item['loc'] ) . '' . $this->xml_escape( $item['lastmod'] ) . '' . PHP_EOL;\n }\n echo '' . PHP_EOL;\n foreach ( $posts as $news_post ) {\n echo '';\n echo '' . $this->xml_escape( get_permalink( $news_post ) ) . '';\n echo '';\n echo 'Peack Newsen';\n echo '' . $this->xml_escape( get_post_time( DATE_W3C, true, $news_post ) ) . '';\n echo '' . $this->xml_escape( wp_strip_all_tags( get_the_title( $news_post ) ) ) . '';\n echo '';\n echo '' . PHP_EOL;\n }\n echo '
';\n if ( $hero ) {\n $hero_image = get_the_post_thumbnail_url( $hero, 'large' );\n $hero_summary = get_the_excerpt( $hero );\n if ( '' === trim( $hero_summary ) ) {\n $hero_summary = $this->build_excerpt( $hero->ID );\n }\n $html .= '
';\n if ( $hero_image ) {\n $html .= '
 . esc_attr( $this->get_featured_image_alt_text( $hero->ID ) ) .
';\n }\n $html .= '
';\n $html .= '

Top story

';\n $html .= '

' . esc_html( get_the_title( $hero ) ) . '

';\n $html .= '

' . esc_html( $this->get_primary_category_label( $hero->ID ) ) . ' | ' . esc_html( get_the_date( 'F j, Y', $hero ) ) . '

';\n if ( '' !== trim( $hero_summary ) ) {\n $html .= '

' . esc_html( $hero_summary ) . '

';\n }\n $html .= '';\n $html .= '
';\n }\n $html .= '
';\n $html .= '

Editor’s Picks

A smaller, more deliberate selection from Peack News coverage across core authority beats.

' . $this->build_compact_post_list( $editors_picks, true ) . '
';\n $analysis_pages = array_values( array_filter( $pillar_pages ) );\n $html .= '

Deep Analysis

These guides connect fast-moving headlines to the wider policy, market and geopolitical picture.

' . $this->build_page_list_html( $analysis_pages ) . '
';\n $html .= '
';\n $html .= '

Trending Topics

Key subjects running through the latest reporting and explainer coverage.

' . $this->build_term_chips( $trending_terms ) . '
';\n $html .= '

Topical Hubs

';\n foreach ( $hub_config as $slug => $config ) {\n $term = get_term_by( 'slug', $slug, 'category' );\n if ( ! $term || is_wp_error( $term ) ) {\n continue;\n }\n $featured_posts = ! empty( $config['featured_post_ids'] ) ? $this->get_curated_posts_by_ids( $config['featured_post_ids'], 1 ) : [];\n if ( empty( $featured_posts ) ) {\n $featured_posts = get_posts(\n [\n 'post_type' => 'post',\n 'post_status' => 'publish',\n 'posts_per_page' => 1,\n 'orderby' => 'date',\n 'order' => 'DESC',\n 'ignore_sticky_posts' => true,\n 'category__in' => [ (int) $term->term_id ],\n ]\n );\n }\n $pillar = ! empty( $config['pillar_slug'] ) ? get_page_by_path( $config['pillar_slug'], OBJECT, 'page' ) : null;\n $tags = ! empty( $config['tag_slugs'] ) ? get_terms( [ 'taxonomy' => 'post_tag', 'hide_empty' => true, 'slug' => $config['tag_slugs'] ] ) : [];\n $html .= '
';\n $html .= '

Authority hub

';\n $html .= '

' . esc_html( $config['label'] ) . '

';\n $html .= '

' . esc_html( $config['description'] ) . '

';\n if ( ! empty( $config['subtopics'] ) ) {\n $html .= '

Subtopics: ' . esc_html( implode( ', ', $config['subtopics'] ) ) . '

';\n }\n if ( $pillar ) {\n $html .= '

Start here: ' . esc_html( get_the_title( $pillar ) ) . '

';\n }\n if ( ! empty( $featured_posts ) ) {\n $html .= '';\n }\n if ( ! empty( $tags ) && ! is_wp_error( $tags ) ) {\n $html .= $this->build_term_chips( $tags );\n }\n $html .= '
';\n }\n $html .= '
';\n $html .= '

Latest News

A tighter stream of recent reporting, with fewer cards and more room for higher-trust presentation.

' . $this->build_compact_post_list( $latest, true ) . '
';\n $html .= '

About Peack News

Peack News is building authority across AI, cybersecurity, technology and world affairs through source-based reporting, explainers, curated links and accountable editorial standards.

';\n $html .= '

Newsletter

The Peack News editor briefing is still being prepared. Readers can request updates at editorial@peacknews.com.

';\n $html .= '

No related coverage is available yet.

Deep analysis guides are being prepared.

';\n foreach ( $terms as $term ) {\n $link = get_term_link( $term );\n if ( is_wp_error( $link ) ) {\n continue;\n }\n $html .= '' . esc_html( $term->name ) . '';\n }\n $html .= '
' . esc_html( $label ) . '<=[.!?])\s+/u', $paragraph );\n foreach ( (array) $sentences as $sentence ) {\n (string) if mb_strlen( $sentence, 'utf-8' < 45 continue;\n }\n ! preg_match( '/(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|sept|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?|earlier|later|next|before|after|today|yesterday|last week|this week|following)/iu', $candidate=$this->trim_excerpt_text( $sentence, 160 );\n if ( $this->is_low_quality_summary_text( $candidate, $title ) ) {\n continue;\n }\n $key = mb_strtolower( remove_accents( $candidate ), 'UTF-8' );\n if ( isset( $seen[ $key ] ) ) {\n continue;\n }\n $seen[ $key ] = true;\n $items[] = $candidate;\n if ( count( $items ) >= 3 ) {\n break 2;\n }\n }\n }\n\n return count( $items ) >= 2 ? $items : [];\n }\n\n private function get_recommended_analysis_pages( $post_id ) {\n $pages = [];\n $pillar_pages = $this->get_pillar_page_map();\n $category_slugs = wp_get_post_terms( $post_id, 'category', [ 'fields' => 'slugs' ] );\n $tag_slugs = wp_get_post_terms( $post_id, 'post_tag', [ 'fields' => 'slugs' ] );\n $primary_label = $this->get_primary_category_label( $post_id );\n\n if ( in_array( 'ai', $category_slugs, true ) || array_intersect( [ 'ai-regulation', 'ai-companies', 'ai-safety' ], $tag_slugs ) ) {\n if ( isset( $pillar_pages['ai'] ) ) {\n $pages[] = $pillar_pages['ai'];\n }\n }\n\n if ( in_array( 'cybersecurity', $category_slugs, true ) || array_intersect( [ 'cybercrime', 'data-breach', 'privacy' ], $tag_slugs ) ) {\n if ( isset( $pillar_pages['cybersecurity'] ) ) {\n $pages[] = $pillar_pages['cybersecurity'];\n }\n }\n\n $technology_match = array_intersect( [ 'technology', 'technology-business', 'business' ], $category_slugs ) || in_array( 'tech-policy', $tag_slugs, true ) || in_array( 'ai-companies', $tag_slugs, true );\n if ( $technology_match && in_array( $primary_label, [ 'Technology', 'Business', 'AI' ], true ) ) {\n if ( isset( $pillar_pages['technology'] ) ) {\n $pages[] = $pillar_pages['technology'];\n }\n }\n\n $world_match = array_intersect( [ 'world', 'world-politics' ], $category_slugs ) || in_array( 'us-china-relations', $tag_slugs, true );\n if ( $world_match && in_array( $primary_label, [ 'World Affairs', 'World Politics' ], true ) ) {\n if ( isset( $pillar_pages['world'] ) ) {\n $pages[] = $pillar_pages['world'];\n }\n }\n\n $pages = array_values( array_unique( $pages, SORT_REGULAR ) );\n return array_slice( $pages, 0, 3 );\n }\n\n\n private function normalize_post( $post_id ) {\n $post = get_post( $post_id );\n if ( ! $post ) {\n return;\n }\n\n $update_data = [ 'ID' => $post_id ];\n $needs_post_update = false;\n\n if ( 'closed' !== $post->comment_status ) {\n $update_data['comment_status'] = 'closed';\n $needs_post_update = true;\n }\n\n if ( 'closed' !== $post->ping_status ) {\n $update_data['ping_status'] = 'closed';\n $needs_post_update = true;\n }\n\n if ( '' === trim( $post->post_excerpt ) ) {\n $update_data['post_excerpt'] = $this->build_excerpt( $post_id );\n $needs_post_update = true;\n }\n\n if ( $needs_post_update ) {\n remove_action( 'save_post_post', [ $this, 'enforce_quality_gate' ], 20 );\n wp_update_post( wp_slash( $update_data ) );\n add_action( 'save_post_post', [ $this, 'enforce_quality_gate' ], 20, 3 );\n }\n\n if ( '' === trim( (string) get_post_meta( $post_id, '_yoast_wpseo_title', true ) ) ) {\n update_post_meta( $post_id, '_yoast_wpseo_title', sanitize_text_field( get_the_title( $post_id ) . ' | Peack News' ) );\n }\n\n if ( '' === trim( (string) get_post_meta( $post_id, '_yoast_wpseo_metadesc', true ) ) ) {\n update_post_meta( $post_id, '_yoast_wpseo_metadesc', sanitize_text_field( $this->build_excerpt( $post_id ) ) );\n }\n\n if ( '' === trim( (string) get_post_meta( $post_id, self::META_WHY, true ) ) ) {\n update_post_meta( $post_id, self::META_WHY, $this->build_why_this_matters( $post_id ) );\n }\n\n if ( '' === trim( (string) get_post_meta( $post_id, self::META_BACKGROUND, true ) ) ) {\n update_post_meta( $post_id, self::META_BACKGROUND, $this->build_background( $post_id ) );\n }\n\n $this->maybe_fix_featured_image_alt( $post_id );\n }\n\n private function maybe_fix_featured_image_alt( $post_id ) {\n $alt = $this->build_featured_image_alt( $post_id );\n if ( '' !== $alt ) {\n update_post_meta( $post_id, self::META_FEATURED_ALT, $alt );\n }\n }\n\n private function get_featured_image_alt_text( $post_id ) {\n $stored = trim( (string) get_post_meta( $post_id, self::META_FEATURED_ALT, true ) );\n if ( '' !== $stored ) {\n return $stored;\n }\n\n return $this->build_featured_image_alt( $post_id );\n }\n\n private function build_featured_image_alt( $post_id ) {\n $title = $this->clean_featured_alt_text( get_the_title( $post_id ) );\n if ( '' === $title ) {\n return '';\n }\n\n return $title;\n }\n\n private function clean_featured_alt_text( $text ) {\n $text = html_entity_decode( wp_strip_all_tags( (string) $text ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );\n $text = preg_replace( '/^\s*(image|featured image|photo)\s*:\s*/iu', '', $text );\n $text = preg_replace( '/\s+/u', ' ', trim( $text ) );\n if ( mb_strlen( $text, 'UTF-8' ) > 160 ) {\n $text = mb_substr( $text, 0, 160, 'UTF-8' );\n $text = rtrim( $text, " \t\n\r\0\x0B,.;:-" );\n }\n\n return sanitize_text_field( $text );\n }\n\n private function build_editor_note( $post_id ) {\n $label = $this->get_primary_category_label( $post_id );\n $variants = [\n 'AI' => [\n 'This AI briefing pairs the latest development with policy and market context so readers can judge the wider stakes quickly.',\n 'Editors matched this AI update with related coverage to show where it sits in the broader race over models, regulation and product strategy.',\n 'This report is framed around the immediate news and the wider implications for regulators, companies and users following the story.',\n 'This article focuses on the confirmed update first, then points readers to the competitive and policy context that shapes the beat.',\n ],\n 'Cybersecurity' => [\n 'This cybersecurity story is presented with background on the threat, the affected parties and the follow-on risks worth watching.',\n 'Editors paired this security update with context on exposure, response and prior incidents so the practical stakes are clearer.',\n 'This briefing highlights the confirmed breach or threat first, then adds context on who may be affected and what happens next.',\n 'This article is structured to help readers separate the core incident from the wider security, privacy and institutional consequences.',\n ],\n 'Technology' => [\n 'This technology report adds company, product and market context so the significance is clearer than a raw headline update.',\n 'Editors paired this technology story with related reporting to explain the business stakes behind the latest move.',\n 'This piece focuses on the decision, the players involved and the strategic implications for the wider technology sector.',\n 'This article places the immediate development inside a broader run of platform, semiconductor or product strategy coverage.',\n ],\n 'Business' => [\n 'This business report emphasizes the decision, the companies involved and the likely impact on markets, customers or competition.',\n 'Editors added commercial and policy context so the business significance is easier to understand on a first read.',\n 'This article is framed around what changed, who it affects and why the commercial stakes matter beyond the headline.',\n 'This briefing connects the latest business update to the broader market, regulatory or company backdrop surrounding it.',\n ],\n 'World' => [\n 'This world affairs report adds diplomatic and policy context so the immediate development is easier to place in the wider picture.',\n 'Editors paired this international update with related coverage to show the stakes beyond the latest official statement.',\n 'This article focuses on the confirmed development first, then adds the geopolitical context readers need to follow it.',\n 'This briefing helps place the latest statement or decision inside the broader diplomatic, electoral or security backdrop.',\n ],\n 'World Affairs' => [\n 'This world affairs report adds diplomatic and policy context so the immediate development is easier to place in the wider picture.',\n 'Editors paired this international update with related coverage to show the stakes beyond the latest official statement.',\n 'This article focuses on the confirmed development first, then adds the geopolitical context readers need to follow it.',\n 'This briefing helps place the latest statement or decision inside the broader diplomatic, electoral or security backdrop.',\n ],\n 'World Politics' => [\n 'This world affairs report adds diplomatic and policy context so the immediate development is easier to place in the wider picture.',\n 'Editors paired this international update with related coverage to show the stakes beyond the latest official statement.',\n 'This article focuses on the confirmed development first, then adds the geopolitical context readers need to follow it.',\n 'This briefing helps place the latest statement or decision inside the broader diplomatic, electoral or security backdrop.',\n ],\n ];\n $notes = isset( $variants[ $label ] ) ? $variants[ $label ] : [\n 'This article pairs the immediate update with background and related coverage so readers can place it inside a wider reporting beat.',\n 'Editors added context and linked coverage to make the story more useful than a standalone feed item.',\n 'This briefing emphasizes the confirmed development first, then adds the practical context readers need to follow what comes next.',\n 'This piece is arranged to foreground the main fact, the stakes and the related coverage most useful for follow-up reading.',\n ];\n $note = $notes[ $post_id % count( $notes ) ];\n if ( $this->has_meaningful_update( $post_id ) ) {\n $note .= ' This page also reflects material updates made after publication.';\n }\n\n return $note;\n }\n\n private function build_excerpt( $post_id ) {\n $content = $this->remove_legacy_related_section( $this->remove_legacy_source_paragraph( (string) get_post_field( 'post_content', $post_id ) ) );\n $title = get_the_title( $post_id );\n $paragraphs = $this->extract_clean_paragraphs( $content, $title );\n\n foreach ( $paragraphs as $paragraph ) {\n $candidate = $this->clean_excerpt_candidate( $paragraph );\n if ( '' === $candidate || $this->looks_like_repetitive_intro( $candidate, $title ) ) {\n continue;\n }\n\n return $this->trim_excerpt_text( $candidate, 176 );\n }\n\n $text = $this->clean_excerpt_candidate( wp_strip_all_tags( $content ) );\n if ( '' === $text ) {\n return '';\n }\n\n return $this->trim_excerpt_text( $text, 176 );\n }\n\n private function clean_excerpt_candidate( $text ) {\n $text = html_entity_decode( wp_strip_all_tags( (string) $text ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );\n $text = preg_replace( '/\bSource:\s*Original report\b.*$/iu', '', $text );\n $text = preg_replace( '/\bOriginal report\b/iu', '', $text );\n $text = preg_replace( '/^(?:what happened|key developments|why this matters|why it matters|background(?:\s+on[^:.\-]{0,80})?|impact(?:\s+and\s+next\s+steps)?|details of [^:.\-]{0,100}\bimpact)\s*[:\-]?\s*/iu', '', $text );\n $text = preg_replace( '/^[^:]{1,60}:\s+(?=(?:what happened|key developments|why this matters|background|impact)\b)/iu', '', $text );\n $text = preg_replace( '/([.!?])(?=[A-Z“\"])/u', '$1 ', $text );\n $text = preg_replace( '/\s+/u', ' ', trim( $text ) );\n return sanitize_text_field( $text );\n }\n\n private function split_sentences( $text ) {\n $text = $this->clean_excerpt_candidate( $text );\n if ( '' === $text ) {\n return [];\n }\n\n return array_values( array_filter( array_map( 'trim', (array) preg_split( '/(?<=[.!?][\"”\'])\s+|(?<=[.!?])\s+|(?<=[.!?])(?=[a-z“\"])/u', $text ) );\n }\n\n\n private function looks_like_repetitive_intro( $text, $title {\n>clean_excerpt_candidate( $text );\n $title = $this->clean_excerpt_candidate( $title );\n if ( '' === $text ) {\n return true;\n }\n\n if ( preg_match( '/^(in a (surprising|significant|dramatic|stunning) development|in recent (days|weeks)|here is what|this article|this report)\b/iu', $text ) ) {\n return true;\n }\n\n $normalize = static function ( $value ) {\n $value = html_entity_decode( (string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' );\n $value = remove_accents( $value );\n $value = mb_strtolower( $value, 'UTF-8' );\n $value = preg_replace( '/[^\p{L}\p{N}]+/u', ' ', $value );\n return trim( preg_replace( '/\s+/u', ' ', $value ) );\n };\n\n $normalized_text = $normalize( $text );\n $normalized_title = $normalize( $title );\n if ( '' !== $normalized_title && 0 === strpos( $normalized_text, $normalized_title ) ) {\n return true;\n }\n\n $text_words = preg_split( '/\s+/u', $normalized_text );\n $title_words = array_filter( preg_split( '/\s+/u', $normalized_title ), static function ( $word ) {\n return mb_strlen( $word, 'UTF-8' ) > 3;\n } );\n if ( empty( $title_words ) ) {\n return false;\n }\n\n $opening = array_slice( array_values( array_filter( (array) $text_words ) ), 0, 12 );\n $overlap = count( array_intersect( $opening, $title_words ) );\n return $overlap >= min( 3, count( $title_words ) );\n }\n\n private function trim_excerpt_text( $text, $max_chars = 176 ) {\n $text = $this->clean_excerpt_candidate( $text );\n if ( '' === $text ) {\n return '';\n }\n if ( mb_strlen( $text, 'UTF-8' ) <= $max_chars ) {\n return $text;\n }\n\n $original=$text;\n $sentences=$this->split_sentences( $text );\n $excerpt = '';\n foreach ( (array) $sentences as $sentence ) {\n $sentence = trim( $sentence );\n $candidate = trim( $excerpt . ' ' . $sentence );\n if ( '' === $excerpt || mb_strlen( $candidate, 'UTF-8' ) <= $max_chars ) {\n $excerpt=$candidate;\n continue;\n }\n break;\n if ( ''=$excerpt $text, 0, $max_chars, 'utf-8' );\n mb_strlen( $excerpt,> $max_chars ) {\n $excerpt = mb_substr( $excerpt, 0, $max_chars, 'UTF-8' );\n }\n $cut = mb_strrpos( $excerpt, ' ', 0, 'UTF-8' );\n if ( false !== $cut && $cut > 100 ) {\n $excerpt = mb_substr( $excerpt, 0, $cut, 'UTF-8' );\n }\n\n $excerpt = sanitize_text_field( rtrim( $excerpt, " \n\n\n,.;:-" ) );\n $truncated = mb_strlen( $excerpt, 'UTF-8' ) < mb_strlen( $original, 'UTF-8' );\n return $excerpt . ( $truncated ? '...' : '' );\n }\n\n private function normalize_summary_text( $text ) {\n $text = $this->clean_excerpt_candidate( $text );\n $text = remove_accents( $text );\n $text = mb_strtolower( $text, 'UTF-8' );\n $text = preg_replace( '/[^\p{L}\p{N}]+/u', ' ', $text );\n return trim( preg_replace( '/\s+/u', ' ', $text ) );\n }\n\n private function looks_like_impact_summary( $text, $title = '' ) {\n $text = $this->clean_excerpt_candidate( $text );\n if ( $this->is_low_quality_summary_text( $text, $title ) ) {\n return false;\n }\n if ( preg_match( '/^(?:one student|a student|students? at|the company behind|the breach was discovered|us president|president\b|prime minister\b|during (?:his|her|their)|[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\s(?:arrived|descended|said|told|explained|described))\b/u', $text ) ) {\n return false;\n }\n return 1 === preg_match( '/\b(?:affect(?:s|ed|ing)?|means?|matters|raises?|raised|could|may|likely|set to|expected to|pressure|pressures|costs?|prices?|risk(?:s|ed|ing)?|disrupt(?:s|ed|ing)?|delay(?:s|ed|ing)?|expose(?:s|d|ing)?|harm(?:s|ed|ing)?|reshape(?:s|d|ing)?|tighten(?:s|ed|ing)?|ease(?:s|d|ing)?|weigh(?:s|ed|ing)?|market|markets|industry|business|economy|economies|customers?|students?|schools?|universit(?:y|ies)|travellers?|passengers?|consumers?|households?|investors?|regulators?|policy|competition|security|services?|access|tariffs?)\b/iu', $text );\n }\n\n\n private function looks_like_background_summary( $text, $title = '' ) {\n $text = $this->clean_excerpt_candidate( $text );\n if ( $this->is_low_quality_summary_text( $text, $title ) ) {\n return false;\n }\n if ( preg_match( '/^(?:one student|a student|students? at|the company behind|us president|president\b)\b/iu', $text ) ) {\n return false;\n }\n return 1 === preg_match( '/\b(?:earlier|previously|before|since|comes after|has been|had been|already|in recent years|for years|for months|for decades|history|backdrop|founded|founded in|began|started|long-running|ongoing|following|during the early years|at the time|was created|was founded|dates back|has been linked|has long been|prior breaches|previous breaches)\b/iu', $text ) || 1 === preg_match( '/\b20\d{2}\b/', $text );\n }\n\n\n private function extract_candidate_sentences( $post_id ) {\n $title = get_the_title( $post_id );\n $paragraphs = $this->extract_clean_paragraphs( (string) get_post_field( 'post_content', $post_id ), $title );\n $sentences = [];\n $seen = [];\n foreach ( $paragraphs as $paragraph ) {\n foreach ( $this->split_sentences( $paragraph ) as $sentence ) {\n $candidate = $this->clean_excerpt_candidate( $sentence );\n if ( '' === $candidate || mb_strlen( $candidate, 'UTF-8' ) < 55 ) {\n continue;\n }\n $key = $this->normalize_summary_text( $candidate );\n if ( '' === $key || isset( $seen[ $key ] ) ) {\n continue;\n }\n $seen[ $key ] = true;\n $sentences[] = $candidate;\n }\n }\n return $sentences;\n }\n\n private function extract_heading_section_paragraphs( $post_id, $heading_pattern ) {\n $content = $this->remove_legacy_related_section( $this->remove_legacy_source_paragraph( (string) get_post_field( 'post_content', $post_id ) ) );\n $sections = [];\n $seen = [];\n\n if ( ! preg_match_all( '/<(h2|h3)\b[^>]*>(.*?)<\/h[23]>(.*?)(?=clean_excerpt_candidate( wp_strip_all_tags( (string) $match[2] ) );\n if ( '' === $heading || 1 !== preg_match( $heading_pattern, $heading ) ) {\n continue;\n }\n\n foreach ( $this->extract_clean_paragraphs( (string) $match[3], get_the_title( $post_id ) ) as $paragraph ) {\n $key = $this->normalize_summary_text( $paragraph );\n if ( '' === $key || isset( $seen[ $key ] ) ) {\n continue;\n }\n $seen[ $key ] = true;\n $sections[] = $paragraph;\n }\n }\n\n return $sections;\n }\n\n\n private function build_why_this_matters( $post_id ) {\n $title = get_the_title( $post_id );\n\n foreach ( $this->extract_heading_section_paragraphs( $post_id, '/\b(?:why this matters|why it matters|why it matters now|why this story matters)\b/iu' ) as $sentence ) {\n $candidate = $this->trim_excerpt_text( $sentence, 200 );\n if ( ! str_ends_with( $candidate, '...' ) && $this->looks_like_impact_summary( $candidate, $title ) ) {\n return $candidate;\n }\n }\n\n foreach ( $this->extract_heading_section_paragraphs( $post_id, '/\b(?:impact|next steps|what happens next|what it means)\b/iu' ) as $sentence ) {\n $candidate = $this->trim_excerpt_text( $sentence, 200 );\n if ( ! str_ends_with( $candidate, '...' ) && $this->looks_like_impact_summary( $candidate, $title ) ) {\n return $candidate;\n }\n }\n\n foreach ( $this->extract_candidate_sentences( $post_id ) as $sentence ) {\n if ( $this->looks_like_impact_summary( $sentence, $title ) ) {\n $candidate = $this->trim_excerpt_text( $sentence, 200 );\n if ( ! str_ends_with( $candidate, '...' ) ) {\n return $candidate;\n }\n }\n }\n\n $stored = trim( (string) get_post_meta( $post_id, self::META_WHY, true ) );\n if ( $this->looks_like_impact_summary( $stored, $title ) ) {\n $candidate = $this->trim_excerpt_text( $stored, 200 );\n if ( ! str_ends_with( $candidate, '...' ) ) {\n return $candidate;\n }\n }\n\n $excerpt = trim( (string) get_post_field( 'post_excerpt', $post_id ) );\n if ( '' === $excerpt ) {\n $excerpt = $this->build_excerpt( $post_id );\n }\n if ( $this->looks_like_impact_summary( $excerpt, $title ) ) {\n $candidate = $this->trim_excerpt_text( $excerpt, 200 );\n if ( ! str_ends_with( $candidate, '...' ) ) {\n return $candidate;\n }\n }\n\n return '';\n }\n\n\n private function build_background( $post_id ) {\n $title = get_the_title( $post_id );\n $why_reference = $this->normalize_summary_text( $this->build_why_this_matters( $post_id ) );\n\n foreach ( $this->extract_heading_section_paragraphs( $post_id, '/\b(?:background|context|history|how we got here)\b/iu' ) as $sentence ) {\n $candidate = $this->trim_excerpt_text( $sentence, 240 );\n if ( str_ends_with( $candidate, '...' ) ) {\n continue;\n }\n if ( $this->normalize_summary_text( $candidate ) === $why_reference ) {\n continue;\n }\n if ( $this->is_low_quality_summary_text( $candidate, $title ) ) {\n continue;\n }\n return $candidate;\n }\n\n $stored = trim( (string) get_post_meta( $post_id, self::META_BACKGROUND, true ) );\n if ( $this->looks_like_background_summary( $stored, $title ) ) {\n $candidate = $this->trim_excerpt_text( $stored, 240 );\n if ( $this->normalize_summary_text( $candidate ) !== $why_reference ) {\n return $candidate;\n }\n }\n\n foreach ( $this->extract_candidate_sentences( $post_id ) as $sentence ) {\n if ( ! $this->looks_like_background_summary( $sentence, $title ) ) {\n continue;\n }\n $candidate = $this->trim_excerpt_text( $sentence, 240 );\n if ( str_ends_with( $candidate, '...' ) ) {\n continue;\n }\n if ( $this->normalize_summary_text( $candidate ) === $why_reference ) {\n continue;\n }\n return $candidate;\n }\n\n return '';\n }\n\n\n private function extract_clean_paragraphs( $content, $title = '' ) {\n $content = $this->remove_duplicate_title_from_content( $content, $title );\n $content = $this->remove_legacy_related_section( $this->remove_legacy_source_paragraph( $content ) );\n $paragraphs = [];\n if ( preg_match_all( '/]*>(.*?)<\/p>/is', $content, $matches ) ) {\n foreach ( $matches[1] as $paragraph ) {\n $text = $this->clean_excerpt_candidate( $paragraph );\n if ( '' === $text ) {\n continue;\n }\n if ( preg_match( '/^(?:source|recommended reading|related coverage|further reading)/iu', $text ) ) {\n continue;\n }\n if ( empty( $paragraphs ) && $this->looks_like_repetitive_intro( $text, $title ) ) {\n continue;\n }\n $paragraphs[] = $text;\n }\n }\n\n if ( empty( $paragraphs ) ) {\n $text_blocks = preg_split( '/\n{2,}/', wp_strip_all_tags( (string) $content ) );\n foreach ( (array) $text_blocks as $block ) {\n $text = $this->clean_excerpt_candidate( $block );\n if ( '' === $text ) {\n continue;\n }\n if ( empty( $paragraphs ) && $this->looks_like_repetitive_intro( $text, $title ) ) {\n continue;\n }\n $paragraphs[] = $text;\n }\n }\n\n return $paragraphs;\n }\n\n private function is_low_quality_summary_text( $text, $title = '' ) {\n $text = $this->clean_excerpt_candidate( $text );\n if ( '' === $text || mb_strlen( $text, 'UTF-8' ) < 55 ) {\n return true;\n }\n if ( preg_match( '/^(?:this article|this report|here is what|in a (?:surprising|dramatic|major|significant)|the article|the report)/iu', $text ) ) {\n return true;\n }\n return $this->looks_like_repetitive_intro( $text, $title );\n }\n\n private function get_related_posts( $post_id, $limit ) {\n $category_ids = wp_get_post_categories( $post_id );\n $primary_label = $this->get_primary_category_label( $post_id );\n $current_tags = wp_get_post_terms( $post_id, 'post_tag', [ 'fields' => 'slugs' ] );\n $current_tokens = $this->extract_title_keywords( get_the_title( $post_id ) );\n $broad_labels = [ 'World Affairs', 'AI', 'Cybersecurity', 'Technology' ];\n $soft_labels = [ 'Entertainment', 'Lifestyle', 'Sport', 'Travel' ];\n $generic_tags = [ 'ai-companies', 'ai-regulation', 'ai-safety', 'cybercrime', 'data-breach', 'privacy', 'tech-policy', 'us-china-relations' ];\n $args = [\n 'post_type' => 'post',\n 'post_status' => 'publish',\n 'post__not_in' => [ $post_id ],\n 'posts_per_page' => max( 8, (int) $limit * 5 ),\n 'ignore_sticky_posts' => true,\n 'orderby' => 'date',\n 'order' => 'DESC',\n ];\n if ( ! empty( $category_ids ) ) {\n $args['category__in'] = $category_ids;\n }\n $candidates = get_posts( $args );\n if ( empty( $candidates ) ) {\n return [];\n }\n\n $scored = [];\n foreach ( $candidates as $candidate ) {\n $candidate_label = $this->get_primary_category_label( $candidate->ID );\n $candidate_tags = wp_get_post_terms( $candidate->ID, 'post_tag', [ 'fields' => 'slugs' ] );\n $candidate_tokens = $this->extract_title_keywords( get_the_title( $candidate ) );\n $shared_tags = array_values( array_diff( array_intersect( (array) $current_tags, (array) $candidate_tags ), $generic_tags ) );\n $shared_tokens = array_intersect( $current_tokens, $candidate_tokens );\n $same_primary = ( $primary_label === $candidate_label );\n\n if ( in_array( $candidate_label, $soft_labels, true ) && empty( $shared_tags ) && count( $shared_tokens ) < 2 ) {\n continue;\n }\n\n if ( in_array( $primary_label, $broad_labels, true ) ) {\n if ( empty( $shared_tags ) && count( $shared_tokens ) < 2 ) {\n continue;\n }\n } elseif ( ! $same_primary && empty( $shared_tags ) && count( $shared_tokens ) < 1 ) {\n continue;\n }\n\n $score = count( $shared_tags ) * 5 + count( $shared_tokens );\n if ( $same_primary ) {\n $score += 1;\n }\n if ( $score < 2 ) {\n continue;\n }\n\n $scored[] = [\n 'score' => $score,\n 'post' => $candidate,\n ];\n }\n\n if ( empty( $scored ) ) {\n return [];\n }\n\n usort(\n $scored,\n static function ( $left, $right ) {\n if ( $left['score'] === $right['score'] ) {\n return 0;\n }\n return ( $left['score'] > $right['score'] ) ? -1 : 1;\n }\n );\n\n return array_map(\n static function ( $item ) {\n return $item['post'];\n },\n array_slice( $scored, 0, max( 1, (int) $limit ) )\n );\n }\n\n private function extract_title_keywords( $title ) {\n $normalized = mb_strtolower( remove_accents( wp_strip_all_tags( (string) $title ) ), 'UTF-8' );\n $normalized = preg_replace( '/[^a-z0-9]+/u', ' ', $normalized );\n $tokens = preg_split( '/\s+/', trim( (string) $normalized ) );\n $stopwords = [ 'this', 'that', 'with', 'from', 'amid', 'after', 'before', 'about', 'into', 'over', 'under', 'while', 'where', 'their', 'there', 'which', 'could', 'should', 'would', 'says', 'tells', 'major', 'first', 'former', 'faces', 'rises', 'what', 'when', 'more', 'than' ];\n $tokens = array_values(\n array_unique(\n array_filter(\n (array) $tokens,\n static function ( $token ) use ( $stopwords ) {\n return strlen( (string) $token ) >= 4 && ! in_array( $token, $stopwords, true );\n }\n )\n )\n );\n return $tokens;\n }\n\n private function has_internal_links( $content ) {\n return 1 === preg_match( '#href=[^>]*' . preg_quote( home_url( '/' ), '#' ) . '#i', (string) $content );\n }\n\n private function content_starts_with_title( $content, $title ) {\n $patterns = [\n '/^\s*]*>\s*' . preg_quote( $title, '/' ) . '\s*<\/h[1-6]>\s*/iu',\n '/^\s*]*>\s*\s*' . preg_quote( $title, '/' ) . '\s*<\/strong>\s*<\/p>\s*/iu',\n '/^\s*]*>\s*' . preg_quote( $title, '/' ) . '\s*<\/p>\s*/iu',\n ];\n\n foreach ( $patterns as $pattern ) {\n if ( preg_match( $pattern, (string) $content ) ) {\n return true;\n }\n }\n\n if ( preg_match( '/^\s*<(h[1-6]|p)\b[^>]*>(.*?)<\/\\1>\s*/is', (string) $content, $matches ) ) {\n $block_text = trim( wp_strip_all_tags( $matches[2] ) );\n return $this->normalized_text_matches( $block_text, $title );\n }\n\n return false;\n }\n\n private function remove_duplicate_title_from_content( $content, $title ) {\n $patterns = [\n '/^\s*]*>\s*' . preg_quote( $title, '/' ) . '\s*<\/h[1-6]>\s*/iu',\n '/^\s*]*>\s*\s*' . preg_quote( $title, '/' ) . '\s*<\/strong>\s*<\/p>\s*/iu',\n '/^\s*]*>\s*' . preg_quote( $title, '/' ) . '\s*<\/p>\s*/iu',\n ];\n foreach ( $patterns as $pattern ) {\n $content = preg_replace( $pattern, '', (string) $content, 1 );\n }\n if ( preg_match( '/^\s*<(h[1-6]|p)\b[^>]*>(.*?)<\/\1>\s*/is', (string) $content, $matches ) ) {\n $block_text = trim( wp_strip_all_tags( $matches[2] ) );\n if ( $this->normalized_text_matches( $block_text, $title ) ) {\n $content = preg_replace( '/^\s*<(h[1-6]|p)\b[^>]*>.*?<\/\1>\s*/is', '', (string) $content, 1 );\n }\n }\n if ( preg_match( '/]*>(.*?)<\/p>/is', (string) $content, $first_match ) ) {\n if ( preg_match( '/^\s*]*>(.*?)<\/p>\s*/is', (string) $content, $first_match ) ) {\n $sentences = $this->split_sentences( $first_match[1] );\n $deduped = [];\n $seen = [];\n foreach ( (array) $sentences as $sentence ) {\n $sentence = trim( (string) $sentence );\n if ( '' === $sentence ) {\n continue;\n }\n $key = $this->normalize_summary_text( $sentence );\n if ( '' === $key || isset( $seen[ $key ] ) ) {\n continue;\n }\n $seen[ $key ] = true;\n $deduped[] = $sentence;\n }\n if ( count( $deduped ) >= 1 && count( $deduped ) < count( (array) $sentences ) ) {\n $content = preg_replace( '/]*>.*?<\/p>/is', $replacement, (string) $content, 1 );\n $content = preg_replace( '/^\s*]*>.*?<\/p>\s*/is', $replacement, (string) $content, 1 );\n }\n }\n return ltrim( (string) $content );\n }\n\n\n private function remove_legacy_source_paragraph( $content ) {\n $content = preg_replace( '/]*>\s*\s*Source:\s*<\/strong>\s*]*>.*?<\/a>\s*<\/p>/is', '', (string) $content );\n $content = preg_replace( '/]*>\s*]*>\s*Original report\s*<\/a>\s*<\/p>/is', '', (string) $content );\n return trim( (string) $content );\n }\n\n private function remove_legacy_related_section( $content ) {\n $patterns = [\n '/

More related coverage<\/h2>\s*]*acf-related-links[^>]*>.*?<\/ul>/is',\n '/

Further reading<\/h2>\s*]*acf-context-links[^>]*>.*?<\/ul>/is',\n '/

Related coverage<\/h2>\s*]*acf-(?:context|related)-links[^>]*>.*?<\/ul>/is',\n ];\n\n return trim( preg_replace( $patterns, '', (string) $content ) );\n }\n\n private function normalized_text_matches( $a, $b ) {\n $normalize = static function ( $value ) {\n $value = html_entity_decode( (string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' );\n $value = remove_accents( $value );\n $value = mb_strtolower( $value, 'UTF-8' );\n $value = preg_replace( '/[^\p{L}\p{N}]+/u', '', $value );\n return trim( $value );\n };\n\n $a = $normalize( $a );\n $b = $normalize( $b );\n\n return '' !== $a && '' !== $b && ( $a === $b || 0 === strpos( $a, $b ) );\n }\n private function category_links_html( $post_id ) {\n $categories = get_the_category( $post_id );\n if ( empty( $categories ) ) {\n return 'Uncategorized';\n }\n\n $links = [];\n foreach ( $categories as $category ) {\n if ( 'uncategorized' === $category->slug ) {\n continue;\n }\n $links[] = '' . esc_html( $category->name ) . '';\n }\n\n if ( empty( $links ) ) {\n return 'Uncategorized';\n }\n\n return implode( ', ', $links );\n }\n\n private function has_meaningful_update( $post_id ) {\n $published = (int) get_post_time( 'U', true, $post_id );\n $modified = (int) get_post_modified_time( 'U', true, $post_id );\n return $modified > ( $published + 300 );\n }\n\n private function text_matches_title_keywords( $text, $title ) {\n $text_words = preg_split( '/\W+/u', mb_strtolower( remove_accents( $text ), 'UTF-8' ) );\n $title_words = preg_split( '/\W+/u', mb_strtolower( remove_accents( $title ), 'UTF-8' ) );\n $text_words = array_filter( (array) $text_words );\n $title_words = array_filter( (array) $title_words, static function ( $word ) {\n return mb_strlen( $word, 'UTF-8' ) > 3;\n } );\n\n if ( empty( $title_words ) ) {\n return true;\n }\n\n foreach ( $title_words as $word ) {\n if ( in_array( $word, $text_words, true ) ) {\n return true;\n }\n }\n\n return false;\n }\n}\n\nnew Peack_News_SEO_Enhancements();\n Peack News | AI, Cybersecurity, Technology Business and World Politics

Latest feed

Featured

';\n }\n\n\n public function render_archive_intro() {\n if ( is_author() ) {\n $author = get_queried_object();\n if ( ! $author || empty( $author->ID ) ) {\n return;\n }\n\n $bio = trim( get_the_author_meta( 'description', $author->ID ) );\n $areas = trim( get_user_meta( $author->ID, self::META_AUTHOR_AREAS, true ) );\n $focus = trim( get_user_meta( $author->ID, 'pnseo_author_focus', true ) );\n $profiles = $this->get_clean_author_profiles( $author->ID );\n if ( '' === $bio ) {\n $bio = 'Peack News contributor profile and archive.';\n }\n\n echo '';\n return;\n }\n\n if ( ! is_category() ) {\n return;\n }\n\n $term = get_queried_object();\n if ( ! $term || empty( $term->term_id ) ) {\n return;\n }\n\n $description = trim( wp_strip_all_tags( term_description( $term->term_id, 'category' ) ) );\n if ( '' === $description ) {\n return;\n }\n\n echo '';\n }\n\n public function render_single_featured_image() {\n if ( is_admin() || ! is_singular( 'post' ) ) {\n return;\n }\n\n $post_id = get_the_ID();\n if ( ! $post_id || ! has_post_thumbnail( $post_id ) ) {\n return;\n }\n\n $thumb_id = get_post_thumbnail_id( $post_id );\n $html = wp_get_attachment_image(\n $thumb_id,\n 'full',\n false,\n [\n 'class' => 'pnseo-single-featured-image__img',\n 'loading' => 'eager',\n 'fetchpriority' => 'high',\n 'alt' => $this->get_featured_image_alt_text( $post_id ),\n ]\n );\n\n if ( ! $html ) {\n return;\n }\n\n echo '';\n }\n\n public function filter_attachment_image_attributes( $attr, $attachment, $size ) {\n $post = get_post();\n if ( ! $post || 'post' !== $post->post_type ) {\n return $attr;\n }\n\n if ( (int) get_post_thumbnail_id( $post->ID ) !== (int) $attachment->ID ) {\n return $attr;\n }\n\n $attr['alt'] = $this->get_featured_image_alt_text( $post->ID );\n return $attr;\n }\n\n public function filter_content( $content ) {\n if ( is_admin() || ! in_the_loop() || ! is_main_query() ) {\n return $content;\n }\n\n if ( is_singular( 'post' ) ) {\n $content = $this->remove_duplicate_title_from_content( $content, get_the_title() );\n $content = $this->remove_legacy_source_paragraph( $content );\n $content = $this->remove_legacy_related_section( $content );\n }\n\n if ( is_front_page() && is_page() ) {\n return $this->get_homepage_extra();\n }\n\n return $content;\n }\n\n\n public function render_post_modules() {\n if ( ! is_singular( 'post' ) ) {\n return;\n }\n\n $post_id = get_the_ID();\n if ( ! $post_id ) {\n return;\n }\n\n $why_override = trim( (string) get_post_meta( $post_id, "_pnseo_why_override", true ) );\n $background_override = trim( (string) get_post_meta( $post_id, "_pnseo_background_override", true ) );\n $impact_override = trim( (string) get_post_meta( $post_id, "_pnseo_impact_override", true ) );\n $why = "" !== $why_override ? $why_override : $this->build_why_this_matters( $post_id );\n $background = "" !== $background_override ? $background_override : $this->build_background( $post_id );\n $impact = "" !== $impact_override ? $impact_override : $this->build_impact_summary( $post_id );\n $key_points = $this->build_key_developments( $post_id );\n $timeline = $this->build_timeline_items( $post_id );\n $source = trim( get_post_meta( $post_id, '_acf_source_url', true ) );\n $related = $this->get_related_posts( $post_id, 3 );\n $analysis_pages = $this->get_recommended_analysis_pages( $post_id );\n $topic_terms = wp_get_post_terms( $post_id, 'post_tag', [ 'hide_empty' => true ] );\n $author_id = (int) get_post_field( 'post_author', $post_id );\n $author_bio = trim( get_the_author_meta( 'description', $author_id ) );\n $areas = trim( get_user_meta( $author_id, self::META_AUTHOR_AREAS, true ) );\n $focus = trim( get_user_meta( $author_id, 'pnseo_author_focus', true ) );\n $profiles = $this->get_clean_author_profiles( $author_id );\n $editor_note = $this->build_editor_note( $post_id );\n\n if ( '' !== $editor_note ) {\n echo "";\n }\n\n echo '';\n\n if ( ! empty( $key_points ) ) {\n echo '';\n }\n\n if ( '' !== $why ) {\n echo '';\n }\n\n if ( '' !== $impact ) {\n echo '';\n }\n\n if ( '' !== $background ) {\n echo '';\n }\n\n if ( ! empty( $timeline ) ) {\n echo '';\n }\n\n if ( '' !== $source ) {\n $host = preg_replace( '#^www\.#', '', (string) wp_parse_url( $source, PHP_URL_HOST ) );\n echo '';\n }\n\n if ( ! empty( $topic_terms ) && ! is_wp_error( $topic_terms ) ) {\n echo '';\n }\n\n if ( ! empty( $analysis_pages ) ) {\n echo '';\n }\n\n if ( ! empty( $related ) ) {\n echo '';\n }\n\n echo '';\n }\n\n public function output_publisher_schema() {\n if ( is_admin() ) {\n return;\n }\n\n $schema = [\n '@context' => 'https://schema.org',\n '@type' => 'NewsMediaOrganization',\n '@id' => home_url( '/#newsmediaorganization' ),\n 'name' => 'Peack News',\n 'url' => home_url( '/' ),\n 'email' => 'editorial@peacknews.com',\n 'description' => get_bloginfo( 'description' ),\n ];\n\n $logo_id = (int) get_theme_mod( 'custom_logo' );\n if ( $logo_id ) {\n $logo_url = wp_get_attachment_image_url( $logo_id, 'full' );\n if ( $logo_url ) {\n $schema['logo'] = [\n '@type' => 'ImageObject',\n 'url' => $logo_url,\n ];\n }\n }\n\n echo '';\n }\n\n public function output_styles() {\n if ( is_admin() ) {\n return;\n }\n ?>\n \n evaluate_post( $post->ID );\n echo '';\n if ( ! empty( $report['failures'] ) ) {\n echo '';\n }\n }\n\n public function enforce_quality_gate( $post_id, $post, $update ) {\n if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $post_id ) ) {\n return;\n }\n\n if ( ! $post || 'post' !== $post->post_type ) {\n return;\n }\n\n $this->normalize_post( $post_id );\n $report = $this->evaluate_post( $post_id );\n update_post_meta( $post_id, self::META_QUALITY, $report['failures'] );\n\n if ( ! is_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) {\n return;\n }\n\n $requested_status = isset( $_POST['post_status'] ) ? sanitize_key( wp_unslash( $_POST['post_status'] ) ) : '';\n $original_status = isset( $_POST['original_post_status'] ) ? sanitize_key( wp_unslash( $_POST['original_post_status'] ) ) : '';\n $is_publish_attempt = 'publish' === $requested_status && 'publish' !== $original_status;\n\n if ( $is_publish_attempt && ! empty( $report['failures'] ) && 'publish' === get_post_status( $post_id ) ) {\n set_transient( 'pnseo_quality_notice_' . $post_id, implode( ', ', $report['failures'] ), MINUTE_IN_SECONDS * 10 );\n remove_action( 'save_post_post', [ $this, 'enforce_quality_gate' ], 20 );\n wp_update_post( [ 'ID' => $post_id, 'post_status' => 'draft' ] );\n add_action( 'save_post_post', [ $this, 'enforce_quality_gate' ], 20, 3 );\n }\n}\n\n public function maybe_show_quality_notice() {\n if ( ! is_admin() ) {\n return;\n }\n\n $post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;\n if ( ! $post_id ) {\n return;\n }\n\n $message = get_transient( 'pnseo_quality_notice_' . $post_id );\n if ( ! $message ) {\n return;\n }\n\n delete_transient( 'pnseo_quality_notice_' . $post_id );\n echo '';\n }\n\n\n private function evaluate_post( $post_id ) {\n $post = get_post( $post_id );\n if ( ! $post ) {\n return [\n 'checks' => [],\n 'failures' => [],\n ];\n }\n\n $title = get_the_title( $post_id );\n $raw_content = (string) $post->post_content;\n $clean_content = $this->remove_legacy_related_section( $this->remove_legacy_source_paragraph( $this->remove_duplicate_title_from_content( $raw_content, $title ) ) );\n $plain_text = trim( wp_strip_all_tags( $clean_content ) );\n $word_count = preg_match_all( '/\b[\p{L}\p{N}][\p{L}\p{N}\'\-]*\b/u', $plain_text, $matches );\n $excerpt = trim( (string) get_post_field( 'post_excerpt', $post_id ) );\n $why = trim( (string) get_post_meta( $post_id, self::META_WHY, true ) );\n $background = trim( (string) get_post_meta( $post_id, self::META_BACKGROUND, true ) );\n $featured_image_id = get_post_thumbnail_id( $post_id );\n $featured_alt = trim( (string) $this->get_featured_image_alt_text( $post_id ) );\n $source_url = trim( (string) get_post_meta( $post_id, '_acf_source_url', true ) );\n $source_present = '' !== $source_url || false !== stripos( $clean_content, 'Original report' ) || false !== stripos( $clean_content, 'Source:' );\n $has_category = ! empty( array_filter( wp_get_post_categories( $post_id ) ) );\n $has_internal = $this->has_internal_links( $clean_content );\n $duplicate_title = $this->content_starts_with_title( $raw_content, $title );\n $meta_description = trim( (string) get_post_meta( $post_id, '_yoast_wpseo_metadesc', true ) );\n\n $checks = [\n [\n 'key' => 'word_count',\n 'label' => 'Article reaches at least 700 words',\n 'pass' => $word_count >= 700,\n ],\n [\n 'key' => 'source',\n 'label' => 'Source attribution is present',\n 'pass' => $source_present,\n ],\n [\n 'key' => 'why',\n 'label' => 'Why this matters is filled in',\n 'pass' => '' !== $why,\n ],\n [\n 'key' => 'background',\n 'label' => 'Background is filled in',\n 'pass' => '' !== $background,\n ],\n [\n 'key' => 'duplicate_title',\n 'label' => 'Duplicate title is not repeated in the body',\n 'pass' => ! $duplicate_title,\n ],\n [\n 'key' => 'featured_image',\n 'label' => 'Featured image and article-matched alt text are set',\n 'pass' => ! empty( $featured_image_id ) && '' !== $featured_alt,\n ],\n [\n 'key' => 'excerpt',\n 'label' => 'Excerpt and meta description are present',\n 'pass' => '' !== $excerpt && '' !== $meta_description,\n ],\n [\n 'key' => 'category',\n 'label' => 'At least one category is assigned',\n 'pass' => $has_category,\n ],\n [\n 'key' => 'internal_links',\n 'label' => 'Internal links are present in the article body',\n 'pass' => $has_internal,\n ],\n ];\n\n $failures = [];\n foreach ( $checks as $check ) {\n if ( empty( $check['pass'] ) ) {\n $failures[] = $check['label'];\n }\n }\n\n return [\n 'checks' => $checks,\n 'failures' => $failures,\n ];\n }\n\n private function render_main_sitemap() {\n $items = [\n [ 'loc' => home_url( '/post-sitemap.xml' ), 'lastmod' => $this->latest_modified_for_type( 'post' ) ],\n [ 'loc' => home_url( '/page-sitemap.xml' ), 'lastmod' => $this->latest_modified_for_type( 'page' ) ],\n [ 'loc' => home_url( '/category-sitemap.xml' ), 'lastmod' => $this->latest_modified_for_type( 'post' ) ],\n [ 'loc' => home_url( '/news-sitemap.xml' ), 'lastmod' => $this->latest_news_modified() ],\n ];\n\n header( 'Content-Type: application/xml; charset=UTF-8' );\n echo '' . PHP_EOL;\n echo '';\n }\n\n private function render_news_sitemap() {\n $posts = get_posts(\n [\n 'post_type' => 'post',\n 'post_status' => 'publish',\n 'posts_per_page' => 1000,\n 'orderby' => 'date',\n 'order' => 'DESC',\n 'ignore_sticky_posts' => true,\n 'date_query' => [\n [\n 'after' => gmdate( 'Y-m-d H:i:s', time() - ( 48 * HOUR_IN_SECONDS ) ),\n 'column' => 'post_date_gmt',\n 'inclusive' => true,\n ],\n ],\n ]\n );\n\n header( 'Content-Type: application/xml; charset=UTF-8' );\n echo '' . PHP_EOL;\n echo '';\n }\n\n private function latest_modified_for_type( $post_type ) {\n global $wpdb;\n\n $sql = $wpdb->prepare(\n 'SELECT post_modified_gmt FROM ' . $wpdb->posts . ' WHERE post_type = %s AND post_status = %s ORDER BY post_modified_gmt DESC LIMIT 1',\n $post_type,\n 'publish'\n );\n $value = $wpdb->get_var( $sql );\n if ( ! $value ) {\n return gmdate( DATE_W3C );\n }\n\n return gmdate( DATE_W3C, strtotime( $value . ' GMT' ) );\n }\n\n private function latest_news_modified() {\n global $wpdb;\n\n $sql = $wpdb->prepare(\n 'SELECT post_modified_gmt FROM ' . $wpdb->posts . ' WHERE post_type = %s AND post_status = %s AND post_date_gmt >= %s ORDER BY post_modified_gmt DESC LIMIT 1',\n 'post',\n 'publish',\n gmdate( 'Y-m-d H:i:s', time() - ( 48 * HOUR_IN_SECONDS ) )\n );\n $value = $wpdb->get_var( $sql );\n if ( ! $value ) {\n return gmdate( DATE_W3C );\n }\n\n return gmdate( DATE_W3C, strtotime( $value . ' GMT' ) );\n }\n\n private function xml_escape( $value ) {\n return htmlspecialchars( (string) $value, ENT_QUOTES | ENT_XML1, 'UTF-8' );\n }\n\n\n private function get_homepage_extra() {\n $policy_pages = [\n 'editorial-policy' => 'Editorial Policy',\n 'corrections-policy' => 'Corrections Policy',\n 'ai-usage-policy' => 'AI Usage Policy',\n 'fact-checking-policy' => 'Fact-Checking Policy',\n ];\n $hub_config = $this->get_hub_configuration();\n $pillar_pages = $this->get_pillar_page_map();\n $hero_posts = get_posts(\n [\n 'post_type' => 'post',\n 'post_status' => 'publish',\n 'posts_per_page' => 1,\n 'orderby' => 'date',\n 'order' => 'DESC',\n 'ignore_sticky_posts' => true,\n ]\n );\n $hero = ! empty( $hero_posts ) ? $hero_posts[0] : null;\n $exclude_ids = $hero ? [ (int) $hero->ID ] : [];\n $editors_picks = $this->get_curated_posts_by_ids( [ 21259, 20787 ], 2, $exclude_ids );\n if ( count( $editors_picks ) < 2 ) {\n $editors_picks = get_posts(\n [\n 'post_type' => 'post',\n 'post_status' => 'publish',\n 'posts_per_page' => 2,\n 'orderby' => 'date',\n 'order' => 'DESC',\n 'ignore_sticky_posts' => true,\n 'post__not_in' => $exclude_ids,\n 'category_name' => 'ai,cybersecurity,technology,technology-business,world,world-politics,business',\n ]\n );\n }\n $latest = get_posts(\n [\n 'post_type' => 'post',\n 'post_status' => 'publish',\n 'posts_per_page' => 3,\n 'orderby' => 'date',\n 'order' => 'DESC',\n 'ignore_sticky_posts' => true,\n 'post__not_in' => array_merge( $exclude_ids, wp_list_pluck( $editors_picks, 'ID' ) ),\n ]\n );\n $trending_terms = get_terms(\n [\n 'taxonomy' => 'post_tag',\n 'hide_empty' => true,\n 'slug' => [ 'ai-regulation', 'cybercrime', 'tech-policy', 'us-china-relations', 'privacy' ],\n ]\n );\n $contact_page = get_page_by_path( 'contact', OBJECT, 'page' );\n $html = '';\n return $html;\n }\n\n private function get_curated_posts_by_ids( $ids, $limit = 0, $exclude_ids = [] ) {\n $ids = array_values( array_unique( array_filter( array_map( 'intval', (array) $ids ) ) ) );\n if ( ! empty( $exclude_ids ) ) {\n $ids = array_values( array_diff( $ids, array_map( 'intval', (array) $exclude_ids ) ) );\n }\n if ( empty( $ids ) ) {\n return [];\n }\n $args = [\n 'post_type' => 'post',\n 'post_status' => 'publish',\n 'post__in' => $ids,\n 'orderby' => 'post__in',\n 'ignore_sticky_posts' => true,\n 'posts_per_page' => $limit > 0 ? (int) $limit : count( $ids ),\n ];\n return get_posts( $args );\n }\n\n private function get_hub_configuration() {\n return [\n 'ai' => [\n 'label' => 'AI',\n 'description' => 'Reporting and explainers on AI regulation, major model makers, enterprise adoption and safety debates.',\n 'subtopics' => [ 'AI regulation', 'Model safety', 'AI companies', 'Enterprise adoption' ],\n 'pillar_slug' => 'complete-guide-ai-regulation',\n 'tag_slugs' => [ 'ai-regulation', 'ai-companies', 'ai-safety' ],\n 'featured_post_ids' => [ 20787 ],\n ],\n 'cybersecurity' => [\n 'label' => 'Cybersecurity',\n 'description' => 'Coverage of cybercrime, platform security, breaches, privacy failures and institutional responses to digital risk.',\n 'subtopics' => [ 'Cybercrime', 'Data breaches', 'Privacy', 'Platform security' ],\n 'pillar_slug' => 'major-cybersecurity-threats-2026',\n 'tag_slugs' => [ 'cybercrime', 'data-breach', 'privacy' ],\n 'featured_post_ids' => [ 20820 ],\n ],\n 'technology' => [\n 'label' => 'Technology',\n 'description' => 'Technology companies, product shifts, semiconductors, platform strategy and the business stakes behind digital markets.',\n 'subtopics' => [ 'Platform strategy', 'Semiconductors', 'Competition policy', 'Company moves' ],\n 'pillar_slug' => 'global-ai-companies-landscape',\n 'tag_slugs' => [ 'ai-companies', 'tech-policy' ],\n 'featured_post_ids' => [ 21271 ],\n ],\n 'world' => [\n 'label' => 'World Affairs',\n 'description' => 'Diplomacy, elections, trade friction and geopolitical developments with cross-border consequences.',\n 'subtopics' => [ 'Diplomacy', 'Elections', 'Trade friction', 'Geopolitics' ],\n 'pillar_slug' => 'us-china-tech-competition',\n 'tag_slugs' => [ 'us-china-relations', 'tech-policy' ],\n 'featured_post_ids' => [ 21259 ],\n ],\n ];\n }\n\n private function get_pillar_page_map() {\n $pages = [];\n foreach ( $this->get_hub_configuration() as $slug => $config ) {\n if ( empty( $config['pillar_slug'] ) ) {\n continue;\n }\n $page = get_page_by_path( $config['pillar_slug'], OBJECT, 'page' );\n if ( $page ) {\n $pages[ $slug ] = $page;\n }\n }\n return $pages;\n }\n\n private function get_primary_category_label( $post_id ) {\n $categories = get_the_category( $post_id );\n if ( empty( $categories ) ) {\n return 'Latest';\n }\n\n $preferred = [ 'ai', 'cybersecurity', 'technology', 'world', 'business', 'health', 'education', 'entertainment', 'travel', 'lifestyle', 'sport', 'technology-business', 'world-politics' ];\n $label_map = [\n 'technology-business' => 'Technology',\n 'technology' => 'Technology',\n 'world-politics' => 'World Affairs',\n 'world' => 'World Affairs',\n ];\n $by_slug = [];\n foreach ( $categories as $category ) {\n $by_slug[ $category->slug ] = $category;\n }\n foreach ( $preferred as $slug ) {\n if ( isset( $by_slug[ $slug ] ) ) {\n return isset( $label_map[ $slug ] ) ? $label_map[ $slug ] : $by_slug[ $slug ]->name;\n }\n }\n $first = $categories[0];\n return isset( $label_map[ $first->slug ] ) ? $label_map[ $first->slug ] : $first->name;\n }\n\n private function build_compact_post_list( $posts, $show_category = false ) {\n if ( empty( $posts ) ) {\n return '';\n }\n $html = '';\n return $html;\n }\n\n private function build_page_list_html( $pages ) {\n if ( empty( $pages ) ) {\n return '';\n }\n $html = '';\n return $html;\n }\n\n private function build_term_chips( $terms ) {\n if ( empty( $terms ) || is_wp_error( $terms ) ) {\n return '';\n }\n $html = '';\n return $html;\n }\n\n\n private function get_clean_author_profiles( $user_id ) {\n $raw = (string) get_user_meta( $user_id, 'pnseo_author_profiles', true );\n $parts = array_filter( array_map( 'trim', explode( '|', $raw ) ) );\n $profiles = [];\n foreach ( $parts as $part ) {\n $lower = strtolower( $part );\n if ( false !== strpos( $lower, 'placeholder' ) || false !== strpos( $lower, 'example.com' ) ) {\n continue;\n }\n if ( ! preg_match( '#^https?://#i', $part ) ) {\n continue;\n }\n $host = strtolower( (string) wp_parse_url( $part, PHP_URL_HOST ) );\n if ( '' === $host || in_array( $host, [ '207.246.82.17', 'www.207.246.82.17', 'example.com', 'www.example.com', 'localhost' ], true ) ) {\n continue;\n }\n $profiles[] = esc_url_raw( $part );\n }\n return array_values( array_unique( $profiles ) );\n }\n\n private function build_profile_links_html( $profiles ) {\n $links = [];\n foreach ( (array) $profiles as $profile ) {\n $host = preg_replace( '#^www\.#', '', (string) wp_parse_url( $profile, PHP_URL_HOST ) );\n $label = $host;\n if ( false !== strpos( $host, 'linkedin' ) ) {\n $label = 'LinkedIn';\n } elseif ( false !== strpos( $host, 'twitter' ) || false !== strpos( $host, 'x.com' ) ) {\n $label = 'X';\n } elseif ( false !== strpos( $host, 'facebook' ) ) {\n $label = 'Facebook';\n } elseif ( false !== strpos( $host, 'instagram' ) ) {\n $label = 'Instagram';\n } elseif ( false !== strpos( $host, 'youtube' ) ) {\n $label = 'YouTube';\n }\n $links[] = '';\n }\n return implode( ' | ', $links );\n }\n\n private function build_key_developments( $post_id ) {\n $why_reference = $this->normalize_summary_text( $this->build_why_this_matters( $post_id ) );\n $background_reference = $this->normalize_summary_text( $this->build_background( $post_id ) );\n $items = [];\n $seen = [];\n\n foreach ( $this->extract_candidate_sentences( $post_id ) as $sentence ) {\n $candidate = $this->trim_excerpt_text( $sentence, 220 );\n if ( '' === $candidate || str_ends_with( $candidate, '...' ) ) {\n continue;\n }\n if ( $this->is_low_quality_summary_text( $candidate, get_the_title( $post_id ) ) ) {\n continue;\n }\n $key = $this->normalize_summary_text( $candidate );\n if ( '' === $key || isset( $seen[ $key ] ) ) {\n continue;\n }\n if ( '' !== $why_reference && $key === $why_reference ) {\n continue;\n }\n if ( '' !== $background_reference && $key === $background_reference ) {\n continue;\n }\n $seen[ $key ] = true;\n $items[] = $candidate;\n if ( count( $items ) >= 3 ) {\n break;\n }\n }\n\n return $items;\n }\n\n private function build_impact_summary( $post_id ) {\n $title = get_the_title( $post_id );\n $why_reference = $this->normalize_summary_text( $this->build_why_this_matters( $post_id ) );\n $background_reference = $this->normalize_summary_text( $this->build_background( $post_id ) );\n\n foreach ( $this->extract_heading_section_paragraphs( $post_id, '/\b(?:impact|next steps|what happens next|what it means)\b/iu' ) as $sentence ) {\n $candidate = $this->trim_excerpt_text( $sentence, 190 );\n $key = $this->normalize_summary_text( $candidate );\n if ( '' === $key || $key === $why_reference || $key === $background_reference || str_ends_with( $candidate, '...' ) ) {\n continue;\n }\n if ( $this->looks_like_impact_summary( $candidate, $title ) ) {\n return $candidate;\n }\n }\n\n foreach ( $this->extract_candidate_sentences( $post_id ) as $sentence ) {\n if ( ! $this->looks_like_impact_summary( $sentence, $title ) ) {\n continue;\n }\n $candidate = $this->trim_excerpt_text( $sentence, 190 );\n $key = $this->normalize_summary_text( $candidate );\n if ( '' === $key || $key === $why_reference || $key === $background_reference ) {\n continue;\n }\n if ( str_ends_with( $candidate, '...' ) ) {\n continue;\n }\n return $candidate;\n }\n\n $excerpt = trim( (string) get_post_field( 'post_excerpt', $post_id ) );\n if ( $this->looks_like_impact_summary( $excerpt, $title ) ) {\n $candidate = $this->trim_excerpt_text( $excerpt, 190 );\n $key = $this->normalize_summary_text( $candidate );\n if ( $key !== $why_reference && $key !== $background_reference && ! str_ends_with( $candidate, '...' ) ) {\n return $candidate;\n }\n }\n\n return '';\n }\n\n private function build_timeline_items( $post_id ) {\n $title = get_the_title( $post_id );\n $paragraphs = $this->extract_clean_paragraphs( (string) get_post_field( 'post_content', $post_id ), $title );\n $items = [];\n $seen = [];\n\n foreach ( $paragraphs as $paragraph ) {\n $sentences = preg_split( '/(?