WordPress: Auto-Import External Images to Media Library (Block Editor Compatible)

Import external images to wp media library when pasted in the WP editor.

Introduction

If you’ve ever pasted content from Google Docs, copied images from other websites, or imported posts that reference external image URLs, you know the pain: WordPress keeps those images as remote links. They’re not in your media library. They break when the source site removes them. And WordPress doesn’t even recognize them as “real” images.

I built this plugin to solve exactly that problem. Every time you save a post, it automatically downloads all external images, uploads them to your media library, and updates your content to use the local versions. No manual intervention is needed.

What This Plugin Does

When you save any post or page (draft, published, or whatever), this plugin:

  1. Scans your content for any external image URLs
  2. Downloads each image to your server
  3. Creates a proper media library entry with all the metadata
  4. Replaces the entire image tag with WordPress-native markup
  5. Adds responsive image sizes (srcset, sizes attributes)
  6. Includes the wp-image-{ID} class so WordPress knows it’s local

It works with both the classic editor and the block editor (Gutenberg). It even handles Google Docs images and other redirected URLs.

Key Features

Smart Image Naming

Images are named based on your post title, not random URLs. If your post is “ChatGPT vs Claude vs Gemini for Product Strategy,” the images become:

  • chatgpt-vs-claude-vs-gemini-for-product-strategy.jpg
  • chatgpt-vs-claude-vs-gemini-for-product-strategy-2.jpg
  • chatgpt-vs-claude-vs-gemini-for-product-strategy-3.jpg

Optional Metadata Auto-Fill

You can configure whether the plugin auto-fills:

  • Image title
  • Alt text
  • Caption
  • Description

By default, all are set to false so you control this yourself.

Performance Safeguards

  • Maximum 50 images per save (configurable)
  • Content size limit of 5 MB to prevent timeouts
  • Deduplication – won’t re-download images that already exist
  • Stores a map of processed URLs to avoid redundant downloads

Block Editor Compatible

This version (2.0.0) is a complete rewrite that properly integrates with Gutenberg. The images now include:

  • Proper wp-image-{ID} classes
  • Responsive image sizes (thumbnail, medium, large, full)
  • Srcset and sizes attributes for responsive loading
  • Integration with WordPress’s native image handling

Configuration

At the top of the plugin file, you’ll find these settings:

php
define( 'RR_EXTIMG_SET_TITLE',       false );
define( 'RR_EXTIMG_SET_ALT',         false );
define( 'RR_EXTIMG_SET_CAPTION',     false );
define( 'RR_EXTIMG_SET_DESCRIPTION', false );
define( 'RR_EXTIMG_DEFAULT_SIZE',    'large' );

Set any to true to auto-fill that metadata field. The RR_EXTIMG_DEFAULT_SIZE controls which image size gets used in your content (options: thumbnail, medium, large, full).

How It Works (Technical Overview)

The plugin hooks into WordPress’s save process at multiple points:

  1. save_post hook – Catches manual saves in the admin
  2. rest_after_insert_post hook – Catches block editor saves via REST API
  3. rest_after_insert_page hook – Catches page saves via REST API

When triggered, it:

Extracts Images:

  • Parses HTML for <img> tags
  • Also parses Gutenberg block JSON for image URLs
  • Captures the full tag, not just the src

Validates & Downloads:

  • Checks if URL is external (not your own domain)
  • Validates image extension (jpg, png, gif, webp, svg, ico, bmp)
  • Handles Google Docs/Drive redirects
  • Downloads to temp file using WordPress’s download_url()

Creates Media Entry:

  • Uses media_handle_sideload() for proper WordPress integration
  • Generates all responsive image sizes
  • Stores original URL as post meta for reference
  • Optionally fills in title, alt, caption, and description

Replaces Content:

  • Generates proper WordPress image HTML with all attributes
  • Includes wp-image-{ID} class
  • Adds srcset and sizes for responsive images
  • Replaces the entire <img> tag in your content
  • Updates the post without triggering another save loop

Stores Mapping:

  • Monitors which URLs have been processed
  • Prevents re-downloading on subsequent saves
  • Stored as post meta: _rr_extimg_map

Use Cases

I use this for:

  • Content imports from other sites or Google Docs
  • Guest posts where contributors paste content with external images
  • Legacy content migration to ensure all images are local
  • SEO to control image file names and metadata
  • Site reliability – no more broken images when external sites go down

Installation

Method 1: Manual Installation

  1. Copy the plugin code into a new file: wp-content/plugins/rr-import-external-images/rr-import-external-images.php
  2. Activate the plugin:
    • Go to WordPress Admin → Plugins
    • Find “RR — Import External Images on Save”
    • Click Activate

Method 2: Use a Code Snippet plugin like WPCodeBox or FluentSnippets.

Copy the snippet below and paste it in your favorite code snippet plugin.

Code Sample

The Code

This is version 2.3.0 – a major update that makes images fully compatible with WordPress’s block editor and media library system.

PHP
<?php
/**
 * Plugin Name: RR — Import External Images on Save
 * Description: On save (draft/publish), sideload external <img> to Media Library, rewrite src, and set alt/title/description from post title + increment.
 * Version: 2.3.0
 * 
 * Changelog:
 * 2.3.0 - Fixed block validation by removing srcset/sizes from saved content
 *       - Gutenberg adds srcset/sizes automatically on frontend render
 *       - Block attributes now perfectly match saved HTML
 *       - No more "unexpected or invalid content" errors
 * 2.2.0 - Complete block rebuild for validation
 * 2.1.0 - Fixed duplicate uploads
 * 2.0.0 - Major update: Full Gutenberg compatibility
 * 1.4.0 - Previous stable version
 */

if ( ! defined( 'ABSPATH' ) ) { exit; }

/**
 * ============================================
 * CONFIGURATION OPTIONS
 * ============================================
 */
define( 'RR_EXTIMG_SET_TITLE',       false );
define( 'RR_EXTIMG_SET_ALT',         false );
define( 'RR_EXTIMG_SET_CAPTION',     false );
define( 'RR_EXTIMG_SET_DESCRIPTION', false );
define( 'RR_EXTIMG_DEFAULT_SIZE',    'large' );

add_action( 'save_post', 'rr_import_external_images_on_save', 10, 3 );
add_action( 'rest_after_insert_post', 'rr_import_external_images_rest', 10, 3 );
add_action( 'rest_after_insert_page', 'rr_import_external_images_rest', 10, 3 );

function rr_import_external_images_rest( $post, $request, $creating ) {
    rr_import_external_images_on_save( $post->ID, $post, ! $creating );
}

function rr_import_external_images_on_save( $post_id, $post, $update ) {
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
    if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) return;
    if ( empty( $post ) || empty( $post->post_content ) ) return;

    rr_require_media_libs();

    $ptype = get_post_type( $post );
    if ( ! post_type_supports( $ptype, 'editor' ) ) return;

    $content = $post->post_content;
    
    if ( strlen( $content ) > 5000000 ) return;
    
    $content = apply_filters( 'rr_extimg_pre_process_content', $content, $post_id );
    
    // Get existing map
    $map = get_post_meta( $post_id, '_rr_extimg_map', true );
    if ( ! is_array( $map ) ) $map = [];

    $site_host = wp_parse_url( home_url(), PHP_URL_HOST );
    $changed = false;
    $image_counter = 1;

    // Process Gutenberg image blocks
    $content = preg_replace_callback(
        '#<!-- wp:image(\s+\{[^}]*\})?\s*-->.*?<!-- /wp:image -->#s',
        function( $matches ) use ( &$map, &$image_counter, $post_id, $post, $site_host, &$changed ) {
            $block = $matches[0];
            
            // Extract the current URL from JSON or img src
            $src = '';
            
            // Try JSON first
            if ( preg_match( '#"url"\s*:\s*"([^"]+)"#', $block, $url_match ) ) {
                $src = trim( html_entity_decode( stripslashes( $url_match[1] ) ) );
            }
            
            // Fallback to img src
            if ( empty( $src ) && preg_match( '#<img[^>]+src=(["\'])([^"\']+)\1#i', $block, $src_match ) ) {
                $src = trim( html_entity_decode( $src_match[2] ) );
            }
            
            if ( empty( $src ) || ! wp_http_validate_url( $src ) ) {
                return $block;
            }
            
            $host = wp_parse_url( $src, PHP_URL_HOST );
            if ( ! $host || strcasecmp( $host, $site_host ) === 0 ) return $block;

            // Check if already processed
            $attach_id = $map[ $src ] ?? 0;
            
            if ( ! $attach_id ) {
                // Check if image already exists in media library
                $existing_id = attachment_url_to_postid( $src );
                if ( $existing_id ) {
                    $attach_id = $existing_id;
                    rr_fill_attachment_texts_if_empty( $existing_id, $src, $post, $image_counter );
                } else {
                    // Download and sideload
                    $attach_id = rr_sideload_image_as_attachment( $src, $post_id, $post, $image_counter );
                    if ( is_wp_error( $attach_id ) || ! $attach_id ) {
                        return $block;
                    }
                }
                
                // Save to map
                $map[ $src ] = $attach_id;
            }

            if ( $attach_id ) {
                $new_block = rr_rebuild_image_block( $block, $attach_id );
                if ( $new_block !== $block ) {
                    $changed = true;
                    $image_counter++;
                    return $new_block;
                }
            }
            
            return $block;
        },
        $content
    );

    // Process any remaining standalone img tags (classic editor)
    $content = preg_replace_callback(
        '#<img[^>]+>#i',
        function( $matches ) use ( &$map, &$image_counter, $post_id, $post, $site_host, &$changed ) {
            $img_tag = $matches[0];
            
            if ( ! preg_match( '#\s+src=(["\'])([^"\']+)\1#i', $img_tag, $src_match ) ) {
                return $img_tag;
            }
            
            $src = trim( html_entity_decode( $src_match[2] ) );
            
            if ( ! wp_http_validate_url( $src ) ) return $img_tag;
            
            $host = wp_parse_url( $src, PHP_URL_HOST );
            if ( ! $host || strcasecmp( $host, $site_host ) === 0 ) return $img_tag;
            
            // Skip if already has wp-image class (already processed)
            if ( preg_match( '#wp-image-\d+#', $img_tag ) ) {
                return $img_tag;
            }

            $attach_id = $map[ $src ] ?? 0;
            
            if ( ! $attach_id ) {
                $existing_id = attachment_url_to_postid( $src );
                if ( $existing_id ) {
                    $attach_id = $existing_id;
                    rr_fill_attachment_texts_if_empty( $existing_id, $src, $post, $image_counter );
                } else {
                    $attach_id = rr_sideload_image_as_attachment( $src, $post_id, $post, $image_counter );
                    if ( is_wp_error( $attach_id ) || ! $attach_id ) {
                        return $img_tag;
                    }
                }
                $map[ $src ] = $attach_id;
            }

            if ( $attach_id ) {
                // Extract existing attributes
                $alt = '';
                $class = '';
                
                if ( preg_match( '#\s+alt=(["\'])([^"\']*)\1#i', $img_tag, $alt_match ) ) {
                    $alt = $alt_match[2];
                }
                if ( preg_match( '#\s+class=(["\'])([^"\']*)\1#i', $img_tag, $class_match ) ) {
                    $class = $class_match[2];
                }
                
                $new_img = rr_generate_wp_image_html( $attach_id, [
                    'alt' => $alt,
                    'class' => $class,
                ] );
                
                if ( $new_img !== $img_tag ) {
                    $changed = true;
                    $image_counter++;
                    return $new_img;
                }
            }
            
            return $img_tag;
        },
        $content
    );

    if ( $changed ) {
        remove_action( 'save_post', 'rr_import_external_images_on_save', 10 );
        remove_action( 'rest_after_insert_post', 'rr_import_external_images_rest', 10 );
        remove_action( 'rest_after_insert_page', 'rr_import_external_images_rest', 10 );
        
        wp_update_post( [ 'ID' => $post_id, 'post_content' => $content ] );
        update_post_meta( $post_id, '_rr_extimg_map', $map );
        
        add_action( 'save_post', 'rr_import_external_images_on_save', 10, 3 );
        add_action( 'rest_after_insert_post', 'rr_import_external_images_rest', 10, 3 );
        add_action( 'rest_after_insert_page', 'rr_import_external_images_rest', 10, 3 );
    }
}

/**
 * Completely rebuild the image block with proper WordPress structure
 */
function rr_rebuild_image_block( string $old_block, int $attach_id ) : string {
    $size = RR_EXTIMG_DEFAULT_SIZE;
    
    // Get image metadata
    $meta = wp_get_attachment_metadata( $attach_id );
    $full_url = wp_get_attachment_url( $attach_id );
    
    // Get the sized image
    $image = wp_get_attachment_image_src( $attach_id, $size );
    if ( ! $image ) {
        $image = wp_get_attachment_image_src( $attach_id, 'full' );
        $size = 'full';
    }
    
    if ( ! $image ) return $old_block;
    
    list( $src, $width, $height ) = $image;
    
    // Get alt text (from AI plugin or metadata)
    $alt = get_post_meta( $attach_id, '_wp_attachment_image_alt', true );
    if ( empty( $alt ) ) {
        // Try to extract from old block
        if ( preg_match( '#<img[^>]+alt=(["\'])([^"\']*)\1#i', $old_block, $alt_match ) ) {
            $alt = $alt_match[2];
        }
    }
    
    // Extract any existing caption
    $caption = '';
    if ( preg_match( '#<figcaption[^>]*>(.*?)</figcaption>#s', $old_block, $caption_match ) ) {
        $caption = trim( $caption_match[1] );
    }
    
    // Extract existing linkDestination if present
    $link_destination = 'none';
    if ( preg_match( '#"linkDestination"\s*:\s*"([^"]+)"#', $old_block, $link_match ) ) {
        $link_destination = $link_match[1];
    }
    
    // Extract href if image is linked
    $href = '';
    if ( preg_match( '#<a[^>]+href=(["\'])([^"\']+)\1#i', $old_block, $href_match ) ) {
        $href = $href_match[2];
    }
    
    // Build block attributes JSON
    $attrs = [
        'id' => $attach_id,
        'sizeSlug' => $size,
        'linkDestination' => $link_destination,
    ];
    
    // Add width/height if available from metadata
    if ( isset( $meta['sizes'][ $size ] ) ) {
        $attrs['width'] = (int) $meta['sizes'][ $size ]['width'];
        $attrs['height'] = (int) $meta['sizes'][ $size ]['height'];
    } elseif ( $size === 'full' && isset( $meta['width'] ) && isset( $meta['height'] ) ) {
        $attrs['width'] = (int) $meta['width'];
        $attrs['height'] = (int) $meta['height'];
    }
    
    $attrs_json = wp_json_encode( $attrs, JSON_UNESCAPED_SLASHES );
    
    // Build srcset and sizes
    $srcset = wp_get_attachment_image_srcset( $attach_id, $size );
    $sizes_attr = wp_get_attachment_image_sizes( $attach_id, $size );
    
    // Build the img tag - DO NOT include srcset/sizes
    // Gutenberg will add them automatically on the frontend
    $img_tag = sprintf(
        '<img src="%s" alt="%s" class="wp-image-%d"/>',
        esc_url( $src ),
        esc_attr( $alt ),
        $attach_id
    );
    
    // Wrap in link if needed
    if ( ! empty( $href ) ) {
        $img_tag = sprintf( '<a href="%s">%s</a>', esc_url( $href ), $img_tag );
    }
    
    // Build figure
    $figure_classes = 'wp-block-image size-' . $size;
    $figure = sprintf( '<figure class="%s">%s', esc_attr( $figure_classes ), $img_tag );
    
    if ( ! empty( $caption ) ) {
        $figure .= '<figcaption class="wp-element-caption">' . $caption . '</figcaption>';
    }
    
    $figure .= '</figure>';
    
    // Build complete block
    $new_block = sprintf(
        "<!-- wp:image %s -->\n%s\n<!-- /wp:image -->",
        $attrs_json,
        $figure
    );
    
    return $new_block;
}

/**
 * Generate proper WordPress image HTML with all required attributes
 * For classic editor content (not Gutenberg blocks)
 */
function rr_generate_wp_image_html( int $attach_id, array $original_data ) : string {
    $size = RR_EXTIMG_DEFAULT_SIZE;
    
    $image = wp_get_attachment_image_src( $attach_id, $size );
    if ( ! $image ) {
        $image = wp_get_attachment_image_src( $attach_id, 'full' );
    }
    
    if ( ! $image ) return '';
    
    list( $src, $width, $height ) = $image;
    
    $alt = get_post_meta( $attach_id, '_wp_attachment_image_alt', true );
    if ( empty( $alt ) && ! empty( $original_data['alt'] ) ) {
        $alt = $original_data['alt'];
    }
    
    $classes = [ "wp-image-{$attach_id}" ];
    
    if ( ! empty( $original_data['class'] ) ) {
        $existing_classes = explode( ' ', $original_data['class'] );
        foreach ( $existing_classes as $cls ) {
            $cls = trim( $cls );
            if ( $cls && ! preg_match( '/^wp-image-\d+$/', $cls ) ) {
                $classes[] = $cls;
            }
        }
    }
    
    $class_attr = implode( ' ', array_unique( $classes ) );
    
    // For classic editor, we CAN include srcset since it's not validated by Gutenberg
    $srcset = wp_get_attachment_image_srcset( $attach_id, $size );
    $sizes_attr = wp_get_attachment_image_sizes( $attach_id, $size );
    
    $img_html = sprintf(
        '<img src="%s" alt="%s" class="%s"',
        esc_url( $src ),
        esc_attr( $alt ),
        esc_attr( $class_attr )
    );
    
    if ( $srcset ) {
        $img_html .= sprintf( ' srcset="%s"', esc_attr( $srcset ) );
    }
    
    if ( $sizes_attr ) {
        $img_html .= sprintf( ' sizes="%s"', esc_attr( $sizes_attr ) );
    }
    
    $img_html .= '/>';
    
    return $img_html;
}

function rr_sideload_image_as_attachment( string $url, int $post_id, $post, int $counter ) {
    if ( strpos( $url, 'googleusercontent.com' ) !== false || 
         strpos( $url, 'docs.google.com' ) !== false ) {
        $url = rr_resolve_redirect( $url );
    }
    
    $path_parts = pathinfo( wp_parse_url( $url, PHP_URL_PATH ) );
    $ext = strtolower( $path_parts['extension'] ?? '' );
    $allowed = [ 'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'ico', 'bmp' ];
    
    if ( ! in_array( $ext, $allowed, true ) ) {
        return new WP_Error( 'invalid_image', 'Unsupported image format: ' . $ext );
    }
    
    $post_title = $post->post_title;
    if ( empty( $post_title ) ) {
        $post_title = 'image';
    }
    
    $custom_filename = rr_generate_filename( $post_title, $counter, $ext );
    
    $temp_file = download_url( $url );
    if ( is_wp_error( $temp_file ) ) {
        return $temp_file;
    }
    
    $file_array = [
        'name'     => $custom_filename,
        'tmp_name' => $temp_file,
    ];
    
    $id = media_handle_sideload( $file_array, $post_id );
    
    if ( file_exists( $temp_file ) ) {
        @unlink( $temp_file );
    }
    
    if ( is_wp_error( $id ) ) {
        return $id;
    }

    $path = get_attached_file( $id );
    if ( $path ) {
        $meta = wp_generate_attachment_metadata( $id, $path );
        if ( $meta ) wp_update_attachment_metadata( $id, $meta );
    }

    update_post_meta( $id, '_rr_source_url', esc_url_raw( $url ) );
    
    // Only fill texts if configured - allows AI plugins to run first
    rr_fill_attachment_texts_if_empty( $id, $url, $post, $counter );

    return (int) $id;
}

function rr_generate_filename( string $post_title, int $counter, string $ext ) : string {
    $slug = sanitize_title( $post_title );
    $slug = preg_replace( '/[^a-z0-9\-]/', '', $slug );
    $slug = substr( $slug, 0, 50 );
    
    if ( $counter > 1 ) {
        $filename = $slug . '-' . $counter . '.' . $ext;
    } else {
        $filename = $slug . '.' . $ext;
    }
    
    return $filename;
}

function rr_resolve_redirect( string $url ) : string {
    $response = wp_safe_remote_head( $url, [
        'timeout'     => 10,
        'redirection' => 5,
    ] );
    
    if ( is_wp_error( $response ) ) {
        return $url;
    }
    
    $final_url = wp_remote_retrieve_header( $response, 'location' );
    return $final_url ? $final_url : $url;
}

/**
 * Fill attachment metadata only if enabled and field is empty
 * This allows AI plugins and other metadata plugins to run first
 */
function rr_fill_attachment_texts_if_empty( int $attach_id, string $fallback_url = '', $post = null, int $counter = 1 ) : void {
    if ( ! RR_EXTIMG_SET_TITLE && ! RR_EXTIMG_SET_ALT && ! RR_EXTIMG_SET_CAPTION && ! RR_EXTIMG_SET_DESCRIPTION ) {
        return;
    }
    
    $label = '';
    
    if ( $post && ! empty( $post->post_title ) ) {
        $label = $post->post_title;
        if ( $counter > 1 ) {
            $label .= ' ' . $counter;
        }
    } else {
        $path = get_attached_file( $attach_id );
        $base = '';

        if ( $path && file_exists( $path ) ) {
            $pi   = pathinfo( $path );
            $base = $pi['filename'] ?? '';
        }
        if ( ! $base && $fallback_url ) {
            $pi   = pathinfo( wp_parse_url( $fallback_url, PHP_URL_PATH ) ?? '' );
            $base = $pi['filename'] ?? '';
        }
        
        $label = rr_humanize_filename( $base );
    }
    
    if ( $label === '' ) return;

    if ( RR_EXTIMG_SET_TITLE ) {
        $att = get_post( $attach_id );
        if ( $att && empty( trim( $att->post_title ) ) ) {
            wp_update_post( [
                'ID'         => $attach_id,
                'post_title' => $label,
            ] );
        }
    }

    if ( RR_EXTIMG_SET_ALT ) {
        $alt = get_post_meta( $attach_id, '_wp_attachment_image_alt', true );
        if ( empty( $alt ) ) {
            update_post_meta( $attach_id, '_wp_attachment_image_alt', $label );
        }
    }

    if ( RR_EXTIMG_SET_CAPTION ) {
        $cur_caption = get_post_field( 'post_excerpt', $attach_id );
        if ( empty( trim( $cur_caption ) ) ) {
            wp_update_post( [
                'ID'           => $attach_id,
                'post_excerpt' => $label,
            ] );
        }
    }
    
    if ( RR_EXTIMG_SET_DESCRIPTION ) {
        $cur_desc = get_post_field( 'post_content', $attach_id );
        if ( empty( trim( $cur_desc ) ) ) {
            wp_update_post( [
                'ID'           => $attach_id,
                'post_content' => $label,
            ] );
        }
    }
}

function rr_humanize_filename( string $name ) : string {
    $name = urldecode( $name );
    $name = preg_replace( '/[_\-]+/u', ' ', $name );
    $name = preg_replace( '/\s+/u', ' ', $name );
    $name = trim( $name );
    if ( $name === '' ) return '';
    
    if ( function_exists( 'mb_convert_case' ) ) {
        $name = mb_convert_case( $name, MB_CASE_TITLE, 'UTF-8' );
    } else {
        $name = ucwords( strtolower( $name ) );
    }
    
    return sanitize_text_field( $name );
}

function rr_require_media_libs() : void {
    if ( ! function_exists( 'download_url' ) ) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
    }
    if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
        require_once ABSPATH . 'wp-admin/includes/image.php';
    }
    if ( ! function_exists( 'media_sideload_image' ) ) {
        require_once ABSPATH . 'wp-admin/includes/media.php';
    }
}

Leave the first comment


  • WordPress
  • WP Admin

Content on this page

This website uses cookies to enhance your browsing experience and ensure the site functions properly. By continuing to use this site, you acknowledge and accept our use of cookies.

Accept All Accept Required Only