vercel-blob
Next.jsアプリケーションにおいて、Vercel Blobオブジェクトストレージを統合し、ファイルアップロード、画像管理、CDN経由でのアセット配信を実現します。プリサインドURLとマルチパート転送を活用したクライアント側のアップロードに対応しています。 画像やPDF、動画などのファイルアップロード機能の実装、ユーザー生成コンテンツの管理、トークンの欠落やサイズ制限エラー、クライアントアップロードの失敗などのトラブルシューティングが必要な場合に活用できます。
description の原文を見る
Integrate Vercel Blob object storage for file uploads, image management, and CDN-delivered assets in Next.js applications. Supports client-side uploads with presigned URLs and multipart transfers. Use when implementing file uploads (images, PDFs, videos), managing user-generated content, or troubleshooting missing tokens, size limit errors, or client upload failures.
SKILL.md 本文
Vercel Blob (オブジェクトストレージ)
ステータス: 本番環境対応済み
最終更新: 2025-10-29
依存関係: なし
最新バージョン: @vercel/blob@2.0.0
クイックスタート(3分)
1. Blob ストアを作成
# Vercel ダッシュボード: 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' // または '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 ダッシュボード:
- Project → Storage → Create Database → 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;
// ファイル検証
if (!file.type.startsWith('image/')) {
throw new Error('Only images allowed');
}
if (file.size > 5 * 1024 * 1024) {
throw new Error('Max file size: 5MB');
}
// アップロード
const blob = await put(`avatars/${Date.now()}-${file.name}`, file, {
access: 'public',
addRandomSuffix: false
});
return {
url: blob.url,
pathname: blob.pathname
};
}
API ルート(Edge ランタイム):
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) => {
// オプション: ユーザーのアクセス権を検証
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
});
// 戻り値: { 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);
ファイルをダウンロード:
// 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');
// または複数削除
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. マルチパートアップロードを開始
const upload = await createMultipartUpload('large-video.mp4', {
access: 'public'
});
// 2. パート(チャンク)をアップロード
const partSize = 100 * 1024 * 1024; // 100MB チャンク
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. アップロードを完了
const blob = await completeMultipartUpload({
uploadId: upload.uploadId,
parts
});
重要なルール
必ず実施すること
✅ クライアント側のアップロードにはクライアントアップロードトークンを使用 - BLOB_READ_WRITE_TOKENをクライアントに公開しない
✅ 正しいアクセスレベルを設定 - public(CDN)またはprivate(認証によるアクセス)
✅ ファイルの種類とサイズを検証 - アップロード前に MIME タイプとサイズを確認
✅ パス名で整理 - avatars/、uploads/、documents/で構造化
✅ アップロードエラーを処理 - ネットワーク障害、サイズ制限、トークン有効期限切れ
✅ 古いファイルを削除 - 不要なファイルを削除してストレージコストを管理
✅ コンテンツタイプを明示的に設定 - ブラウザでの正しい処理のため(ビデオ、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 を超えるファイルをアップロード
予防方法: アップロード前にファイルサイズを検証し、大きなファイルはマルチパートアップロードを使用
問題 #4: コンテンツタイプが間違っている
エラー: ブラウザがファイルをダウンロードする代わりに表示(例: PDF がテキストとして開く)
ソース: 本番環境デバッグ
発生理由: contentTypeオプションを設定していない、Blob が正しく推測できない
予防方法: 常にcontentType: file.typeまたは明示的な MIME タイプを設定
問題 #5: 公開ファイルがキャッシュされない
エラー: ファイル配信が遅い、出力コストが高い
ソース: Vercel Blob ベストプラクティス
発生理由: 公開されるべきファイルにaccess: 'private'を使用
予防方法: 公開アクセス可能なファイルにaccess: 'public'を使用(CDN キャッシュ)
問題 #6: リストページネーションが処理されていない
エラー: 最初の 1000 ファイルのみ返される、ファイルが欠落
ソース: https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#list
発生理由: 大きなファイルリストでカーソルを使用した反復処理をしていない
予防方法: ループでカーソルベースのページネーションを使用し、cursorが undefined になるまで続ける
問題 #7: 削除がサイレントに失敗
エラー: ファイルが削除されない、ストレージクォータが満杯になる
ソース: https://github.com/vercel/storage/issues/150
発生理由: 間違った URL 形式を使用している、blob が見つからない
予防方法: put()レスポンスの完全な blob URL を使用し、削除結果を確認
問題 #8: アップロードタイムアウト(大きなファイル)
エラー: Error: Request timeout(100MB を超えるファイルの場合)
ソース: Vercel 関数のタイムアウト制限
発生理由: サーバーレス関数のタイムアウト(無料層 10s、Pro 60s)
予防方法: 大きなファイルにはhandleUpload()を使用したクライアント側のアップロードを使用
問題 #9: ファイル名の衝突
エラー: ファイルが上書きされた、データが失われた
ソース: 本番環境デバッグ
発生理由: 複数のアップロードで同じファイル名を使用
予防方法: タイムスタンプ/UUID を追加: `uploads/${Date.now()}-${file.name}`またはaddRandomSuffix: true
問題 #10: アップロード完了コールバックが見落とされた
エラー: アップロードは完了するがアプリの状態が更新されない
ソース: https://vercel.com/docs/storage/vercel-blob/client-upload#callback-after-upload
発生理由: handleUpload()で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;
// 検証
if (!file.type.startsWith('image/')) {
throw new Error('Only images allowed');
}
// 古いアバターを削除
const user = await db.query.users.findFirst({ where: eq(users.id, userId) });
if (user?.avatarUrl) {
await del(user.avatarUrl);
}
// 新しいアバターをアップロード
const blob = await put(`avatars/${userId}.jpg`, file, {
access: 'public',
contentType: file.type
});
// データベースを更新
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;
// プライベートとしてアップロード
const blob = await put(`documents/${session.user.id}/${file.name}`, file, {
access: 'private' // アクセスには認証が必要
});
// ユーザーリファレンス付きでデータベースに保存
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コマース: 商品画像、ユーザーアップロード(500K+ ファイル)
- ブログプラットフォーム: アイキャッチ画像、著者アバター
- 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()を使用(直接トークンではない) - アップロード時にコンテンツタイプを設定
- アクセスレベルが正しい(
public対private) - 古いファイルの削除を実装
- リストページネーションがカーソルを処理
- ローカルおよび本番環境でファイルのアップロード/ダウンロード/削除をテスト
ご質問やお問い合わせ?
- 公式ドキュメント確認: https://vercel.com/docs/storage/vercel-blob
- 環境変数が設定されていることを確認
- クライアント側のアップロードにクライアントアップロードトークンを使用していることを確認
- Vercel ダッシュボードでストレージ使用量を監視
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- TryTraza
- ライセンス
- MIT
- 最終更新
- 2026/4/19
Source: https://github.com/TryTraza/fde-discovery / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。