registry = $registry; $this->meta = $meta ? $meta : new MetaGenerator(); } public function register() { if ( ! \PublishRankSEO\Core\OutputSettings::metadata_output() ) { return; } add_action( 'after_setup_theme', array( $this, 'ensure_title_support' ), 1 ); add_filter( 'pre_get_document_title', array( $this, 'title' ), 99 ); add_action( 'wp_head', array( $this, 'render' ), 1 ); // PublishRank prints the canonical and robots tags itself; remove core's copies to avoid duplicates. remove_action( 'wp_head', 'rel_canonical' ); remove_action( 'wp_head', 'wp_robots', 1 ); } /** Add core title-tag support only when the active theme has not declared it. */ public function ensure_title_support() { if ( function_exists( 'current_theme_supports' ) && ! current_theme_supports( 'title-tag' ) ) { add_theme_support( 'title-tag' ); } } public function title( $title ) { // The saved switch is honoured at run time as well, so turning metadata off takes effect on the next request // even for hooks registered earlier in the same request. if ( is_admin() || wp_doing_ajax() || ! \PublishRankSEO\Core\OutputSettings::metadata_output() ) { return $title; } $generated = $this->meta->title(); if ( '' !== $generated ) { $this->registry->claim( 'title', 'head-output' ); } return '' !== $generated ? $generated : $title; } /** * Fail-safe entry point: a problem while building this output can never take the public page down. * Partial markup is discarded and the failure is recorded for the integrity/diagnostics screens. */ public function render() { ob_start(); try { $this->render_output(); echo ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- markup was escaped where it was built. } catch ( \Throwable $e ) { ob_end_clean(); do_action( 'publishrank_module_failed', __CLASS__, 'runtime_exception', false, $e->getMessage() ); } } private function render_output() { if ( is_admin() || wp_doing_ajax() || ! \PublishRankSEO\Core\OutputSettings::metadata_output() ) { return; } // Always disclose technical provenance in the public source. This is separate from the optional visible footer credit. $markers = \PublishRankSEO\Core\OutputSettings::source_markers(); if ( $markers ) { echo "\n"; } echo '' . "\n"; if ( $markers ) { echo '' . "\n"; echo '' . "\n"; } echo "\n"; $description = $this->meta->description(); if ( $this->registry->claim( 'description' ) && '' !== $description ) { echo '' . "\n"; } if ( $this->registry->claim( 'canonical' ) ) { echo '' . "\n"; } if ( $this->registry->claim( 'robots' ) ) { echo '' . "\n"; } echo "\n"; if ( $this->is_editorial_article() && $this->registry->claim( 'news-meta' ) ) { echo "\n"; $post_id = get_queried_object_id(); echo '' . "\n"; echo '' . "\n"; echo '' . "\n"; foreach ( $this->meta->keywords() as $keyword ) { echo '' . "\n"; } $accessible = '1' === (string) PostMeta::get( $post_id, PostMeta::ACCESSIBLE_FOR_FREE, '1' ); echo '' . "\n"; $author_name = (string) PostMeta::get( $post_id, PostMeta::AUTHOR_NAME, '' ); if ( '' === $author_name ) { $author_name = (string) get_the_author_meta( 'display_name', (int) get_post_field( 'post_author', $post_id ) ); } if ( '' !== $author_name ) { echo '' . "\n"; } $news_keywords = implode( ', ', $this->meta->keywords() ); if ( $news_keywords ) { echo '' . "\n"; } echo "\n"; } $image = $this->image(); $image_details = $this->image_details( $image ); if ( $this->registry->claim( 'og' ) ) { echo "\n"; echo '' . "\n"; echo '' . "\n"; echo '' . "\n"; echo '' . "\n"; echo '' . "\n"; if ( $this->is_editorial_article() ) { echo '' . "\n"; } $locale = str_replace( '-', '_', ! empty( LanguageAlternates::current_locale() ) ? LanguageAlternates::current_locale() : sanitize_text_field( (string) get_option( 'publishrank_default_locale', get_locale() ) ) ); echo '' . "\n"; foreach ( LanguageAlternates::alternates() as $alternate_locale => $alternate_url ) { $alternate_locale = str_replace( '-', '_', $alternate_locale ); if ( $alternate_locale !== $locale ) { echo '' . "\n"; } } if ( $image ) { echo '' . "\n"; $image_alt = $this->image_alt( $image ); if ( $image_alt ) { echo '' . "\n"; } if ( 0 < $image_details['width'] ) { echo '' . "\n"; } if ( 0 < $image_details['height'] ) { echo '' . "\n"; } if ( $image_details['type'] ) { echo '' . "\n"; } if ( 0 === strpos( strtolower( $image ), 'https://' ) ) { echo '' . "\n"; } } if ( is_singular() ) { $video_url = (string) PostMeta::get( get_queried_object_id(), PostMeta::VIDEO_URL, '' ); if ( '' !== $video_url ) { echo '' . "\n"; if ( 0 === strpos( strtolower( $video_url ), 'https://' ) ) { echo '' . "\n"; } } } if ( $this->is_editorial_article() ) { $publisher = $this->publisher_profile(); if ( $publisher ) { echo '' . "\n"; } $author_id = (int) get_post_field( 'post_author', get_queried_object_id() ); $author_url = (string) get_user_meta( $author_id, '_publishrank_author_facebook', true ); if ( ! $author_url ) { $author_url = get_author_posts_url( $author_id ); } if ( $author_url ) { echo '' . "\n"; } } $facebook_page = esc_url_raw( (string) get_option( 'publishrank_facebook_page_url', '' ) ); if ( $facebook_page ) { echo '' . "\n"; } $app_id = sanitize_text_field( (string) get_option( 'publishrank_facebook_app_id', '' ) ); if ( $app_id ) { echo '' . "\n"; } echo "\n"; } if ( $this->registry->claim( 'twitter' ) ) { echo "\n"; $twitter_card = is_singular() ? PostMeta::get( get_queried_object_id(), PostMeta::TWITTER_CARD, get_option( 'publishrank_twitter_card', 'summary_large_image' ) ) : get_option( 'publishrank_twitter_card', 'summary_large_image' ); $twitter_card = in_array( $twitter_card, array( 'summary', 'summary_large_image' ), true ) ? $twitter_card : 'summary_large_image'; echo '' . "\n"; $handle = (string) get_option( 'publishrank_seo_twitter_site', '' ); if ( '' !== $handle ) { echo '' . "\n"; } $creator = is_singular() ? PostMeta::get( get_queried_object_id(), PostMeta::TWITTER_CREATOR, '' ) : ''; if ( $creator ) { echo '' . "\n"; } echo '' . "\n"; echo '' . "\n"; if ( $image ) { echo '' . "\n"; $image_alt = $this->image_alt( $image ); if ( $image_alt ) { echo '' . "\n"; } } if ( $this->is_editorial_article() ) { // Enhanced X/Slack previews: author and reading time. $tw_post = get_post( get_queried_object_id() ); $tw_author = $tw_post ? (string) get_the_author_meta( 'display_name', (int) $tw_post->post_author ) : ''; $tw_words = $tw_post ? str_word_count( wp_strip_all_tags( strip_shortcodes( (string) $tw_post->post_content ) ) ) : 0; $tw_minutes = max( 1, (int) ceil( $tw_words / 200 ) ); if ( '' !== $tw_author ) { echo '' . "\n"; echo '' . "\n"; } if ( $tw_words > 0 ) { echo '' . "\n"; /* translators: %d: minutes. */ echo '' . "\n"; } } echo "\n"; } echo \PublishRankSEO\Core\OutputSettings::source_markers() ? "\n" : "\n"; } /** Per-post social text override, falling back to the generated value. */ private function social( $meta_key, $fallback ) { if ( is_singular() ) { $override = PostMeta::get( get_queried_object_id(), $meta_key, '' ); if ( '' !== (string) $override ) { // The override is a template: resolve %%variables%% (unknown ones are dropped) instead of printing raw tokens. $field = PostMeta::OG_TITLE === $meta_key ? 'title' : 'description'; $resolved = $this->meta->resolve_template( (string) $override, (int) get_queried_object_id(), $field ); if ( '' !== $resolved ) { return $resolved; } } } return $fallback; } /** Robots directive: respects "discourage search engines" and adds large image previews for indexable pages. */ private function robots_value() { if ( '0' === (string) get_option( 'blog_public', '1' ) ) { return 'noindex,nofollow'; } $robots = $this->meta->robots(); if ( false === strpos( $robots, 'noindex' ) ) { // Google's recommended preview controls for indexable pages; editor-set values are never overridden. foreach ( array( 'max-image-preview' => 'max-image-preview:large', 'max-snippet' => 'max-snippet:-1', 'max-video-preview' => 'max-video-preview:-1' ) as $directive => $value ) { if ( false === strpos( $robots, $directive ) && false === strpos( $robots, 'nosnippet' ) ) { $robots .= ',' . $value; } } } return (string) apply_filters( 'publishrank_seo_robots', $robots ); } /** @var array Details of the social image chosen for this request. */ private $image_info = array(); /** @return string Social image: per-post override, featured image (any post type), first content image, then the site default. */ private function image() { $this->image_info = array(); if ( is_singular() ) { $found = SeoData::image_for_post( get_queried_object_id() ); if ( $found && ! empty( $found['url'] ) ) { $url = 'override' === ( $found['source'] ?? '' ) ? PublicationProfile::normalize_url( (string) $found['url'] ) : (string) $found['url']; if ( $url ) { $url = PublicationProfile::asset_url( $url ); $this->image_info = array_merge( $found, array( 'url' => $url ) ); $processed = SocialWatermark::url( $url, $this->image_info ); $this->image_info['original_url'] = $url; $this->image_info['url'] = $processed; return $processed; } } } $default = (string) get_option( 'publishrank_seo_default_social_image', '' ); if ( '' === $default ) { return ''; } $attachment_id = function_exists( 'attachment_url_to_postid' ) ? absint( attachment_url_to_postid( $default ) ) : 0; if ( $attachment_id ) { $this->image_info = SeoData::attachment_info( $attachment_id ); } $default = PublicationProfile::asset_url( $default ); $processed = SocialWatermark::url( $default, $this->image_info ); $this->image_info['original_url'] = $default; $this->image_info['url'] = $processed; return $processed; } /** @param string $url @return string Override, then attachment alt text, caption or post title. */ private function image_alt( $url ) { if ( ! empty( $this->image_info['url'] ) && $this->image_info['url'] === $url && ! empty( $this->image_info['alt'] ) ) { return wp_strip_all_tags( (string) $this->image_info['alt'] ); } $attachment_id = function_exists( 'attachment_url_to_postid' ) && $url ? absint( attachment_url_to_postid( $url ) ) : 0; return $attachment_id ? sanitize_text_field( (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) : ''; } /** @param string $url @return array */ private function image_details( $url ) { $details = array( 'width' => 0, 'height' => 0, 'type' => '', ); if ( ! $url ) { return $details; } if ( ! empty( $this->image_info['url'] ) && $this->image_info['url'] === $url && ( ! empty( $this->image_info['width'] ) || ! empty( $this->image_info['type'] ) ) ) { return array( 'width' => absint( $this->image_info['width'] ?? 0 ), 'height' => absint( $this->image_info['height'] ?? 0 ), 'type' => sanitize_text_field( (string) ( $this->image_info['type'] ?? '' ) ), ); } if ( ! function_exists( 'attachment_url_to_postid' ) ) { return $details; } $attachment_id = absint( attachment_url_to_postid( $url ) ); if ( ! $attachment_id ) { return $details; } $metadata = wp_get_attachment_metadata( $attachment_id ); if ( is_array( $metadata ) ) { $details['width'] = isset( $metadata['width'] ) ? absint( $metadata['width'] ) : 0; $details['height'] = isset( $metadata['height'] ) ? absint( $metadata['height'] ) : 0; } $mime = get_post_mime_type( $attachment_id ); $filetype = wp_check_filetype( (string) wp_parse_url( $url, PHP_URL_PATH ) ); $details['type'] = ! empty( $filetype['type'] ) ? sanitize_text_field( $filetype['type'] ) : ( $mime ? sanitize_text_field( $mime ) : '' ); return $details; } /** @return string */ private function publisher_profile() { $configured = esc_url_raw( (string) get_option( 'publishrank_facebook_page_url', '' ) ); if ( $configured ) { return $configured; } $profiles = PublicationProfile::social_profiles(); foreach ( $profiles as $profile ) { if ( false !== strpos( strtolower( $profile ), 'facebook.com' ) ) { return $profile; } } return ! empty( $profiles ) ? (string) reset( $profiles ) : ''; } private function is_editorial_article() { $post = get_queried_object(); $type = $post instanceof \WP_Post ? get_post_type_object( $post->post_type ) : false; return is_singular() && $type && $type->public && $type->publicly_queryable && ! in_array( $post->post_type, array( 'page', 'attachment' ), true ); } } /* EOF */