WordPress Media Library Image Import
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=xxxformat 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
↓
Content-Type: application/octet-stream
↓
post_mime_type
↓
WordPress thinks it’s not an image
↓
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
// WRONG
$attachment = [
'post_mime_type' => $response_headers['content-type']
];
// CORRECT
$wp_filetype = wp_check_filetype_and_ext($upload['file'], $filename);
$attachment = [
'post_mime_type' => $wp_filetype['type']
];
// WRONG - No thumbnails
wp_insert_attachment($attachment, $file_path, $parent_id);
// Missing: wp_generate_attachment_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);
// WRONG - Breaks portability
update_post_meta($attachment_id, '_wp_attached_file',
'/home/user/public_html/wp-content/uploads/image.png');
// 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.
