UploadThing + Next.js App Router: بناء نظام رفع ملفات متكامل مع السحب والإفلات

رفع الملفات أصبح سهلاً مع Next.js. UploadThing هي خدمة رفع ملفات حديثة مصممة خصيصاً لتطبيقات TypeScript. في هذا الدليل، ستبني نظام رفع متكامل مع السحب والإفلات ومعاينة الصور وأشرطة التقدم والتحقق من جانب الخادم — كل ذلك بأمان أنواع كامل من العميل إلى الخادم.
ما ستتعلمه
بنهاية هذا الدليل، ستكون قادراً على:
- إعداد UploadThing في مشروع Next.js 15 App Router
- إنشاء موجّهات ملفات آمنة الأنواع مع التحقق من جانب الخادم
- بناء منطقة سحب وإفلات مع تغذية بصرية مباشرة
- عرض تقدم الرفع الفوري مع مؤشرات النسبة المئوية
- تنفيذ معاينة الصور قبل وبعد الرفع
- التعامل مع حدود حجم الملف وقيود النوع والتحقق المخصص
- بناء معرض ملفات لعرض وإدارة الملفات المرفوعة
- حذف الملفات برمجياً عبر واجهة UploadThing البرمجية
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت (
node --version) - خبرة في Next.js (App Router، Server Components، Server Actions)
- أساسيات TypeScript (الأنواع، الأنواع العامة)
- حساب UploadThing — سجّل مجاناً على uploadthing.com
- محرر أكواد — يُنصح بـ VS Code أو Cursor
لماذا UploadThing؟
التعامل مع رفع الملفات في تطبيقات الويب كان دائماً أمراً مؤلماً. إما أن تجمع عناوين S3 الموقّعة مسبقاً مع وسيط مخصص، أو تعتمد على مكتبات ثقيلة تتعارض مع إطار عملك. UploadThing تتخذ نهجاً مختلفاً:
| الميزة | UploadThing | S3 يدوي | Multer + Express |
|---|---|---|---|
| أمان الأنواع | TypeScript كامل | أنواع يدوية | لا يوجد |
| تكامل الإطار | Next.js أصلي | إعداد مخصص | Express فقط |
| عناوين URL موقّعة | تلقائي | إعداد IAM يدوي | غير متاح |
| التحقق من الملفات | تصريحي | وسيط مخصص | وسيط مخصص |
| مكونات React | مدمجة | ابنِها بنفسك | ابنِها بنفسك |
| تتبع التقدم | مدمج | WebSocket مخصص | مخصص |
الطبقة المجانية تشمل 2 جيجابايت من التخزين و2 جيجابايت من عرض النطاق الشهري — أكثر من كافٍ للتطوير والمشاريع الصغيرة.
الخطوة 1: إنشاء مشروع Next.js
ابدأ بمشروع Next.js 15 جديد:
npx create-next-app@latest upload-demo --typescript --tailwind --app --src-dir
cd upload-demoاختر الخيارات الافتراضية عند السؤال. يمنحك هذا مشروعاً مع TypeScript وTailwind CSS وApp Router.
الخطوة 2: تثبيت UploadThing
ثبّت الحزمة الأساسية وتكامل React:
npm install uploadthing @uploadthing/reactبعد ذلك، أنشئ ملف .env.local مع بيانات اعتماد UploadThing الخاصة بك. يمكنك إيجادها في لوحة تحكم UploadThing بعد إنشاء تطبيق جديد:
UPLOADTHING_TOKEN=your_token_hereيتم التقاط UPLOADTHING_TOKEN تلقائياً من المكتبة — لا حاجة لإعدادات إضافية.
الخطوة 3: تعريف موجّه الملفات
موجّه الملفات هو جوهر UploadThing. يحدد أنواع الملفات التي يقبلها تطبيقك وحجمها الأقصى وما يحدث بعد الرفع.
أنشئ src/app/api/uploadthing/core.ts:
import { createUploadthing, type FileRouter } from "uploadthing/next";
import { UploadThingError } from "uploadthing/server";
const f = createUploadthing();
export const ourFileRouter = {
imageUploader: f({
image: {
maxFileSize: "4MB",
maxFileCount: 4,
},
})
.middleware(async ({ req }) => {
// تشغيل أي منطق من جانب الخادم قبل الرفع
// مثلاً، التحقق من المصادقة
const user = { id: "user_123" }; // استبدل بالمصادقة الحقيقية
if (!user) throw new UploadThingError("Unauthorized");
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("اكتمل الرفع للمستخدم:", metadata.userId);
console.log("رابط الملف:", file.ufsUrl);
// إرجاع البيانات للعميل
return { uploadedBy: metadata.userId, url: file.ufsUrl };
}),
documentUploader: f({
pdf: { maxFileSize: "16MB", maxFileCount: 1 },
"application/msword": { maxFileSize: "16MB", maxFileCount: 1 },
})
.middleware(async ({ req }) => {
return { uploadedAt: new Date().toISOString() };
})
.onUploadComplete(async ({ metadata, file }) => {
return { url: file.ufsUrl, uploadedAt: metadata.uploadedAt };
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;المفاهيم الأساسية:
createUploadthing()يُرجع دالة بناءfلتعريف المسارات- إعداد نوع الملف يحدد أنواع MIME المسموحة والحجم الأقصى والعدد الأقصى
middleware()يعمل على الخادم قبل بدء الرفع — مثالي للمصادقةonUploadComplete()يُنفّذ بعد تخزين الملف — احفظ البيانات الوصفية في قاعدة بياناتك هناsatisfies FileRouterيضمن أمان الأنواع عبر السلسلة بأكملها
الخطوة 4: إنشاء مسار API
أنشئ معالج المسار في Next.js في src/app/api/uploadthing/route.ts:
import { createRouteHandler } from "uploadthing/next";
import { ourFileRouter } from "./core";
export const { GET, POST } = createRouteHandler({
router: ourFileRouter,
});هذا الملف من سطرين ينشئ معالجات GET وPOST التي يحتاجها UploadThing للتفاوض على عمليات الرفع مع العميل.
الخطوة 5: توليد مساعدات React
أنشئ ملف أدوات مساعدة يولّد خطافات ومكونات React آمنة الأنواع من موجّه الملفات.
أنشئ src/utils/uploadthing.ts:
import {
generateUploadButton,
generateUploadDropzone,
generateReactHelpers,
} from "@uploadthing/react";
import type { OurFileRouter } from "@/app/api/uploadthing/core";
export const UploadButton = generateUploadButton<OurFileRouter>();
export const UploadDropzone = generateUploadDropzone<OurFileRouter>();
export const { useUploadThing } = generateReactHelpers<OurFileRouter>();هذه المكونات المولّدة آمنة الأنواع بالكامل — سيُكمل محرر الأكواد أسماء نقاط النهاية تلقائياً ويعرف بالضبط البيانات الوصفية التي يُرجعها كل مسار.
الخطوة 6: إضافة أنماط UploadThing
استورد أنماط UploadThing الافتراضية في src/app/layout.tsx:
import "@uploadthing/react/styles.css";أضف هذا السطر بجانب استيرادات CSS الموجودة. توفر الأنماط مظهراً افتراضياً أنيقاً لزر الرفع ومنطقة الإفلات.
الخطوة 7: بناء زر الرفع الأساسي
لنبدأ بأبسط تكامل — زر رفع مُنسّق.
أنشئ src/components/BasicUpload.tsx:
"use client";
import { UploadButton } from "@/utils/uploadthing";
import { useState } from "react";
interface UploadedFile {
url: string;
name: string;
}
export default function BasicUpload() {
const [files, setFiles] = useState<UploadedFile[]>([]);
return (
<div className="flex flex-col items-center gap-4">
<h2 className="text-xl font-bold">ارفع صورة</h2>
<UploadButton
endpoint="imageUploader"
onClientUploadComplete={(res) => {
if (res) {
const uploaded = res.map((file) => ({
url: file.ufsUrl,
name: file.name,
}));
setFiles((prev) => [...prev, ...uploaded]);
}
}}
onUploadError={(error: Error) => {
alert(`فشل الرفع: ${error.message}`);
}}
/>
{files.length > 0 && (
<div className="grid grid-cols-2 gap-4 mt-4">
{files.map((file, i) => (
<div key={i} className="relative">
<img
src={file.url}
alt={file.name}
className="w-48 h-48 object-cover rounded-lg"
/>
<p className="text-sm text-center mt-1 truncate w-48">
{file.name}
</p>
</div>
))}
</div>
)}
</div>
);
}خاصية endpoint آمنة الأنواع بالكامل — جرّب كتابة اسم نقطة نهاية خاطئ وسيكتشف TypeScript الخطأ فوراً.
الخطوة 8: بناء منطقة السحب والإفلات
توفر منطقة الإفلات مساحة أكبر مع دعم السحب والإفلات.
أنشئ src/components/DropzoneUpload.tsx:
"use client";
import { UploadDropzone } from "@/utils/uploadthing";
import { useState } from "react";
interface UploadedFile {
url: string;
name: string;
size: number;
}
export default function DropzoneUpload() {
const [files, setFiles] = useState<UploadedFile[]>([]);
return (
<div className="w-full max-w-xl mx-auto">
<h2 className="text-xl font-bold mb-4">أفلِت صورك هنا</h2>
<UploadDropzone
endpoint="imageUploader"
onClientUploadComplete={(res) => {
if (res) {
const uploaded = res.map((file) => ({
url: file.ufsUrl,
name: file.name,
size: file.size,
}));
setFiles((prev) => [...prev, ...uploaded]);
}
}}
onUploadError={(error: Error) => {
alert(`فشل الرفع: ${error.message}`);
}}
config={{ mode: "auto" }}
/>
{files.length > 0 && (
<div className="mt-6 space-y-2">
<h3 className="font-semibold">الملفات المرفوعة:</h3>
{files.map((file, i) => (
<div
key={i}
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg"
>
<img
src={file.url}
alt={file.name}
className="w-12 h-12 object-cover rounded"
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{file.name}</p>
<p className="text-xs text-gray-500">
{(file.size / 1024).toFixed(1)} كيلوبايت
</p>
</div>
</div>
))}
</div>
)}
</div>
);
}ضبط config mode على "auto" يعني أن الملفات تُرفع فوراً عند إفلاتها — لا حاجة لنقرة إضافية. احذف هذه الخاصية لطلب نقرة يدوية على زر "رفع" بعد الإفلات.
الخطوة 9: بناء رفع مخصص مع تتبع التقدم
للتحكم الكامل في الواجهة، استخدم خطاف useUploadThing مباشرة.
أنشئ src/components/CustomUpload.tsx:
"use client";
import { useUploadThing } from "@/utils/uploadthing";
import { useCallback, useState } from "react";
import { useDropzone } from "react-dropzone";
interface FileWithPreview extends File {
preview: string;
}
export default function CustomUpload() {
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [uploadProgress, setUploadProgress] = useState<number>(0);
const [uploadedUrls, setUploadedUrls] = useState<string[]>([]);
const [isUploading, setIsUploading] = useState(false);
const { startUpload } = useUploadThing("imageUploader", {
onUploadProgress: (progress) => {
setUploadProgress(progress);
},
onClientUploadComplete: (res) => {
if (res) {
setUploadedUrls(res.map((file) => file.ufsUrl));
}
setIsUploading(false);
setUploadProgress(0);
setFiles([]);
},
onUploadError: (error) => {
alert(`خطأ: ${error.message}`);
setIsUploading(false);
setUploadProgress(0);
},
});
const onDrop = useCallback((acceptedFiles: File[]) => {
const withPreviews = acceptedFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
);
setFiles(withPreviews);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: { "image/*": [".png", ".jpg", ".jpeg", ".webp"] },
maxFiles: 4,
maxSize: 4 * 1024 * 1024,
});
const handleUpload = async () => {
if (files.length === 0) return;
setIsUploading(true);
await startUpload(files);
};
return (
<div className="w-full max-w-xl mx-auto space-y-4">
<h2 className="text-xl font-bold">رفع مخصص مع معاينة</h2>
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-colors ${
isDragActive
? "border-blue-500 bg-blue-50"
: "border-gray-300 hover:border-gray-400"
}`}
>
<input {...getInputProps()} />
{isDragActive ? (
<p className="text-blue-600">أفلِت الملفات هنا...</p>
) : (
<div>
<p className="text-gray-600">
اسحب وأفلت الصور هنا، أو انقر للاختيار
</p>
<p className="text-sm text-gray-400 mt-1">
PNG، JPG، WebP — حتى 4 ميغابايت لكل ملف، بحد أقصى 4 ملفات
</p>
</div>
)}
</div>
{files.length > 0 && (
<div className="grid grid-cols-4 gap-2">
{files.map((file, i) => (
<div key={i} className="relative aspect-square">
<img
src={file.preview}
alt={file.name}
className="w-full h-full object-cover rounded-lg"
/>
</div>
))}
</div>
)}
{isUploading && (
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className="bg-blue-600 h-3 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
/>
<p className="text-sm text-center mt-1">{uploadProgress}%</p>
</div>
)}
<button
onClick={handleUpload}
disabled={files.length === 0 || isUploading}
className="w-full py-2 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isUploading ? "جارٍ الرفع..." : `رفع ${files.length} ملف(ات)`}
</button>
{uploadedUrls.length > 0 && (
<div className="p-4 bg-green-50 rounded-lg">
<p className="font-semibold text-green-800">اكتمل الرفع!</p>
{uploadedUrls.map((url, i) => (
<a
key={i}
href={url}
target="_blank"
rel="noopener noreferrer"
className="block text-sm text-green-600 hover:underline truncate"
>
{url}
</a>
))}
</div>
)}
</div>
);
}ستحتاج لتثبيت react-dropzone لهذا المكون:
npm install react-dropzoneهذا التنفيذ المخصص يمنحك:
- معاينة الصور قبل الرفع باستخدام
URL.createObjectURL - شريط تقدم يمتلئ مع تقدم الرفع
- سحب وإفلات مع تغذية بصرية (تغيير لون الحدود)
- قيود نوع وحجم الملف مفروضة على العميل
- تحكم كامل في كل عنصر من عناصر الواجهة
الخطوة 10: التحقق من الملفات من جانب الخادم
دالة middleware في موجّه الملفات هي المكان الذي يحدث فيه التحقق الجدي. إليك نسخة محسّنة مع فحوصات متعددة:
import { createUploadthing, type FileRouter } from "uploadthing/next";
import { UploadThingError } from "uploadthing/server";
const f = createUploadthing();
async function authenticateUser(req: Request) {
// استبدل بمنطق المصادقة الفعلي
const token = req.headers.get("authorization");
if (!token) return null;
return { id: "user_123", plan: "pro" as const };
}
const PLAN_LIMITS = {
free: { maxSize: "2MB" as const, maxCount: 2 },
pro: { maxSize: "8MB" as const, maxCount: 10 },
};
export const ourFileRouter = {
imageUploader: f({
image: {
maxFileSize: "8MB",
maxFileCount: 10,
},
})
.middleware(async ({ req, files }) => {
const user = await authenticateUser(req);
if (!user) throw new UploadThingError("Unauthorized");
const limits = PLAN_LIMITS[user.plan];
// التحقق من عدد الملفات حسب الخطة
if (files.length > limits.maxCount) {
throw new UploadThingError(
`خطتك تسمح بحد أقصى ${limits.maxCount} ملفات لكل عملية رفع`
);
}
return { userId: user.id, plan: user.plan };
})
.onUploadComplete(async ({ metadata, file }) => {
// الحفظ في قاعدة البيانات
// await db.insert(uploads).values({
// userId: metadata.userId,
// url: file.ufsUrl,
// name: file.name,
// size: file.size,
// });
return { url: file.ufsUrl };
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;يعمل middleware بالكامل على الخادم — لا يمكن للمستخدمين تجاوزه عن طريق تعديل كود العميل. هذا هو المكان الذي تفرض فيه المصادقة وحدود الخطط وتحديد المعدل وأي قواعد عمل.
الخطوة 11: بناء معرض الملفات مع الحذف
نظام الرفع المتكامل يحتاج لإدارة الملفات. إليك مكون معرض يعرض الملفات المرفوعة ويسمح بالحذف.
أولاً، أنشئ Server Action للحذف في src/app/actions.ts:
"use server";
import { UTApi } from "uploadthing/server";
const utapi = new UTApi();
export async function deleteFile(fileKey: string) {
try {
await utapi.deleteFiles(fileKey);
return { success: true };
} catch (error) {
return { success: false, error: "فشل حذف الملف" };
}
}الآن أنشئ src/components/FileGallery.tsx:
"use client";
import { useState } from "react";
import { deleteFile } from "@/app/actions";
interface GalleryFile {
key: string;
url: string;
name: string;
size: number;
}
export default function FileGallery({
initialFiles,
}: {
initialFiles: GalleryFile[];
}) {
const [files, setFiles] = useState<GalleryFile[]>(initialFiles);
const [deleting, setDeleting] = useState<string | null>(null);
const handleDelete = async (fileKey: string) => {
setDeleting(fileKey);
const result = await deleteFile(fileKey);
if (result.success) {
setFiles((prev) => prev.filter((f) => f.key !== fileKey));
} else {
alert("فشل حذف الملف");
}
setDeleting(null);
};
if (files.length === 0) {
return (
<div className="text-center py-12 text-gray-500">
<p>لم تُرفع أي ملفات بعد.</p>
</div>
);
}
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{files.map((file) => (
<div
key={file.key}
className="group relative bg-white rounded-xl shadow-sm overflow-hidden"
>
<div className="aspect-square">
<img
src={file.url}
alt={file.name}
className="w-full h-full object-cover"
/>
</div>
<div className="p-2">
<p className="text-sm font-medium truncate">{file.name}</p>
<p className="text-xs text-gray-400">
{(file.size / 1024).toFixed(1)} كيلوبايت
</p>
</div>
<button
onClick={() => handleDelete(file.key)}
disabled={deleting === file.key}
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center text-sm transition-opacity hover:bg-red-600 disabled:opacity-50"
>
{deleting === file.key ? "..." : "X"}
</button>
</div>
))}
</div>
);
}فئة UTApi توفر طرقاً من جانب الخادم لإدارة الملفات: الإدراج والحذف وإعادة التسمية والحصول على روابط الملفات. استخدمها في Server Actions أو مسارات API — ولا تكشفها أبداً للعميل.
الخطوة 12: ربط كل شيء معاً
حدّث صفحتك الرئيسية لعرض جميع مكونات الرفع.
استبدل src/app/page.tsx:
import BasicUpload from "@/components/BasicUpload";
import DropzoneUpload from "@/components/DropzoneUpload";
import CustomUpload from "@/components/CustomUpload";
export default function Home() {
return (
<main className="min-h-screen bg-gray-50 py-12 px-4">
<div className="max-w-4xl mx-auto space-y-16">
<div className="text-center">
<h1 className="text-3xl font-bold">عرض UploadThing</h1>
<p className="text-gray-600 mt-2">
ثلاث طرق للتعامل مع رفع الملفات في Next.js
</p>
</div>
<section className="bg-white rounded-2xl p-8 shadow-sm">
<BasicUpload />
</section>
<section className="bg-white rounded-2xl p-8 shadow-sm">
<DropzoneUpload />
</section>
<section className="bg-white rounded-2xl p-8 shadow-sm">
<CustomUpload />
</section>
</div>
</main>
);
}شغّل خادم التطوير:
npm run devزُر http://localhost:3000 واختبر كل طريقة رفع. جرّب سحب الصور والنقر لاختيار الملفات ومشاهدة شريط التقدم يمتلئ.
اختبار التنفيذ
تأكد من أن هذه السيناريوهات تعمل بشكل صحيح:
- رفع ملف واحد — انقر زر الرفع واختر صورة واحدة
- رفع ملفات متعددة — اختر حتى 4 صور في وقت واحد
- السحب والإفلات — اسحب صوراً من مدير الملفات إلى منطقة الإفلات
- رفض حجم الملف — جرّب رفع ملف أكبر من 4 ميغابايت
- نوع ملف خاطئ — جرّب رفع ملف
.txtإلى رافع الصور - تتبع التقدم — ارفع صورة أكبر وراقب شريط التقدم
- المعاينة قبل الرفع — أفلت ملفات في المكون المخصص وتحقق من الصور المصغرة
- الحذف — مرّر فوق صورة في المعرض وانقر زر الحذف
استكشاف الأخطاء وإصلاحها
"UPLOADTHING_TOKEN is not set"
تأكد من وجود ملف .env.local في جذر المشروع ويحتوي على التوكن. أعد تشغيل خادم التطوير بعد إضافة متغيرات البيئة.
فشل الرفع بصمت
افحص تبويب Network في المتصفح لطلبات /api/uploadthing. المشاكل الشائعة:
- ملف مسار API في الموقع الخطأ (يجب أن يكون
app/api/uploadthing/route.ts) - التوكن منتهي الصلاحية أو ينتمي لتطبيق مختلف
- مشاكل CORS عند الاختبار من نطاق مختلف
الملفات تُرفع لكن الروابط تُرجع 404
روابط UploadThing تتبع التنسيق https://ufs.sh/f/.... إذا رأيت روابط بنمط utfs.io القديم، حدّث حزمتك لأحدث إصدار.
أخطاء TypeScript في أسماء نقاط النهاية
شغّل npm run dev مرة واحدة على الأقل بعد تغيير موجّه الملفات. تُولّد الأنواع من تعريف الموجّه — يحتاج محرر الأكواد لتشغيل خادم التطوير لالتقاطها.
النشر للإنتاج
عند النشر للإنتاج، ضع هذه النقاط في الاعتبار:
- متغيرات البيئة — عيّن
UPLOADTHING_TOKENفي مزود الاستضافة (Vercel، Railway، إلخ) - رابط الاستدعاء — يحتاج UploadThing للوصول إلى خادمك لـ
onUploadComplete. على Vercel يعمل تلقائياً. للنشر المخصص، هيّئ رابط الاستدعاء في لوحة تحكم UploadThing - تنظيف الملفات — نفّذ مهمة مجدولة أو خلفية لحذف الملفات اليتيمة (الملفات المرفوعة لكن لم تُحفظ في قاعدة بياناتك)
- تحديد المعدل — أضف تحديد المعدل في middleware لمنع الإساءة
- إشراف المحتوى — للمحتوى المُنشأ من المستخدمين، فكّر في دمج واجهات إشراف الصور في
onUploadComplete
الخطوات التالية
الآن بعد أن لديك نظام رفع يعمل، فكّر في هذه التحسينات:
- الحفظ في قاعدة البيانات — احفظ بيانات الملف الوصفية في قاعدة بياناتك في
onUploadCompleteباستخدام Prisma أو Drizzle - المصادقة — ادمج مع NextAuth.js أو Clerk للتحقق من المستخدمين في middleware
- تحسين الصور — استخدم مكون
Imageمن Next.js مع روابط الملفات المرفوعة للتحسين التلقائي - روابط موقّعة مسبقاً — للملفات الخاصة، ولّد روابط وصول محدودة الوقت مع
UTApi - Webhooks — أعدّ webhooks UploadThing للمعالجة غير المتزامنة (فحص الفيروسات، توليد الصور المصغرة)
الخلاصة
بنيت نظام رفع ملفات متكاملاً باستخدام UploadThing وNext.js App Router. يغطي التنفيذ ثلاث طرق — من UploadButton بدون إعداد إلى واجهة سحب وإفلات مخصصة بالكامل مع تتبع التقدم ومعاينة الصور.
يزيل UploadThing تعقيدات بنية رفع الملفات: لا حاويات S3 لتهيئتها، لا سياسات IAM لتصحيحها، لا منطق روابط موقّعة مسبقاً لكتابته. تحدد الملفات التي تقبلها ومن يمكنه رفعها وما يحدث بعد ذلك — الباقي يُعالج نيابة عنك.
أمان الأنواع من العميل إلى الخادم يعني أن محرر الأكواد يكتشف الأخطاء في وقت التطوير وليس في الإنتاج. عند تغيير اسم مسار ملف أو إضافة نقطة نهاية جديدة أو تعديل شكل البيانات الوصفية، يرشدك TypeScript خلال كل ملف يحتاج تحديثاً.
لمزيد من القراءة، اطّلع على وثائق UploadThing واستكشف UTApi لعمليات الملفات المتقدمة من جانب الخادم مثل إدراج الملفات والحصول على إحصائيات الاستخدام وتوليد روابط موقّعة للمحتوى الخاص.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

إضافة المصادقة لتطبيق Next.js 15 باستخدام Auth.js v5: البريد الإلكتروني وOAuth والتحكم بالأدوار
تعلم كيفية إضافة نظام مصادقة جاهز للإنتاج لتطبيق Next.js 15 باستخدام Auth.js v5. يغطي هذا الدليل الشامل تسجيل الدخول عبر Google OAuth وبيانات الاعتماد بالبريد الإلكتروني وكلمة المرور والمسارات المحمية والتحكم بالوصول حسب الأدوار.

بناء روبوت دردشة ذكاء اصطناعي محلي باستخدام Ollama و Next.js: الدليل الشامل
ابنِ روبوت دردشة ذكاء اصطناعي خاص يعمل بالكامل على جهازك المحلي باستخدام Ollama و Next.js. يغطي هذا الدليل العملي التثبيت والبث المباشر واختيار النماذج وبناء واجهة دردشة جاهزة للإنتاج — كل ذلك دون إرسال بياناتك إلى السحابة.

بناء موقع محتوى متكامل باستخدام Payload CMS 3 و Next.js
تعلّم كيفية بناء موقع محتوى كامل الميزات باستخدام Payload CMS 3 الذي يعمل مباشرة داخل Next.js App Router. يغطي هذا الدرس المجموعات، محرر النصوص الغنية، رفع الوسائط، المصادقة، والنشر في بيئة الإنتاج.