vercel-blob について
vercel-blob は、@vercel/blob でファイルを保存するためのスキルです。アップロードやファイル管理機能を開発する人向けに、サーバー・クライアントからのアップロード、一覧表示、ダウンロード、削除、大容量ファイルのマルチパート処理を扱います。
主な機能
- 01サーバー・クライアントからのアップロード
- 02ファイルの一覧表示・ダウンロード・削除
- 03大容量ファイルのマルチパートアップロード
- 04公開・非公開アクセスの設定
- 05アップロード検証とセキュリティの指針
活用シーン
- 01ユーザーのアバターや文書のアップロード
- 02ページネーション付き画像ギャラリーの管理
- 03クライアントアップロードトークンでのファイル受付
- 04500 MB を超えるファイルのアップロード
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jackspace/ claudeskillz --skill vercel-blobインストール後、Claude Code で/vercel-blobを入力して読み込まれたか確認します。
同梱ファイルREADME.mdSKILL.json
詳細
Vercel Blob(オブジェクトストレージ)
ステータス:本番環境対応
最終更新日:2025-10-29
依存関係:なし
最新バージョン:@vercel/[email protected]
クイックスタート(3 分)
1. Blob ストアを作成
# In Vercel dashboard: Storage → Create Database → Blob
vercel env pull .env.local
作成されるもの:BLOB_READ_WRITE_TOKEN
2. インストール
npm install @vercel/blob
3. ファイルをアップロード(サーバーアクション)
'use server';
import { put } from '@vercel/blob';
export async function uploadFile(formData: FormData) {
const file = formData.get('file') as File;
const blob = await put(file.name, file, {
access: 'public' // or 'private'
});
return blob.url; // https://xyz.public.blob.vercel-storage.com/file.jpg
}
重要:
- クライアントから直接アップロードする場合は、クライアントアップロードトークンを使用します(
BLOB_READ_WRITE_TOKENを公開しないでください) - 適切な
accessレベル(publicまたはprivate)を設定します - ファイルは CDN を通じて自動的に配信されます
5 ステップのセットアップ手順
ステップ 1:Blob ストアを作成
Vercel ダッシュボード:
- プロジェクト → Storage → データベースを作成 → Blob
BLOB_READ_WRITE_TOKENをコピーします
ローカル開発:
vercel env pull .env.local
.env.local が作成されます:
BLOB_READ_WRITE_TOKEN="vercel_blob_rw_xxx"
要点:
- 無料枠:月間 100GB の帯域幅
- ファイルサイズ上限:1 ファイルあたり 500MB
- CDN による自動配信
- 公開ファイルはグローバルにキャッシュされます
ステップ 2:サーバーサイドアップロード
Next.js サーバーアクション:
'use server';
import { put } from '@vercel/blob';
export async function uploadAvatar(formData: FormData) {
const file = formData.get('avatar') as File;
// Validate file
if (!file.type.startsWith('image/')) {
throw new Error('Only images allowed');
}
if (file.size > 5 * 1024 * 1024) {
throw new Error('Max file size: 5MB');
}
// Upload
const blob = await put(`avatars/${Date.now()}-${file.name}`, file, {
access: 'public',
addRandomSuffix: false
});
return {
url: blob.url,
pathname: blob.pathname
};
}
API ルート(Edge Runtime):
import { put } from '@vercel/blob';
export const runtime = 'edge';
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get('file') as File;
const blob = await put(file.name, file, { access: 'public' });
return Response.json(blob);
}
ステップ 3:クライアントサイドアップロード(署名付き URL)
アップロードトークンを作成(サーバーアクション):
'use server';
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
export async function getUploadToken(filename: string) {
const jsonResponse = await handleUpload({
body: {
type: 'blob.generate-client-token',
payload: {
pathname: `uploads/${filename}`,
access: 'public',
onUploadCompleted: {
callbackUrl: `${process.env.NEXT_PUBLIC_URL}/api/upload-complete`
}
}
},
request: new Request('https://dummy'),
onBeforeGenerateToken: async (pathname) => {
// Optional: validate user permissions
return {
allowedContentTypes: ['image/jpeg', 'image/png', 'image/webp'],
maximumSizeInBytes: 5 * 1024 * 1024 // 5MB
};
},
onUploadCompleted: async ({ blob, tokenPayload }) => {
console.log('Upload completed:', blob.url);
}
});
return jsonResponse;
}
クライアントからのアップロード:
'use client';
import { upload } from '@vercel/blob/client';
import { getUploadToken } from './actions';
export function UploadForm() {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
const file = (form.elements.namedItem('file') as HTMLInputElement).files?.[0];
if (!file) return;
const tokenResponse = await getUploadToken(file.name);
const blob = await upload(file.name, file, {
access: 'public',
handleUploadUrl: tokenResponse.url
});
console.log('Uploaded:', blob.url);
}
return (
<form onSubmit={handleSubmit}>
<input type="file" name="file" required />
<button type="submit">Upload</button>
</form>
);
}
ステップ 4:一覧表示、ダウンロード、削除
ファイルを一覧表示:
import { list } from '@vercel/blob';
const { blobs } = await list({
prefix: 'avatars/',
limit: 100
});
// Returns: { url, pathname, size, uploadedAt, ... }[]
ページネーション付きで一覧表示:
let cursor: string | undefined;
const allBlobs = [];
do {
const { blobs, cursor: nextCursor } = await list({
prefix: 'uploads/',
cursor
});
allBlobs.push(...blobs);
cursor = nextCursor;
} while (cursor);
ファイルをダウンロード:
// Files are publicly accessible if access: 'public'
const url = 'https://xyz.public.blob.vercel-storage.com/file.pdf';
const response = await fetch(url);
const blob = await response.blob();
ファイルを削除:
import { del } from '@vercel/blob';
await del('https://xyz.public.blob.vercel-storage.com/file.jpg');
// Or delete multiple
await del([url1, url2, url3]);
ステップ 5:ストリーミングとマルチパート
ストリーミングアップロード:
import { put } from '@vercel/blob';
import { createReadStream } from 'fs';
const stream = createReadStream('./large-file.mp4');
const blob = await put('videos/large-file.mp4', stream, {
access: 'public',
contentType: 'video/mp4'
});
マルチパートアップロード(500MB を超える大容量ファイル):
import { createMultipartUpload, uploadPart, completeMultipartUpload } from '@vercel/blob';
// 1. Start multipart upload
const upload = await createMultipartUpload('large-video.mp4', {
access: 'public'
});
// 2. Upload parts (chunks)
const partSize = 100 * 1024 * 1024; // 100MB chunks
const parts = [];
for (let i = 0; i < totalParts; i++) {
const chunk = getChunk(i, partSize);
const part = await uploadPart(chunk, {
uploadId: upload.uploadId,
partNumber: i + 1
});
parts.push(part);
}
// 3. Complete upload
const blob = await completeMultipartUpload({
uploadId: upload.uploadId,
parts
});
重要なルール
必ず行うこと
✅ クライアントサイドのアップロードにはクライアントアップロードトークンを使用する - クライアントに BLOB_READ_WRITE_TOKEN を絶対に公開しないでください
✅ 適切なアクセスレベルを設定する - public(CDN)または private(認証済みアクセス)
✅ ファイルの種類とサイズを検証する - アップロード前に MIME タイプとサイズを確認します
✅ パス名で整理する - avatars/、uploads/、documents/ を使用して構造化します
✅ アップロードエラーに対処する - ネットワーク障害、サイズ制限、トークンの有効期限切れなどに対応します
✅ 古いファイルを整理する - ストレージコストを管理するため、不要なファイルを削除します
✅ content-type を明示的に設定する - ブラウザーで正しく処理されるようにします(動画、PDF など)
絶対に行わないこと
❌ BLOB_READ_WRITE_TOKEN をクライアントに絶対に公開しない - クライアントからのアップロードには handleUpload() を使用します
❌ ファイル検証を絶対に省略しない - アップロード前に必ず種類、サイズ、内容を検証します
❌ マルチパートを使用せずに 500MB を超えるファイルを絶対にアップロードしない - 大容量ファイルにはマルチパートアップロードを使用します
❌ 汎用的なファイル名を絶対に使用しない - file.jpg は重複するため、${timestamp}-${name} または UUID を使用します
❌ アップロードの成功を絶対に前提としない - 必ずエラー(ネットワーク、クォータなど)に対処します
❌ 機密データを暗号化せずに絶対に保存しない - 必要に応じて、アップロード前に暗号化します
❌ 一時ファイルの削除を絶対に忘れない - 古いアップロードファイルはクォータを消費します
既知の問題の予防
このスキルは、記録されている 10 件の問題を防ぎます:
問題 #1:環境変数がない
エラー:Error: BLOB_READ_WRITE_TOKEN is not defined
参照元:https://vercel.com/docs/storage/vercel-blob
原因:トークンが環境に設定されていません
予防策:vercel env pull .env.local を実行し、.env.local が .gitignore に含まれていることを確認します。
問題 #2:クライアントアップロードトークンの公開
エラー:セキュリティ上の脆弱性、不正なアップロード
参照元:https://vercel.com/docs/storage/vercel-blob/client-upload
原因:クライアントコードで BLOB_READ_WRITE_TOKEN を直接使用しています
予防策:handleUpload() を使用して、制約付きのクライアント専用トークンを生成します。
問題 #3:ファイルサイズ上限の超過
エラー:Error: File size exceeds limit(500MB)
参照元:https://vercel.com/docs/storage/vercel-blob/limits
原因:マルチパートアップロードを使用せずに 500MB を超えるファイルをアップロードしています
予防策:アップロード前にファイルサイズを検証し、大容量ファイルにはマルチパートアップロードを使用します。
Issue #4:Content-Type が正しくない
エラー:ブラウザーでファイルが表示されず、ダウンロードされます(例:PDF がテキストとして開く)
発生元:本番環境でのデバッグ
原因:contentType オプションが設定されておらず、Blob が誤って推測します
予防策:常に contentType: file.type または明示的な MIME タイプを設定します。
Issue #5:公開ファイルがキャッシュされない
エラー:ファイル配信が遅く、エグレスコストが高くなります
発生元:Vercel Blob のベストプラクティス
原因:公開すべきファイルに access: 'private' を使用しています
予防策:一般公開するファイルには access: 'public' を使用します(CDN キャッシュ)。
Issue #6:リストのページネーションが処理されていない
エラー:最初の 1000 件のファイルしか返されず、ファイルが不足します
発生元:https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#list
原因:大量のファイルリストでカーソルを使った反復処理を行っていません
予防策:cursor が undefined になるまで、ループ内でカーソルベースのページネーションを使用します。
Issue #7:削除が失敗しても通知されない
エラー:ファイルが削除されず、ストレージクォータを使い切ります
発生元:https://github.com/vercel/storage/issues/150
原因:誤った URL 形式を使用しており、Blob が見つかりません
予防策:put() の応答に含まれる完全な Blob URL を使用し、削除結果を確認します。
Issue #8:アップロードのタイムアウト(大容量ファイル)
エラー:100MB を超えるファイルで Error: Request timeout が発生します
発生元:Vercel 関数のタイムアウト制限
原因:サーバーレス関数のタイムアウト(無料プランでは 10 秒、Pro では 60 秒)
予防策:大容量ファイルには handleUpload() を使ったクライアント側アップロードを使用します。
Issue #9:ファイル名の衝突
エラー:ファイルが上書きされ、データが失われます
発生元:本番環境でのデバッグ
原因:複数のアップロードで同じファイル名を使用しています
予防策:タイムスタンプ/UUID を追加します:`uploads/${Date.now()}-${file.name}` または addRandomSuffix: true。
Issue #10:アップロードコールバックがない
エラー:アップロードは完了しますが、アプリの状態が更新されません
発生元:https://vercel.com/docs/storage/vercel-blob/client-upload#callback-after-upload
原因:onUploadCompleted コールバックを実装していません
予防策:handleUpload() で onUploadCompleted を使用し、データベース/状態を更新します。
設定ファイルのリファレンス
package.json
{
"dependencies": {
"@vercel/blob": "^2.0.0"
}
}
.env.local
BLOB_READ_WRITE_TOKEN="vercel_blob_rw_xxxxx"
一般的なパターン
パターン 1:アバターのアップロード
'use server';
import { put, del } from '@vercel/blob';
export async function updateAvatar(userId: string, formData: FormData) {
const file = formData.get('avatar') as File;
// Validate
if (!file.type.startsWith('image/')) {
throw new Error('Only images allowed');
}
// Delete old avatar
const user = await db.query.users.findFirst({ where: eq(users.id, userId) });
if (user?.avatarUrl) {
await del(user.avatarUrl);
}
// Upload new
const blob = await put(`avatars/${userId}.jpg`, file, {
access: 'public',
contentType: file.type
});
// Update database
await db.update(users).set({ avatarUrl: blob.url }).where(eq(users.id, userId));
return blob.url;
}
パターン 2:保護されたファイルのアップロード
'use server';
import { put } from '@vercel/blob';
import { auth } from '@/lib/auth';
export async function uploadDocument(formData: FormData) {
const session = await auth();
if (!session) throw new Error('Unauthorized');
const file = formData.get('document') as File;
// Upload as private
const blob = await put(`documents/${session.user.id}/${file.name}`, file, {
access: 'private' // Requires authentication to access
});
// Store in database with user reference
await db.insert(documents).values({
userId: session.user.id,
url: blob.url,
filename: file.name,
size: file.size
});
return blob;
}
パターン 3:ページネーション付き画像ギャラリー
import { list } from '@vercel/blob';
export async function getGalleryImages(cursor?: string) {
const { blobs, cursor: nextCursor } = await list({
prefix: 'gallery/',
limit: 20,
cursor
});
const images = blobs.map(blob => ({
url: blob.url,
uploadedAt: blob.uploadedAt,
size: blob.size
}));
return { images, nextCursor };
}
依存関係
必須:
@vercel/blob@^2.0.0- Vercel Blob SDK
任意:
sharp@^0.33.0- アップロード前の画像処理zod@^3.24.0- ファイル検証スキーマ
公式ドキュメント
- Vercel Blob:https://vercel.com/docs/storage/vercel-blob
- クライアントアップロード:https://vercel.com/docs/storage/vercel-blob/client-upload
- SDK リファレンス:https://vercel.com/docs/storage/vercel-blob/using-blob-sdk
- GitHub:https://github.com/vercel/storage
パッケージバージョン(2025-10-29 確認済み)
{
"dependencies": {
"@vercel/blob": "^2.0.0"
}
}
本番環境での例
- E コマース:商品画像、ユーザーによるアップロード(50 万件以上のファイル)
- ブログプラットフォーム:注目画像、著者のアバター
- SaaS:ドキュメントのアップロード、PDF 生成、CSV エクスポート
- エラー:0 件(既知の問題 10 件をすべて予防)
トラブルシューティング
問題:BLOB_READ_WRITE_TOKEN is not defined
解決策:vercel env pull .env.local を実行し、.env.local が .gitignore に含まれていることを確認します。
問題:ファイルサイズが上限を超えた(>500MB)
解決策:createMultipartUpload() API を使ったマルチパートアップロードを使用します。
問題:クライアントアップロードがトークンエラーで失敗する
解決策:サーバー側で handleUpload() を使用していることを確認し、読み書きトークンをクライアントに公開しないでください。
問題:ファイルが削除されない
解決策:put() の応答に含まれる正確な URL を使用し、del() の戻り値を確認します。
完全セットアップチェックリスト
- Vercel ダッシュボードで Blob ストアを作成済み
-
BLOB_READ_WRITE_TOKEN環境変数を設定済み -
@vercel/blobパッケージをインストール済み - ファイルの検証(種類、サイズ)を実装済み
- クライアント側のアップロードで(トークンを直接使用せず)
handleUpload()を使用 - アップロード時に Content-Type を設定
- アクセスレベルを適切に設定(
publicとprivate) - 古いファイルの削除を実装済み
- リストのページネーションでカーソルを処理
- ローカル環境と本番環境でファイルのアップロード/ダウンロード/削除をテスト済み
ご質問や問題はありますか?
- 公式ドキュメントを確認してください:https://vercel.com/docs/storage/vercel-blob
- 環境変数が設定されていることを確認してください
- クライアント側のアップロードには、クライアントアップロードトークンを使用してください
- Vercel ダッシュボードでストレージ使用量を監視してください