We were building a WooCommerce site for our customer, and all the products is from the other affiliate site. There are over 10,000 products. While importing all these products is simple (from a CSV!), we don’t want to put over 40,000 product images on our sites (average around 4 images per product). That takes lots of space, and that takes lots of time to upload! So we decided to just reference all images from the original site. Here is how we do it.

The Code

The code contains a Uyond_External_Image class and a product-thumbnails.php template. You can just simply copy and paste this to your theme to make it work.

<?php

// Ref: https://www.uyond.com/resource/use-external-url-on-woocommerce-product-images/

defined( 'ABSPATH' ) || exit;

class Uyond_External_Image {

	protected static $instance;

	public static function get_instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	protected function __construct() {
		add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
		add_action( 'save_post', array( $this, 'save_post' ) );

		// show the product image in shop list.
		add_filter( 'woocommerce_product_get_image', array( $this, 'get_image' ), 10, 6 );

		// show the gallery images in single product page.
		add_filter( 'woocommerce_single_product_image_thumbnail_html', array( $this, 'thumbnail_html' ), 10, 2 );
		add_filter( 'wc_get_template', array( $this, 'get_template' ), 10, 5 );
	}

	public function add_meta_boxes() {
		add_meta_box(
			'uyond_product_img_url',
			'Product Image URL',
			array( $this, 'echo_product_img_url_box' ),
			'product',
			'side',
			'default'
		);

		add_meta_box(
			'uyond_product_gallery_urls',
			'Product Gallery URL',
			array( $this, 'echo_product_gallery_url_box' ),
			'product',
			'side',
			'default'
		);
	}

	public function echo_product_img_url_box( $post ) {
		wp_nonce_field( 'uyond_product_img_url_metabox_nonce', 'uyond_product_img_url_nonce' );

		$img_url = $this->get_product_img_url( $post->ID );

		if ( $img_url ) {
			?>
			<img style="max-width: 100%;" src="<?php echo esc_url( $img_url ); ?>" />
			<?php
		}

		?>
		<input id="uyond_product_img_url" type="text" name="uyond_product_img_url" placeholder="Image URL" value="<?php echo esc_url( $img_url ); ?>" style="width:100%;font-size:13px;">
		<?php
	}

	public function echo_product_gallery_url_box( $post ) {
		wp_nonce_field( 'uyond_product_gallery_url_metabox_nonce', 'uyond_product_gallery_url_nonce' );

		$gallery_urls = $this->get_product_gallery_url( $post->ID );

		for ( $i = 0; $i < 10; $i++ ) {
			$gallery_url = '';
			if ( $i < count( $gallery_urls ) ) {
				$gallery_url = $gallery_urls[ $i ];
			}

			if ( $gallery_url ) {
				?>
				<img style="max-width: 50%;" src="<?php echo esc_url( $gallery_url ); ?>" />
				<?php
			}

			?>
			<input type="text" id="uyond_product_gallery_url_<?php echo esc_html( $i ); ?>"  name="uyond_product_gallery_url[]" placeholder="Gallery URL <?php echo esc_html( $i ); ?>" value="<?php echo esc_url( $gallery_url ); ?>" style="width:100%; font-size:13px;">
			<?php
		}

	}

	public function save_post( $post_id ) {
		if ( get_post_type( $post_id ) !== 'product' ) {
			return;
		}

		$this->save_post_product_url( $post_id );
		$this->save_post_product_gallery_url( $post_id );
	}

	public function save_post_product_url( $post_id ) {
		if ( ! isset( $_POST['uyond_product_img_url'] ) ) {
			return;
		}

		if ( ! isset( $_POST['uyond_product_img_url_nonce'] ) ) {
			return;
		}

		if ( ! wp_verify_nonce( sanitize_key( $_POST['uyond_product_img_url_nonce'] ), 'uyond_product_img_url_metabox_nonce' ) ) {
			return;
		}

		$url = esc_url_raw( rtrim( $_POST['uyond_product_img_url'] ) );

		if ( $url ) {
			update_post_meta( $post_id, '_uyond_product_img_url', $url );
		} else {
			delete_post_meta( $post_id, '_uyond_product_img_url' );
		}
	}

	public function save_post_product_gallery_url( $post_id ) {
		if ( ! isset( $_POST['uyond_product_gallery_url'] ) ) {
			return;
		}

		if ( ! isset( $_POST['uyond_product_gallery_url_nonce'] ) ) {
			return;
		}

		if ( ! wp_verify_nonce( sanitize_key( $_POST['uyond_product_gallery_url_nonce'] ), 'uyond_product_gallery_url_metabox_nonce' ) ) {
			return;
		}

		$urls = $_POST['uyond_product_gallery_url'];

		if ( $urls ) {
			update_post_meta( $post_id, '_uyond_product_gallery_url', $urls );
		} else {
			delete_post_meta( $post_id, '_uyond_product_gallery_url' );
		}
	}

	public function get_product_img_url( $id ) {
		$value = get_post_meta( $id, '_uyond_product_img_url', true );

		if ( $value ) {
			return $value;
		}

		return '';
	}

	public function get_product_gallery_url( $id ) {
		$value = get_post_meta( $id, '_uyond_product_gallery_url', true );

		if ( $value ) {
			return $value;
		}

		return array();
	}

	public function get_image( $html, $product, $woosize, $attr, $placeholder, $image ) {
		$img_url = $this->get_product_img_url( $product->get_id() );
		return '<img width="260" height="300" src="' . esc_url( $img_url ) . '" class="attachment-woocommerce_thumbnail size-woocommerce_thumbnail" alt="" loading="lazy" />';
	}

	public function get_template( $template, $template_name, $args, $template_path, $default_path ) {
		if ( 'single-product/product-thumbnails.php' === $template_name ) {
			$template = path_join( get_stylesheet_directory(), 'templates/single-product/product-thumbnails.php' );
		}

		return $template;
	}

	public function get_gallery_single_image( $img_url ) {
		return sprintf(
			'<div data-thumb="%1$s" data-thumb-alt="" class="woocommerce-product-gallery__image"><a href="%1$s"><img width="600" height="642" src="%1$s" class="" alt="" loading="lazy" title="61S2qlMWh6L._AC_SX679_" data-caption="" data-src="%1$s" data-large_image="%1$s" data-large_image_width="679" data-large_image_height="727" /></a></div>',
			$img_url
		);
	}

	public function thumbnail_html( $html, $post_thumbnail_id ) {
		global $product;
		$img_url = $this->get_product_img_url( $product->get_id() );

		if ( '' === $img_url ) {
			return $html;
		}

		return $this->get_gallery_single_image( $img_url );
	}
}

This template file has to be put in the correct location so that it can be referenced from the above get_template function.

<?php

defined( 'ABSPATH' ) || exit;

// Note: `wc_get_gallery_image_html` was added in WC 3.3.2 and did not exist prior. This check protects against theme overrides being used on older versions of WC.
if ( ! function_exists( 'wc_get_gallery_image_html' ) ) {
	return;
}

global $product;

$external_image = uyond_External_Image::get_instance();

$image_urls = $external_image->get_product_gallery_url( $product->get_id() );

foreach ( $image_urls as $image_url ) {
	if ( ! $image_url ) {
		continue;
	}

	echo $external_image->get_gallery_single_image( $image_url ); // phpcs:disable WordPress.XSS.EscapeOutput.OutputNotEscaped
}

Explain

This is not a straight forward approach because the way WordPress handle media is tightly built into the Core. There is just no way to make it support external image URL nicely.

What we had done is to add new meta data into the product post (Product in WooCommerce is just a custom post type named product).

Everything is inside the class

This is the very basic golden rule in WordPress. You should try your best to avoid naming collision. The best way to do it is to choose a very unique class name, and then put all your functions inside the class. By doing that, all your function names are only in the scope of the class. Any other functions outside of this class won’t have collision problem to you.

In this example, we have created the Uyond_External_Image and wrap it into a singleton. You should reference this singleton object in your functions.php.

	require_once __DIR__ . '/includes/class_uyond_external_image.php';
	Uyond_External_Image::get_instance();

Meta Data

We have added:

  • _uyond_product_img_url, this stores the image url of the product’s featured image.
  • _uyond_product_gallery_url , this stores an array of image urls of the product’s gallery images.

We are using the standard get_post_meta and update_post_meta to handle the database operation. This already take care of the data serialisation for us, so that we can use simple PHP array() for _uyond_product_gallery_url.

Meta Boxes

We have added 2 new meta boxes to handle the editing of above to new meta records in the product edit page. That is in added in the add_meta_boxes function above.

Keep in mind that we should add nonce for each meta box. Security is very important. Check this out if you don’t know what nonce is.

The meta boxes look like this in the product edit page.

The Product Listing Page

We are overriding the woocommerce_product_get_image hook to echo the external image tag here. See function get_image for the detail implementation. The result looks like this:

The Product Page

We are going need 2 more filter hooks to override the display to show external image links. One for the featured photo, and the other one for the gallery.

The tricky one is the gallery. There is just no simple filter you can find to override it. But you can override the whole template from the wc_get_template filter. We redirect it to use our own product-thumbnails.php template. That will echo a list of <div> for the gallery.

Please notice that we are using Divi as the parent theme. The exact html that you need to echo may varies depends on which theme you are using.

Conclusion

See how flexible WordPress can be! There is no limitation of implementation any custom functions. It’s just a matter of finding the right way, and doing it in the correct way.