آموزش Next.js App Router از صفر تا ساخت یک وبلاگ کامل
آموزش Next.js با App Router از صفر: ساخت یک وبلاگ واقعی با Server Components، مسیرهای پویا، Server Actions، متادیتای سئو و Route Handler، قدمبهقدم.

فهرست مطالب
- ساخت پروژه و شناخت ساختار App Router
- لایه داده: یک منبع ساده برای پستها
- Layout ریشه و متادیتای سئو
- Server Components: خواندن داده بدون useEffect
- آموزش Next.js برای مسیرهای پویا با [slug]
- Client Components: وقتی تعامل لازم است
- Layoutهای تودرتو
- Server Actions: فرم ارسال پست بدون API جداگانه
- Route Handler: ساخت API در کنار صفحهها
- مدیریت خطا با error.tsx
- build و انتشار
- جمعبندی
آموزش Next.js با App Router امروز یکی از پرطرفدارترین مسیرها برای ورود به توسعه وب مدرن است. Next.js فریمورکی مبتنی بر React است که مسیریابی، رندر سمت سرور، بهینهسازی تصاویر و خیلی چیزهای دیگر را آماده در اختیارتان میگذارد. در این آموزش قدمبهقدم یک وبلاگ کوچک اما واقعی با Next.js 16 و App Router میسازیم.
پیشنیاز: آشنایی با React و نصب بودن Node.js نسخه LTS روی سیستم.
ساخت پروژه و شناخت ساختار App Router
با دستور زیر یک پروژه تازه بسازید و در پرسشها TypeScript و App Router را انتخاب کنید:
npx create-next-app@latest devna-blog
cd devna-blog
npm run dev
حالا آدرس http://localhost:3000 را باز کنید. قلب پروژه پوشه app است. در App Router ساختار پوشهها همان ساختار آدرسهاست و چند نام فایل معنای ویژه دارند:
| فایل | نقش |
|---|---|
page.tsx |
محتوای قابل مشاهده یک مسیر |
layout.tsx |
قالب مشترک که بین صفحهها حفظ میشود |
loading.tsx |
رابط کاربری بارگذاری با Suspense |
error.tsx |
مرز خطا برای همان بخش |
not-found.tsx |
صفحه ۴۰۴ سفارشی |
route.ts |
Route Handler برای ساخت API |
ساختاری که تا پایان این آموزش میسازیم این شکلی است:
app/
├── layout.tsx
├── page.tsx
├── api/posts/route.ts
└── blog/
├── page.tsx
├── loading.tsx
├── new/page.tsx
└── [slug]/
├── page.tsx
└── like-button.tsx
lib/
└── posts.ts
لایه داده: یک منبع ساده برای پستها
برای تمرکز روی Next.js، دادهها را فعلاً در حافظه نگه میداریم. فایل lib/posts.ts را بسازید:
export type Post = {
slug: string;
title: string;
body: string;
};
const posts: Post[] = [
{ slug: "hello-nextjs", title: "Hello Next.js", body: "Our first post with the App Router." },
{ slug: "server-components", title: "Why Server Components?", body: "Less JavaScript, faster pages." },
];
export async function getPosts(): Promise<Post[]> {
return posts;
}
export async function getPost(slug: string): Promise<Post | undefined> {
return posts.find((post) => post.slug === slug);
}
export async function addPost(post: Post): Promise<void> {
posts.push(post);
}
توابع را async تعریف کردهایم تا بعدها بدون تغییر بقیه کد، بهجای آرایه از پایگاه داده بخوانیم.
Layout ریشه و متادیتای سئو
فایل app/layout.tsx قالب همه صفحههاست. چون سایت فارسی است، lang و dir را تنظیم میکنیم و متادیتای پیشفرض را هم همینجا تعریف میکنیم:
import type { Metadata } from "next";
import Link from "next/link";
import "./globals.css";
export const metadata: Metadata = {
title: { default: "DevNA Blog", template: "%s | DevNA Blog" },
description: "A small blog built with the Next.js App Router",
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="fa" dir="rtl">
<body>
<nav>
<Link href="/">Home</Link> | <Link href="/blog">Blog</Link> |{" "}
<Link href="/blog/new">New post</Link>
</nav>
<main>{children}</main>
</body>
</html>
);
}
الگوی template باعث میشود عنوان هر صفحه بهطور خودکار با نام سایت ترکیب شود. کامپوننت Link هم ناوبری سمت کلاینت و پیشبارگذاری صفحهها را انجام میدهد.
Server Components: خواندن داده بدون useEffect
در App Router همه کامپوننتها بهطور پیشفرض کامپوننت سرور (Server Component) هستند. یعنی روی سرور اجرا میشوند، میتوانند async باشند و مستقیم داده بخوانند؛ بیآنکه کدشان به مرورگر ارسال شود. صفحه فهرست پستها را در app/blog/page.tsx بسازید:
import Link from "next/link";
import { getPosts } from "@/lib/posts";
export const metadata = { title: "Blog" };
export default async function BlogPage() {
const posts = await getPosts();
return (
<section>
<h1>All posts</h1>
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
</section>
);
}
نه useEffect لازم است، نه state برای وضعیت بارگذاری. برای تجربه بهتر هنگام کندی داده، فایل app/blog/loading.tsx را اضافه کنید تا Next.js آن را خودکار داخل Suspense نمایش دهد:
export default function Loading() {
return <p>Loading posts...</p>;
}
خواندن داده از API خارجی با کش
fetch در Server Componentها قابلیت کش و بازاعتبارسنجی دارد. مثلاً این کامپوننت تعداد ستارههای مخزن Next.js را حداکثر هر یک ساعت یک بار از GitHub میگیرد:
export default async function RepoStars() {
const res = await fetch("https://api.github.com/repos/vercel/next.js", {
next: { revalidate: 3600 },
});
if (!res.ok) throw new Error("Failed to fetch repo data");
const repo: { stargazers_count: number } = await res.json();
return <p>Next.js stars: {repo.stargazers_count}</p>;
}
آموزش Next.js برای مسیرهای پویا با [slug]
برای صفحه هر پست، پوشهای با نام [slug] میسازیم. نکته مهم: از Next.js 15 مقدار params یک Promise است و در Next.js 16 حتماً باید با await خوانده شود. فایل app/blog/[slug]/page.tsx:
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getPost, getPosts } from "@/lib/posts";
import LikeButton from "./like-button";
type Props = { params: Promise<{ slug: string }> };
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post?.title ?? "Post not found",
description: post?.body.slice(0, 150),
};
}
export default async function PostPage({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<LikeButton />
</article>
);
}
generateStaticParams صفحه پستهای موجود را در زمان build بهصورت ایستا میسازد و generateMetadata برای هر پست عنوان و توضیح اختصاصی تولید میکند؛ دو ابزار کلیدی برای سئو. تابع notFound() هم کاربر را به صفحه ۴۰۴ میبرد.
Client Components: وقتی تعامل لازم است
هر جا به state، رویداد کلیک یا APIهای مرورگر نیاز دارید، کامپوننت را با دستور "use client" به کامپوننت کلاینت تبدیل کنید. فایل app/blog/[slug]/like-button.tsx:
"use client";
import { useState } from "react";
export default function LikeButton() {
const [likes, setLikes] = useState(0);
return (
<button onClick={() => setLikes((n) => n + 1)}>
Like ({likes})
</button>
);
}
قاعده طلایی: "use client" را تا جای ممکن پایین درخت کامپوننتها بگذارید. هرچه جاوااسکریپت کمتری به مرورگر برود، صفحه سریعتر تعاملی میشود و شاخصهایی مثل INP بهتر میشوند. درباره این شاخصها در راهنمای Core Web Vitals مفصل نوشتهایم.
Server Component یا Client Component؟
این جدول کمک میکند سریع تصمیم بگیرید:
| نیاز | نوع کامپوننت |
|---|---|
| خواندن داده از پایگاه داده یا API | Server |
| استفاده از کلید API یا راز سمت سرور | Server |
| نمایش محتوای ایستا و سئوپذیر | Server |
useState، useEffect و هوکهای دیگر |
Client |
رویدادهایی مثل onClick و onChange |
Client |
APIهای مرورگر مثل localStorage |
Client |
نکته مهم دیگر این است که میتوانید یک Server Component را بهعنوان children به Client Component بدهید. به این ترتیب بخش تعاملی کوچک میماند و محتوای سنگین همچنان روی سرور رندر میشود.
Layoutهای تودرتو
هر پوشه میتواند layout.tsx مخصوص خودش را داشته باشد. مثلاً با ساختن app/blog/layout.tsx میتوانید یک نوار کناری فقط برای بخش وبلاگ اضافه کنید:
export default function BlogLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<div className="blog-layout">
<aside>Categories, popular posts, ...</aside>
<div>{children}</div>
</div>
);
}
Layoutها هنگام جابهجایی بین صفحههای همان بخش دوباره رندر نمیشوند و state خود را حفظ میکنند؛ یکی از مزیتهای اصلی App Router نسبت به Pages Router.
Server Actions: فرم ارسال پست بدون API جداگانه
Server Actions توابعی هستند که روی سرور اجرا میشوند اما میتوانید مستقیم به فرم وصلشان کنید. فایل app/blog/new/page.tsx:
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { addPost } from "@/lib/posts";
async function createPost(formData: FormData) {
"use server";
const title = String(formData.get("title") ?? "").trim();
const body = String(formData.get("body") ?? "").trim();
if (!title || !body) return;
const slug = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "") || String(Date.now());
await addPost({ slug, title, body });
revalidatePath("/blog");
redirect(`/blog/${slug}`);
}
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="body" placeholder="Write something..." required />
<button type="submit">Publish</button>
</form>
);
}
revalidatePath کش صفحه فهرست را باطل میکند تا پست جدید دیده شود و redirect کاربر را به صفحه پست میبرد. این فرم حتی پیش از بارگذاری جاوااسکریپت هم کار میکند.
هشدار: ذخیره داده در حافظه فقط برای یادگیری است. با هر ریاستارت سرور پاک میشود و بین چند نمونه سرور مشترک نیست. در پروژه واقعی از پایگاه داده استفاده کنید و ورودیها را اعتبارسنجی و کاربر را احراز هویت کنید؛ Server Actionها endpoint عمومی هستند.
Route Handler: ساخت API در کنار صفحهها
گاهی به یک API واقعی نیاز دارید؛ مثلاً برای اپلیکیشن موبایل. فایل app/api/posts/route.ts:
import { getPosts } from "@/lib/posts";
export async function GET() {
const posts = await getPosts();
return Response.json(posts);
}
حالا آدرس /api/posts فهرست پستها را بهصورت JSON برمیگرداند.
مدیریت خطا با error.tsx
اگر در یک بخش خطایی رخ دهد، error.tsx جلوی خراب شدن کل صفحه را میگیرد. این فایل باید کامپوننت کلاینت باشد:
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
build و انتشار
پیش از انتشار، نسخه تولیدی را بسازید و اجرا کنید:
npm run build
npm run start
خروجی build نشان میدهد کدام مسیرها ایستا و کدام پویا رندر شدهاند. برای استقرار روی سرور شخصی، ساخت ایمیج کانتینر گزینه مطمئنی است؛ آموزش Docker برای توسعهدهندگان را ببینید. و فراموش نکنید Next.js را همیشه بهروز نگه دارید؛ بهروزرسانی امنیتی اخیر Next.js نشان داد این کار چقدر اهمیت دارد.
جمعبندی
در این آموزش Next.js یاد گرفتید چطور با App Router مسیر بسازید، در Server Components داده بخوانید، مسیر پویا و متادیتای سئو تعریف کنید، با Client Components تعامل اضافه کنید و با Server Actions فرم را بدون API جداگانه پردازش کنید. قدم بعدی، اتصال به یک پایگاه داده واقعی و افزودن احراز هویت است.



