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 */