mcpField
V
vercel-blob

vercel-blob

github.com/jezwebクラウド・DevOps1.1k333 回インストール3 週間前に更新
リポジトリ

vercel-blob について

Vercel Blob でファイルを保存、アップロード、管理する開発者向けの実装ガイドです。クライアントからの安全なアップロードやマルチパート転送、ファイル操作、問題の予防を扱います。

主な機能

  1. 01サーバー側でのファイルアップロード
  2. 02安全なクライアント用トークンの発行
  3. 03Blob の一覧表示と削除
  4. 04大容量ファイルのマルチパート転送
  5. 05ファイル検証とパス整理
  6. 06一般的な問題の対処

活用シーン

  1. 01ユーザーのアバターやファイルのアップロード
  2. 02大容量ファイルのクライアントアップロード
  3. 03ページングによる保存ファイルの確認と削除

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jezweb/claude-skills --skill vercel-blob

インストール後、Claude Code で/vercel-blobを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
.claude-plugin/
plugin.json
assets/
example-template.txt
references/
example-reference.md
scripts/
example-script.sh
templates/
avatar-upload-flow.tsx
drag-drop-upload.tsx
file-list-manager.tsx
package.json

詳細

Vercel Blob

最終更新日: 2026-01-21 バージョン: @vercel/[email protected] スキルバージョン: 2.1.0


クイックスタート

# Create Blob store: Vercel Dashboard → Storage → Blob
vercel env pull .env.local  # Creates BLOB_READ_WRITE_TOKEN
npm install @vercel/blob

サーバーアップロード:

'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' });
  return blob.url;
}

重要: クライアントに BLOB_READ_WRITE_TOKEN を決して公開しないでください。クライアントからのアップロードには handleUpload() を使用してください。


クライアントアップロード(セキュア)

サーバーアクション(署名付きトークンを生成):

'use server';
import { handleUpload } from '@vercel/blob/client';

export async function getUploadToken(filename: string) {
  return await handleUpload({
    body: {
      type: 'blob.generate-client-token',
      payload: { pathname: `uploads/${filename}`, access: 'public' }
    },
    request: new Request('https://dummy'),
    onBeforeGenerateToken: async (pathname) => ({
      allowedContentTypes: ['image/jpeg', 'image/png'],
      maximumSizeInBytes: 5 * 1024 * 1024
    })
  });
}

クライアントコンポーネント:

'use client';
import { upload } from '@vercel/blob/client';

const tokenResponse = await getUploadToken(file.name);
const blob = await upload(file.name, file, {
  access: 'public',
  handleUploadUrl: tokenResponse.url
});

ファイル管理

一覧表示/削除:

import { list, del } from '@vercel/blob';

// List with pagination
const { blobs, cursor } = await list({ prefix: 'uploads/', cursor });

// Delete
await del(blobUrl);

マルチパート(>500MB):

import { createMultipartUpload, uploadPart, completeMultipartUpload } from '@vercel/blob';

const upload = await createMultipartUpload('large-video.mp4', { access: 'public' });
// Upload chunks in loop...
await completeMultipartUpload({ uploadId: upload.uploadId, parts });

重要なルール

必ず行うこと:

  • ✅ クライアントからのアップロードには handleUpload() を使用する(BLOB_READ_WRITE_TOKEN を決して公開しない)
  • ✅ アップロード前にファイルの種類とサイズを検証する
  • ✅ パス名で整理する(avatars/、uploads/)
  • ✅ ファイル名にタイムスタンプ/UUIDを追加する(衝突を避ける)

決して行わないこと:

  • ❌ クライアントに BLOB_READ_WRITE_TOKEN を公開する
  • ❌ マルチパートを使わずに500MBを超えるファイルをアップロードする
  • ❌ ファイル検証を省略する

既知の問題の防止

このスキルは、文書化されている16件の問題を防止します。

問題 #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: Content-Typeの誤り

エラー: ブラウザーがファイルを表示せずにダウンロードする(例: 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 が未定義になるまでループ内でカーソルベースのページネーションを使用してください。

問題 #7: 削除が失敗しても通知されない

エラー: ファイルが削除されず、ストレージクォータがいっぱいになる 出典: https://github.com/vercel/storage/issues/150 発生理由: URL形式が誤っているため、Blobが見つかりません。 防止策: put() のレスポンスから完全なBlob URLを使用し、削除結果を確認してください。

問題 #8: アップロードのタイムアウト(大きなファイル)+サーバー側の4.5MB制限

エラー: 100MBを超えるファイルで Error: Request timeout が発生する(サーバー側)、または4.5MBでファイルのアップロードが失敗する(サーバーレス関数の制限) 出典: Vercel function timeout limits + 4.5MB serverless limit + Community Discussion 発生理由:

  • サーバー側アップロードでは、サーバーレス関数のタイムアウトが発生します(無料プランは10秒、Proは60秒)。
  • 重要: Vercelのサーバーレス関数には、リクエストボディのハードリミットが4.5MBあります。サーバーアクション/APIルートで put() を使用すると、4.5MBを超えるファイルのアップロードは失敗します。

防止策: 4.5MBを超えるファイルには、クライアント側アップロードで handleUpload() を使用するか、マルチパートアップロードを使用してください。

// ❌ Server-side upload fails at 4.5MB
export async function POST(request: Request) {
  const formData = await request.formData();
  const file = formData.get('file') as File; // Fails if >4.5MB
  await put(file.name, file, { access: 'public' });
}

// ✅ Client upload bypasses 4.5MB limit (supports up to 500MB)
const blob = await upload(file.name, file, {
  access: 'public',
  handleUploadUrl: '/api/upload/token',
  multipart: true, // For files >500MB, use multipart
});
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 を使用し、データベース/状態を更新します。

Issue #11:大容量ファイルのクライアントアップロードトークンの有効期限切れ

エラー:Error: Access denied, please provide a valid token for this resource 出典:GitHub Issue #443 発生理由:デフォルトのトークンは30秒で有効期限が切れます。大容量ファイル(>100MB)はアップロードに時間がかかるため、検証前にトークンの有効期限が切れます。 防止策:大容量ファイルをアップロードする場合は、validUntil パラメーターを設定します。

// For large files (>100MB), extend token expiration
const jsonResponse = await handleUpload({
  body,
  request,
  onBeforeGenerateToken: async (pathname) => {
    return {
      maximumSizeInBytes: 200 * 1024 * 1024,
      validUntil: Date.now() + 300000, // 5 minutes
    };
  },
});
Issue #12:v2.0.0の破壊的変更 - onUploadCompletedにはcallbackUrlが必要(Vercel以外でのホスティング)

エラー:Vercelでホストしていない場合、onUploadCompletedコールバックが実行されません 出典:Release Notes @vercel/[email protected] 発生理由:セキュリティ上の理由から、v2.0.0ではクライアント側の location.href からコールバックURLを自動推測する機能が削除されました。Vercelのシステム環境変数を使用しない場合は、callbackUrl を明示的に指定する必要があります。 防止策:Vercel以外でホストする場合は、onBeforeGenerateToken で callbackUrl を明示的に指定します。

// v2.0.0+ for non-Vercel hosting
await handleUpload({
  body,
  request,
  onBeforeGenerateToken: async (pathname) => {
    return {
      callbackUrl: 'https://example.com', // Required for non-Vercel hosting
    };
  },
  onUploadCompleted: async ({ blob, tokenPayload }) => {
    // Now fires correctly
  },
});

// For local development with ngrok:
// VERCEL_BLOB_CALLBACK_URL=https://abc123.ngrok-free.app
Issue #13:FirefoxではReadableStreamのアップロードがサポートされていない

エラー:Firefoxでアップロードが完了しません 出典:GitHub Issue #881 発生理由:TypeScriptインターフェースではボディの型として ReadableStream を受け付けますが、Firefoxはfetchのボディとして ReadableStream をサポートしていません。 防止策:クロスブラウザーに対応するには、ストリームをBlobまたはArrayBufferに変換します。

// ❌ Works in Chrome/Edge, hangs in Firefox
const stream = new ReadableStream({ /* ... */ });
await put('file.bin', stream, { access: 'public' }); // Never completes in Firefox

// ✅ Convert stream to Blob for cross-browser support
const chunks: Uint8Array[] = [];
const reader = stream.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  chunks.push(value);
}
const blob = new Blob(chunks);
await put('file.bin', blob, { access: 'public' });
Issue #14:onBeforeGenerateTokenではpathnameを変更できない

エラー:サーバー側でpathnameを上書きしようとしても、ファイルが誤ったパスにアップロードされます 出典:GitHub Issue #863 発生理由:onBeforeGenerateToken の pathname パラメーターは変更できません。クライアント側で upload(pathname, ...) を呼び出した時点で設定されます。 防止策:クライアント側でpathnameを構築し、サーバー側で検証します。メタデータの受け渡しには clientPayload を使用します。

// Client: Construct pathname before upload
await upload(`uploads/${Date.now()}-${file.name}`, file, {
  access: 'public',
  handleUploadUrl: '/api/upload',
  clientPayload: JSON.stringify({ userId: '123' }),
});

// Server: Validate pathname matches expected pattern
await handleUpload({
  body,
  request,
  onBeforeGenerateToken: async (pathname, clientPayload) => {
    const { userId } = JSON.parse(clientPayload || '{}');

    // Validate pathname starts with expected prefix
    if (!pathname.startsWith(`uploads/`)) {
      throw new Error('Invalid upload path');
    }

    return {
      allowedContentTypes: ['image/jpeg', 'image/png'],
      tokenPayload: JSON.stringify({ userId }), // Pass to onUploadCompleted
    };
  },
});
Issue #15:マルチパートアップロードの最小チャンクサイズ(5MB)

エラー:小さいチャンクを使用した手動マルチパートアップロードが失敗します 出典:公式ドキュメント + コミュニティディスカッション 発生理由:手動マルチパートアップロードでは、各パートは5MB以上である必要があります(最後のパートを除く)。これはVercelのサーバーレス関数の4.5MB制限と競合するため、サーバー側のルート経由では手動マルチパートアップロードを実行できません。 防止策:自動マルチパート(put() で multipart: true を指定)またはクライアントアップロードを使用します。

// ❌ Manual multipart upload fails (can't upload 5MB chunks via serverless function)
const upload = await createMultipartUpload('large.mp4', { access: 'public' });
// uploadPart() requires 5MB minimum - hits serverless limit

// ✅ Use automatic multipart via client upload
await upload('large.mp4', file, {
  access: 'public',
  handleUploadUrl: '/api/upload',
  multipart: true, // Automatically handles 5MB+ chunks
});
Issue #16:ファイル拡張子がないとアクセス拒否エラーが発生する

エラー:Error: Access denied, please provide a valid token for this resource 出典:GitHub Issue #664 発生理由:ファイル拡張子のないパス名を指定すると、詳細の不明なアクセス拒否エラーが発生します。 防止策:パス名には必ずファイル拡張子を含めます。

// ❌ Fails with confusing error
await upload('user-12345', file, {
  access: 'public',
  handleUploadUrl: '/api/upload',
}); // Error: Access denied

// ✅ Extract extension and include in pathname
const extension = file.name.split('.').pop();
await upload(`user-${userId}.${extension}`, file, {
  access: 'public',
  handleUploadUrl: '/api/upload',
});

よくあるパターン

アバターの置き換えアップロード:

'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); // Delete old

  const blob = await put(`avatars/${userId}.jpg`, file, { access: 'public' });
  await db.update(users).set({ avatarUrl: blob.url }).where(eq(users.id, userId));
  return blob.url;
}

保護されたアップロード(access: 'private'):

const blob = await put(`documents/${userId}/${file.name}`, file, { access: 'private' });

コメント