بناء تطبيق متكامل باستخدام PocketBase و Next.js في 2026

PocketBase هو خادم خلفي مفتوح المصدر مكتوب بلغة Go يأتي في ملف تنفيذي واحد. يوفر قاعدة بيانات SQLite ومصادقة مدمجة وتخزين ملفات واشتراكات في الوقت الفعلي ولوحة تحكم للإدارة — كل ذلك بدون إعدادات معقدة. عند دمجه مع Next.js، يشكّل حزمة تقنية حديثة وخفيفة ومثالية للمشاريع الشخصية والنماذج الأولية والتطبيقات متوسطة الحجم.
في هذا الدليل، ستبني تطبيقًا كاملاً لإدارة المهام مع مصادقة المستخدم وعمليات CRUD في الوقت الفعلي والنشر الجاهز للإنتاج.
المتطلبات الأساسية
قبل البدء، تأكد من توفر ما يلي:
- Node.js 18+ مثبّت على جهازك
- npm أو pnpm كمدير حزم
- معرفة أساسية بـ React و TypeScript
- محرر أكواد (يُنصح بـ VS Code)
- طرفية (bash أو zsh أو PowerShell)
ما ستبنيه
تطبيق لإدارة المهام يتضمن الميزات التالية:
- تسجيل الدخول وإنشاء الحساب
- إنشاء المهام وقراءتها وتحديثها وحذفها
- تحديثات في الوقت الفعلي عبر اشتراكات PocketBase
- واجهة متجاوبة مع Tailwind CSS
- جاهز للنشر في بيئة الإنتاج
الخطوة 1: تثبيت PocketBase
يتم توزيع PocketBase كملف تنفيذي واحد. قم بتنزيله من الموقع الرسمي.
# إنشاء مجلد للخادم الخلفي
mkdir pocketbase-backend && cd pocketbase-backend
# تنزيل PocketBase (Linux/macOS)
# زُر https://pocketbase.io/docs/ للحصول على أحدث إصدار
wget https://github.com/pocketbase/pocketbase/releases/download/v0.25.0/pocketbase_0.25.0_linux_amd64.zip
# أو على macOS باستخدام Homebrew
brew install pocketbase
# فك الضغط
unzip pocketbase_0.25.0_linux_amd64.zipشغّل PocketBase:
./pocketbase serveستظهر في الطرفية:
> Server started at: http://127.0.0.1:8090
> - REST API: http://127.0.0.1:8090/api/
> - Admin UI: http://127.0.0.1:8090/_/
افتح http://127.0.0.1:8090/_/ في المتصفح للوصول إلى لوحة الإدارة. عند أول دخول، أنشئ حساب مسؤول.
الخطوة 2: إعداد مجموعات PocketBase
في لوحة الإدارة، أنشئ مجموعة tasks بالحقول التالية:
| الحقل | النوع | الخيارات |
|---|---|---|
title | Text | مطلوب، 200 حرف كحد أقصى |
description | Text | اختياري |
completed | Bool | القيمة الافتراضية: false |
user | Relation | المجموعة: users، مطلوب |
إعداد قواعد الوصول
في تبويب API Rules لمجموعة tasks:
- List/Search:
@request.auth.id != "" && user = @request.auth.id - View:
@request.auth.id != "" && user = @request.auth.id - Create:
@request.auth.id != "" - Update:
@request.auth.id != "" && user = @request.auth.id - Delete:
@request.auth.id != "" && user = @request.auth.id
تضمن هذه القواعد أن كل مستخدم يمكنه فقط عرض وتعديل مهامه الخاصة.
الخطوة 3: إنشاء مشروع Next.js
افتح طرفية جديدة وأنشئ مشروع الواجهة الأمامية:
npx create-next-app@latest pocketbase-todo --typescript --tailwind --app --src-dir --use-npm
cd pocketbase-todoثبّت حزمة PocketBase SDK:
npm install pocketbaseالخطوة 4: إعداد عميل PocketBase
أنشئ ملف إعداد عميل PocketBase:
// src/lib/pocketbase.ts
import PocketBase from "pocketbase";
const pb = new PocketBase("http://127.0.0.1:8090");
// تعطيل الإلغاء التلقائي لتجنب التعارض مع React
pb.autoCancellation(false);
export default pb;أنشئ أنواع TypeScript لبياناتك:
// src/types/index.ts
export interface Task {
id: string;
title: string;
description: string;
completed: boolean;
user: string;
created: string;
updated: string;
}
export interface User {
id: string;
email: string;
name: string;
avatar: string;
}الخطوة 5: إنشاء سياق المصادقة
أنشئ مزوّد React لإدارة حالة المصادقة بشكل عام:
// src/contexts/AuthContext.tsx
"use client";
import {
createContext,
useContext,
useEffect,
useState,
useCallback,
type ReactNode,
} from "react";
import pb from "@/lib/pocketbase";
import type { User } from "@/types";
interface AuthContextType {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
register: (email: string, password: string, name: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// التحقق مما إذا كان المستخدم مسجل الدخول بالفعل
if (pb.authStore.isValid) {
const model = pb.authStore.model;
if (model) {
setUser({
id: model.id,
email: model.email,
name: model.name || "",
avatar: model.avatar || "",
});
}
}
setIsLoading(false);
// الاستماع لتغييرات المصادقة
const unsubscribe = pb.authStore.onChange((_token, model) => {
if (model) {
setUser({
id: model.id,
email: model.email,
name: model.name || "",
avatar: model.avatar || "",
});
} else {
setUser(null);
}
});
return () => unsubscribe();
}, []);
const login = useCallback(async (email: string, password: string) => {
await pb.collection("users").authWithPassword(email, password);
}, []);
const register = useCallback(
async (email: string, password: string, name: string) => {
await pb.collection("users").create({
email,
password,
passwordConfirm: password,
name,
});
// تسجيل الدخول تلقائيًا بعد إنشاء الحساب
await pb.collection("users").authWithPassword(email, password);
},
[]
);
const logout = useCallback(() => {
pb.authStore.clear();
setUser(null);
}, []);
return (
<AuthContext.Provider value={{ user, isLoading, login, register, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}الخطوة 6: إنشاء Hook لإدارة المهام
يغلّف هذا الـ Hook المخصص جميع عمليات CRUD والاشتراكات في الوقت الفعلي:
// src/hooks/useTasks.ts
"use client";
import { useState, useEffect, useCallback } from "react";
import pb from "@/lib/pocketbase";
import type { Task } from "@/types";
import { useAuth } from "@/contexts/AuthContext";
export function useTasks() {
const [tasks, setTasks] = useState<Task[]>([]);
const [isLoading, setIsLoading] = useState(true);
const { user } = useAuth();
// جلب المهام
const fetchTasks = useCallback(async () => {
if (!user) return;
try {
setIsLoading(true);
const records = await pb.collection("tasks").getFullList<Task>({
sort: "-created",
filter: `user = "${user.id}"`,
});
setTasks(records);
} catch (error) {
console.error("خطأ في تحميل المهام:", error);
} finally {
setIsLoading(false);
}
}, [user]);
// الاشتراك في الوقت الفعلي
useEffect(() => {
if (!user) return;
fetchTasks();
// الاشتراك في تغييرات المجموعة
pb.collection("tasks").subscribe<Task>("*", (event) => {
switch (event.action) {
case "create":
setTasks((prev) => [event.record, ...prev]);
break;
case "update":
setTasks((prev) =>
prev.map((task) =>
task.id === event.record.id ? event.record : task
)
);
break;
case "delete":
setTasks((prev) =>
prev.filter((task) => task.id !== event.record.id)
);
break;
}
});
return () => {
pb.collection("tasks").unsubscribe("*");
};
}, [user, fetchTasks]);
// إنشاء مهمة
const createTask = useCallback(
async (title: string, description: string = "") => {
if (!user) return;
await pb.collection("tasks").create({
title,
description,
completed: false,
user: user.id,
});
},
[user]
);
// تبديل حالة المهمة
const toggleTask = useCallback(async (task: Task) => {
await pb.collection("tasks").update(task.id, {
completed: !task.completed,
});
}, []);
// حذف مهمة
const deleteTask = useCallback(async (taskId: string) => {
await pb.collection("tasks").delete(taskId);
}, []);
// تحديث مهمة
const updateTask = useCallback(
async (taskId: string, data: Partial<Task>) => {
await pb.collection("tasks").update(taskId, data);
},
[]
);
return {
tasks,
isLoading,
createTask,
toggleTask,
deleteTask,
updateTask,
refetch: fetchTasks,
};
}الخطوة 7: بناء مكونات واجهة المستخدم
نموذج تسجيل الدخول والتسجيل
// src/components/AuthForm.tsx
"use client";
import { useState, type FormEvent } from "react";
import { useAuth } from "@/contexts/AuthContext";
export default function AuthForm() {
const [isLogin, setIsLogin] = useState(true);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const [error, setError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const { login, register } = useAuth();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError("");
setIsSubmitting(true);
try {
if (isLogin) {
await login(email, password);
} else {
await register(email, password, name);
}
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : "حدث خطأ ما";
setError(message);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="mx-auto max-w-md rounded-xl bg-white p-8 shadow-lg">
<h2 className="mb-6 text-center text-2xl font-bold text-gray-800">
{isLogin ? "تسجيل الدخول" : "إنشاء حساب"}
</h2>
{error && (
<div className="mb-4 rounded-lg bg-red-50 p-3 text-sm text-red-600">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
{!isLogin && (
<input
type="text"
placeholder="اسمك"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-lg border px-4 py-3 focus:border-blue-500 focus:outline-none"
required
/>
)}
<input
type="email"
placeholder="البريد الإلكتروني"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full rounded-lg border px-4 py-3 focus:border-blue-500 focus:outline-none"
required
/>
<input
type="password"
placeholder="كلمة المرور"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded-lg border px-4 py-3 focus:border-blue-500 focus:outline-none"
required
minLength={8}
/>
<button
type="submit"
disabled={isSubmitting}
className="w-full rounded-lg bg-blue-600 py-3 font-medium text-white transition hover:bg-blue-700 disabled:opacity-50"
>
{isSubmitting
? "جارٍ التحميل..."
: isLogin
? "تسجيل الدخول"
: "إنشاء حساب"}
</button>
</form>
<p className="mt-4 text-center text-sm text-gray-600">
{isLogin ? "ليس لديك حساب؟" : "لديك حساب بالفعل؟"}
<button
onClick={() => setIsLogin(!isLogin)}
className="mr-1 font-medium text-blue-600 hover:underline"
>
{isLogin ? "سجّل الآن" : "سجّل دخولك"}
</button>
</p>
</div>
);
}مكون قائمة المهام
// src/components/TaskList.tsx
"use client";
import { useState, type FormEvent } from "react";
import { useTasks } from "@/hooks/useTasks";
import { useAuth } from "@/contexts/AuthContext";
import type { Task } from "@/types";
function TaskItem({
task,
onToggle,
onDelete,
}: {
task: Task;
onToggle: (task: Task) => void;
onDelete: (id: string) => void;
}) {
return (
<div className="group flex items-center gap-3 rounded-lg border bg-white p-4 transition hover:shadow-md">
<button
onClick={() => onToggle(task)}
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2 transition ${
task.completed
? "border-green-500 bg-green-500 text-white"
: "border-gray-300 hover:border-blue-400"
}`}
>
{task.completed && (
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</button>
<div className="flex-1">
<h3
className={`font-medium ${
task.completed ? "text-gray-400 line-through" : "text-gray-800"
}`}
>
{task.title}
</h3>
{task.description && (
<p className="mt-1 text-sm text-gray-500">{task.description}</p>
)}
</div>
<button
onClick={() => onDelete(task.id)}
className="rounded-lg p-2 text-gray-400 opacity-0 transition hover:bg-red-50 hover:text-red-500 group-hover:opacity-100"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
);
}
export default function TaskList() {
const [newTitle, setNewTitle] = useState("");
const [newDescription, setNewDescription] = useState("");
const { tasks, isLoading, createTask, toggleTask, deleteTask } = useTasks();
const { user, logout } = useAuth();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!newTitle.trim()) return;
await createTask(newTitle.trim(), newDescription.trim());
setNewTitle("");
setNewDescription("");
};
const completedCount = tasks.filter((t) => t.completed).length;
return (
<div className="mx-auto max-w-2xl">
{/* الرأس */}
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-800">مهامي</h1>
<p className="mt-1 text-gray-500">
مرحبًا، {user?.name || user?.email}
</p>
</div>
<button
onClick={logout}
className="rounded-lg px-4 py-2 text-sm text-gray-600 transition hover:bg-gray-100"
>
تسجيل الخروج
</button>
</div>
{/* نموذج الإضافة */}
<form onSubmit={handleSubmit} className="mb-6 space-y-3">
<input
type="text"
placeholder="إضافة مهمة جديدة..."
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
className="w-full rounded-xl border-2 border-gray-200 px-4 py-3 text-lg focus:border-blue-500 focus:outline-none"
/>
<div className="flex gap-3">
<input
type="text"
placeholder="الوصف (اختياري)"
value={newDescription}
onChange={(e) => setNewDescription(e.target.value)}
className="flex-1 rounded-lg border px-4 py-2 focus:border-blue-500 focus:outline-none"
/>
<button
type="submit"
disabled={!newTitle.trim()}
className="rounded-lg bg-blue-600 px-6 py-2 font-medium text-white transition hover:bg-blue-700 disabled:opacity-50"
>
إضافة
</button>
</div>
</form>
{/* الإحصائيات */}
<div className="mb-4 flex gap-4 text-sm text-gray-500">
<span>{tasks.length} مهمة إجمالاً</span>
<span>{completedCount} مكتملة</span>
<span>{tasks.length - completedCount} قيد التنفيذ</span>
</div>
{/* قائمة المهام */}
{isLoading ? (
<div className="py-12 text-center text-gray-400">
جارٍ تحميل المهام...
</div>
) : tasks.length === 0 ? (
<div className="py-12 text-center text-gray-400">
<p className="text-lg">لا توجد مهام حتى الآن</p>
<p className="mt-2 text-sm">أنشئ مهمتك الأولى أعلاه</p>
</div>
) : (
<div className="space-y-2">
{tasks.map((task) => (
<TaskItem
key={task.id}
task={task}
onToggle={toggleTask}
onDelete={deleteTask}
/>
))}
</div>
)}
</div>
);
}الخطوة 8: تجميع الصفحة الرئيسية
ادمج مزوّد المصادقة في التخطيط:
// src/app/layout.tsx
import type { Metadata } from "next";
import { AuthProvider } from "@/contexts/AuthContext";
import "./globals.css";
export const metadata: Metadata = {
title: "تطبيق المهام - PocketBase + Next.js",
description: "تطبيق إدارة المهام باستخدام PocketBase و Next.js",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ar" dir="rtl">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}أنشئ الصفحة الرئيسية التي تعرض إما نموذج المصادقة أو قائمة المهام:
// src/app/page.tsx
"use client";
import AuthForm from "@/components/AuthForm";
import TaskList from "@/components/TaskList";
import { useAuth } from "@/contexts/AuthContext";
export default function Home() {
const { user, isLoading } = useAuth();
if (isLoading) {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent" />
</div>
);
}
return (
<main className="min-h-screen bg-gray-50 px-4 py-12">
{user ? <TaskList /> : <AuthForm />}
</main>
);
}الخطوة 9: متغيرات البيئة
أنشئ ملف .env.local لإعداد رابط PocketBase:
NEXT_PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090حدّث عميل PocketBase لاستخدام هذا المتغير:
// src/lib/pocketbase.ts
import PocketBase from "pocketbase";
const pb = new PocketBase(
process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090"
);
pb.autoCancellation(false);
export default pb;الخطوة 10: تشغيل التطبيق
افتح نافذتي طرفية:
# الطرفية 1: PocketBase
cd pocketbase-backend
./pocketbase serve
# الطرفية 2: Next.js
cd pocketbase-todo
npm run devافتح http://localhost:3000 في متصفحك. يجب أن ترى نموذج تسجيل الدخول. أنشئ حسابًا ثم ابدأ بإضافة المهام.
ميزات متقدمة
التصفية والبحث
أضف نظام تصفية إلى قائمة المهام:
// في useTasks.ts، أضف دالة البحث
const searchTasks = useCallback(
async (query: string) => {
if (!user) return;
const records = await pb.collection("tasks").getFullList<Task>({
sort: "-created",
filter: `user = "${user.id}" && title ~ "${query}"`,
});
setTasks(records);
},
[user]
);التصفّح بالصفحات
للتطبيقات التي تحتوي على بيانات كثيرة، استخدم التصفّح بالصفحات:
const fetchTasksPaginated = useCallback(
async (page: number = 1, perPage: number = 20) => {
if (!user) return;
const result = await pb.collection("tasks").getList<Task>(page, perPage, {
sort: "-created",
filter: `user = "${user.id}"`,
});
return {
items: result.items,
totalPages: result.totalPages,
totalItems: result.totalItems,
};
},
[user]
);رفع الملفات
يدير PocketBase الملفات بشكل أصلي. إليك كيفية إضافة مرفقات للمهام:
const createTaskWithFile = useCallback(
async (title: string, file: File) => {
if (!user) return;
const formData = new FormData();
formData.append("title", title);
formData.append("user", user.id);
formData.append("completed", "false");
formData.append("attachment", file);
await pb.collection("tasks").create(formData);
},
[user]
);النشر في بيئة الإنتاج
نشر PocketBase
يمكن نشر PocketBase على أي خادم Linux:
# على الخادم
mkdir -p /opt/pocketbase
cd /opt/pocketbase
# تنزيل واستخراج PocketBase
wget https://github.com/pocketbase/pocketbase/releases/download/v0.25.0/pocketbase_0.25.0_linux_amd64.zip
unzip pocketbase_0.25.0_linux_amd64.zip
# إنشاء خدمة systemd
sudo tee /etc/systemd/system/pocketbase.service > /dev/null << 'EOF'
[Unit]
Description=PocketBase
After=network.target
[Service]
Type=simple
User=root
ExecStart=/opt/pocketbase/pocketbase serve --http="0.0.0.0:8090"
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
# تفعيل وتشغيل الخدمة
sudo systemctl enable pocketbase
sudo systemctl start pocketbaseنشر Next.js
انشر الواجهة الأمامية على Vercel أو Netlify أو خادمك الخاص:
# تحديث رابط PocketBase للإنتاج
# .env.production
NEXT_PUBLIC_POCKETBASE_URL=https://api.your-domain.com
# بناء الإنتاج
npm run build
npm startإعداد Nginx (بروكسي عكسي)
server {
listen 80;
server_name api.your-domain.com;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# دعم WebSocket للوقت الفعلي
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}استكشاف الأخطاء وإصلاحها
أخطاء CORS
إذا واجهت أخطاء CORS، شغّل PocketBase مع خيار origins:
./pocketbase serve --origins="http://localhost:3000,https://your-domain.com"أخطاء اتصال WebSocket
تأكد من أن البروكسي العكسي مُعدّ لدعم WebSockets (راجع إعداد Nginx أعلاه مع ترويسات Upgrade و Connection).
البيانات لا تُحدَّث في الوقت الفعلي
تحقق من:
- أن عميل PocketBase مُعدّ بـ
autoCancellation(false) - أن الاشتراك (
subscribe) مُهيّأ بشكل صحيح - أن التنظيف (
unsubscribe) يتم في return الخاص بـuseEffect
الخطوات التالية
الآن بعد أن يعمل تطبيقك، إليك بعض الأفكار للمضي قدمًا:
- إضافة تصنيفات: أنشئ مجموعة "categories" واربطها بالمهام
- تنفيذ السحب والإفلات: استخدم
@dnd-kit/coreلإعادة ترتيب المهام - إضافة إشعارات: أرسل تذكيرات بالبريد الإلكتروني عبر hooks في PocketBase
- وضع بدون اتصال: استخدم service worker للسماح بالاستخدام بدون إنترنت
- اختبارات شاملة: أضف اختبارات Playwright للتحقق من تدفقات المستخدم
الخلاصة
لقد بنيت تطبيقًا متكاملاً باستخدام PocketBase و Next.js. يقدم PocketBase بديلاً خفيفًا وقويًا للخوادم الخلفية التقليدية، مع ميزات مثل المصادقة والتحديثات الفورية وتخزين الملفات — كل ذلك في ملف تنفيذي واحد.
مزيج PocketBase + Next.js مناسب بشكل خاص لـ:
- النماذج الأولية السريعة والـ MVP
- التطبيقات الشخصية والمشاريع الجانبية
- التطبيقات الصغيرة والمتوسطة في بيئة الإنتاج
- المطورين الذين يرغبون في التحكم الكامل في حزمتهم التقنية
الكود المصدري الكامل لهذا الدليل متاح كمرجع ويمكن تكييفه لمشاريعكم الخاصة.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء تطبيق كامل يعمل بالوقت الحقيقي باستخدام Convex و Next.js 15
تعلّم كيفية بناء تطبيق كامل يعمل بالوقت الحقيقي باستخدام Convex و Next.js 15. يغطي هذا الدليل تصميم المخططات والاستعلامات والتعديلات والاشتراكات الفورية والمصادقة ورفع الملفات — مع أمان أنواع شامل.

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

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