۱۴۰۵ مهر ۶, دوشنبه
دلار آمریکا۲۴۳٬۴۱۵▲ ۳٫۵۸٪یورو۲۷۷٬۰۰۰▲ ۳٫۳۹٪درهم امارات۶۶٬۳۰۵▲ ۳٫۶۱٪سکه امامی۲۴۴٬۵۰۵٬۰۰۰▲ ۱٫۶۶٪طلای ۱۸ عیار (گرم)۲۴٬۳۱۹٬۰۰۰▲ ۱٫۷۵٪انس طلا۴٬۱۵۰ $▼ ۳٫۲۶٪تتر۲۴۴٬۴۵۶▲ ۳٫۲۴٪بیت‌کوین۸۲٬۶۵۳ $▼ ۲٫۴۱٪اتریوم۲٬۶۴۰ $▼ ۱٫۸۹٪سولانا۱۱۷٫۸۴ $▼ ۴٫۳۴٪اپل۳۴۱٫۰۷ $▲ ۱٫۵۳٪انویدیا۲۲۵٫۰۷ $▲ ۰٫۲۲٪مایکروسافت۵۱۶٫۱۷ $▲ ۳٫۶۶٪آلفابت (گوگل)۳۴۳٫۹۲ $▲ ۰٫۴۶٪تسلا۳۷۲٫۱۱ $▼ ۱٫۵۴٪شاخص نزدک۲۷٬۰۶۹ $▲ ۰٫۴۸٪
نرخ ارز

آموزش Next.js App Router از صفر تا ساخت یک وبلاگ کامل

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

توسعه وب۵ دقیقه مطالعه
آموزش Next.js App Router از صفر تا ساخت یک وبلاگ کامل
فهرست مطالب
  1. ساخت پروژه و شناخت ساختار App Router
  2. لایه داده: یک منبع ساده برای پست‌ها
  3. Layout ریشه و متادیتای سئو
  4. Server Components: خواندن داده بدون useEffect
  5. آموزش Next.js برای مسیرهای پویا با [slug]
  6. Client Components: وقتی تعامل لازم است
  7. Layoutهای تودرتو
  8. Server Actions: فرم ارسال پست بدون API جداگانه
  9. Route Handler: ساخت API در کنار صفحه‌ها
  10. مدیریت خطا با error.tsx
  11. build و انتشار
  12. جمع‌بندی

آموزش 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 جداگانه پردازش کنید. قدم بعدی، اتصال به یک پایگاه داده واقعی و افزودن احراز هویت است.

پرسش‌های پرتکرار

App Router بهتر است یا Pages Router؟

برای پروژه‌های جدید، App Router مسیر توصیه‌شده Next.js است و قابلیت‌هایی مثل Server Components، Layoutهای تودرتو و Server Actions فقط در آن وجود دارند. Pages Router همچنان پشتیبانی می‌شود و برای پروژه‌های قدیمی مشکلی ندارد.

چرا params در Next.js جدید Promise است؟

از Next.js 15 به بعد، params و searchParams به‌صورت ناهمگام ارائه می‌شوند تا فریم‌ورک بتواند بخش‌های مستقل صفحه را زودتر رندر کند. در Next.js 16 دسترسی هم‌زمان به آن‌ها حذف شده و باید با await خوانده شوند.

کی باید از use client استفاده کنم؟

فقط وقتی کامپوننت به state، رویدادهای کاربر مثل onClick، هوک‌های React یا APIهای مرورگر نیاز دارد. بقیه کامپوننت‌ها را Server Component نگه دارید تا جاوااسکریپت کمتری به مرورگر ارسال شود.

برای دنبال کردن این آموزش چه پیش‌نیازی لازم است؟

آشنایی با React و جاوااسکریپت مدرن و نصب بودن Node.js نسخه LTS. آشنایی پایه با TypeScript کمک می‌کند، اما ضروری نیست.

خطایی در این مطلب دیدید؟ به تحریریه گزارش دهید؛ اصلاحیه‌ها طبق اصول تحریریه ثبت می‌شوند.

مطالب مرتبط