Files
manoon-headless/media-migration-guide.md
Unchained 7b94537670 feat(saleor): Phase 1 - GraphQL Client Setup
- Add Apollo Client for Saleor GraphQL API
- Create GraphQL fragments (Product, Variant, Checkout)
- Create GraphQL queries (Products, Checkout)
- Create GraphQL mutations (Checkout operations)
- Add TypeScript types for Saleor entities
- Add product helper functions
- Install @apollo/client and graphql dependencies

Part of WordPress/WooCommerce → Saleor migration
2026-03-21 12:36:21 +02:00

12 KiB

Media & Image Migration Guide

Current Setup

WordPress/WooCommerce (Current)

  • Storage: MinIO
  • Bucket: manoon-media
  • Plugin: Advanced Media Offloader (ADVMO)
  • Endpoint: http://minio:9000
  • Public URL: https://minio-api.nodecrew.me/manoon-media/

Saleor (New)

  • Storage: MinIO (same instance)
  • Bucket: saleor
  • Endpoint: http://minio.manoonoils:9000
  • Media URL: /media/ (served via Saleor API)
  • PVC: saleor-media-pvc (5GB local cache)

Architecture

┌─────────────────┐         ┌─────────────────┐
│   WordPress     │         │     Saleor      │
│                 │         │                 │
│  WooCommerce    │         │    API/Dashboard│
│                 │         │                 │
└────────┬────────┘         └────────┬────────┘
         │                           │
         │  ADVMO Plugin             │  django-storages
         │  (S3-compatible)          │  (S3-compatible)
         │                           │
         └───────────┬───────────────┘
                     │
         ┌───────────┴───────────┐
         │        MinIO          │
         │   (S3-compatible      │
         │    object storage)    │
         └───────────┬───────────┘
                     │
     ┌───────────────┼───────────────┐
     │               │               │
┌────▼────┐    ┌────▼────┐   ┌─────▼─────┐
│ manoon- │    │ saleor  │   │  other    │
│ media   │    │ bucket  │   │  buckets  │
│ (WP)    │    │(Saleor) │   │           │
└─────────┘    └─────────┘   └───────────┘

Step 1: Verify Buckets

# Access MinIO container
kubectl exec -ti deployment/minio -n manoonoils -- /bin/sh

# List all buckets
mc alias set local http://localhost:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD
mc ls local

# Expected output:
# [bucket] manoon-media    (WordPress)
# [bucket] saleor          (Saleor)
# [bucket] other...        (if any)

If saleor bucket doesn't exist, create it:

mc mb local/saleor

Step 2: Image Migration Strategies

Option A: Copy Images from WordPress to Saleor Bucket

Best for: Clean separation, full control

# Copy all images from WordPress bucket to Saleor bucket
kubectl exec -ti deployment/minio -n manoonoils -- \
  mc cp --recursive local/manoon-media/wp-content/uploads/ local/saleor/

# Or sync (faster for subsequent runs)
kubectl exec -ti deployment/minio -n manoonoils -- \
  mc mirror local/manoon-media/wp-content/uploads/ local/saleor/products/

After copy, images will be at:

  • http://minio-api.nodecrew.me/saleor/products/2024/01/image.jpg

Option B: Share Bucket (Keep WordPress Images in Place)

Best for: Quick migration, no duplication

Configure Saleor to read from manoon-media bucket:

# Update deployment to use WordPress bucket temporarily
env:
  - name: AWS_MEDIA_BUCKET_NAME
    value: "manoon-media"  # Instead of "saleor"
  - name: MEDIA_URL
    value: "https://minio-api.nodecrew.me/manoon-media/"

Pros: No copying needed Cons: WordPress and Saleor share bucket (risk of conflicts)

Option C: Keep Separate + URL Mapping

Best for: Gradual migration

  1. Keep WordPress images in manoon-media
  2. New Saleor uploads go to saleor bucket
  3. Use URL mapping for old images
// Storefront image component
const ProductImage = ({ imageUrl }) => {
  // If image is from old WordPress, rewrite URL
  const mappedUrl = imageUrl.includes('manoon-media')
    ? imageUrl.replace('manoon-media', 'saleor')
    : imageUrl;
    
  return <img src={mappedUrl} />;
};

Step 3: Add Images to Saleor Products

Saleor Product Media Structure

Saleor stores media in product_productmedia table:

-- Check table structure
\d product_productmedia

-- Columns:
-- id, product_id, image (file path), alt, sort_order, type

Migration Script

-- Create temporary mapping table
CREATE TEMP TABLE wp_image_mapping (
    wp_product_id INTEGER,
    saleor_product_id INTEGER,
    wp_image_url VARCHAR(500),
    saleor_image_path VARCHAR(500)
);

-- After copying images to saleor bucket, insert media records
INSERT INTO product_productmedia (product_id, image, alt, sort_order, type)
SELECT 
    p.id as product_id,
    'products/' || SPLIT_PART(m.saleor_image_path, '/', -1) as image,
    p.name as alt,
    0 as sort_order,
    'IMAGE' as type
FROM temp_woocommerce_import t
JOIN product_product p ON p.slug = t.slug
JOIN wp_image_mapping m ON m.wp_product_id = t.wc_id;

Using Saleor Dashboard (Manual)

For small catalogs, use the Saleor Dashboard:

  1. Go to https://dashboard.manoonoils.com
  2. Catalog → Products → Select product
  3. Media tab → Upload images
  4. Set alt text, sort order

Using GraphQL API (Programmatic)

mutation ProductMediaCreate($product: ID!, $image: Upload!, $alt: String) {
  productMediaCreate(input: {product: $product, image: $image, alt: $alt}) {
    media {
      id
      url
    }
    errors {
      field
      message
    }
  }
}

Python script example:

import requests
from saleor.graphql import Client

# Upload image to Saleor
def upload_product_image(product_id, image_path, alt_text):
    url = "https://api.manoonoils.com/graphql/"
    
    query = """
    mutation ProductMediaCreate($product: ID!, $image: Upload!, $alt: String) {
      productMediaCreate(input: {product: $product, image: $image, alt: $alt}) {
        media { id url }
        errors { field message }
      }
    }
    """
    
    operations = {
        "query": query,
        "variables": {
            "product": product_id,
            "alt": alt_text
        }
    }
    
    map_data = {"0": ["variables.image"]}
    
    with open(image_path, 'rb') as f:
        files = {
            'operations': (None, json.dumps(operations)),
            'map': (None, json.dumps(map_data)),
            '0': (image_path, f, 'image/jpeg')
        }
        
        response = requests.post(url, files=files)
        return response.json()

Step 4: Handle Logos & Assets

Upload logos to Saleor as product media for a "Store" product, or serve via CDN:

# Upload logo to MinIO saleor bucket
mc cp logo.png local/saleor/assets/
mc cp favicon.ico local/saleor/assets/

Access URLs:

  • Logo: https://minio-api.nodecrew.me/saleor/assets/logo.png
  • Favicon: https://minio-api.nodecrew.me/saleor/assets/favicon.ico

Option 2: Store in Next.js Public Folder

For storefront-specific assets:

storefront/
├── public/
│   ├── logo.png
│   ├── favicon.ico
│   └── images/
│       └── hero-banner.jpg

Access: https://dev.manoonoils.com/logo.png

Option 3: Keep in WordPress (Transition Period)

Continue serving assets from WordPress during migration:

// Storefront config
const ASSETS_URL = process.env.NEXT_PUBLIC_ASSETS_URL || 
                   'https://minio-api.nodecrew.me/manoon-media/assets/';

// Usage
<img src={`${ASSETS_URL}logo.png`} alt="Logo" />

Step 5: Storefront Image Component

Handle both old and new image URLs:

// components/ProductImage.tsx
import { useState } from 'react';

interface ProductImageProps {
  url: string;
  alt: string;
  className?: string;
}

export function ProductImage({ url, alt, className }: ProductImageProps) {
  const [error, setError] = useState(false);
  
  // Map old WordPress URLs to new Saleor URLs
  const mappedUrl = url?.includes('manoon-media')
    ? url.replace('manoon-media', 'saleor')
    : url;
  
  if (error) {
    return <div className="image-placeholder">No Image</div>;
  }
  
  return (
    <img
      src={mappedUrl}
      alt={alt}
      className={className}
      onError={() => setError(true)}
      loading="lazy"
    />
  );
}

Step 6: Image Optimization

Saleor Thumbnails

Saleor automatically generates thumbnails:

query ProductImages {
  product(slug: "organsko-maslinovo-ulje", channel: "default-channel") {
    media {
      id
      url
      alt
      type
      # Thumbnails
      thumbnail(size: 255) {
        url
      }
      thumbnail(size: 510) {
        url
      }
      thumbnail(size: 1020) {
        url
      }
    }
  }
}

Next.js Image Optimization

import Image from 'next/image';

// Optimized image component
export function OptimizedProductImage({ media }) {
  return (
    <Image
      src={media.thumbnail?.url || media.url}
      alt={media.alt}
      width={400}
      height={400}
      quality={80}
      placeholder="blur"
      blurDataURL={media.thumbnail?.url}
    />
  );
}

Step 7: Bulk Image Migration Script

#!/bin/bash
# migrate-images.sh

# 1. Export WooCommerce product images list
kubectl exec deployment/wordpress -n manoonoils -- \
  wp db query "SELECT p.ID, p.post_title, pm.meta_value as image_url 
  FROM wp_posts p 
  JOIN wp_postmeta pm ON p.ID = pm.post_id 
  WHERE p.post_type = 'product' AND pm.meta_key = '_wp_attached_file'" \
  > /tmp/wp-images.csv

# 2. Copy images to Saleor bucket
while IFS=',' read -r product_id title image_path; do
  echo "Copying: $image_path"
  kubectl exec deployment/minio -n manoonoils -- \
    mc cp "local/manoon-media/$image_path" "local/saleor/products/"
done < /tmp/wp-images.csv

# 3. Update Saleor database with image paths
# (Run SQL script to insert into product_productmedia)

Step 8: Verification Checklist

  • All products have at least one image
  • Images load correctly in Saleor Dashboard
  • Images display in storefront
  • Thumbnails generate properly
  • Alt text is set for SEO
  • Logo loads correctly
  • Favicon works
  • No broken image links

Troubleshooting

Images not showing in Saleor Dashboard

# Check if Saleor can access MinIO
kubectl exec deployment/saleor-api -n saleor -- \
  curl -I http://minio.manoonoils:9000/saleor/

# Check bucket permissions
kubectl exec deployment/minio -n manoonoils -- \
  mc policy get local/saleor

# Set bucket to public (if needed)
kubectl exec deployment/minio -n manoonoils -- \
  mc policy set public local/saleor

Image URLs returning 404

  1. Check image exists in bucket:
mc ls local/saleor/products/2024/01/
  1. Check image path in database:
SELECT * FROM product_productmedia WHERE product_id = 1;
  1. Verify MEDIA_URL configuration:
kubectl get deployment saleor-api -n saleor -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="MEDIA_URL")].value}'

Summary

Component Current (WP) Target (Saleor) Action
Product Images MinIO: manoon-media MinIO: saleor Copy or share bucket
Logo WP media MinIO: saleor/assets/ or Next.js public Upload to new location
Favicon WP root Next.js public or MinIO Move to storefront
Thumbnails WP generates Saleor generates Automatic
CDN MinIO direct MinIO direct or Cloudflare Optional upgrade
  1. Create saleor bucket in existing MinIO
  2. Copy all product images from manoon-media to saleor
  3. Upload logos to saleor/assets/ or Next.js public folder
  4. Run SQL to insert image records into product_productmedia
  5. Update storefront to handle both old and new URLs during transition
  6. Test all images load correctly