diff --git a/src/wp-includes/embed.php b/src/wp-includes/embed.php
index 3fb8968c7c62c..c6d9433f37677 100644
--- a/src/wp-includes/embed.php
+++ b/src/wp-includes/embed.php
@@ -1233,11 +1233,24 @@ function print_embed_sharing_dialog() {
* @since 4.5.0
*/
function the_embed_site_title() {
+ $fallback_icon_url = includes_url( 'images/w-logo-blue.png' );
+ $site_icon_url = get_site_icon_url( 32, $fallback_icon_url );
+
+ $icon_img = '';
+ if ( $site_icon_url ) {
+ $site_icon_url_2x = get_site_icon_url( 64, $fallback_icon_url );
+ $srcset = ( $site_icon_url_2x && $site_icon_url !== $site_icon_url_2x ) ? sprintf( ' srcset="%s 2x"', esc_url( $site_icon_url_2x ) ) : '';
+ $icon_img = sprintf(
+ '
',
+ esc_url( $site_icon_url ),
+ $srcset
+ );
+ }
+
$site_title = sprintf(
- '
%s',
+ '%s%s',
esc_url( home_url() ),
- esc_url( get_site_icon_url( 32, includes_url( 'images/w-logo-blue.png' ) ) ),
- esc_url( get_site_icon_url( 64, includes_url( 'images/w-logo-blue.png' ) ) ),
+ $icon_img,
esc_html( get_bloginfo( 'name' ) )
);
diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php
index 47e2aeb2ebb05..1ba2912953ae8 100644
--- a/src/wp-includes/general-template.php
+++ b/src/wp-includes/general-template.php
@@ -978,7 +978,10 @@ function get_site_icon_url( $size = 512, $url = '', $blog_id = 0 ) {
} else {
$size_data = array( $size, $size );
}
- $url = wp_get_attachment_image_url( $site_icon_id, $size_data );
+ $attachment_url = wp_get_attachment_image_url( $site_icon_id, $size_data );
+ if ( $attachment_url ) {
+ $url = $attachment_url;
+ }
}
if ( $switched_blog ) {
diff --git a/tests/phpunit/includes/utils.php b/tests/phpunit/includes/utils.php
index 014f4b83c2301..62cc7805f4898 100644
--- a/tests/phpunit/includes/utils.php
+++ b/tests/phpunit/includes/utils.php
@@ -432,10 +432,19 @@ function dmp_filter( $a ) {
return $a;
}
-function get_echo( $callback, $args = array() ) {
+/**
+ * Gets the output of a given callback via output buffering.
+ *
+ * The return value of the callback is disregarded.
+ *
+ * @param callable $callback Callback to capture the output from.
+ * @param array $args The positional params to pass to the callback.
+ * @return string Output buffer.
+ */
+function get_echo( callable $callback, array $args = array() ): string {
ob_start();
call_user_func_array( $callback, $args );
- return ob_get_clean();
+ return (string) ob_get_clean();
}
// Recursively generate some quick assertEquals() tests based on an array.
diff --git a/tests/phpunit/tests/general/template.php b/tests/phpunit/tests/general/template.php
index d3b35a2c46c2b..bbf798f126578 100644
--- a/tests/phpunit/tests/general/template.php
+++ b/tests/phpunit/tests/general/template.php
@@ -122,6 +122,22 @@ public function test_get_site_icon_url() {
$this->assertEmpty( get_site_icon_url(), 'Site icon URL should not be set after removal.' );
}
+ /**
+ * @ticket 65098
+ * @group site_icon
+ * @covers ::get_site_icon_url
+ * @requires function imagejpeg
+ */
+ public function test_get_site_icon_url_returns_fallback_when_attachment_url_fails(): void {
+ $this->set_site_icon();
+
+ $fallback = 'https://example.com/fallback-icon.png';
+ add_filter( 'wp_get_attachment_image_src', '__return_false' );
+ $url = get_site_icon_url( 32, $fallback );
+
+ $this->assertSame( $fallback, $url, 'Fallback URL should be returned when attachment URL lookup fails.' );
+ }
+
/**
* @group site_icon
* @covers ::site_icon_url
@@ -807,4 +823,103 @@ public function test_get_the_archive_title_is_correct_for_author_queries() {
$this->assertSame( $user_with_posts->display_name, $title_when_posts );
$this->assertSame( $user_with_no_posts->display_name, $title_when_no_posts );
}
+
+ /**
+ * @ticket 65098
+ * @group site_icon
+ * @covers ::the_embed_site_title
+ * @requires function imagejpeg
+ */
+ public function test_the_embed_site_title_contains_site_icon_when_set(): void {
+ $this->set_site_icon();
+
+ $url_32 = get_site_icon_url( 32 );
+ $url_64 = get_site_icon_url( 64 );
+
+ $output = get_echo( 'the_embed_site_title' );
+ $processor = new WP_HTML_Tag_Processor( $output );
+
+ $this->assertTrue( $processor->next_tag( 'IMG' ), 'Expected IMG tag.' );
+ $this->assertTrue( $processor->has_class( 'wp-embed-site-icon' ), 'Expected IMG to have wp-embed-site-icon class.' );
+ $this->assertSame( $url_32, $processor->get_attribute( 'src' ), 'Output should contain 32px site icon URL in src.' );
+ $srcset = $processor->get_attribute( 'srcset' );
+ $this->assertIsString( $srcset, 'Expected srcset to be present.' );
+ $this->assertStringContainsString( $url_64, $srcset, 'Output should contain 64px site icon URL in srcset.' );
+ }
+
+ /**
+ * @ticket 65098
+ * @group site_icon
+ * @covers ::the_embed_site_title
+ * @requires function imagejpeg
+ */
+ public function test_the_embed_site_title_uses_fallback_when_attachment_url_fails(): void {
+ $this->set_site_icon();
+
+ // Simulate wp_get_attachment_image_url() failing.
+ add_filter( 'wp_get_attachment_image_src', '__return_false' );
+ $output = get_echo( 'the_embed_site_title' );
+
+ $fallback = includes_url( 'images/w-logo-blue.png' );
+ $processor = new WP_HTML_Tag_Processor( $output );
+
+ $this->assertTrue( $processor->next_tag( 'IMG' ), 'Expected IMG tag with fallback.' );
+ $this->assertTrue( $processor->has_class( 'wp-embed-site-icon' ), 'Expected IMG to have wp-embed-site-icon class.' );
+ $this->assertSame( $fallback, $processor->get_attribute( 'src' ), 'Output should contain fallback URL in src when attachment URL fails.' );
+ }
+
+ /**
+ * @ticket 65098
+ * @group site_icon
+ * @covers ::the_embed_site_title
+ */
+ public function test_the_embed_site_title_omits_img_when_url_is_empty(): void {
+ // Force get_site_icon_url() to return empty string via filter.
+ add_filter( 'get_site_icon_url', '__return_empty_string' );
+ $output = get_echo( 'the_embed_site_title' );
+
+ $processor = new WP_HTML_Tag_Processor( $output );
+
+ $this->assertFalse( $processor->next_tag( 'IMG' ), 'IMG tag should be omitted when URL is empty.' );
+ $this->assertStringContainsString( get_bloginfo( 'name' ), $output, 'Site name should still be present.' );
+ }
+
+ /**
+ * @ticket 65098
+ * @group site_icon
+ * @covers ::the_embed_site_title
+ */
+ public function test_the_embed_site_title_omits_srcset_when_1x_and_2x_urls_are_identical(): void {
+ // Force both sizes to return the same URL.
+ $svg_url = 'https://example.com/icon.svg';
+ $filter = static function () use ( $svg_url ) {
+ return $svg_url;
+ };
+
+ add_filter( 'get_site_icon_url', $filter );
+ $output = get_echo( 'the_embed_site_title' );
+
+ $processor = new WP_HTML_Tag_Processor( $output );
+
+ $this->assertTrue( $processor->next_tag( 'IMG' ), 'Expected IMG tag.' );
+ $this->assertSame( $svg_url, $processor->get_attribute( 'src' ), '1x URL should be present in src.' );
+ $this->assertNull( $processor->get_attribute( 'srcset' ), 'srcset should be omitted when 1x and 2x URLs are identical.' );
+ }
+
+ /**
+ * @ticket 65098
+ * @group site_icon
+ * @covers ::the_embed_site_title
+ */
+ public function test_the_embed_site_title_uses_fallback_without_srcset_when_no_site_icon_set(): void {
+ $output = get_echo( 'the_embed_site_title' );
+ $fallback = includes_url( 'images/w-logo-blue.png' );
+
+ $processor = new WP_HTML_Tag_Processor( $output );
+
+ $this->assertTrue( $processor->next_tag( 'IMG' ), 'Expected IMG tag with fallback.' );
+ $this->assertTrue( $processor->has_class( 'wp-embed-site-icon' ), 'Expected IMG to have wp-embed-site-icon class.' );
+ $this->assertSame( $fallback, $processor->get_attribute( 'src' ), 'Output should contain fallback icon URL in src.' );
+ $this->assertNull( $processor->get_attribute( 'srcset' ), 'srcset should be omitted when 1x and 2x fallback URLs are identical.' );
+ }
}