# מדריך אפליקציית סרטים ב-Next.js

> המדריך בונה אתר סרטים בן שלושה עמודים עם Next.js 15 ו-App Router ב-JavaScript רגיל. רכיבי שרת קוראים ל-amodb עם fetch ו-revalidate, המפתח חי רק במשתנה סביבה של השרת, ו-next.config.js מתיר את מארח הפוסטרים img.amodb.dev. שלושת העמודים הם סרטים פופולריים, חיפוש ופרטי סרט. זה לוקח כ-25 דקות.

## מה בונים

בונים אתר סרטים קטן עם שלושה עמודים: רשימת טרנדים בעמוד הבית, עמוד חיפוש ועמוד סרט עם ציונים של IMDb, Kinopoisk ו-TMDB. הוא משתמש ב-Next.js 15 עם App Router, שדורש Node.js 18.18 ומעלה, וב-JavaScript רגיל. רכיבי שרת קוראים ל-amodb, ולכן המפתח אף פעם לא מגיע לדפדפן. תכננו כ-25 דקות.

## שלב 1: קבלת מפתח API ושמירתו בשרת

בחרו מסלול בעמוד [תמחור](https://amodb.dev/he/pricing/): 20, 40 או 120 בקשות לדקה, עם נתונים זהים. צרו מפתח בלוח הבקרה של החשבון ושימו אותו בקובץ `.env.local` בשורש הפרויקט:

```bash
AMODB_KEY=YOUR_API_KEY
```

אל תוסיפו לשם את הקידומת `NEXT_PUBLIC_`. Next.js מעתיק משתנים עם הקידומת הזו לחבילה של הדפדפן, ושם רגיל נשאר בשרת. הפעילו מחדש את שרת הפיתוח אחרי שמשנים את הקובץ. עמוד [אימות](https://amodb.dev/he/authentication/) מתאר את צורות הכותרת והשאילתה של המפתח.

## שלב 2: יצירת הפרויקט

צרו את `package.json`, התקינו את החבילות והוסיפו את מארח התמונות ל-`next.config.js`.

```json
{
  "name": "movie-finder",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "^15.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}
```

התקינו את החבילות:

```bash
npm install
```

פוסטרים נמצאים ב-`img.amodb.dev`, ו-`next/image` טוען רק מארחים שרשמתם. תבנית `pathname` מגבילה אותו לנתיב הפוסטרים.

```js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'img.amodb.dev', pathname: '/t/p/**' },
    ],
  },
};

module.exports = nextConfig;
```

## שלב 3: כתיבת פונקציית העזר ל-fetch

כל עמוד עובר דרך פונקציה אחת ב-`lib/amodb.js`. היא מוסיפה את כותרת Bearer, פסק זמן של 10 שניות ואת האפשרות `revalidate`, שאומרת ל-Next.js עד מתי מותר לו להשתמש שוב בתשובה. Next.js 15 לא שומר `fetch` במטמון כברירת מחדל, ולכן האפשרות הזו היא שמפעילה את המטמון.

```js
const BASE = 'https://api.amodb.dev/3';

export async function amodb(path, { params = {}, revalidate = 21600 } = {}) {
  const key = process.env.AMODB_KEY;
  if (!key) throw new Error('AMODB_KEY is not set');

  const url = new URL(BASE + path);
  for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);

  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url.toString(), {
      headers: { Authorization: `Bearer ${key}` },
      next: { revalidate },
      signal: AbortSignal.timeout(10000),
    });

    if (res.status === 429 && attempt < 2) {
      const seconds = Math.min(Number(res.headers.get('retry-after')) || 5, 10);
      await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
      continue;
    }
    if (res.status === 401) throw new Error('amodb rejected the API key (401)');
    if (res.status === 404) return null;
    if (!res.ok) throw new Error(`amodb returned ${res.status}`);
    return res.json();
  }
}
```

הנתיב מתחיל בלוכסן ומתווסף לבסיס, ולכן `/3` נשאר בכתובת. תגובת 404 מחזירה `null`, כדי שעמוד יוכל לקרוא ל-`notFound()`. הניסיון החוזר ממתין 10 שניות לכל היותר, כי מבקר מחכה לעמוד.

## שלב 4: בניית העמודים

התחילו מהפריסה. היא כוללת את טופס החיפוש, טופס `GET` רגיל ששולח את `q` אל `/search`.

```js
import Link from 'next/link';

export const metadata = {
  title: 'Movie finder',
  description: 'Trending movies and search, powered by the amodb API.',
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body style={{ margin: 0, fontFamily: 'system-ui, sans-serif' }}>
        <header style={{ padding: 16, background: '#111', color: '#fff' }}>
          <Link href="/" style={{ color: '#fff', marginRight: 16 }}>Movie finder</Link>
          <form action="/search" style={{ display: 'inline' }}>
            <input name="q" placeholder="Search movies" />
            <button type="submit">Search</button>
          </form>
        </header>
        <main style={{ maxWidth: 960, margin: '0 auto', padding: 16 }}>{children}</main>
      </body>
    </html>
  );
}
```

רשת הפוסטרים ב-`components/MovieGrid.js` משרתת את עמוד הבית ואת עמוד החיפוש. כתובת פוסטר היא `https://img.amodb.dev/t/p/w342` ועוד הערך של `poster_path`. הגדלים בעמוד [תמונות](https://amodb.dev/he/images-api/).

```js
import Image from 'next/image';
import Link from 'next/link';

export default function MovieGrid({ movies }) {
  if (movies.length === 0) return <p>No movies found.</p>;
  return (
    <ul style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 16, padding: 0, listStyle: 'none' }}>
      {movies.map((movie) => (
        <li key={movie.id}>
          <Link href={`/movie/${movie.id}`}>
            {movie.poster_path && (
              <Image
                src={`https://img.amodb.dev/t/p/w342${movie.poster_path}`}
                alt={movie.title}
                width={342}
                height={513}
                style={{ width: '100%', height: 'auto' }}
              />
            )}
            <strong>{movie.title}</strong>
          </Link>
          <div>{(movie.release_date || '').slice(0, 4)}</div>
        </li>
      ))}
    </ul>
  );
}
```

עמוד הבית ב-`app/page.js` הוא רכיב שרת אסינכרוני. הוא קורא את הסרטים הפופולריים של השבוע, כמתואר בעמוד [טרנדים ורשימות](https://amodb.dev/he/trending-lists/).

```js
import MovieGrid from '../components/MovieGrid';
import { amodb } from '../lib/amodb';

export default async function Home() {
  const data = await amodb('/trending/movie/week', {
    params: { language: 'en-US' },
    revalidate: 1800,
  });
  return (
    <section>
      <h1>Trending this week</h1>
      <MovieGrid movies={data.results.slice(0, 18)} />
    </section>
  );
}
```

ב-Next.js 15, `searchParams` ו-`params` הם promises, ולכן כל עמוד ממתין להם עם await. עמוד החיפוש ב-`app/search/page.js` משתמש בנקודת הקצה של [חיפוש](https://amodb.dev/he/search/).

```js
import MovieGrid from '../../components/MovieGrid';
import { amodb } from '../../lib/amodb';

export default async function SearchPage({ searchParams }) {
  const { q } = await searchParams;
  const query = (Array.isArray(q) ? q[0] : (q ?? '')).trim();
  if (!query) return <p>Type a title in the box above.</p>;

  const data = await amodb('/search/movie', {
    params: { query, language: 'en-US' },
    revalidate: 600,
  });
  return (
    <section>
      <h1>Results for {query}</h1>
      <MovieGrid movies={data.results} />
    </section>
  );
}
```

עמוד הסרט ב-`app/movie/[id]/page.js` מבקש סרט אחד עם `append_to_response=ratings`, ולכן פרטים וציונים מגיעים בקריאה אחת. כל מקור הוא `{ value, votes }` או `null`. מקור `null` אומר שהציון לא ידוע, ו-`votes` שהוא `null` מסתיר את מספר ההצבעות, ולכן העמוד כותב "לא ידוע" או מדפיס רק את הערך. עבור סרט 550 הוא מציג `8.8 (2,669,989 votes)` עבור IMDb, `8.7` עבור Kinopoisk ו-`8.437 (33,039 votes)` עבור TMDB.

```js
import Image from 'next/image';
import { notFound } from 'next/navigation';
import { amodb } from '../../../lib/amodb';

function score(source) {
  if (!source) return 'not known';
  const votes = source.votes ? ` (${source.votes.toLocaleString('en-US')} votes)` : '';
  return `${source.value}${votes}`;
}

export default async function MoviePage({ params }) {
  const { id } = await params;
  if (!/^\d+$/.test(id)) notFound();

  const movie = await amodb(`/movie/${id}`, {
    params: { language: 'en-US', append_to_response: 'ratings' },
  });
  if (!movie) notFound();

  return (
    <article style={{ display: 'flex', gap: 24, flexWrap: 'wrap' }}>
      {movie.poster_path && (
        <Image src={`https://img.amodb.dev/t/p/w500${movie.poster_path}`} alt={movie.title} width={300} height={450} priority />
      )}
      <div style={{ flex: 1, minWidth: 260 }}>
        <h1>{movie.title} ({(movie.release_date || '').slice(0, 4)})</h1>
        {movie.tagline && <p><em>{movie.tagline}</em></p>}
        <p>{movie.overview}</p>
        <ul>
          <li>Runtime: {movie.runtime ? `${movie.runtime} min` : 'not known'}</li>
          <li>IMDb: {score(movie.ratings?.imdb)}</li>
          <li>Kinopoisk: {score(movie.ratings?.kinopoisk)}</li>
          <li>TMDB: {score(movie.ratings?.tmdb)}</li>
        </ul>
      </div>
    </article>
  );
}
```

## שלב 5: טיפול בשגיאות ובמגבלות קצב

פונקציית העזר זורקת שגיאה בכשלים, ו-Next.js מציג את קובץ `error.js` הקרוב ביותר.

| סטטוס      | משמעות                   | מה האפליקציה עושה                                              |
| ---------- | ------------------------ | -------------------------------------------------------------- |
| 401        | המפתח חסר, שגוי או מושעה | פונקציית העזר זורקת, `error.js` מוצג, והסיבה ביומן השרת.       |
| 402        | תקופת המסלול הסתיימה     | אותו נתיב. לחדש בעמוד [תמחור](https://amodb.dev/he/pricing/).  |
| 404        | אין רשומה למזהה הזה      | פונקציית העזר מחזירה `null` והעמוד קורא ל-`notFound()`.        |
| 429        | מעל הבקשות שלכם לדקה     | להמתין `Retry-After` שניות, עד 10, לנסות שוב פעמיים ואז לזרוק. |
| 500 או 504 | כשל בצד השרת             | פונקציית העזר זורקת ו-`error.js` מוצג.                         |

גבול שגיאה חייב להיות רכיב לקוח. בייצור Next.js מסתיר את הודעת השגיאה מהדפדפן ומדפיס אותה ביומן השרת, ולכן מפתח שנדחה מופיע בטרמינל שלכם ולא בעמוד. צרו את `app/error.js` ואת `app/not-found.js`:

```js
'use client';

export default function ErrorPage({ reset }) {
  return (
    <div>
      <h2>The movie service did not answer</h2>
      <p>Try again in a moment.</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}
```

עמוד ה-not-found הוא רכיב אחד:

```js
export default function NotFound() {
  return <p>That movie does not exist.</p>;
}
```

עמוד [שגיאות](https://amodb.dev/he/errors/) מפרט כל קוד סטטוס, ו[מגבלות קצב](https://amodb.dev/he/rate-limits/) מסביר את הכותרות.

## שלב 6: מטמון עם revalidate

ערכי `revalidate` מעתיקים את משכי מטמון הקצה, ולכן עמוד אף פעם לא מתרענן מהר יותר ממה שהנתונים משתנים. פגיעה במטמון הנתונים של Next.js לא קוראת ל-amodb, ולכן גם לא צורכת בקשות לדקה.

| עמוד        | `revalidate` בשניות | משך מטמון הקצה |
| ----------- | ------------------- | -------------- |
| בית, טרנדים | 1800                | 30 דקות        |
| חיפוש       | 600                 | 10 דקות        |
| פרטי סרט    | 21600               | 6 שעות         |

לעמוד הבית אין קלט דינמי, ולכן `next build` מרנדר אותו פעם אחת ומרענן אותו ברקע כל 30 דקות. הבנייה הזו קוראת ל-amodb, ולכן `AMODB_KEY` חייב להיות מוגדר גם בסביבת הבנייה.

## שלב 7: הרצה ובדיקה

```bash
npm run dev
```

פתחו את הכתובת המקומית ש-Next.js מדפיס. עמוד הבית מציג פוסטרים, תיבת החיפוש מוצאת כותרים ופוסטר פותח את עמוד הסרט שלו. להרצת ייצור השתמשו ב-`npm run build` וב-`npm run start`. בפרויקט יש עכשיו את הקבצים האלה:

| קובץ                               | תפקיד                                                    |
| ---------------------------------- | -------------------------------------------------------- |
| `.env.local`                       | המפתח, בשרת בלבד.                                        |
| `next.config.js`                   | מתיר את `img.amodb.dev` עבור `next/image`.               |
| `lib/amodb.js`                     | פונקציית עזר ל-fetch עם פסק זמן וטיפול ב-401, 404 ו-429. |
| `app/layout.js`                    | מעטפת העמוד וטופס החיפוש.                                |
| `app/page.js`                      | רשימת הטרנדים.                                           |
| `app/search/page.js`               | תוצאות חיפוש.                                            |
| `app/movie/[id]/page.js`           | פרטי סרט עם דירוגים.                                     |
| `app/error.js`, `app/not-found.js` | עמודי כשל ומזהה חסר.                                     |

## הצעדים הבאים

- הוסיפו פונקציית `generateMetadata` לעמוד הסרט כדי שלכל כותר תהיה כותרת עמוד משלו. Next.js משתמש שוב באותה קריאת fetch.
- הציגו שחקנים עם `append_to_response=credits`, כמתואר בעמוד [קרדיטים](https://amodb.dev/he/credits/).
- החליפו את `language` לשפת המבקר, עם 14 שפות הבסיס שבעמוד [שפות](https://amodb.dev/he/languages/).
- קראו את אותה משימה לטלפון ב-[מדריך Swift](https://amodb.dev/he/swift-movie-api/).

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

### האם מפתח ה-API יכול לדלוף לדפדפן ב-Next.js?

רק אם חושפים אותו. השאירו את AMODB_KEY בלי הקידומת NEXT_PUBLIC_ וקראו ל-amodb מרכיבי שרת, ממטפלי נתיבים או מפעולות שרת. לעולם אל תייבאו את פונקציית העזר של fetch לקובץ שמתחיל ב-use client.

### למה next/image צריך remotePatterns בשביל הפוסטרים?

הרכיב Image מייעל רק תמונות ממארחים שאתם מתירים. הוסיפו את img.amodb.dev עם הנתיב /t/p/** ל-remotePatterns ב-next.config.js, אחרת הרכיב זורק שגיאה לכל כתובת פוסטר.

### כמה זמן revalidate צריך להיות בכל עמוד?

בהתאם למטמון הקצה: 1800 שניות לרשימות טרנדים, 600 לחיפוש ו-21600 לפרטי סרט. ערך קצר יותר רק מוסיף קריאות שמחזירות אותם נתונים וצורכות בקשות לדקה.

### מה קורה כש-amodb עונה 429?

פונקציית העזר קוראת את Retry-After, ממתינה עד 10 שניות ומנסה שוב פעמיים. אם המגבלה עדיין חלה היא זורקת שגיאה, ו-error.js מציג עמוד עם כפתור ניסיון חוזר במקום רשימה שבורה.

### האם צריך TypeScript לאפליקציה הזו?

לא. כל קובץ כאן הוא JavaScript רגיל עם JSX בקבצי .js. יכולות ה-App Router פועלות באותו אופן, ואפשר להמיר קבצים ל-TypeScript מאוחר יותר בלי לשנות אף קריאה ל-amodb.

## עמודים קשורים

- [מדריך API לסרטים ב-Swift עם SwiftUI](https://amodb.dev/he/swift-movie-api/): בנו אפליקציית SwiftUI ב-Swift 5.9 ל-iOS 16 שמציגה סרטים פופולריים עם פוסטרים בעזרת URLSession ו-async/await ו-AsyncImage, ולמדו למה המפתח שייך לשרת.
- [מדריך API לסרטים ב-JavaScript](https://amodb.dev/he/javascript-movie-api/): כתבו סקריפט Node.js שטוען פרטים, קרדיטים ודירוגים לרשימת מזהי סרטים בבקשה אחת לכל סרט עם append_to_response, עם מטמון TTL, ניסיון חוזר ב-429 ובלי תלויות.
- [API חיפוש](https://amodb.dev/he/search/): API חיפוש עם אותם נתיבים ו-JSON כמו TMDB v3. מצאו סרטים, סדרות, אנשים, אוספים, חברות ומילות מפתח לפי טקסט, עם סינון לפי שנה ועימוד של 20 תוצאות בעמוד.
- [נקודות קצה של מגמות ורשימות](https://amodb.dev/he/trending-lists/): API סרטים פופולריים עם נתיבי TMDB v3: טבלאות יומיות ושבועיות, ורשימות מוקרנים עכשיו, פופולריים, מדורגים וצפויים לסרטים ולסדרות, במטמון 30 דקות.
- [API תמונות ושרת תמונות](https://amodb.dev/he/images-api/): API תמונות סרטים עם נתיבי TMDB v3: הציגו פוסטרים, תמונות רקע, לוגואים ותמונות סצנה לכל כותר, וטענו אותם מ-img.amodb.dev ב-10 גדלים בלי מפתח.
- [אימות](https://amodb.dev/he/authentication/): איך שולחים את מפתח amodb: api_key בשאילתה, כותרת Authorization Bearer או apikey בקריאות OMDb. פורמט המפתח, שמירה, החלפה ושגיאות 401 שכדאי להכיר.

עודכן: 2026-10-18. Source: https://amodb.dev/he/nextjs-movie-app/
