Astro กับ Cloudinary: Image Management

#astro13 เม.ย. 2569

Astro กับ Cloudinary: Image Management

Cloudinary เป็น cloud-based image และ video management platform ที่ครบครัน ด้วย automatic optimization, transformation, และ CDN delivery ทำให้ Cloudinary เป็นตัวเลือกยอดนิยมสำหรับการจัดการ media ใน Astro projects

ทำไมต้องใช้ Cloudinary?

  • Auto Optimization: ปรับขนาดและ format อัตโนมัติ
  • Transformations: ครอป, resize, filter ผ่าน URL
  • CDN: ส่ง images จาก edge locations ทั่วโลก
  • AI Features: Background removal, object detection
  • Video Support: จัดการ video ได้ด้วย

การติดตั้ง

npm install @cloudinary/url-gen cloudinary

ตั้งค่า environment variables:

# .env
CLOUDINARY_CLOUD_NAME=your-cloud-name
CLOUDINARY_API_KEY=your-api-key
CLOUDINARY_API_SECRET=your-api-secret

สร้าง Cloudinary Helper

// src/lib/cloudinary.ts
import { Cloudinary } from '@cloudinary/url-gen';
import { fill, scale, crop } from '@cloudinary/url-gen/actions/resize';
import { autoGravity, focusOn } from '@cloudinary/url-gen/qualifiers/gravity';
import { face } from '@cloudinary/url-gen/qualifiers/focusOn';
import { auto } from '@cloudinary/url-gen/qualifiers/format';
import { auto as autoQuality } from '@cloudinary/url-gen/qualifiers/quality';
import { format, quality } from '@cloudinary/url-gen/actions/delivery';

export const cld = new Cloudinary({
  cloud: {
    cloudName: import.meta.env.CLOUDINARY_CLOUD_NAME,
  },
});

// Helper functions
export function getImageUrl(
  publicId: string,
  options?: {
    width?: number;
    height?: number;
    crop?: 'fill' | 'scale' | 'crop';
  }
) {
  const image = cld.image(publicId);

  image.delivery(format(auto())).delivery(quality(autoQuality()));

  if (options?.width || options?.height) {
    const w = options.width ?? 0;
    const h = options.height ?? 0;

    switch (options.crop) {
      case 'fill':
        image.resize(fill().width(w).height(h).gravity(autoGravity()));
        break;
      case 'scale':
        image.resize(scale().width(w).height(h));
        break;
      default:
        image.resize(fill().width(w).height(h));
    }
  }

  return image.toURL();
}

export function getAvatarUrl(publicId: string, size = 100) {
  return cld
    .image(publicId)
    .resize(fill().width(size).height(size).gravity(focusOn(face())))
    .delivery(format(auto()))
    .delivery(quality(autoQuality()))
    .toURL();
}

export function getOgImageUrl(publicId: string) {
  return getImageUrl(publicId, { width: 1200, height: 630, crop: 'fill' });
}

export function getThumbnailUrl(publicId: string) {
  return getImageUrl(publicId, { width: 400, height: 250, crop: 'fill' });
}

Upload API Route

// src/pages/api/upload.ts
import type { APIRoute } from 'astro';
import { v2 as cloudinary } from 'cloudinary';

cloudinary.config({
  cloud_name: import.meta.env.CLOUDINARY_CLOUD_NAME,
  api_key: import.meta.env.CLOUDINARY_API_KEY,
  api_secret: import.meta.env.CLOUDINARY_API_SECRET,
});

export const POST: APIRoute = async ({ request }) => {
  const formData = await request.formData();
  const file = formData.get('file') as File;
  const folder = formData.get('folder') as string ?? 'uploads';

  if (!file) {
    return new Response(JSON.stringify({ error: 'No file provided' }), {
      status: 400,
    });
  }

  const buffer = Buffer.from(await file.arrayBuffer());
  const base64 = `data:${file.type};base64,${buffer.toString('base64')}`;

  const result = await cloudinary.uploader.upload(base64, {
    folder,
    resource_type: 'auto',
    transformation: [
      { quality: 'auto', fetch_format: 'auto' },
    ],
  });

  return new Response(
    JSON.stringify({
      publicId: result.public_id,
      url: result.secure_url,
      width: result.width,
      height: result.height,
    }),
    { headers: { 'Content-Type': 'application/json' } }
  );
};

Astro Image Component กับ Cloudinary

---
// src/components/CloudinaryImage.astro
interface Props {
  publicId: string;
  alt: string;
  width: number;
  height: number;
  class?: string;
}

const { publicId, alt, width, height, class: className } = Astro.props;

import { getImageUrl } from '../lib/cloudinary';

const src = getImageUrl(publicId, { width, height });
const src2x = getImageUrl(publicId, { width: width * 2, height: height * 2 });
const webpSrc = getImageUrl(publicId, { width, height });
---

<picture>
  <source
    srcset={`${webpSrc} 1x, ${src2x} 2x`}
    type="image/webp"
  />
  <img
    src={src}
    srcset={`${src} 1x, ${src2x} 2x`}
    alt={alt}
    width={width}
    height={height}
    loading="lazy"
    decoding="async"
    class={className}
  />
</picture>

Responsive Images

// src/lib/cloudinaryResponsive.ts
import { cld } from './cloudinary';
import { fill } from '@cloudinary/url-gen/actions/resize';
import { auto } from '@cloudinary/url-gen/qualifiers/format';
import { auto as autoQuality } from '@cloudinary/url-gen/qualifiers/quality';
import { format, quality } from '@cloudinary/url-gen/actions/delivery';

const breakpoints = [320, 640, 768, 1024, 1280, 1536];

export function getResponsiveSrcSet(publicId: string, aspectRatio = 16 / 9) {
  return breakpoints
    .map((width) => {
      const height = Math.round(width / aspectRatio);
      const url = cld
        .image(publicId)
        .resize(fill().width(width).height(height))
        .delivery(format(auto()))
        .delivery(quality(autoQuality()))
        .toURL();
      return `${url} ${width}w`;
    })
    .join(', ');
}

Image Upload Component (React)

// src/components/ImageUploader.tsx
import { useState, useRef } from 'react';

export default function ImageUploader({
  onUpload,
}: {
  onUpload: (publicId: string, url: string) => void;
}) {
  const [uploading, setUploading] = useState(false);
  const [preview, setPreview] = useState<string | null>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  async function handleUpload(file: File) {
    setUploading(true);
    const formData = new FormData();
    formData.append('file', file);
    formData.append('folder', 'blog');

    try {
      const res = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
      });
      const data = await res.json();
      onUpload(data.publicId, data.url);
      setPreview(data.url);
    } finally {
      setUploading(false);
    }
  }

  return (
    <div className="uploader">
      <input
        ref={inputRef}
        type="file"
        accept="image/*"
        onChange={(e) => {
          const file = e.target.files?.[0];
          if (file) handleUpload(file);
        }}
        hidden
      />
      <button onClick={() => inputRef.current?.click()} disabled={uploading}>
        {uploading ? 'กำลังอัปโหลด...' : 'เลือกรูปภาพ'}
      </button>
      {preview && <img src={preview} alt="Preview" className="preview" />}
    </div>
  );
}

สรุป

Cloudinary กับ Astro ทำให้การจัดการ images เป็นเรื่องง่ายและมีประสิทธิภาพ ด้วย automatic optimization, responsive images, และ CDN delivery ทำให้ website โหลดเร็วขึ้นอย่างมาก การใช้ URL-based transformations ทำให้ไม่ต้องเก็บ image หลายขนาด