לדלג לתוכן

9.6 מידלוור ואופטימיזציה הרצאה

מידלוור ואופטימיזציה - Middleware and Optimization

בשיעור זה נלמד לעומק את המידלוור של נקסט ואת כלי האופטימיזציה המובנים: תמונות, פונטים ומטא-דאטה לשיפור SEO.


מידלוור - Middleware

המידלוור רץ לפני כל בקשה ומאפשר לבצע פעולות כמו הפניות, אותנטיקציה והוספת headers.

הגדרה בסיסית

// middleware.ts (ברמה הראשית של הפרויקט)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  console.log("בקשה:", request.nextUrl.pathname);
  return NextResponse.next();
}
  • הקובץ חייב להיות ברמה הראשית של הפרויקט (ליד package.json)
  • רץ על כל בקשה כברירת מחדל
  • חייב להחזיר Response

הגבלת נתיבים - Matcher

export const config = {
  matcher: [
    // נתיבים ספציפיים
    "/dashboard/:path*",
    "/admin/:path*",

    // התאמה עם regex
    "/((?!api|_next/static|_next/image|favicon.ico).*)",
  ],
};
  • matcher מגדיר על אילו נתיבים המידלוור ירוץ
  • :path* תופס נתיבים מקוננים
  • אפשר להשתמש ב-regex

הפניות - Redirects

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // הפניה קבועה
  if (pathname === "/old-page") {
    return NextResponse.redirect(new URL("/new-page", request.url));
  }

  // הפניה עם שמירת query params
  if (pathname === "/search-old") {
    const url = request.nextUrl.clone();
    url.pathname = "/search";
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

שכתוב נתיבים - Rewrites

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // שכתוב - ה-URL לא משתנה בדפדפן
  if (pathname.startsWith("/api/v1")) {
    const url = request.nextUrl.clone();
    url.pathname = pathname.replace("/api/v1", "/api");
    return NextResponse.rewrite(url);
  }

  return NextResponse.next();
}
  • ההבדל בין redirect ל-rewrite: ב-redirect ה-URL משתנה בדפדפן, ב-rewrite לא

אותנטיקציה במידלוור

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

const protectedRoutes = ["/dashboard", "/settings", "/profile"];
const publicRoutes = ["/login", "/register"];

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const token = request.cookies.get("session")?.value;

  // נתיב מוגן - בדיקת אותנטיקציה
  const isProtected = protectedRoutes.some((route) =>
    pathname.startsWith(route)
  );

  if (isProtected && !token) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("callbackUrl", pathname);
    return NextResponse.redirect(loginUrl);
  }

  // משתמש מחובר שמנסה לגשת לדף התחברות
  const isPublic = publicRoutes.some((route) =>
    pathname.startsWith(route)
  );

  if (isPublic && token) {
    return NextResponse.redirect(new URL("/dashboard", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*", "/profile/:path*", "/login", "/register"],
};

הוספת Headers

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // הוספת headers לתגובה
  response.headers.set("x-custom-header", "my-value");
  response.headers.set("x-request-id", crypto.randomUUID());

  // headers לאבטחה
  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("X-Content-Type-Options", "nosniff");

  return response;
}

אופטימיזציית תמונות - next/image

קומפוננטת Image של נקסט מבצעת אופטימיזציות אוטומטיות:

שימוש בסיסי

import Image from "next/image";

export default function Page() {
  return (
    <div>
      {/* תמונה מקומית */}
      <Image
        src="/hero.jpg"
        alt="תמונת כותרת"
        width={800}
        height={400}
      />

      {/* תמונה חיצונית */}
      <Image
        src="https://images.unsplash.com/photo-123"
        alt="תמונה מ-Unsplash"
        width={600}
        height={400}
      />
    </div>
  );
}

יתרונות

  • המרה אוטומטית לפורמט WebP/AVIF (קטן יותר)
  • שינוי גודל אוטומטי לפי הצורך
  • Lazy loading כברירת מחדל - תמונות נטענות רק כשהן בתצוגה
  • מניעת Layout Shift - המקום נשמר עוד לפני שהתמונה נטענת

מצב מילוי - fill

כשלא יודעים את הגודל המדויק של התמונה:

<div className="relative w-full h-64">
  <Image
    src="/hero.jpg"
    alt="כותרת"
    fill
    className="object-cover"
  />
</div>
  • עם fill, התמונה ממלאת את האלמנט האב
  • האלמנט האב חייב להיות relative או absolute
  • object-cover שומר על יחס הגובה-רוחב

תמונות מתגובות - Responsive Images

<Image
  src="/photo.jpg"
  alt="תמונה"
  width={800}
  height={600}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
  • sizes מגדיר כמה מסך התמונה תופסת בכל גודל מסך
  • נקסט מייצר תמונות בגדלים שונים ובוחר את המתאים

עדיפות טעינה - Priority

// תמונה שצריכה להיטען מיד (מעל ל-fold)
<Image
  src="/hero.jpg"
  alt="כותרת"
  width={1200}
  height={600}
  priority
/>
  • priority מבטל את ה-lazy loading ומטעין את התמונה מיד
  • השתמשו רק ב-LCP image (התמונה הגדולה הראשונה שנראית)

הגדרת דומיינים חיצוניים

// next.config.ts
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "images.unsplash.com",
      },
      {
        protocol: "https",
        hostname: "**.amazonaws.com",
      },
    ],
  },
};

אופטימיזציית פונטים - next/font

נקסט מאפשר טעינת פונטים אופטימלית בלי Layout Shift:

פונטים של גוגל - Google Fonts

// app/layout.tsx
import { Inter, Heebo } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter",
});

const heebo = Heebo({
  subsets: ["hebrew", "latin"],
  variable: "--font-heebo",
});

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="he" dir="rtl" className={`${inter.variable} ${heebo.variable}`}>
      <body className="font-heebo">{children}</body>
    </html>
  );
}
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  font-family: var(--font-heebo), sans-serif;
}

code {
  font-family: var(--font-inter), monospace;
}

פונטים מקומיים - Local Fonts

import localFont from "next/font/local";

const myFont = localFont({
  src: [
    {
      path: "../public/fonts/MyFont-Regular.woff2",
      weight: "400",
      style: "normal",
    },
    {
      path: "../public/fonts/MyFont-Bold.woff2",
      weight: "700",
      style: "normal",
    },
  ],
  variable: "--font-my-font",
});

יתרונות

  • הפונטים מתארחים על השרת שלכם - אין בקשה לשרתי גוגל
  • font-display: swap אוטומטי - מניעת Layout Shift
  • טעינה אוטומטית רק של subset הנדרש
  • אין flash of unstyled text (FOUT)

מטא-דאטה - Metadata API

נקסט מספק API מובנה להגדרת מטא-דאטה ל-SEO:

מטא-דאטה סטטי

// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: {
    template: "%s | האתר שלי",
    default: "האתר שלי",
  },
  description: "אתר מדהים שנבנה עם Next.js",
  keywords: ["Next.js", "React", "פיתוח ווב"],
  authors: [{ name: "ישראל ישראלי" }],
  creator: "ישראל ישראלי",
};
  • title.template מוסיף סיומת אחידה לכל הדפים
  • title.default משמש כשלדף אין כותרת משלו
// app/about/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "אודות", // ייצור: "אודות | האתר שלי"
  description: "למדו עלינו ועל הצוות שלנו",
};

מטא-דאטה דינמי - generateMetadata

// app/blog/[slug]/page.tsx
import type { Metadata } from "next";

interface Props {
  params: { slug: string };
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await fetchPost(params.slug);

  if (!post) {
    return { title: "פוסט לא נמצא" };
  }

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  };
}

Open Graph ו-Twitter Cards

export const metadata: Metadata = {
  title: "האתר שלי",
  description: "אתר מדהים",

  openGraph: {
    title: "האתר שלי",
    description: "אתר מדהים שנבנה עם Next.js",
    url: "https://mysite.com",
    siteName: "האתר שלי",
    images: [
      {
        url: "https://mysite.com/og.jpg",
        width: 1200,
        height: 630,
        alt: "תמונת תצוגה מקדימה",
      },
    ],
    locale: "he_IL",
    type: "website",
  },

  twitter: {
    card: "summary_large_image",
    title: "האתר שלי",
    description: "אתר מדהים שנבנה עם Next.js",
    images: ["https://mysite.com/og.jpg"],
    creator: "@myhandle",
  },
};
  • Open Graph משפיע על איך הלינק נראה כשמשתפים בפייסבוק/לינקדאין
  • Twitter Cards משפיע על התצוגה בטוויטר

אייקונים ו-Manifest

export const metadata: Metadata = {
  icons: {
    icon: "/favicon.ico",
    shortcut: "/favicon-16x16.png",
    apple: "/apple-touch-icon.png",
  },
  manifest: "/manifest.json",

  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      "max-video-preview": -1,
      "max-image-preview": "large",
      "max-snippet": -1,
    },
  },
};

דוגמה מלאה - אתר אופטימלי

// app/layout.tsx
import type { Metadata } from "next";
import { Heebo } from "next/font/google";
import "./globals.css";
import Navbar from "./components/Navbar";

const heebo = Heebo({
  subsets: ["hebrew", "latin"],
  variable: "--font-heebo",
});

export const metadata: Metadata = {
  title: {
    template: "%s | חנות הספרים",
    default: "חנות הספרים - הספרים הטובים ביותר",
  },
  description: "חנות ספרים מקוונת עם מבחר עצום של ספרים בעברית ובאנגלית",
  keywords: ["ספרים", "חנות ספרים", "קריאה"],
  openGraph: {
    type: "website",
    locale: "he_IL",
    url: "https://bookstore.com",
    siteName: "חנות הספרים",
    images: [
      {
        url: "https://bookstore.com/og.jpg",
        width: 1200,
        height: 630,
      },
    ],
  },
  robots: {
    index: true,
    follow: true,
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="he" dir="rtl" className={heebo.variable}>
      <body className="font-heebo min-h-screen">
        <Navbar />
        {children}
      </body>
    </html>
  );
}
// app/books/[id]/page.tsx
import type { Metadata } from "next";
import Image from "next/image";

interface Book {
  id: string;
  title: string;
  author: string;
  description: string;
  coverImage: string;
  price: number;
}

async function getBook(id: string): Promise<Book | null> {
  // שליפה מבסיס נתונים
  return null;
}

export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  const book = await getBook(params.id);
  if (!book) return { title: "ספר לא נמצא" };

  return {
    title: book.title,
    description: `${book.title} מאת ${book.author} - ${book.description.substring(0, 160)}`,
    openGraph: {
      title: `${book.title} - חנות הספרים`,
      description: book.description,
      images: [book.coverImage],
    },
  };
}

export default async function BookPage({
  params,
}: {
  params: { id: string };
}) {
  const book = await getBook(params.id);
  if (!book) return null;

  return (
    <div className="max-w-4xl mx-auto p-6">
      <div className="flex gap-8">
        <div className="relative w-64 h-96 flex-shrink-0">
          <Image
            src={book.coverImage}
            alt={`כריכת ${book.title}`}
            fill
            className="object-cover rounded-lg shadow"
            priority
          />
        </div>
        <div>
          <h1 className="text-3xl font-bold">{book.title}</h1>
          <p className="text-xl text-gray-600 mt-2">{book.author}</p>
          <p className="text-2xl text-green-600 font-bold mt-4">
            {book.price} ש
          </p>
          <p className="mt-4 text-gray-700 leading-relaxed">
            {book.description}
          </p>
        </div>
      </div>
    </div>
  );
}

סיכום

  • מידלוור רץ לפני כל בקשה ומתאים להפניות, אותנטיקציה ו-headers
  • next/image מספק אופטימיזציית תמונות אוטומטית: WebP, lazy loading, מניעת Layout Shift
  • next/font טוען פונטים בצורה אופטימלית בלי FOUT
  • Metadata API מאפשר הגדרת SEO סטטי ודינמי
  • Open Graph ו-Twitter Cards משפרים את התצוגה ברשתות חברתיות
  • שימוש ב-generateMetadata למטא-דאטה דינמי בדפים עם פרמטרים