WordPress Image Import Solution

WordPress Media Library Image Import – Complete Solution Guide

WordPress Media Library Image Import

Complete Solution Guide
Published by Minute Launch | Last Updated: November 11, 2025

The Problem

When programmatically importing images to WordPress Media Library from external sources (APIs, remote URLs), images upload successfully but display as gray placeholder boxes in the Media Library with broken ?attachment_id=xxx URLs instead of proper image permalinks.

Symptoms

  • Images uploaded to /wp-content/uploads/ successfully
  • File URLs are correct and accessible
  • Media Library shows gray placeholders instead of thumbnails
  • Attachment URLs use ?attachment_id=xxx format instead of direct file URLs
  • WordPress doesn’t recognize files as images
  • No thumbnails generated
  • WooCommerce products show placeholder images

Root Cause

WordPress was being told the files were NOT images.

When downloading images through external APIs, many sources return generic MIME types like:

Content-Type: application/octet-stream

If you use this header value as the post_mime_type when creating WordPress attachments, WordPress sees application/octet-stream and treats the files as generic binary data, not images.

The Solution

Critical Fix: MIME Type Detection

ALWAYS detect the actual MIME type from the saved file itself. NEVER trust the download source’s Content-Type header.

Use WordPress’s built-in wp_check_filetype_and_ext() function to inspect the file’s extension and signature to determine the correct MIME type.

The MIME Type Flow

WRONG WAY
External API

Content-Type: application/octet-stream

post_mime_type

WordPress thinks it’s not an image
RIGHT WAY
External API

Save file

wp_check_filetype_and_ext()

image/png

WordPress recognizes as image

Complete Working Code

/**
 * Attach product image from external source
 * 
 * @param int $product_id WooCommerce product ID
 * @param string $image_url Full URL to image
 * @param string $product_name Product name for logging
 * @return array ['attachment_id' => int] or ['error' => string]
 */
private function attach_product_image($product_id, $image_url, $product_name) {
    // Download image from external source
    $image_result = $this->download_image($image_url);
    
    if (isset($image_result['error'])) {
        return ['error' => $image_result['error']];
    }
    
    // Extract filename from URL
    $filename = basename(parse_url($image_url, PHP_URL_PATH));
    
    // Upload to WordPress
    $upload = wp_upload_bits($filename, null, $image_result['data']);
    
    if ($upload['error']) {
        return ['error' => $upload['error']];
    }
    
    // CRITICAL FIX: Detect actual MIME type from saved file
    $wp_filetype = wp_check_filetype_and_ext($upload['file'], $filename);
    $detected_mime = !empty($wp_filetype['type']) ? $wp_filetype['type'] : 'image/jpeg';
    
    $file_title = sanitize_file_name(pathinfo($filename, PATHINFO_FILENAME));
    
    // Build attachment array with CORRECT MIME TYPE
    $attachment = [
        'post_mime_type' => $detected_mime,  // USE DETECTED MIME!
        'post_title'     => $file_title,
        'post_name'      => $file_title,
        'post_content'   => '',
        'post_status'    => 'inherit',
        'post_parent'    => $product_id,
        'guid'           => $upload['url'],
    ];
    
    // Create attachment
    $attachment_id = wp_insert_attachment($attachment, $upload['file'], $product_id);
    
    if (is_wp_error($attachment_id)) {
        return ['error' => $attachment_id->get_error_message()];
    }
    
    // Force post_name slug (wp_insert_attachment sometimes ignores it)
    wp_update_post(['ID' => $attachment_id, 'post_name' => $file_title]);
    
    // Set file path meta so WordPress "owns" the file
    $uploads = wp_get_upload_dir();
    $relative_path = ltrim(str_replace($uploads['basedir'], '', $upload['file']), '/');
    update_post_meta($attachment_id, '_wp_attached_file', $relative_path);
    
    // Generate thumbnails and metadata
    require_once ABSPATH . 'wp-admin/includes/image.php';
    $metadata = wp_generate_attachment_metadata($attachment_id, $upload['file']);
    wp_update_attachment_metadata($attachment_id, $metadata);
    
    // Set as product featured image (for WooCommerce)
    set_post_thumbnail($product_id, $attachment_id);
    update_post_meta($product_id, '_thumbnail_id', $attachment_id);
    
    return ['attachment_id' => $attachment_id];
}

Key Concepts

Why wp_check_filetype_and_ext() Works

This WordPress function:

  • Inspects the file extension (.png, .jpg, etc.)
  • Checks the file signature/magic bytes
  • Returns the correct MIME type based on actual file content
  • Provides a trustworthy MIME type that WordPress recognizes as an image

Essential WordPress Functions

// 1. Upload file to WordPress uploads directory
wp_upload_bits($filename, null, $binary_data)

// 2. Detect MIME type from saved file
wp_check_filetype_and_ext($file_path, $filename)

// 3. Create attachment post in database
wp_insert_attachment($attachment_array, $file_path, $parent_id)

// 4. Force permalink slug
wp_update_post(['ID' => $attachment_id, 'post_name' => $slug])

// 5. Set file path meta
update_post_meta($attachment_id, '_wp_attached_file', $relative_path)

// 6. Generate thumbnails and metadata
require_once ABSPATH . 'wp-admin/includes/image.php';
wp_generate_attachment_metadata($attachment_id, $file_path)

// 7. Save metadata to database
wp_update_attachment_metadata($attachment_id, $metadata)

// 8. Set as featured image
set_post_thumbnail($post_id, $attachment_id)

Common Mistakes

DON’T: Trust Content-Type Header
// WRONG
$attachment = [
    'post_mime_type' => $response_headers['content-type']
];
DO: Detect MIME Type
// CORRECT
$wp_filetype = wp_check_filetype_and_ext($upload['file'], $filename);
$attachment = [
    'post_mime_type' => $wp_filetype['type']
];
DON’T: Skip Metadata
// WRONG - No thumbnails
wp_insert_attachment($attachment, $file_path, $parent_id);
// Missing: wp_generate_attachment_metadata()
DO: Generate Metadata
// CORRECT
$attachment_id = wp_insert_attachment($attachment, $file_path, $parent_id);
require_once ABSPATH . 'wp-admin/includes/image.php';
$metadata = wp_generate_attachment_metadata($attachment_id, $file_path);
wp_update_attachment_metadata($attachment_id, $metadata);
DON’T: Use Absolute Paths
// WRONG - Breaks portability
update_post_meta($attachment_id, '_wp_attached_file', 
    '/home/user/public_html/wp-content/uploads/image.png');
DO: Use Relative Paths
// CORRECT
$uploads = wp_get_upload_dir();
$relative_path = ltrim(str_replace($uploads['basedir'], '', $upload['file']), '/');
update_post_meta($attachment_id, '_wp_attached_file', $relative_path);

Debugging

Check MIME Type Detection

$wp_filetype = wp_check_filetype_and_ext($upload['file'], $filename);
error_log('Detected MIME: ' . print_r($wp_filetype, true));
// Should show: ['type' => 'image/png', 'ext' => 'png', ...]

Verify File Exists

if (!file_exists($upload['file'])) {
    error_log('ERROR: File does not exist at ' . $upload['file']);
} else {
    $metadata = wp_generate_attachment_metadata($attachment_id, $upload['file']);
}

Check Metadata

$metadata = wp_generate_attachment_metadata($attachment_id, $upload['file']);
error_log('Generated metadata: ' . print_r($metadata, true));
// Should include: width, height, file, sizes array

Verify MIME Type in Database

$attachment_post = get_post($attachment_id);
error_log('Attachment mime type: ' . $attachment_post->post_mime_type);
// Should be: image/png or image/jpeg, NOT application/octet-stream

Performance Tips

Batch Processing

When importing many images, process in batches to avoid timeouts:

// AJAX handler for batch import
public function ajax_import_batch() {
    $offset = isset($_POST['offset']) ? intval($_POST['offset']) : 0;
    $batch_size = 20; // Process 20 at a time
    
    $products = array_slice($all_products, $offset, $batch_size);
    
    foreach ($products as $product_data) {
        $this->import_single_product($product_data);
    }
    
    wp_send_json_success([
        'imported' => count($products),
        'has_more' => ($offset + $batch_size) < count($all_products)
    ]);
}

Progress Updates

function importBatch(offset) {
    $.post(ajaxurl, {
        action: 'import_batch',
        offset: offset,
        batch_size: 20
    })
    .done(function(response) {
        if (response.success && response.data.has_more) {
            updateProgress(offset + 20);
            importBatch(offset + 20);
        } else {
            showComplete();
        }
    });
}

Server Requirements

1. GD Library or Imagick

Required for thumbnail generation:

if (extension_loaded('gd') || extension_loaded('imagick')) {
    // Image processing available
}

2. Memory Limit

Set adequate PHP memory:

ini_set('memory_limit', '256M');

3. Upload Directory Permissions

Ensure writable:

$upload_dir = wp_upload_dir();
if (!is_writable($upload_dir['path'])) {
    // Handle error
}

Summary

The One-Line Fix

// BEFORE (Broken)
'post_mime_type' => $download_headers['content-type']

// AFTER (Working)
'post_mime_type' => wp_check_filetype_and_ext($upload['file'], $filename)['type']

Why It Matters

  • External sources often return generic MIME types
  • WordPress needs accurate MIME types to recognize images
  • wp_check_filetype_and_ext() inspects actual file content
  • Proper MIME type enables thumbnail generation and Media Library display

This fix is essential for ANY programmatic image import from external sources.

Additional Resources

Document Version: 1.0

Last Updated: November 11, 2025

Published by Minute Launch