9.6 מידלוור ואופטימיזציה פתרון
פתרון - מידלוור ואופטימיזציה - Middleware and Optimization¶
פתרון תרגיל 1 - מידלוור מלא¶
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const redirects: Record<string, string> = {
"/blog": "/articles",
"/team": "/about",
};
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const method = request.method;
const timestamp = new Date().toISOString();
// לוג
console.log(`[${timestamp}] ${method} ${pathname}`);
// הפניות
if (redirects[pathname]) {
return NextResponse.redirect(
new URL(redirects[pathname], request.url)
);
}
// אותנטיקציה לדשבורד
if (pathname.startsWith("/dashboard")) {
const token = request.cookies.get("token")?.value;
if (!token) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("from", pathname);
return NextResponse.redirect(loginUrl);
}
}
// בדיקת שפה
const response = NextResponse.next();
const acceptLanguage = request.headers.get("accept-language") || "";
if (!acceptLanguage.includes("he")) {
response.headers.set("x-locale", "en");
} else {
response.headers.set("x-locale", "he");
}
response.headers.set("x-request-time", timestamp);
return response;
}
export const config = {
matcher: [
"/blog",
"/team",
"/dashboard/:path*",
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};
פתרון תרגיל 2 - גלריית תמונות¶
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "picsum.photos",
},
],
},
};
export default nextConfig;
// app/gallery/page.tsx
import Image from "next/image";
import GalleryModal from "./GalleryModal";
const images = Array.from({ length: 12 }, (_, i) => ({
id: i + 1,
src: `https://picsum.photos/seed/${i + 1}/600/400`,
alt: `תמונה ${i + 1}`,
}));
export default function GalleryPage() {
return (
<div className="p-6">
<h1 className="text-3xl font-bold mb-6">גלריה</h1>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{images.map((image, index) => (
<GalleryModal key={image.id} image={image}>
<div className="relative h-48 cursor-pointer hover:opacity-80 transition">
<Image
src={image.src}
alt={image.alt}
width={300}
height={200}
className="rounded object-cover w-full h-full"
priority={index === 0}
/>
</div>
</GalleryModal>
))}
</div>
</div>
);
}
// app/gallery/GalleryModal.tsx
"use client";
import { useState } from "react";
import Image from "next/image";
interface GalleryModalProps {
image: { id: number; src: string; alt: string };
children: React.ReactNode;
}
export default function GalleryModal({ image, children }: GalleryModalProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<div onClick={() => setIsOpen(true)}>{children}</div>
{isOpen && (
<div
className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4"
onClick={() => setIsOpen(false)}
>
<div className="relative w-full max-w-4xl h-[80vh]">
<Image
src={image.src}
alt={image.alt}
fill
className="object-contain"
sizes="(max-width: 1024px) 100vw, 80vw"
/>
</div>
<button
className="absolute top-4 left-4 text-white text-2xl hover:text-gray-300"
onClick={() => setIsOpen(false)}
>
X
</button>
</div>
)}
</>
);
}
פתרון תרגיל 3 - פונטים מותאמים¶
// app/layout.tsx
import { Inter, Heebo } from "next/font/google";
import "./globals.css";
const heebo = Heebo({
subsets: ["hebrew", "latin"],
variable: "--font-heebo",
display: "swap",
});
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
display: "swap",
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="he" dir="rtl" className={`${heebo.variable} ${inter.variable}`}>
<body>{children}</body>
</html>
);
}
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: var(--font-heebo), sans-serif;
}
code, pre {
font-family: var(--font-inter), monospace;
}
// app/fonts-demo/page.tsx
export default function FontsDemoPage() {
return (
<div className="max-w-3xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">הדגמת פונטים</h1>
<section className="mb-8">
<h2 className="text-xl font-semibold mb-3">
פונט ראשי - Heebo (עברית)
</h2>
<p className="text-lg">
זהו טקסט בעברית שמוצג בפונט Heebo. הפונט מתאים לכתיבה בעברית
עם תמיכה מלאה בתווים עבריים.
</p>
<p className="font-bold mt-2">טקסט בבולד</p>
<p className="font-light mt-1">טקסט דק</p>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold mb-3">
פונט משני - Inter (אנגלית/קוד)
</h2>
<code className="block bg-gray-100 p-4 rounded text-lg">
const greeting = "Hello, World!";
console.log(greeting);
</code>
<pre className="bg-gray-100 p-4 rounded mt-2">
{`function add(a: number, b: number): number {
return a + b;
}`}
</pre>
</section>
<section>
<h2 className="text-xl font-semibold mb-3">השוואה</h2>
<p>טקסט רגיל בעברית: שלום עולם</p>
<p>
טקסט עם <code className="bg-gray-100 px-1 rounded">code</code> באנגלית
</p>
</section>
</div>
);
}
פתרון תרגיל 4 - מטא-דאטה מלא¶
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: {
template: "%s | טק בלוג",
default: "טק בלוג - חדשות טכנולוגיה בעברית",
},
description: "הבלוג הטכנולוגי המוביל בעברית. מאמרים על פיתוח, AI, וטכנולוגיות חדשות.",
keywords: ["טכנולוגיה", "פיתוח", "תכנות", "Next.js", "React"],
authors: [{ name: "ישראל ישראלי" }],
creator: "טק בלוג",
openGraph: {
type: "website",
locale: "he_IL",
url: "https://techblog.co.il",
siteName: "טק בלוג",
title: "טק בלוג - חדשות טכנולוגיה בעברית",
description: "הבלוג הטכנולוגי המוביל בעברית",
images: [
{
url: "https://techblog.co.il/og-default.jpg",
width: 1200,
height: 630,
alt: "טק בלוג",
},
],
},
twitter: {
card: "summary_large_image",
title: "טק בלוג",
description: "הבלוג הטכנולוגי המוביל בעברית",
images: ["https://techblog.co.il/og-default.jpg"],
creator: "@techblog_il",
},
icons: {
icon: "/favicon.ico",
shortcut: "/favicon-16x16.png",
apple: "/apple-touch-icon.png",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
},
},
};
// app/about/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "אודות",
description: "מי אנחנו ומה המשימה שלנו בטק בלוג",
};
export default function AboutPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">אודות טק בלוג</h1>
<p className="mt-4">צוות כותבים מקצועיים שמביאים את הטכנולוגיה בעברית</p>
</main>
);
}
// app/contact/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "צור קשר",
description: "צרו איתנו קשר עם שאלות, הצעות או בקשות שיתוף פעולה",
};
export default function ContactPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">צור קשר</h1>
<p className="mt-4">נשמח לשמוע מכם</p>
</main>
);
}
// app/privacy/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "מדיניות פרטיות",
description: "מדיניות הפרטיות של טק בלוג",
robots: {
index: false, // לא לאנדקס את דף הפרטיות
follow: false,
},
};
export default function PrivacyPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">מדיניות פרטיות</h1>
<p className="mt-4">פרטי מדיניות הפרטיות שלנו</p>
</main>
);
}
פתרון תרגיל 5 - מטא-דאטה דינמי¶
// lib/recipes.ts
export interface Recipe {
id: string;
title: string;
description: string;
image: string;
tags: string[];
time: number;
}
export const recipes: Recipe[] = [
{
id: "shakshuka",
title: "שקשוקה",
description: "שקשוקה ישראלית קלאסית עם ביצים ברוטב עגבניות חריף. מנה מושלמת לארוחת בוקר או ערב, מוכנה תוך 20 דקות.",
image: "https://picsum.photos/seed/shakshuka/800/600",
tags: ["ארוחת בוקר", "ישראלי", "קל", "צמחוני"],
time: 20,
},
{
id: "hummus",
title: "חומוס",
description: "חומוס ביתי חלק וקרמי עם טחינה, לימון ושום. המתכון הסודי לחומוס מושלם כמו של המסעדות.",
image: "https://picsum.photos/seed/hummus/800/600",
tags: ["מנה ראשונה", "ישראלי", "טבעוני"],
time: 30,
},
{
id: "schnitzel",
title: "שניצל",
description: "שניצל עוף פריך ומדהים. הסוד הוא בפירורי הלחם ובטכניקת הטיגון. ילדים ומבוגרים אוהבים.",
image: "https://picsum.photos/seed/schnitzel/800/600",
tags: ["מנה עיקרית", "ילדים", "בשרי"],
time: 45,
},
];
// app/recipes/page.tsx
import Link from "next/link";
import Image from "next/image";
import type { Metadata } from "next";
import { recipes } from "@/lib/recipes";
export const metadata: Metadata = {
title: "מתכונים",
description: "מתכונים ביתיים טעימים וקלים להכנה",
};
export default function RecipesPage() {
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">מתכונים</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{recipes.map((recipe) => (
<Link
key={recipe.id}
href={`/recipes/${recipe.id}`}
className="border rounded-lg overflow-hidden hover:shadow-lg transition"
>
<div className="relative h-48">
<Image
src={recipe.image}
alt={recipe.title}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
<div className="p-4">
<h2 className="text-xl font-semibold">{recipe.title}</h2>
<p className="text-gray-600 mt-1 text-sm">
{recipe.description.substring(0, 80)}...
</p>
<p className="text-sm text-gray-500 mt-2">{recipe.time} דקות</p>
</div>
</Link>
))}
</div>
</div>
);
}
// app/recipes/[id]/page.tsx
import type { Metadata } from "next";
import Image from "next/image";
import { notFound } from "next/navigation";
import { recipes } from "@/lib/recipes";
interface Props {
params: { id: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const recipe = recipes.find((r) => r.id === params.id);
if (!recipe) {
return { title: "מתכון לא נמצא" };
}
return {
title: recipe.title,
description: recipe.description.substring(0, 160),
keywords: recipe.tags,
openGraph: {
title: `${recipe.title} - מתכונים`,
description: recipe.description.substring(0, 160),
images: [
{
url: recipe.image,
width: 800,
height: 600,
alt: recipe.title,
},
],
type: "article",
},
};
}
export default function RecipePage({ params }: Props) {
const recipe = recipes.find((r) => r.id === params.id);
if (!recipe) notFound();
return (
<div className="max-w-3xl mx-auto p-6">
<div className="relative h-80 rounded-lg overflow-hidden mb-6">
<Image
src={recipe.image}
alt={recipe.title}
fill
className="object-cover"
priority
/>
</div>
<h1 className="text-4xl font-bold mb-2">{recipe.title}</h1>
<p className="text-gray-500 mb-4">{recipe.time} דקות הכנה</p>
<div className="flex gap-2 mb-6">
{recipe.tags.map((tag) => (
<span
key={tag}
className="px-3 py-1 bg-gray-100 rounded-full text-sm"
>
{tag}
</span>
))}
</div>
<p className="text-lg leading-relaxed">{recipe.description}</p>
</div>
);
}
פתרון תרגיל 6 - אופטימיזציה מלאה¶
// app/page.tsx
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
export const metadata: Metadata = {
title: "דף הבית",
description: "דף נחיתה מותאם ומהיר עם Next.js",
openGraph: {
title: "האתר שלנו - דף נחיתה",
description: "דף נחיתה מותאם ומהיר",
images: [{ url: "/og-home.jpg", width: 1200, height: 630 }],
},
};
export default function HomePage() {
return (
<div>
{/* Hero */}
<section className="relative h-[80vh]">
<Image
src="https://picsum.photos/seed/hero/1920/1080"
alt="תמונת רקע"
fill
className="object-cover"
priority
sizes="100vw"
/>
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<div className="text-center text-white">
<h1 className="text-5xl md:text-7xl font-bold mb-4">
ברוכים הבאים
</h1>
<p className="text-xl md:text-2xl mb-8">
אתר מהיר ומותאם עם Next.js
</p>
<Link
href="/about"
className="px-8 py-3 bg-white text-black rounded-lg font-semibold hover:bg-gray-200"
>
למדו עוד
</Link>
</div>
</div>
</section>
{/* Features */}
<section className="max-w-6xl mx-auto p-8">
<h2 className="text-3xl font-bold text-center mb-12">למה אנחנו</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[1, 2, 3].map((i) => (
<div key={i} className="text-center">
<div className="relative w-32 h-32 mx-auto mb-4">
<Image
src={`https://picsum.photos/seed/feature${i}/256/256`}
alt={`תכונה ${i}`}
fill
className="object-cover rounded-full"
sizes="128px"
/>
</div>
<h3 className="text-xl font-semibold mb-2">תכונה {i}</h3>
<p className="text-gray-600">
תיאור קצר של התכונה שמסביר את היתרונות שלה
</p>
</div>
))}
</div>
</section>
</div>
);
}
- תמונת hero עם
priorityכי זו ה-LCP sizes="100vw"כי התמונה ברוחב מלא- תמונות features עם
sizes="128px"כי הן קטנות fill+object-coverלתמונות שממלאות את המיכל
תשובות לשאלות¶
-
ההבדל בין redirect ל-rewrite: ב-redirect ה-URL בדפדפן משתנה לנתיב החדש (הדפדפן מבצע בקשה חדשה). ב-rewrite ה-URL נשאר כפי שהוא אבל התוכן מגיע מנתיב אחר. Rewrite שימושי כשרוצים URL נקי שמאחוריו יש לוגיקה מורכבת.
-
next/image לעומת img רגיל: next/image מבצע: המרה אוטומטית לפורמטים יעילים (WebP/AVIF), שינוי גודל אוטומטי, lazy loading כברירת מחדל, מניעת Layout Shift, ו-srcset אוטומטי. תגית img רגילה לא עושה שום אופטימיזציה.
-
font-display: swap: זה מגדיר שהדפדפן יציג טקסט מיד בפונט גנרי, ויחליף לפונט המותאם ברגע שהוא נטען. זה מונע "הבהוב של טקסט לא מעוצב" (FOUT). next/font מגדיר את זה אוטומטית ובנוסף מארח את הפונט על השרת שלכם כך שהטעינה מהירה יותר.
-
metadata סטטי לעומת generateMetadata: metadata סטטי מוגדר כקבוע ומתאים לדפים עם תוכן קבוע (דף ראשי, אודות). generateMetadata הוא פונקציה אסינכרונית שיכולה לשלוף נתונים ולייצר מטא-דאטה דינמי, ומתאים לדפים עם פרמטרים (דף מוצר, פוסט בבלוג).
-
Open Graph: זהו פרוטוקול שמגדיר איך הלינק שלכם נראה כשמשתפים אותו ברשתות חברתיות. הוא מגדיר כותרת, תיאור ותמונת תצוגה מקדימה. בלי Open Graph, הרשת החברתית מנסה לנחש מה להציג ובדרך כלל התוצאה לא טובה.