לדלג לתוכן

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

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

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

# Build
npm run build

# Run in production
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

// Server variables - not available in the browser
function getServerEnv() {
  const vars = {
    DATABASE_URL: process.env.DATABASE_URL,
    AUTH_SECRET: process.env.AUTH_SECRET,
    API_KEY: process.env.API_KEY,
  };

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

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

// Client variables - available in the browser
export const publicEnv = {
  SITE_NAME: process.env.NEXT_PUBLIC_SITE_NAME || "האתר שלי",
  SITE_URL: process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000",
};

// Export only for the server
export const serverEnv = getServerEnv();
// Usage in a Server Component
import { serverEnv } from "@/lib/env";

export default function Page() {
  const apiKey = serverEnv.API_KEY; // works
  return <div>מחובר</div>;
}
// Usage in a 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 || "");
    // Here you could send to Sentry or a similar service
  },
};

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

# Create a repository on 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 - with analytics
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 חייב להכיל tags html ו-body כי הלייאאוט לא זמין.

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