Is it possible set a featured image with external image URL

Yes, it’s possible and pretty easy.

This is the workflow I suggest:

  1. Put somewhere a UI to insert the URL of the featured image. Probably best choice is to use 'admin_post_thumbnail_html' filter hook
  2. Use 'save_post' action hook to save the URL (after security and validation routine) in a custom post meta
  3. Use 'post_thumbnail_html' filter hook to output proper <img> markup, overriding default, if the post for which the featured image is required has the post meta with the external featured image

To work, this workflow needs the featured image to be shown in the template using get_the_post_thumnbail() or the_post_thumbnail() functions.

Moreover, we must be sure that '_thumbnail_id' meta value has a non-empty value when we set the meta for the external URL, otherwise has_post_thumbnail() will return false for posts that only have an external featured image.

In fact, it is possible that a post has both a standard local featured image and one set via our workflow, and in this case the external will be used.

To implement our workflow we need a function to validate the URL used as an external featured image, because we have to be sure it is a valid image URL.

There are different ways to do this task; here I use a very simple way that only looks at the URL, without downloading the image. This works only for static image URLs and does not verify that the image actually exists, but it’s fast. Modify it to something more advanced if you need to (here is some help).

function url_is_image( $url ) {
    if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
        return FALSE;
    }
    $ext = array( 'jpeg', 'jpg', 'gif', 'png' );
    $info = (array) pathinfo( parse_url( $url, PHP_URL_PATH ) );
    return isset( $info['extension'] )
        && in_array( strtolower( $info['extension'] ), $ext, TRUE );
}

Pretty easy. Now let’s add the 3 hooks described in the workflow above:

add_filter( 'admin_post_thumbnail_html', 'thumbnail_url_field' );

add_action( 'save_post', 'thumbnail_url_field_save', 10, 2 );

add_filter( 'post_thumbnail_html', 'thumbnail_external_replace', 10, PHP_INT_MAX );

and the related functions. First the one that outputs the field in the admin:

function thumbnail_url_field( $html ) {
    global $post;
    $value = get_post_meta( $post->ID, '_thumbnail_ext_url', TRUE ) ? : "";
    $nonce = wp_create_nonce( 'thumbnail_ext_url_' . $post->ID . get_current_blog_id() );
    $html .= '<input type="hidden" name="thumbnail_ext_url_nonce" value="' 
        . esc_attr( $nonce ) . '">';
    $html .= '<div><p>' . __('Or', 'txtdomain') . '</p>';
    $html .= '<p>' . __( 'Enter the url for external image', 'txtdomain' ) . '</p>';
    $html .= '<p><input type="url" name="thumbnail_ext_url" value="' . $value . '"></p>';
    if ( ! empty($value) && url_is_image( $value ) ) {
        $html .= '<p><img style="max-width:150px;height:auto;" src="' 
            . esc_url($value) . '"></p>';
        $html .= '<p>' . __( 'Leave url blank to remove.', 'txtdomain' ) . '</p>';
    }
    $html .= '</div>';
    return $html;
}

Note that I’ve used 'txtdomain' as the text domain, but you should use a proper, registered text domain.

This is how the output looks when empty:

External URL for featured image: the field

And this is how it looks after having added an image URL and saved/updated the post:

External URL for featured image: the field after filling up and saved

So, now our admin UI is done, let’s write the saving routine:

function thumbnail_url_field_save( $pid, $post ) {
    $cap = $post->post_type === 'page' ? 'edit_page' : 'edit_post';
    if (
        ! current_user_can( $cap, $pid )
        || ! post_type_supports( $post->post_type, 'thumbnail' )
        || defined( 'DOING_AUTOSAVE' )
    ) {
        return;
    }
    $action = 'thumbnail_ext_url_' . $pid . get_current_blog_id();
    $nonce = filter_input( INPUT_POST, 'thumbnail_ext_url_nonce', FILTER_SANITIZE_STRING );
    $url = filter_input( INPUT_POST,  'thumbnail_ext_url', FILTER_VALIDATE_URL );
    if (
        empty( $nonce )
        || ! wp_verify_nonce( $nonce, $action )
        || ( ! empty( $url ) && ! url_is_image( $url ) )
    ) {
        return;
    }
    if ( ! empty( $url ) ) {
        update_post_meta( $pid, '_thumbnail_ext_url', esc_url($url) );
        if ( ! get_post_meta( $pid, '_thumbnail_id', TRUE ) ) {
            update_post_meta( $pid, '_thumbnail_id', 'by_url' );
        }
    } elseif ( get_post_meta( $pid, '_thumbnail_ext_url', TRUE ) ) {
        delete_post_meta( $pid, '_thumbnail_ext_url' );
        if ( get_post_meta( $pid, '_thumbnail_id', TRUE ) === 'by_url' ) {
            delete_post_meta( $pid, '_thumbnail_id' );
        }
    }
}

The function, after some security checks, looks at the URL posted and if it’s fine, saves it in '_thumbnail_ext_url' post meta. If the URL is empty and the meta was saved, it is deleted, allowing to remove the meta simply by emptying the external URL field.

Last thing to do is output the featured image markup when our external image URL is set in meta:

function thumbnail_external_replace( $html, $post_id ) {
    $url =  get_post_meta( $post_id, '_thumbnail_ext_url', TRUE );
    if ( empty( $url ) || ! url_is_image( $url ) ) {
        return $html;
    }
    $alt = get_post_field( 'post_title', $post_id ) . ' ' .  __( 'thumbnail', 'txtdomain' );
    $attr = array( 'alt' => $alt );
    $attr = apply_filters( 'wp_get_attachment_image_attributes', $attr, NULL );
    $attr = array_map( 'esc_attr', $attr );
    $html = sprintf( '<img src="%s"', esc_url($url) );
    foreach ( $attr as $name => $value ) {
        $html .= " $name=" . '"' . $value . '"';
    }
    $html .= ' />';
    return $html;
}

We are done.

What’s left to do

In the featured image output I’ve not used width or height properties, nor classes that WordPress usually adds, like 'attachment-$size'. That’s because sniffing the size of an image requires extra work that will slow down page loading, especially if you have more than one featured image in the page.

If you need those attributes, you can use my code adding a callback to wp_get_attachment_image_attributes' filter (it’s a standard WordPress hook) or maybe you can alter my code to sniff image size and output related attributes and classes.

Plugin Gist

All the code posted here, with the exception of adding a proper text domain initialization, is available as a full-working plugin in a Gist here. The code there uses a namespace, so it requires PHP 5.3+.

Notes

Of course, you should be sure that you have license and authorization to use and hotlink images in your site from external ones.

Leave a Comment