לדלג לתוכן

9.9 דיפלוימנט וארכיטקטורה פתרון

פתרון - דיפלוימנט וארכיטקטורה - Deployment and Architecture

פתרון תרגיל 1 - בנייה ובדיקת ייצור

# בנייה
npm run build

# הרצה בייצור
npm run start
  • ברגע שה-build עובר, נבדוק את הפלט
  • דפים עם הם סטטיים - נבנו בזמן build
  • דפים עם λ הם דינמיים - נבנים בכל בקשה
  • אם דף שיכול להיות סטטי מסומן כדינמי, בדקו שאין cache: "no-store" מיותר

בדיקת Lighthouse:
- פתחו את כלי הפיתוח בדפדפן
- לשונית Lighthouse
- הריצו על כל הקטגוריות
- שפרו בהתאם להמלצות


פתרון תרגיל 2 - משתני סביבה

# .env.local
DATABASE_URL="file:./dev.db"
AUTH_SECRET="my-super-secret-key-that-is-long-enough"
API_KEY="sk-12345678"

NEXT_PUBLIC_SITE_NAME="האתר שלי"
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
// lib/env.ts

// משתני שרת - לא זמינים בדפדפן
function getServerEnv() {
  const vars = {
    DATABASE_URL: process.env.DATABASE_URL,
    AUTH_SECRET: process.env.AUTH_SECRET,
    API_KEY: process.env.API_KEY,
  };

  // ולידציה
  for (const [key, value] of Object.entries(vars)) {
    if (!value) {
      throw new Error(`משתנה סביבה חסר: ${key}`);
    }
  }

  return vars as Record<keyof typeof vars, string>;
}

// משתני קליינט - זמינים בדפדפן
export const publicEnv = {
  SITE_NAME: process.env.NEXT_PUBLIC_SITE_NAME || "האתר שלי",
  SITE_URL: process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000",
};

// ייצוא רק לשרת
export const serverEnv = getServerEnv();
// שימוש ב-Server Component
import { serverEnv } from "@/lib/env";

export default function Page() {
  const apiKey = serverEnv.API_KEY; // עובד
  return <div>מחובר</div>;
}
// שימוש ב-Client Component
"use client";

import { publicEnv } from "@/lib/env";

export default function SiteName() {
  return <span>{publicEnv.SITE_NAME}</span>;
}

פתרון תרגיל 3 - ארגון מחדש של פרויקט

app/
  layout.tsx
  page.tsx
  not-found.tsx
  error.tsx
  global-error.tsx

  (auth)/
    login/page.tsx
    register/page.tsx
    layout.tsx

  (main)/
    layout.tsx
    dashboard/page.tsx
    settings/page.tsx

    posts/
      page.tsx
      [id]/page.tsx
      new/page.tsx
      components/
        PostCard.tsx
        PostForm.tsx
        PostList.tsx
      actions.ts
      types.ts

    users/
      page.tsx
      [id]/page.tsx
      components/
        UserCard.tsx
        UserList.tsx
      actions.ts
      types.ts

  api/
    posts/route.ts
    users/route.ts

  components/
    ui/
      Button.tsx
      Input.tsx
      Card.tsx
      Modal.tsx
      Skeleton.tsx
    layout/
      Navbar.tsx
      Footer.tsx
      Sidebar.tsx
    forms/
      FormField.tsx
      SubmitButton.tsx

lib/
  prisma.ts
  utils.ts
  validations.ts
  constants.ts
  env.ts

types/
  index.ts
// app/components/ui/Button.tsx
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "primary" | "secondary" | "danger";
  size?: "sm" | "md" | "lg";
  loading?: boolean;
}

export default function Button({
  variant = "primary",
  size = "md",
  loading = false,
  children,
  className,
  disabled,
  ...props
}: ButtonProps) {
  const variants = {
    primary: "bg-blue-500 text-white hover:bg-blue-600",
    secondary: "bg-gray-100 text-gray-700 hover:bg-gray-200",
    danger: "bg-red-500 text-white hover:bg-red-600",
  };

  const sizes = {
    sm: "px-3 py-1 text-sm",
    md: "px-4 py-2",
    lg: "px-6 py-3 text-lg",
  };

  return (
    <button
      className={`rounded font-medium transition ${variants[variant]} ${sizes[size]} ${
        disabled || loading ? "opacity-50 cursor-not-allowed" : ""
      } ${className || ""}`}
      disabled={disabled || loading}
      {...props}
    >
      {loading ? "טוען..." : children}
    </button>
  );
}
// types/index.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  createdAt: Date;
}

export interface Post {
  id: number;
  title: string;
  content: string;
  slug: string;
  published: boolean;
  createdAt: Date;
  author: Pick<User, "id" | "name">;
}

export interface ActionResult<T = void> {
  success: boolean;
  data?: T;
  error?: string;
}

פתרון תרגיל 4 - טיפול בשגיאות מקיף

// app/error.tsx
"use client";

import Button from "@/app/components/ui/Button";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="min-h-[50vh] flex flex-col items-center justify-center p-8">
      <div className="text-6xl mb-4">!</div>
      <h2 className="text-2xl font-bold mb-2">שגיאה</h2>
      <p className="text-gray-600 mb-6 text-center max-w-md">
        אירעה שגיאה בטעינת הדף. נסו לרענן או לחזור מאוחר יותר.
      </p>
      <div className="flex gap-3">
        <Button onClick={reset}>נסה שוב</Button>
        <Button variant="secondary" onClick={() => window.location.href = "/"}>
          חזרה לבית
        </Button>
      </div>
    </div>
  );
}
// app/global-error.tsx
"use client";

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <html lang="he" dir="rtl">
      <body>
        <div className="min-h-screen flex items-center justify-center bg-gray-50">
          <div className="text-center p-8">
            <h1 className="text-4xl font-bold text-red-600 mb-4">שגיאה קריטית</h1>
            <p className="text-gray-600 mb-6">
              האפליקציה נתקלה בשגיאה חמורה. אנא נסו שוב.
            </p>
            <button
              onClick={reset}
              className="px-6 py-3 bg-blue-500 text-white rounded hover:bg-blue-600"
            >
              טען מחדש
            </button>
          </div>
        </div>
      </body>
    </html>
  );
}
// app/not-found.tsx
import Link from "next/link";

export default function NotFound() {
  return (
    <div className="min-h-[60vh] flex flex-col items-center justify-center text-center p-8">
      <h1 className="text-8xl font-bold text-gray-200">404</h1>
      <h2 className="text-2xl font-bold mt-4 mb-2">הדף לא נמצא</h2>
      <p className="text-gray-600 mb-6 max-w-md">
        הדף שחיפשת לא קיים. יכול להיות שהכתובת שגויה או שהדף הועבר.
      </p>
      <div className="flex gap-3">
        <Link
          href="/"
          className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
        >
          דף הבית
        </Link>
        <Link
          href="/contact"
          className="px-6 py-2 border rounded hover:bg-gray-50"
        >
          צור קשר
        </Link>
      </div>
    </div>
  );
}
// lib/logger.ts
type LogLevel = "info" | "warn" | "error";

function formatMessage(level: LogLevel, message: string, data?: any): string {
  const timestamp = new Date().toISOString();
  return `[${timestamp}] [${level.toUpperCase()}] ${message}`;
}

export const logger = {
  info(message: string, data?: any) {
    console.log(formatMessage("info", message), data || "");
  },

  warn(message: string, data?: any) {
    console.warn(formatMessage("warn", message), data || "");
  },

  error(message: string, error?: any) {
    console.error(formatMessage("error", message), error || "");
    // כאן אפשר לשלוח ל-Sentry או שירות דומה
  },
};

פתרון תרגיל 5 - דיפלוי לוורסל

# יצירת repository ב-GitHub
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/user/project.git
git push -u origin main

בוורסל:
1. New Project -> Import Git Repository
2. בחירת ה-repo
3. הגדרת משתני סביבה:
- DATABASE_URL
- AUTH_SECRET
- NEXT_PUBLIC_SITE_NAME
4. לחיצה על Deploy
5. ודאו שה-build עובר ללא שגיאות


פתרון תרגיל 6 - אופטימיזציה ובדיקות סופיות

רשימת בדיקות:

[x] npm run build - ללא שגיאות
[x] npm run lint - ללא שגיאות
[x] Lighthouse Performance > 90
[x] Lighthouse Accessibility > 90
[x] Lighthouse Best Practices > 90
[x] Lighthouse SEO > 90
[x] כל התמונות דרך next/image
[x] כל הלינקים דרך next/link
[x] פונטים דרך next/font
[x] Metadata מלא בכל דף
[x] Open Graph tags
[x] responsive design - mobile, tablet, desktop
[x] alt text לכל התמונות
[x] ניגודיות צבעים מספקת
[x] טפסים עם labels
// app/layout.tsx - עם אנליטיקס
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="he" dir="rtl">
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  );
}

תשובות לשאלות

  1. ההבדל בין .env.local ל-.env: קובץ .env עולה ל-git ומכיל ערכי ברירת מחדל שלא רגישים. קובץ .env.local לא עולה ל-git ומכיל ערכים רגישים (סיסמאות, מפתחות API). .env.local דורס את .env כשיש ערכים כפולים.

  2. למה NEXT_PUBLIC_ מסוכנים למידע רגיש: משתנים עם NEXT_PUBLIC_ מוכנסים לתוך ה-JavaScript bundle שנשלח לדפדפן. כל מי שמסתכל ב-source code של הדף יכול לראות את הערכים. לכן אסור לשים שם מפתחות API, סיסמאות או כל מידע רגיש.

  3. יתרון Feature-Based: כל הקוד הקשור לפיצ'ר מסוים נמצא במקום אחד. קל למצוא קוד רלוונטי. קל להוסיף או להסיר פיצ'רים שלמים. מפחית תלויות בין חלקים שונים. מתאים לצוותים שכל אחד אחראי על פיצ'ר.

  4. global-error.tsx לעומת error.tsx: error.tsx תופס שגיאות בתוך ה-layout - הלייאאוט עצמו עדיין מוצג. global-error.tsx תופס שגיאות בלייאאוט הראשי עצמו - כשהלייאאוט נשבר. global-error חייב להכיל תגיות html ו-body כי הלייאאוט לא זמין.

  5. מה Vercel עושה שונה: Vercel בנויה ספציפית ל-Next.js ותומכת ב-SSR, ISR, Middleware, Edge Functions ו-Image Optimization מהקופסה. בנוסף, היא מספקת CDN גלובלי, Preview deployments לכל PR, אנליטיקס, ו-automatic scaling. ב-hosting רגיל צריך להגדיר את כל זה ידנית.