Photos are the product. The pipeline should treat them that way.
For a dealership, the photo is the listing. A buyer makes their first purchase decision based on the photos before they ever read a spec. The image pipeline determines how quickly photos get on the site, how they're protected from being scraped and reposted, how fast they load, and how much storage they consume. Getting this right is not optional.
Before the pipeline was built, photos lived in a chaotic mix of locations: some in WordPress media, some in Google Drive with no consistent folder structure, some emailed in by salespeople. To add photos to a listing, someone had to download them from wherever they were, resize them manually, upload them one at a time through WordPress admin, then set the featured image. For a unit with 40 photos, that was 20–30 minutes of repetitive work per listing.
There was no watermarking — photos were being scraped and reposted on competitor sites and Craigslist without attribution. There was no CDN — images were served directly from the origin server, and the listing gallery was the primary source of page weight.
Every unit gets a Google Drive folder named by stock number: STOCK-12345/. Salespeople drop photos into that folder from their phones. The folder is the photo source of record. Photos never need to be emailed or downloaded manually — they're always in the folder by stock number.
The bulk upload script reads the folder, downloads each image, and uploads it to WordPress via the REST API, attaching it to the inventory CPT post with the matching stock number.
function bulkUploadImages(stockNumber, postId) {
var folder = getOrCreateDriveFolder(stockNumber);
var files = folder.getFiles();
var props = PropertiesService.getScriptProperties();
var baseUrl = props.getProperty('WP_BASE_URL');
var auth = getAuthHeader();
var count = 0;
while (files.hasNext()) {
var file = files.next();
if (!file.getMimeType().startsWith('image/')) continue;
var blob = file.getBlob();
var uploadUrl = baseUrl + '/wp-json/wp/v2/media';
var response = UrlFetchApp.fetch(uploadUrl, {
method: 'post',
headers: {
'Authorization': auth,
'Content-Disposition': 'attachment; filename="' + file.getName() + '"',
'Content-Type': file.getMimeType(),
},
payload: blob.getBytes(),
muteHttpExceptions: true
});
var media = JSON.parse(response.getContentText());
// Attach to the inventory post
if (media.id && count === 0) {
// First image becomes featured image
UrlFetchApp.fetch(baseUrl + '/wp-json/wp/v2/[client]_inventory/' + postId, {
method: 'post',
headers: { 'Authorization': auth, 'Content-Type': 'application/json' },
payload: JSON.stringify({ featured_media: media.id }),
muteHttpExceptions: true
});
}
count++;
}
return count;
}
Watermarking on upload is the obvious approach, but it's the wrong one. When you watermark on upload, you're permanently altering the stored image. If you want to change the watermark (update the logo, move its position, adjust opacity), you have to re-process every image that was ever uploaded. That's a maintenance nightmare.
On-serve watermarking applies the watermark at request time, caches the result, and preserves the original. Changing the watermark means clearing the cache and regenerating — no stored images need to be touched.
add_filter( 'wp_get_attachment_image_src', '[client]_maybe_watermark', 10, 4 );
function [client]_maybe_watermark( $image, $attachment_id, $size, $icon ) {
if ( ! $image ) return $image;
if ( ! [client]_attachment_is_inventory_photo( $attachment_id ) ) return $image;
if ( ! in_array( $size, [ '[client]-listing-hero', '[client]-gallery-thumb' ] ) ) {
return $image;
}
$watermarked_url = [client]_get_or_create_watermarked( $image[0], $attachment_id );
if ( $watermarked_url ) {
$image[0] = $watermarked_url;
}
return $image;
}
function [client]_get_or_create_watermarked( $src_url, $attachment_id ) {
$cache_key = '[client]_wm_' . $attachment_id;
$cached = get_transient( $cache_key );
if ( $cached ) return $cached;
$result = [client]_apply_watermark_gd( $src_url );
if ( $result ) {
set_transient( $cache_key, $result, WEEK_IN_SECONDS );
}
return $result;
}
WordPress's default image sizes are soft-cropped (they scale to fit). For a listing gallery, you want hard crop — a fixed aspect ratio that produces consistent thumbnails. A grid of gallery thumbnails where each image has a different height looks broken.
add_action( 'after_setup_theme', '[client]_register_image_sizes' );
function [client]_register_image_sizes() {
add_image_size( '[client]-listing-hero', 1200, 675, true ); // 16:9 hard crop
add_image_size( '[client]-gallery-thumb', 480, 360, true ); // 4:3 hard crop
add_image_size( '[client]-grid-card', 600, 400, true ); // 3:2 hard crop
}
The true third argument is the hard crop flag. Without it, WordPress scales the image to fit within the dimensions without cropping — you get inconsistent heights in a grid layout.
WebP is 25–35% smaller than JPEG at equivalent visual quality. Converting every image to WebP at upload time is possible but adds significant processing overhead and storage. Cloudflare Polish does it for free at the CDN edge — images are stored as JPEG/PNG on the origin, and Cloudflare serves WebP automatically to browsers that support it.
Enable it in the Cloudflare dashboard: Speed → Optimization → Cloudflare Images → Polish. Set it to "Lossy" for maximum compression, or "Lossless" if you need to preserve pixel-perfect quality. The browser gets WebP; the origin keeps the original. Zero additional storage, zero processing on the server.
Photos are the product. A listing page with slow images is a listing page that doesn't convert. The pipeline automates the work that was previously 20–30 minutes per listing, protects against scrapers with on-serve watermarks that don't corrupt the originals, and uses the CDN to serve the smallest possible file to every visitor.
The Cloudflare CDN caching means photos are served from an edge node geographically close to the visitor, not from the origin server in Arizona. Image load time drops from 300–500ms (origin) to 20–50ms (edge). For a gallery with 40 images, that's the difference between the page feeling fast and feeling slow.
The image pipeline processes hundreds of photos across the inventory. Every featured image was uploaded through the bulk script from the Drive folder structure. On-serve watermarking protects the branded images without modifying the stored originals. Cloudflare Polish serves WebP to browsers that support it, reducing gallery page weight by approximately 30%.
When a watermark update was needed — the dealership's logo changed — the watermark transient cache was cleared and the new watermark applied on the next request for each image. Zero stored images were touched.
true). Soft-crop produces inconsistent heights in grid layouts. Hard crop produces the consistent dimensions your grid expects.muteHttpExceptions: true on bulk upload requests. A single failed upload shouldn't terminate the entire batch. Log the failure and continue.Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.
None of this gates a single lesson. The curriculum was free before you got here and it stays that way.
You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.
See GAP Concierge →