قسمت بیست و چهارم: آپلود تصویر در Next.js با Cloudinary
راه اندازی پیش نیازهای آپلود عکس در سرویس Cloudinary
#آپلود تصویر در Next.js با Cloudinary
تا اینجای مسیر، ما بخش مهمی از پنل ادمین را ساختهایم:
- احراز هویت ادمین
- محافظت از مسیرها و APIها
- مدیریت پروژهها
- تمرین ساخت ماژول مدیریت پستها
اما هنوز یک بخش مهم در سیستم مدیریت محتواباقی مانده است: آپلود تصویر.
در یک portfolio یا CMS واقعی، تقریباً همیشه به این قابلیت نیاز داریم:
- تصویر کاور پروژه
- گالری چند تصویر برای پروژهها
- تصویر کاور مقاله
- و در بعضی موارد حتی آپلود فایلهای رسانهای دیگر
در این قسمت میخواهیم زیرساخت آپلود تصویر را با استفاده از Cloudinary به پروژه اضافه کنیم.
چرا به سرویس بیرونی نیاز داریم؟
ممکن است این سؤال پیش بیاید که چرا تصاویر را مستقیم داخل خود پروژه Next.js نگه نداریم.
در پروژههای ساده، شاید وسوسه شوید که فایلها را داخل پوشهای مثل public/ ذخیره کنید.
اما در اپلیکیشنهای واقعی، این روش خیلی زود محدودیتهای خودش را نشان میدهد.
چند دلیل مهم برای استفاده از یک سرویس بیرونی مثل Cloudinary:
- فایلهای تصویری نباید به ساختار deploy شما گره بخورند
- در هاستهای مدرن، فایلسیستم معمولاً برای ذخیره دائمی فایل مناسب نیست
- مدیریت resize، optimization و delivery تصاویر با سرویس تخصصی بهتر انجام میشود
- CDN و بهینهسازی تحویل تصویر را رایگان یا سادهتر در اختیار میگیرید
- اگر بعداً پروژه بزرگتر شود، معماری شما از اول درست چیده شده است
بهصورت خلاصه:
- دیتابیس برای دادههای ساختیافته است
- Cloudinary برای فایلهای رسانهای است
- Next.js برای UI و منطق اپلیکیشن است
این تفکیک، معماری شما را تمیزتر و حرفهایتر میکند.
Cloudinary چیست؟
Cloudinary یک سرویس مدیریت فایلهای رسانهای است که برای آپلود، نگهداری، بهینهسازی و نمایش تصویر و ویدیو استفاده میشود.
برای پروژه ما، فعلاً مهمترین قابلیتهای آن اینها هستند:
- آپلود تصویر
- دریافت URL نهایی فایل
- نگهداری امن و پایدار فایل
- استفاده از CDN
- امکان مدیریت چند تصویر
ما در این آموزش از Cloudinary بهعنوان محل ذخیره تصاویر استفاده میکنیم و فقط URL یا اطلاعات لازم را داخل دیتابیس ذخیره خواهیم کرد.
راهاندازی Cloudinary
اول باید در Cloudinary یک حساب بسازید.
بعد از ساخت حساب، چند مقدار مهم در اختیار شما قرار میگیرد:
cloud_nameapi_keyapi_secret
این مقادیر را باید در فایل .env پروژه قرار دهید.
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
نکته مهم
مقدار api_secret باید فقط در سمت سرور استفاده شود و هرگز در Client Componentها یا کدهای عمومی قرار نگیرد.
نصب پکیجهای موردنیاز
برای کار با Cloudinary، معمولاً به پکیج رسمی آن نیاز داریم:
npm install cloudinary
اگر در بخشی از پیادهسازی از آپلود مستقیم با فرم استفاده کنید، ممکن است به ابزارهای دیگری هم نیاز داشته باشید، اما برای مسیر پایه این آموزش، همین پکیج کافی است.
ساخت helper برای Cloudinary
بهتر است منطق تنظیم Cloudinary را در یک فایل جدا قرار دهید تا در چند جای پروژه قابل استفاده باشد.
مثلاً فایلی مثل:
lib/cloudinary.ts
در این فایل، cloudinary را با envها تنظیم میکنید تا در route handlerهای آپلود از آن استفاده شود.
ایده کلی این است که:
- یک instance تنظیمشده داشته باشید
- envها در یک نقطه متمرکز باشند
- بقیه بخشهای برنامه فقط از همان helper استفاده کنند
ساخت Route Handler برای آپلود تصویر
بهترین محل برای آپلود فایل در معماری این پروژه، یک Route Handler سمت سرور است.
مثلاً مسیری مثل:
/api/uploads/image
این endpoint باید:
- فقط برای ادمین قابل دسترسی باشد
- فایل را از درخواست دریافت کند
- فایل را به Cloudinary بفرستد
- نتیجه آپلود را برگرداند
چرا Route Handler؟
چون:
- کنترل امنیتی بهتری دارید
- میتوانید session ادمین را بررسی کنید
api_secretفقط در سرور باقی میماند- ساختار پروژه تمیزتر میماند
ساختار کلی flow آپلود
جریان آپلود تصویر معمولاً به این شکل است:
- کاربر در پنل ادمین یک فایل انتخاب میکند
- فرم یا کامپوننت Client فایل را به endpoint داخلی شما میفرستد
- Route Handler فایل را دریافت میکند
- فایل به Cloudinary آپلود میشود
- Cloudinary یک
secure_urlبرمیگرداند - شما آن URL را در فرم یا دیتابیس استفاده میکنید
نکته مهم این است که خود فایل را داخل دیتابیس ذخیره نمیکنیم.
ما فقط اطلاعاتی مثل اینها را ذخیره میکنیم:
url- در صورت نیاز
publicId - و شاید
widthوheight
آپلود تصویر پروژه
اولین استفاده عملی ما از Cloudinary، تصویر پروژه است.
در ماژول پروژهها معمولاً حداقل به یک تصویر نیاز داریم، مثلاً:
- تصویر کاور پروژه
- یا یک thumbnail برای نمایش در لیست
اگر مدل پروژه شما فیلدی شبیه این داشته باشد:
imageUrl String?
آنوقت میتوانید URL فایل آپلودشده را داخل همین فیلد ذخیره کنید.
روند کار در فرم پروژه
در ProjectForm باید یک فیلد برای انتخاب فایل اضافه کنید.
وقتی کاربر فایل را انتخاب کرد:
-
فایل به endpoint آپلود ارسال میشود
-
پاسخ Cloudinary دریافت میشود پروژه در
ProjectFormباید یک فیلد برای انتخاب فایل اضافه کنید.
وقتی کاربر فایل را انتخاب کرد: -
فایل به endpoint آپلود ارسال میشود
-
پاسخ Cloudinary دریافت میشود
-
secure_urlدر state فرم قرار میگیرد -
هنگام submit نهایی فرم، همین URL همراه بقیه دادهها جدا میشود
-
API پروژه سادهتر میماند
-
مدیریت خطا هم روشنتر میشود
فیلد تصویر در schema پروژه
اگر قرار است پروژهها تصویر داشته باشند، schema اعتبارسنجی پروژه را هم باید بهروز کنید.
مثلاً اگر قبلاً projectSchema فقط فیلدهای متنی داشت، حالا میتوانید فیلدی مثل این هم به آن اضافه کنید:
imageUrl: z.string().url().optional().or(z.literal(''))
اگر چند تصویر دارید، طبیعتاً ساختار فرق میکند و جلوتر به آن میرسیم.
آپلود تصویر مقاله
در ماژول پستها یا مقالهها هم معمولاً یک تصویر کاور لازم داریم.
اینجا منطق تقریباً همان چیزی است که برای پروژهها داشتیم.
مثلاً اگر مدل Post شما چنین فیلدی داشته باشد:
coverImage String?
آنوقت در PostForm هم باید:
- یک ورودی فایل اضافه کنید
- فایل را به endpoint آپلود بفرستید
secure_urlرا بگیرید- و آن را در
coverImageذخیره کنید
اینجا دوباره همان الگو را میبینید:
- آپلود فایل در endpoint مخصوص آپلود
- ذخیره داده نهایی در endpoint مربوط به موجودیت
این الگو باعث میشود ماژولها از هم جدا و قابل نگهداری باقی بمانند.
تجربه کاربری بهتر در فرم آپلود
وقتی آپلود تصویر را به فرم اضافه میکنید، بهتر است فقط به «کار کردن» فکر نکنید.
تجربه کاربری هم مهم است.
حداقل این موارد را در نظر بگیرید:
- نمایش وضعیت
Uploading... - غیرفعالکردن دکمه آپلود هنگام ارسال
- نمایش پیشنمایش تصویر بعد از موفقیت
- امکان حذف یا جایگزینی تصویر
- نمایش پیام خطا در صورت شکست آپلود
مثلاً اگر فایل خیلی بزرگ باشد یا فرمت نامعتبر باشد، کاربر باید پیام مناسبی ببیند.
مدیریت چند تصویر
تا اینجا درباره یک تصویر برای هر پروژه یا پست صحبت کردیم.
اما در بسیاری از portfolioها، پروژهها به چند تصویر نیاز دارند:
- اسکرینشات صفحه اصلی
- تصویر نسخه موبایل
- تصویر پنل مدیریت
- چند نمای مختلف از پروژه
در این حالت، بهجای یک imageUrl باید ساختاری برای چند تصویر داشته باشید.
چند راه برای ذخیره چند تصویر
برای مدیریت چند تصویر، چند رویکرد رایج وجود دارد.
روش اول: آرایهای از URLها
اگر ساختار پروژه شما ساده است، میتوانید آرایهای از URLها نگه دارید.
مثلاً از نظر مفهومی چیزی شبیه این:
ts images: string[]
یا در Prisma، بسته به نوع دیتابیس و ساختار پروژه، از فیلد مناسب برای آرایه یا JSON استفاده کنید.
روش دوم: مدل جداگانه برای تصاویر پروژه
در پروژههای تمیزتر و مقیاسپذیرتر، بهتر است یک مدل جدا برای تصاویر داشته باشید.
مثلاً از نظر مفهومی:
model ProjectImage {
id String @id @default(cuid())
url String
publicId String?
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
}
این روش مزیتهای زیادی دارد:
- هر تصویر رکورد مستقل دارد
- حذف و مرتبسازی راحتتر میشود
- بعداً میتوانید فیلدهای بیشتری مثل
alt,order,captionاضافه کنید
برای پروژه portfolio واقعی، این مدل معمولاً انتخاب بهتری است.
آپلود چند تصویر در فرم پروژه
اگر پروژه شما چند تصویر دارد، فرم پروژه باید بتواند چند فایل را بپذیرد.
در این حالت معمولاً:
- ورودی فایل با
multipleساخته میشود - فایلها یکییکی یا بهصورت گروهی آپلود میشوند
- خروجی نهایی یک آرایه از تصاویر است
- کاربر باید بتواند تصاویر آپلودشده را ببیند و در صورت نیاز حذف کند
نکته مهم
حتی در حالت چند تصویر هم بهتر است منطق API پروژه ساده بماند.
یعنی:
- اول فایلها را آپلود کنید
- URLها را بگیرید
- بعد در submit اصلی فرم، فقط داده نهایی را به API پروژه بفرستید
ذخیره URL در دیتابیس
یکی از مهمترین بخشهای این معماری این است که بهجای ذخیره فایل، فقط URL را در دیتابیس نگه داریم.
مثلاً برای یک پروژه:
imageUrl String?
یا برای یک پست:
coverImage String?
یا برای مدل جداگانه تصویر:
url String
publicId String?
چرا ذخیره URL انتخاب درستی است؟
چون:
- دیتابیس سبکتر میماند
- فایلها خارج از دیتابیس مدیریت میشوند
- تغییر CDN یا سرویس فایل سادهتر میشود
- بازیابی دادهها آسانتر است
اگر publicId را هم ذخیره کنید، بعداً حذف فایل از Cloudinary هم برایتان راحتتر میشود.
آیا فقط URL کافی است؟
در سادهترین حالت، بله.
اما در پروژههای واقعی، معمولاً بهتر است این اطلاعات را هم در نظر بگیرید:
urlpublicIdwidthheightformatalt
این موضوع مخصوصاً وقتی مهم میشود که:
- بخواهید فایل را بعداً حذف کنید
- بخواهید ابعاد را در UI استفاده کنید
- بخواهید برای سئو و دسترسپذیری،
altمناسب ذخیره کنید
پس اگر الان با url شروع میکنید، انتخاب درستی است؛
اما بهتر است از همین حالا بدانید که در آینده احتمالاً به دادههای بیشتری نیاز خواهید داشت.
نکات امنیتی مهم
در پیادهسازی آپلود فایل، چند نکته امنیتی را حتماً رعایت کنید:
- endpoint آپلود باید فقط برای ادمین در دسترس باشد
api_secretنباید به کلاینت نشت کند- نوع فایل را بررسی کنید
- محدودیت حجم فایل داشته باشید
- به هر فایلی اجازه آپلود ندهید
- نتیجه آپلود را قبل از ذخیره نهایی اعتبارسنجی کنید
اینکه کاربر وارد پنل ادمین شده به این معنا نیست که هر دادهای را بدون کنترل بپذیریم.
ساختار پیشنهادی فایلها
برای اضافهکردن این قابلیت، به فایلهایی شبیه این نیاز دارید:
lib/
cloudinary.ts
app/
api/
uploads/
image/
route.ts
و بعد در فرمهای مدیریتی خودتان، مثل:
components/admin/project-form.tsx
components/admin/post-form.tsx
منطق استفاده از این endpoint را اضافه میکنید.
اگر بعداً بخواهید uploader را reusable کنید، حتی میتوانید یک کامپوننت مشترک هم بسازید، مثلاً:
components/admin/image-upload.tsx
این کامپوننت میتواند هم در فرم پروژه و هم در فرم مقاله استفاده شود.
این بخش چه چیزی را برای ما آماده میکند؟
با اضافهشدن Cloudinary، پنل ادمین شما یک قدم خیلی مهم به CMS واقعی نزدیکتر میشود.
بعد از این بخش، شما میتوانید:
- برای پروژهها تصویر واقعی آپلود کنید
- برای مقالهها کاور قرار دهید
- دادههای رسانهای را حرفهایتر مدیریت کنید
- و برای گسترش ماژولهای دیگر هم همین الگو را تکرار کنید
این یعنی از یک داشبورد صرفاً متنی، به سمت یک سیستم مدیریت محتوای واقعی حرکت کردهاید.
جمعبندی
در این قسمت دیدیم که برای آپلود تصویر در Next.js بهتر است از یک سرویس بیرونی مثل Cloudinary استفاده کنیم.
همچنین فهمیدیم که:
- فایلها نباید داخل دیتابیس ذخیره شوند
- بهتر است فقط URL یا اطلاعات لازم در دیتابیس نگه داشته شود
- آپلود فایل باید از طریق یک Route Handler امن انجام شود
- پروژهها میتوانند یک تصویر یا چند تصویر داشته باشند
- پستها هم میتوانند تصویر کاور مستقل داشته باشند
- با ذخیره
publicId، مدیریت فایلها در آینده سادهتر میشود