MATIN MOLKARA
SERIES: آموزش Next js — PART 6 OF 28

قسمت ششم - ساخت هدر، منو و فوتر در Next.js

ما قبلاً یک نسخه ساده از Header و Footer را ساخته بودیم. در این پست همان ساختار قبلی را ارتقا می‌دهیم

ساخت هدر، منو و فوتر حرفه‌ای در Next.js

در پست‌های قبلی، هم با App Router در Next.js 16 آشنا شدیم، هم Tailwind CSS را به پروژه اضافه کردیم، و هم صفحه اصلی پرتفولیو را با sectionهای جداگانه ساختیم.
حالا وقت آن است که بخش‌های مشترک سایت را حرفه‌ای‌تر کنیم: Header، Navigation Menu و Footer.

نکته مهم اینجاست که ما قبلاً یک نسخه ساده از Header و Footer را ساخته بودیم. در این پست قرار نیست از صفر و بدون زمینه شروع کنیم؛ بلکه همان ساختار قبلی را ارتقا می‌دهیم تا:

  • ناوبری سایت منظم‌تر شود
  • لینک‌های داخلی با Link به‌شکل درست استفاده شوند
  • لینک فعال مشخص باشد
  • در موبایل منوی جداگانه داشته باشیم
  • فوتر کامل‌تر و حرفه‌ای‌تر شود
  • لینک‌های اجتماعی هم به انتهای سایت اضافه شوند

ساختار ناوبری سایت

قبل از نوشتن کد، بهتر است مشخص کنیم هدر ما قرار است چه لینک‌هایی داشته باشد.
با توجه به ساختار پروژه‌ای که تا اینجا ساخته‌ایم، ناوبری عمومی سایت می‌تواند شامل این مسیرها باشد:

  • /
  • /about
  • /projects
  • /blog
  • /contact

و در کنار آن، یک لینک جدا برای داشبورد هم می‌توانیم داشته باشیم:

  • /dashboard

از آنجا که در پست مربوط به Routing گفتیم مسیرها بر اساس ساختار فایل‌های داخل app/ ساخته می‌شوند، این منو باید با همان routeهایی هماهنگ باشد که قبلاً تعریف کرده‌ایم.

برای تمیزتر شدن کد، بهتر است آیتم‌های منو را داخل یک آرایه قرار بدهیم.

// components/layout/site-nav-links.ts
export const siteNavLinks = [
  { href: '/', label: 'Home' },
  { href: '/about', label: 'About' },
  { href: '/projects', label: 'Projects' },
  { href: '/blog', label: 'Blog' },
  { href: '/contact', label: 'Contact' },
]

این کار چند مزیت دارد:

  • لینک‌ها در یک نقطه متمرکز می‌شوند
  • برای نسخه دسکتاپ و موبایل از یک داده مشترک استفاده می‌کنیم
  • نگهداری و توسعه منو راحت‌تر می‌شود

استفاده از Link

در آموزش Routing قبلی گفتیم که برای navigation داخلی در Next.js باید از Link استفاده کنیم، نه از تگ معمولی a.

چرا؟

  • جابه‌جایی بین صفحه‌ها سریع‌تر می‌شود
  • reload کامل صفحه اتفاق نمی‌افتد
  • layoutهای مشترک حفظ می‌شوند
  • Next.js می‌تواند routeها را prefetch کند

پس در Header و Footer، هرجا لینک داخلی داریم، از Link استفاده می‌کنیم.

بیایید اول یک نسخه تمیزتر از Header بسازیم.


ساخت Header اصلی

از آنجا که برای active state و منوی موبایل به hookهای کلاینت نیاز داریم، Header ما یک Client Component خواهد بود.

// components/layout/header.tsx
'use client'

import Link from 'next/link'
import { Menu, X } from 'lucide-react'
import { useState } from 'react'
import { Container } from './container'
import { siteNavLinks } from './site-nav-links'
import { NavLink } from './nav-link'

export function Header() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <header className="sticky top-0 z-50 border-b border-slate-800 bg-slate-950/80 backdrop-blur">
      <Container>
        <div className="flex h-16 items-center justify-between">
          <Link
            href="/"
            className="text-lg font-semibold tracking-tight text-slate-50"
          >
            Matin
          </Link>

          <nav className="hidden items-center gap-6 md:flex">
            {siteNavLinks.map((link) => (
              <NavLink key={link.href} href={link.href}>
                {link.label}
              </NavLink>
            ))}

            <Link
              href="/dashboard"
              className="inline-flex items-center justify-center rounded-md bg-cyan-500 px-3 py-2 text-xs font-semibold text-slate-950 transition hover:bg-cyan-400"
            >
              Dashboard
            </Link>
          </nav>

          <button
            type="button"
            onClick={() => setIsOpen((prev) => !prev)}
            className="inline-flex items-center justify-center rounded-md border border-slate-700 p-2 text-slate-200 transition hover:bg-slate-900 md:hidden"
            aria-label={isOpen ? 'Close menu' : 'Open menu'}
            aria-expanded={isOpen}
          >
            {isOpen ? <X className="size-5" /> : <Menu className="size-5" />}
          </button>
        </div>

        {isOpen ? (
          <div className="border-t border-slate-800 py-4 md:hidden">
            <nav className="flex flex-col gap-2">
              {siteNavLinks.map((link) => (
                <NavLink
                  key={link.href}
                  href={link.href}
                  onNavigate={() => setIsOpen(false)}
                  mobile
                >
                  {link.label}
                </NavLink>
              ))}

              <Link
                href="/dashboard"
                onClick={() => setIsOpen(false)}
                className="mt-2 inline-flex items-center justify-center rounded-md bg-cyan-500 px-4 py-2 text-sm font-semibold text-slate-950 transition hover:bg-cyan-400"
              >
                Dashboard
              </Link>
            </nav>
          </div>
        ) : null}
      </Container>
    </header>
  )
}

در این نسخه:

  • هدر با sticky top-0 به بالای صفحه می‌چسبد
  • در دسکتاپ، منو به‌صورت افقی نمایش داده می‌شود
  • در موبایل، دکمه باز/بسته کردن منو داریم
  • از همان palette آبی تیره‌ای استفاده کرده‌ایم که در دو پست قبلی ساختیم

active state

یکی از مهم‌ترین بخش‌های navigation حرفه‌ای این است که کاربر بداند الان در کدام صفحه قرار دارد.
برای این کار باید active state داشته باشیم.

در App Router، برای گرفتن مسیر فعلی، از usePathname استفاده می‌کنیم:

import { usePathname } from 'next/navigation'

حالا بهتر است یک کامپوننت جدا برای لینک‌های ناوبری بسازیم.

// components/layout/nav-link.tsx
'use client'

import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { cn } from '@/lib/cn'

type NavLinkProps = {
  href: string
  children: React.ReactNode
  mobile?: boolean
  onNavigate?: () => void
}

export function NavLink({
  href,
  children,
  mobile = false,
  onNavigate,
}: NavLinkProps) {
  const pathname = usePathname()

  const isActive =
    href === '/' ? pathname === '/' : pathname === href || pathname.startsWith(`${href}/`)

  return (
    <Link
      href={href}
      onClick={onNavigate}
      className={cn(
        'transition',
        mobile
          ? 'rounded-md px-3 py-2 text-sm'
          : 'text-sm font-medium',
        isActive
          ? 'text-cyan-400'
          : 'text-slate-300 hover:text-cyan-300'
      )}
      aria-current={isActive ? 'page' : undefined}
    >
      {children}
    </Link>
  )
}

منطق active state

این بخش مهم است:

const isActive =
  href === '/' ? pathname === '/' : pathname === href || pathname.startsWith(`${href}/`)

چرا این‌طور نوشتیم؟

  • برای صفحه اصلی، فقط وقتی مسیر دقیقاً / باشد باید active شود
  • برای بقیه مسیرها:
    • اگر مسیر دقیقاً برابر باشد، active است
    • اگر در زیرمسیر همان بخش باشیم هم active است

مثال:

  • اگر کاربر در /blog باشد، لینک Blog active می‌شود
  • اگر کاربر در /blog/nextjs-routing باشد، باز هم Blog active می‌ماند
  • اگر کاربر در /projects/my-app باشد، Projects active است

این رفتار برای سایت پرتفولیو و بلاگ کاملاً مناسب است.


یک utility کوچک برای className

در کد بالا از cn استفاده کردیم. اگر هنوز این utility را نداری، یک فایل ساده برای آن بساز:

// lib/cn.ts
export function cn(...classes: Array<string | false | null | undefined>) {
  return classes.filter(Boolean).join(' ')
}

این تابع کمک می‌کند کلاس‌ها را تمیزتر و شرطی‌تر مدیریت کنیم.


منوی موبایل

در پست‌های قبلی فقط یک navigation ساده داشتیم، اما در طراحی واقعی، منوی موبایل بسیار مهم است.
در نمایشگرهای کوچک، اگر همان منوی دسکتاپ را نگه داریم، هدر شلوغ و شکسته می‌شود.

برای همین:

  • منوی دسکتاپ را با md:flex نمایش می‌دهیم
  • در موبایل آن را hidden می‌کنیم
  • یک دکمه hamburger اضافه می‌کنیم
  • با useState باز و بسته شدن منو را کنترل می‌کنیم

نکات مهم در منوی موبایل

  1. Accessibility

    • از aria-label استفاده کردیم
    • از aria-expanded برای نشان دادن وضعیت منو استفاده کردیم
  2. بسته شدن منو بعد از کلیک روی لینک

    • در NavLink یک prop به نام onNavigate گذاشتیم
    • در موبایل، بعد از کلیک روی هر لینک، setIsOpen(false) اجرا می‌شود
  3. استفاده از داده مشترک

    • همان siteNavLinks هم در دسکتاپ و هم در موبایل استفاده شده است

این باعث می‌شود منو در دو جا از یک منبع داده مشترک تغذیه کند.


ساخت فوتر

در پست قبلی یک Footer ساده داشتیم. حالا می‌خواهیم آن را کمی حرفه‌ای‌تر کنیم تا:

  • اطلاعات کلی سایت را نشان دهد
  • لینک‌های مهم را داشته باشد
  • لینک‌های اجتماعی را نمایش دهد

نمونه پیشنهادی:

// components/layout/footer.tsx
import Link from 'next/link'
import { Container } from './container'

const footerLinks = [
  { href: '/', label: 'Home' },
  { href: '/projects', label: 'Projects' },
  { href: '/blog', label: 'Blog' },
  { href: '/about', label: 'About' },
  { href: '/contact', label: 'Contact' },
]

const socialLinks = [
  { href: 'https://github.com/your-username', label: 'GitHub' },
  { href: 'https://linkedin.com/in/your-username', label: 'LinkedIn' },
  { href: 'mailto:hello@example.com', label: 'Email' },
]

export function Footer() {
  return (
    <footer className="mt-16 border-t border-slate-800 bg-slate-950">
      <Container>
        <div className="grid gap-10 py-10 md:grid-cols-3">
          <div>
            <h2 className="text-base font-semibold text-slate-50">Matin</h2>
            <p className="mt-3 max-w-xs text-sm leading-6 text-slate-400">
              Personal portfolio, blog, and developer dashboard built with
              Next.js, Prisma, and Tailwind CSS.
            </p>
          </div>

          <div>
            <h3 className="text-sm font-semibold text-slate-100">Navigation</h3>
            <ul className="mt-3 space-y-2">
              {footerLinks.map((link) => (
                <li key={link.href}>
                  <Link
                    href={link.href}
                    className="text-sm text-slate-400 transition hover:text-cyan-300"
                  >
                    {link.label}
                  </Link>
                </li>
              ))}
            </ul>
          </div>

          <div>
            <h3 className="text-sm font-semibold text-slate-100">Connect</h3>
            <ul className="mt-3 space-y-2">
              {socialLinks.map((link) => (
                <li key={link.href}>
                  <a
                    href={link.href}
                    target="_blank"
                    rel="noreferrer"
                    className="text-sm text-slate-400 transition hover:text-cyan-300"
                  >
                    {link.label}
                  </a>
                </li>
              ))}
            </ul>
          </div>
        </div>

        <div className="border-t border-slate-800 py-4 text-xs text-slate-500">
          <p>© {new Date().getFullYear()} Matin. All rights reserved.</p>
        </div>
      </Container>
    </footer>
  )
}

این فوتر سه بخش اصلی دارد:

  • معرفی کوتاه برند یا شخص
  • لینک‌های ناوبری داخلی
  • لینک‌های ارتباطی و اجتماعی

لینک‌های اجتماعی

در Footer، لینک‌های اجتماعی معمولاً external هستند، پس اینجا از a استفاده می‌کنیم، نه Link.

قاعده‌ای که در آموزش Routing گفتیم هنوز برقرار است:

  • لینک داخلی $\rightarrow$ Link
  • لینک خارجی $\rightarrow$ a

مثلاً:

<a
  href="https://github.com/your-username"
  target="_blank"
  rel="noreferrer"
>
  GitHub
</a>

چرا target="_blank" و rel="noreferrer"؟

  • target="_blank" لینک را در تب جدید باز می‌کند
  • rel="noreferrer" از نظر امنیت و حریم خصوصی برای لینک‌های خارجی بهتر است

اگر بخواهیم، بعدا می‌توانیم این بخش را با lucide-react حرفه‌ای‌تر کنیم و به‌جای متن ساده، آیکون GitHub، LinkedIn و Mail هم بگذاریم.

مثلاً:

import { Github, Linkedin, Mail } from 'lucide-react'

اما برای این مرحله، متن ساده هم کاملاً کافی و تمیز است.


اتصال همه چیز به Layout

از آنجا که در پست قبلی Header و Footer را در app/layout.tsx قرار داده بودیم، فقط کافی است نسخه جدید آن‌ها را جایگزین کنیم.

// app/layout.tsx
import type { Metadata } from 'next'
import './globals.css'
import { Header } from '@/components/layout/header'
import { Footer } from '@/components/layout/footer'
import { Container } from '@/components/layout/container'

export const metadata: Metadata = {
  title: 'Matin Portfolio',
  description: 'Personal portfolio, blog, and dashboard built with Next.js',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className="min-h-screen bg-slate-950 text-slate-100">
        <Header />

        <main className="min-h-[calc(100vh-4rem-3rem)] py-8">
          <Container>{children}</Container>
        </main>

        <Footer />
      </body>
    </html>
  )
}

این همان ساختاری است که در پست قبلی داشتیم، فقط حالا Header و Footer حرفه‌ای‌تر شده‌اند.


ساختار نهایی فایل‌ها

بعد از این بخش، ساختار مربوط به layout می‌تواند چیزی شبیه این باشد:

app/
├── layout.tsx
├── page.tsx
├── about/
│   └── page.tsx
├── blog/
│   └── page.tsx
├── contact/
│   └── page.tsx
└── projects/
    └── page.tsx

components/
├── layout/
│   ├── container.tsx
│   ├── footer.tsx
│   ├── header.tsx
│   ├── nav-link.tsx
│   └── site-nav-links.ts
└── sections/
    ├── about-snippet.tsx
    ├── cta-section.tsx
    ├── hero-section.tsx
    └── skills-section.tsx

lib/
└── cn.ts

جمع‌بندی

در این پست، نسخه اولیه Header و Footer را که قبلاً ساخته بودیم، به یک نسخه کامل‌تر و حرفه‌ای‌تر ارتقا دادیم.

چیزهایی که اضافه کردیم:

  • ساختار بهتر برای لینک‌های ناوبری
  • استفاده درست از Link برای routeهای داخلی
  • active state با usePathname
  • منوی موبایل با useState
  • Footer حرفه‌ای‌تر با لینک‌های داخلی و اجتماعی
// 0 comments
#Next.js#سایت شخصی#طراحی رابط کاربری#JavaScript#توسعه وب#React#فرانت‌اند#صفحه Home#صفحه اصلی#سایت پرتفولیو#Next.js Portfolio#آموزش

نظرات (0)

نظر خود را بنویسید