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 หลายขนาด