$image_id = jetpack_get_site_logo( 'id' );
$logo = wp_get_attachment_image_src( $image_id, 'full' );
isset( $logo[0] ) && isset( $logo[1] ) && isset( $logo[2] )
&& ( _jetpack_og_get_image_validate_size( $logo[1], $logo[2], $width, $height ) )
// Third fall back, Core's site logo.
if ( has_custom_logo() ) {
$custom_logo_id = get_theme_mod( 'custom_logo' );
$sl_details = wp_get_attachment_image_src(
isset( $sl_details[0] ) && isset( $sl_details[1] ) && isset( $sl_details[2] )
&& ( _jetpack_og_get_image_validate_size( $sl_details[1], $sl_details[2], $width, $height ) )
'width' => $sl_details[1],
'height' => $sl_details[2],
'alt_text' => Images::get_alt_text( $custom_logo_id ),
// Fourth fall back, Core Site Icon, if valid in size.
$image_id = get_option( 'site_icon' );
$icon = wp_get_attachment_image_src( $image_id, 'full' );
isset( $icon[0] ) && isset( $icon[1] ) && isset( $icon[2] )
&& ( _jetpack_og_get_image_validate_size( $icon[1], $icon[2], $width, $height ) )
* Get the site's fallback image.
function jetpack_og_get_site_fallback_blank_image() {
* Filter the default Open Graph Image tag, used when no Image can be found in a post.
* @param string $str Default Image URL.
return apply_filters( 'jetpack_open_graph_image_default', 'https://s0.wp.com/i/blank.jpg' );
* Get available templates for Social Image Generator.
* @return array The available templates.
function jetpack_og_get_available_templates() {
if ( ! class_exists( '\Automattic\Jetpack\Publicize\Social_Image_Generator\Templates' ) ) {
return \Automattic\Jetpack\Publicize\Social_Image_Generator\Templates::TEMPLATES;
* Get a social image token from Social Image Generator.
* @param string $site_title The site title.
* @param string $image_url The image URL.
* @param string $template The template to use.
* @return string|WP_Error The social image token, or a WP_Error if the token could not be generated.
function jetpack_og_get_social_image_token( $site_title, $image_url, $template ) {
// Let's check if we have a cached token.
$cache_key = wp_hash( $site_title . $image_url . $template );
$transient_name = 'jetpack_og_social_image_token_' . $cache_key;
$cached_token = get_transient( $transient_name );
if ( ! empty( $cached_token ) ) {
* Filter the social image token for testing purposes.
* @param string|WP_Error|null $token The token to return, or null to use default behavior.
$token = apply_filters( 'jetpack_og_get_social_image_token', null );
if ( ! function_exists( '\Automattic\Jetpack\Publicize\Social_Image_Generator\fetch_token' ) ) {
return new WP_Error( 'jetpack_og_get_social_image_token_error', __( 'Social Image Generator is not available.', 'jetpack' ) );
$token = \Automattic\Jetpack\Publicize\Social_Image_Generator\fetch_token( $site_title, $image_url, $template );
* We want to cache 2 types of responses:
* - Successful responses with a token.
* - WP_Error responses that denote a WordPress.com connection issue.
&& 'invalid_user_permission_publicize' === $token->get_error_code()
set_transient( $transient_name, $token, DAY_IN_SECONDS );
* Generate and create a fallback social image.
* @param array $representative_image The representative image of the site.
* @param string $template The template to use.
* @return array The source ('src'), 'width', and 'height' of the image.
function jetpack_og_generate_fallback_social_image( $representative_image, $template ) {
$site_title = get_bloginfo( 'name' );
'src' => $representative_image['src'],
'width' => $representative_image['width'],
'height' => $representative_image['height'],
// Ensure that we use a valid template.
jetpack_og_get_available_templates(),
// Let's generate the token matching the image..
$token = jetpack_og_get_social_image_token( $site_title, $representative_image['src'], $template );
if ( is_wp_error( $token ) ) {
// Build the image URL and return it.
'https://s0.wp.com/_si/?t=%s',
* Validate the width and height against required width and height
* @param int $width Width of the image.
* @param int $height Height of the image.
* @param int $req_width Required width to pass validation.
* @param int $req_height Required height to pass validation.
* @return bool - True if the image passed the required size validation
function _jetpack_og_get_image_validate_size( $width, $height, $req_width, $req_height ) {
if ( ! $width || ! $height ) {
$valid_width = ( $width >= $req_width );
$valid_height = ( $height >= $req_height );
$is_image_acceptable = $valid_width && $valid_height;
return $is_image_acceptable;
* Gets a gravatar URL of the specified size.
* @param string $email E-mail address to get gravatar for.
* @param int $width Size of returned gravatar.
* @return array|bool|mixed|string
function jetpack_og_get_image_gravatar( $email, $width ) {
* Clean up text meant to be used as Description Open Graph tag.
* - no html tags or their contents
* - no content within wp:query blocks
* @param string $description Text coming from WordPress (autogenerated or manually generated by author).
* @param WP_Post|null $data Information about our post.
* @return string $description Cleaned up description string.
function jetpack_og_get_description( $description = '', $data = null ) {
// Remove content within wp:query blocks.
$description = jetpack_og_remove_query_blocks( $description );
// Remove tags such as <style or <script.
$description = wp_strip_all_tags( $description );
* Clean up any plain text entities left into formatted entities.
* Intentionally not using a filter to prevent pollution.
* @see https://github.com/Automattic/jetpack/pull/2899#issuecomment-151957382
wptexturize( $description )
$description = strip_shortcodes( $description );
$description = preg_replace(
* Limit things to a small text blurb.
* There isn't a hard limit set by Facebook, so let's rely on WP's own limit.
* (55 words or the localized equivalent).
* This limit can be customized with the wp_trim_words filter.
$description = wp_trim_words( $description );
// Let's set a default if we have no text by now.
if ( empty( $description ) ) {
* Filter the fallback `og:description` used when no excerpt information is provided.
* @module sharedaddy, publicize
* @param string $var Fallback og:description. Default is translated `Visit the post for more'.
* @param object $data Post object for the current post.
$description = apply_filters(
'jetpack_open_graph_fallback_description',
__( 'Visit the post for more.', 'jetpack' ),
* Remove content within wp:query blocks from the description.
* @param string $description The description text that may contain block markup.
* @return string The description with wp:query blocks removed.
function jetpack_og_remove_query_blocks( $description ) {
// Handle non-string input
if ( ! is_string( $description ) ) {
$scanner = Block_Scanner::create( $description );
while ( $scanner->next_delimiter() ) {
$span = $scanner->get_span();
$match_at = $span->start;
// Check if this is a query block.
if ( $scanner->is_block_type( 'query' ) ) {
switch ( $scanner->get_delimiter_type() ) {
case Block_Scanner::OPENER:
if ( ! $in_query_block ) {
// Copy content before the query block.
$output .= substr( $description, $offset, $match_at - $offset );
case Block_Scanner::CLOSER:
if ( $in_query_block && $depth === 0 ) {
// We've exited the query block, continue from after it.
$offset = $match_at + $length;
// Remove extra newline if present
str_starts_with( substr( $description, $offset ), "\n" )
&& str_ends_with( $output, "\n" )
case Block_Scanner::VOID:
// Void query blocks should be removed entirely.
if ( ! $in_query_block ) {
$output .= substr( $description, $offset, $match_at - $offset );
$offset = $match_at + $length;
// Remove extra newline if present
str_starts_with( substr( $description, $offset ), "\n" )
&& str_ends_with( $output, "\n" )
} elseif ( ! $in_query_block ) {
// Not a query block, copy content including the delimiter if we're not inside a query block.
$output .= substr( $description, $offset, $match_at - $offset + $length );
$offset = $match_at + $length;
// Add any remaining content after the last delimiter.
if ( ! $in_query_block ) {
$output .= substr( $description, $offset );
* Display a Fediverse actor Open Graph tag when the post author has a Mastodon connection.
* @see https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/
* @param array $tags Current tags.
function jetpack_add_fediverse_creator_open_graph_tag( $tags ) {
* Let's not do this on WordPress.com Simple for now,
* because of its performance impact.
* See p1723574138779019/1723572983.803009-slack-C01U2KGS2PQ
if ( ( new Host() )->is_wpcom_simple() ) {
// Let's not add any tags when the ActivityPub plugin already adds its own.
$is_activitypub_opengraph_integration_active = get_option( 'activitypub_use_opengraph' );
if ( $is_activitypub_opengraph_integration_active ) {
// We pull the Mastodon connection data from Publicize.
if ( ! function_exists( 'publicize_init' ) ) {
$publicize = publicize_init();
|| ! $post instanceof WP_Post
|| empty( $post->post_author )
$post_mastodon_connections = array();
// Loop through active connections.
foreach ( (array) $publicize->get_services( 'connected' ) as $service_name => $connections ) {
if ( 'mastodon' !== $service_name ) {
// services can have multiple connections. Store them all in our array.
foreach ( $connections as $connection ) {
$connection_id = $publicize->get_connection_id( $connection );
$connection_meta = $publicize->get_connection_meta( $connection );
$connection_data = $connection_meta['connection_data'] ?? array();
$mastodon_handle = $connection_meta['external_display'] ?? '';
$connection_user_id = $connection_data['user_id'] ?? 0;
if ( empty( $mastodon_handle ) ) {
// Did we skip this connection for this post?
if ( get_post_meta( $post->ID, $publicize->POST_SKIP_PUBLICIZE . $connection_id, true ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$post_mastodon_connections[] = array(
'user_id' => (int) $connection_user_id,
'connection_id' => (int) $connection_id,
'handle' => $mastodon_handle,
'global' => 0 === (int) $connection_user_id,
// If we have no Mastodon connections, skip.
if ( empty( $post_mastodon_connections ) ) {
* Select a single Mastodon connection to use.
* It should be either the first connection belonging to the post author,
* or the first global connection.
foreach ( $post_mastodon_connections as $mastodon_connection ) {
if ( (int) $post->post_author === $mastodon_connection['user_id'] ) {
$tags['fediverse:creator'] = esc_attr( $mastodon_connection['handle'] );
if ( $mastodon_connection['global'] ) {
$tags['fediverse:creator'] = esc_attr( $mastodon_connection['handle'] );