xiaohongshu
xiaohongshu
vercel-blob
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 trou...
Full skill instructions
Status: Production Ready
Last Updated: 2025-10-29
Dependencies: None
Latest Versions: @vercel/[email protected]
# In Vercel dashboard: Storage → Create Database → 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' // or 'private'
});
return blob.url; // https://xyz.public.blob.vercel-storage.com/file.jpg
}
CRITICAL:
BLOB_READ_WRITE_TOKEN)access level (public vs private)Vercel Dashboard:
BLOB_READ_WRITE_TOKENLocal Development:
vercel env pull .env.local
Creates .env.local:
BLOB_READ_WRITE_TOKEN="vercel_blob_rw_xxx"
Key Points:
Next.js Server Action:
'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 Route (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);
}
Create Upload Token (Server Action):
'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;
}
Client Upload:
'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>
);
}
List Files:
import { list } from '@vercel/blob';
const { blobs } = await list({
prefix: 'avatars/',
limit: 100
});
// Returns: { url, pathname, size, uploadedAt, ... }[]
List with Pagination:
let cursor: string | undefined;
const allBlobs = [];
do {
const { blobs, cursor: nextCursor } = await list({
prefix: 'uploads/',
cursor
});
allBlobs.push(...blobs);
cursor = nextCursor;
} while (cursor);
Download File:
// 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();
Delete File:
import { del } from '@vercel/blob';
await del('https://xyz.public.blob.vercel-storage.com/file.jpg');
// Or delete multiple
await del([url1, url2, url3]);
Streaming Upload:
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'
});
Multipart Upload (Large Files >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
});
✅ Use client upload tokens for client-side uploads - Never expose BLOB_READ_WRITE_TOKEN to client
✅ Set correct access level - public (CDN) or private (authenticated access)
✅ Validate file types and sizes - Before upload, check MIME type and size
✅ Use pathname organization - avatars/, uploads/, documents/ for structure
✅ Handle upload errors - Network failures, size limits, token expiration
✅ Clean up old files - Delete unused files to manage storage costs
✅ Set content-type explicitly - For correct browser handling (videos, PDFs)
❌ Never expose BLOB_READ_WRITE_TOKEN to client - Use handleUpload() for client uploads
❌ Never skip file validation - Always validate type, size, content before upload
❌ Never upload files >500MB without multipart - Use multipart upload for large files
❌ Never use generic filenames - file.jpg collides, use ${timestamp}-${name} or UUID
❌ Never assume uploads succeed - Always handle errors (network, quota, etc.)
❌ Never store sensitive data unencrypted - Encrypt before upload if needed
❌ Never forget to delete temporary files - Old uploads consume quota
This skill prevents 10 documented issues:
Error: Error: BLOB_READ_WRITE_TOKEN is not defined
Source: https://vercel.com/docs/storage/vercel-blob
Why It Happens: Token not set in environment
Prevention: Run vercel env pull .env.local and ensure .env.local in .gitignore.
Error: Security vulnerability, unauthorized uploads
Source: https://vercel.com/docs/storage/vercel-blob/client-upload
Why It Happens: Using BLOB_READ_WRITE_TOKEN directly in client code
Prevention: Use handleUpload() to generate client-specific tokens with constraints.
Error: Error: File size exceeds limit (500MB)
Source: https://vercel.com/docs/storage/vercel-blob/limits
Why It Happens: Uploading file >500MB without multipart upload
Prevention: Validate file size before upload, use multipart upload for large files.
Error: Browser downloads file instead of displaying (e.g., PDF opens as text)
Source: Production debugging
Why It Happens: Not setting contentType option, Blob guesses incorrectly
Prevention: Always set contentType: file.type or explicit MIME type.
Error: Slow file delivery, high egress costs
Source: Vercel Blob best practices
Why It Happens: Using access: 'private' for files that should be public
Prevention: Use access: 'public' for publicly accessible files (CDN caching).
Error: Only first 1000 files returned, missing files
Source: https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#list
Why It Happens: Not iterating with cursor for large file lists
Prevention: Use cursor-based pagination in loop until cursor is undefined.
Error: Files not deleted, storage quota fills up
Source: https://github.com/vercel/storage/issues/150
Why It Happens: Using wrong URL format, blob not found
Prevention: Use full blob URL from put() response, check deletion result.
Error: Error: Request timeout for files >100MB
Source: Vercel function timeout limits
Why It Happens: Serverless function timeout (10s free tier, 60s pro)
Prevention: Use client-side upload with handleUpload() for large files.
Error: Files overwritten, data loss
Source: Production debugging
Why It Happens: Using same filename for multiple uploads
Prevention: Add timestamp/UUID: `uploads/${Date.now()}-${file.name}` or addRandomSuffix: true.
Error: Upload completes but app state not updated
Source: https://vercel.com/docs/storage/vercel-blob/client-upload#callback-after-upload
Why It Happens: Not implementing onUploadCompleted callback
Prevention: Use onUploadCompleted in handleUpload() to update database/state.
{
"dependencies": {
"@vercel/blob": "^2.0.0"
}
}
BLOB_READ_WRITE_TOKEN="vercel_blob_rw_xxxxx"
'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;
}
'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;
}
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 };
}
Required:
@vercel/blob@^2.0.0 - Vercel Blob SDKOptional:
sharp@^0.33.0 - Image processing before uploadzod@^3.24.0 - File validation schemas{
"dependencies": {
"@vercel/blob": "^2.0.0"
}
}
BLOB_READ_WRITE_TOKEN is not definedSolution: Run vercel env pull .env.local, ensure .env.local in .gitignore.
Solution: Use multipart upload with createMultipartUpload() API.
Solution: Ensure using handleUpload() server-side, don't expose read/write token to client.
Solution: Use exact URL from put() response, check del() return value.
BLOB_READ_WRITE_TOKEN environment variable set@vercel/blob package installedhandleUpload() (not direct token)public vs private)Questions? Issues?
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience