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 Shiftnext/fontטוען פונטים בצורה אופטימלית בלי FOUT- Metadata API מאפשר הגדרת SEO סטטי ודינמי
- Open Graph ו-Twitter Cards משפרים את התצוגה ברשתות חברתיות
- שימוש ב-
generateMetadataלמטא-דאטה דינמי בדפים עם פרמטרים