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

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:…

Canonical: https://www.ruhanirabin.com/code-snippet/wordpress-external-image-import-plugin-snippet/

Dec 24, 2025WPCodeBox

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

```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.

## Additional Snippets

## 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
/**
 * 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';
    }
}
```
