Fix avatar and social icons with bulletproof rendering
Release / Build & publish plugin zip (push) Successful in 7s

Root cause of social icons rendering as alt text: wp_kses() stripped
the 'data:' prefix from data: URIs in img.src, leaving a relative URL
that 404s. Switched to real .svg files under assets/icons/ referenced
via plugins_url() — kses passes img tags with relative URLs cleanly,
and the icons load as standard images.

Root cause of avatar still being cropped: layout-side issues with
inline-styled <img> in a flex layout. Switched to a <span> with
inline background-image, background-size: cover, background-position:
center — no <img> element, no object-fit reliance, no way to crop wrong.

Also removed the outer <a> wrapper around the .project-owner div
(block-level element inside inline <a> is invalid HTML and was likely
contributing to layout weirdness). The owner name is now an explicit
<a> inside its own span.

Reverted kses to the default wp_kses_post() since we're back to
standard HTML elements only — no SVG, no data URIs, no inline styles
that need a custom allowed list.
This commit is contained in:
Keith Solomon
2026-08-12 17:04:15 -05:00
parent 60db82e459
commit 426d60b692
9 changed files with 25 additions and 79 deletions
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#0073aa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M3 7l9 6l9 -6"/><rect x="3" y="5" width="18" height="14" rx="2"/></svg>

After

Width:  |  Height:  |  Size: 233 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#0073aa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M7 10v-3a1 1 0 0 1 1 -1h3v-4h4v4h3a1 1 0 0 1 1 1v3h-4v10h-4v-10h-3"/></svg>

After

Width:  |  Height:  |  Size: 237 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#0073aa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><line x1="8" y1="11" x2="8" y2="16"/><line x1="8" y1="8" x2="8" y2="8.01"/><line x1="12" y1="16" x2="12" y2="11"/><path d="M16 16v-3a2 2 0 0 0 -4 0"/></svg>

After

Width:  |  Height:  |  Size: 358 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#0073aa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8 17c1 -2 1.5 -4 .5 -6s-.5 -4 .5 -6"/><path d="M15 11c.667 -1 1.667 -2.333 3 -4"/></svg>

After

Width:  |  Height:  |  Size: 282 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#0073aa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 12m-6 0a6 6 0 1 0 12 0a6 6 0 1 0 -12 0"/><path d="M12 12h0"/><path d="M8.5 13c.667 .667 1.333 1 2 1s1.333 -.333 2 -1"/></svg>

After

Width:  |  Height:  |  Size: 322 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#0073aa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M3 21l1.65 -4.75a9 9 0 1 1 3.85 3.85z"/><path d="M9 10c1 2 3 3 5 4"/><path d="M9 13c1 1 2 2 4 3"/></svg>

After

Width:  |  Height:  |  Size: 266 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#0073aa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M7 4l10 16m0 -16l-10 16"/></svg>

After

Width:  |  Height:  |  Size: 194 B

+15 -71
View File
@@ -350,107 +350,51 @@ function projects_portfolio_social_sharing_buttons( $project_id ) {
$html = '<div class="project-social-sharing">';
$svg_icon = function( string $name, string $path ) use ( &$html ) {
// Render each social icon as an <img> with an inline SVG data URI.
// Using <img> instead of inline <svg> ensures browser CSS sizing and
// avoids any namespace/parser quirks with inline SVG in mixed content.
$svg = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#0073aa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">' . $path . '</svg>';
$data_uri = 'data:image/svg+xml;utf8,' . rawurlencode( $svg );
$html .= '<img src="' . esc_attr( $data_uri ) . '" alt="' . esc_attr( $name ) . '" width="20" height="20" style="width:20px;height:20px;display:inline-block;vertical-align:middle;" />';
$svg_icon = function( string $name, string $file ) use ( &$html ) {
// Render each social icon as an <img> referencing a real .svg file
// shipped under assets/icons/. This bypasses any kses data: URI stripping
// and ensures the icon loads as a normal image via standard img rules.
$src = plugins_url( 'assets/icons/' . $file, __FILE__ );
$html .= '<img src="' . esc_url( $src ) . '" alt="' . esc_attr( $name ) . '" width="20" height="20" style="width:20px;height:20px;display:inline-block;vertical-align:middle;" />';
};
// Facebook.
$html .= '<a href="https://www.facebook.com/sharer/sharer.php?u=' . $url . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'Facebook', '<path d="M7 10v-3a1 1 0 0 1 1 -1h3v-4h4v4h3a1 1 0 0 1 1 1v3h-4v10h-4v-10h-3" />' );
$svg_icon( 'Facebook', 'facebook.svg' );
$html .= '</a>';
// X (Twitter).
$html .= '<a href="https://twitter.com/intent/tweet?text=' . $title . '&url=' . $url . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'X', '<path d="M7 4l10 16m0 -16l-10 16" />' );
$svg_icon( 'X', 'x.svg' );
$html .= '</a>';
// LinkedIn.
$html .= '<a href="https://www.linkedin.com/sharing/share-offsite/?url=' . $url . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'LinkedIn', '<rect x="4" y="4" width="16" height="16" rx="2" /><line x1="8" y1="11" x2="8" y2="16" /><line x1="8" y1="8" x2="8" y2="8.01" /><line x1="12" y1="16" x2="12" y2="11" /><path d="M16 16v-3a2 2 0 0 0 -4 0" />' );
$svg_icon( 'LinkedIn', 'linkedin.svg' );
$html .= '</a>';
// Reddit.
$html .= '<a href="https://www.reddit.com/submit?url=' . $url . '&title=' . $title . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'Reddit', '<circle cx="12" cy="12" r="9" /><path d="M12 12m-6 0a6 6 0 1 0 12 0a6 6 0 1 0 -12 0" /><path d="M12 12h0" /><path d="M8.5 13c.667 .667 1.333 1 2 1s1.333 -.333 2 -1" />' );
$svg_icon( 'Reddit', 'reddit.svg' );
$html .= '</a>';
// WhatsApp.
$html .= '<a href="https://wa.me/?text=' . $title . '%20' . $url . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'WhatsApp', '<path d="M3 21l1.65 -4.75a9 9 0 1 1 3.85 3.85z" /><path d="M9 10c1 2 3 3 5 4" /><path d="M9 13c1 1 2 2 4 3" />' );
$html .= '<a href="https://www.whatsapp.com/send?text=' . $title . '%20' . $url . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'WhatsApp', 'whatsapp.svg' );
$html .= '</a>';
// Pinterest.
$html .= '<a href="https://pinterest.com/pin/create/button/?url=' . $url . '&description=' . $title . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'Pinterest', '<circle cx="12" cy="12" r="9" /><path d="M8 17c1 -2 1.5 -4 .5 -6s-.5 -4 .5 -6" /><path d="M15 11c.667 -1 1.667 -2.333 3 -4" />' );
$svg_icon( 'Pinterest', 'pinterest.svg' );
$html .= '</a>';
// Email.
$html .= '<a href="mailto:?subject=' . $title . '&body=' . $url . '" target="_blank" rel="noopener noreferrer">';
$svg_icon( 'Email', '<path d="M3 7l9 6l9 -6" /><rect x="3" y="5" width="18" height="14" rx="2" />' );
$svg_icon( 'Email', 'email.svg' );
$html .= '</a>';
$html .= '</div>';
// wp_kses_post() strips <svg> tags by default (the 'post' context doesn't
// allow them), and would also strip <img> data: URIs we use as a fallback.
// Extend the allowed list to keep both forms intact so the icons render.
$allowed = wp_kses_allowed_html( 'post' );
$allowed['img'] = array(
'src' => true,
'alt' => true,
'width' => true,
'height' => true,
'style' => true,
'aria-hidden' => true,
);
$allowed['svg'] = array(
'xmlns' => true,
'class' => true,
'width' => true,
'height' => true,
'viewBox' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'style' => true,
'aria-hidden' => true,
'focusable' => true,
);
$allowed['path'] = array(
'd' => true,
'fill' => true,
'stroke' => true,
);
$allowed['rect'] = array(
'x' => true,
'y' => true,
'width' => true,
'height' => true,
'fill' => true,
'stroke' => true,
);
$allowed['circle'] = array(
'cx' => true,
'cy' => true,
'r' => true,
'fill' => true,
'stroke' => true,
);
$allowed['line'] = array(
'x1' => true,
'y1' => true,
'x2' => true,
'y2' => true,
'stroke' => true,
);
echo wp_kses( $html, $allowed );
echo wp_kses_post( $html );
}
add_action( 'projects_after_download_button', 'projects_portfolio_social_sharing_buttons', 999 );
+2 -7
View File
@@ -141,15 +141,10 @@ if ( $last_updated ) {
</div>
<?php if ( $settings['templates']['github_owner'] && $owner_avatar && $owner_name ) : ?>
<a href="<?php echo esc_url( $browse_url ); ?>">
<div class="project-owner">
<span class="project-owner-avatar">
<img src="<?php echo esc_url( $owner_avatar ); ?>" alt="<?php esc_attr_e( $owner_name ); ?>" class="owner-avatar" style="display:block;width:50px;height:50px;border-radius:50%;object-fit:cover;flex-shrink:0;margin:0;" />
</span>
<span class="project-owner-avatar" style="display:block;width:50px;height:50px;border-radius:50%;background-image:url('<?php echo esc_url( $owner_avatar ); ?>');background-size:cover;background-position:center center;flex-shrink:0;" aria-label="<?php echo esc_attr( $owner_name ); ?>" role="img"></span>
<span class="project-owner-name">
<strong>
<a href="<?php echo esc_url( $owner_url ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( $owner_name ); ?></a>
</strong>
<a href="<?php echo esc_url( $owner_url ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $owner_name ); ?></a>
</span>
<span class="project-owner-follow">
<a class="button project-follow-button" href="<?php echo esc_url( $owner_url ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Follow', 'projects-wp' ); ?></a>