1. サーバーアクションによるファイルアップロード
// actions/upload.ts
'use server';
import { writeFile, mkdir } from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
export async function uploadFile(formData: FormData) {
const file = formData.get('file') as File;
if (!file || file.size === 0) {
return { error: 'Chưa chọn file' };
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
return { error: 'Chỉ cho phép JPEG, PNG, WebP' };
}
// Validate size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
return { error: 'File tối đa 5MB' };
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// Generate unique filename
const ext = path.extname(file.name);
const filename = `${crypto.randomUUID()}${ext}`;
const uploadDir = path.join(process.cwd(), 'public/uploads');
await mkdir(uploadDir, { recursive: true });
await writeFile(path.join(uploadDir, filename), buffer);
return { url: `/uploads/${filename}` };
}
2. フォームコンポーネントをアップロードする
'use client';
import { useState, useRef } from 'react';
import { uploadFile } from '@/actions/upload';
export function UploadForm() {
const [preview, setPreview] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setPreview(URL.createObjectURL(file));
}
};
const handleSubmit = async (formData: FormData) => {
setUploading(true);
const result = await uploadFile(formData);
setUploading(false);
if (result.error) {
alert(result.error);
} else {
alert(`Uploaded: ${result.url}`);
}
};
return (
<form action={handleSubmit}>
<input
ref={inputRef}
type="file"
name="file"
accept="image/*"
onChange={handleChange}
/>
{preview && <img src={preview} alt="Preview" width={200} />}
<button type="submit" disabled={uploading}>
{uploading ? 'Đang upload...' : 'Upload'}
</button>
</form>
);
}
3. next/imageコンポーネント
import Image from 'next/image';
// Local image — tự động optimize
<Image
src="/images/hero.jpg"
alt="Hero"
width={1200}
height={630}
priority // Preload for LCP image
/>
// Remote image — cần config domain
<Image
src="https://cdn.example.com/photo.jpg"
alt="Photo"
width={800}
height={600}
quality={80}
placeholder="blur"
blurDataURL="data:image/..."
/>
// Fill mode — responsive container
<div className="relative w-full h-64">
<Image
src="/images/banner.jpg"
alt="Banner"
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
/>
</div>
4. イメージコンフィグレーション
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
},
{
protocol: 'https',
hostname: '*.amazonaws.com',
},
],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
},
};
export default nextConfig;
5.S3にアップロードする
// lib/s3.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
export async function getPresignedUrl(filename: string, contentType: string) {
const key = `uploads/${Date.now()}-${filename}`;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: key,
ContentType: contentType,
});
const url = await getSignedUrl(s3, command, { expiresIn: 600 });
return { url, key };
}
次の記事: リアルタイムと WebSocket — リアルタイムのインタラクション。