מאות עמודים שגוגל סורק, בלי SSR

ספרייה, קטלוג או גלריה שיושבים בתוך רשימה אחת באפליקציית React הם תוכן שגוגל לא רואה. לפריט בתוך לולאה אין כתובת משלו, אין כותרת, ואי אפשר להגיע אליו מחיפוש. ה-Skill הזה נותן את התבנית שמייצרת עמוד אמיתי לכל פריט מתוך קובץ נתונים, בעזרת סקריפט רינדור מקדים ובלי לעבור ל-SSR ובלי framework חדש. הוא כולל גם את השאלה שרוב האנשים מדלגים עליה, מתי פריט בכלל ראוי לעמוד משלו, כי מאתיים עמודים דלילים פוגעים בכל האתר ולא רק בעצמם. זהו Skill ל-Claude: תיקייה עם קובץ SKILL.md, שנטענת אוטומטית כשהמשימה מתאימה לתיאור שבראש הקובץ. בניגוד לפרומפט שמדביקים בכל פעם מחדש, Skill מותקן פעם אחת וממשיך לעבוד לבד.

בניית אתרים · SEO · React · Vite · רינדור מקדים · קטלוג

מתי הוא נכנס לפעולה

---
name: spa-page-factory
description: "Generate hundreds of unique indexable pages from a data file in a Vite or React SPA, using a prerender script instead of SSR. Use when adding a library, catalog, gallery, glossary or directory to a site, when content lives in one long list with no individual URLs, or when asked to make SPA content visible to Google."
---

# ממערך נתונים למאות עמודים שגוגל סורק

## הבעיה

פריט בתוך `map` הוא לא עמוד. אין לו כתובת, אין לו כותרת, ואי אפשר
להביא אליו אף אחד מחיפוש.

התבנית הזו הפיקה בפרויקט אחד יותר משבע מאות כתובות מארבעה מקורות נתונים,
בלי SSR. הוספת ספרייה נוספת עלתה בקובץ אחד ולולאה אחת.

## שלושה קבצים לכל ספרייה

```
src/library/<name>/index.ts   נתונים בלבד. בלי React
src/lib/<name>-seo.ts          כותרת, תיאור, פסקת פתיחה, פריטים קרובים
src/pages/<Name>Item.tsx       העמוד. משתמש באותן פונקציות
```

**הכלל שחייב להישמר: קובצי הנתונים וה-SEO לא מייבאים React.**

זה לא סגנון. סקריפט הרינדור רץ ב-Node, ואם הקובץ גורר אייקונים או
קומפוננטות עצלות הוא גורר איתם את כל עץ התלויות ונשבר. בלי הכלל הזה
תיאלצו לשכפל את הטקסטים לתוך הסקריפט, והם ייפרדו זה מזה תוך שבוע.

## לטעון את הנתונים מתוך הסקריפט

הנתונים כתובים ב-TypeScript ומשמשים גם את העמוד. במקום לשכפל אותם, לארוז
ולטעון בזמן הבנייה:

```js
async function loadFromSource(entry) {
  const out = join(root, 'scripts/.prerender-data.mjs')
  await build({
    entryPoints: [join(root, entry)],
    bundle: true, platform: 'node', format: 'esm',
    outfile: out, logLevel: 'error',
  })
  const mod = await import(pathToFileURL(out).href + `?t=${Date.now()}`)
  rmSync(out, { force: true })
  return mod
}
```

## הלולאה

```js
for (const item of ITEMS) {
  const path = `/library/${item.slug}`
  writePage(path, pageHtml({
    title: itemTitle(item),
    description: itemDescription(item),
    path,
    heading: { h1: item.name, p: itemIntro(item) },
    extra: `<pre>${esc(item.body)}</pre>`,
    jsonLd: [schemaFor(item), breadcrumbs([...])],
  }))
}
```

`writePage` כותב `dist/<path>/index.html`. שרת סטטי מגיש את הקובץ אם
הוא קיים, ואחרת נופל להפניית ה-SPA. אין צורך בקונפיגורציה.

**להוסיף את הכתובות למפת האתר באותה לולאה**, אחרת נבנו עמודים שאיש לא
יודע עליהם.

## מה הופך עמוד לעמוד ולא לשלד

- **התוכן הייחודי נכתב ל-HTML הסטטי**, ולא רק מוזרק על ידי React. מה
  שהמבקר בא בשבילו חייב להיות שם גם בלי JavaScript.
- **פסקת פתיחה תמיד**, גם כשלפריט יש הסבר משלו.
- **פריטים קרובים וקישורים משלימים.** זה מה שהופך חמש מאות עמודים לרשת
  ולא לחמש מאות מבויי סתום.
- **JSON-LD לפי הסוג:** `HowTo` להוראות, `FAQPage` לשאלות, ותמיד
  `BreadcrumbList`.

## שמות כפולים הם תוכן משוכפל

שני פריטים עם אותו שם מייצרים שני עמודים שמתחרים זה בזה, ואף אחד מהם לא
עולה. הפתרון מבני ולא רשימת תיקונים ידנית:

```js
const mark = item.tags.find((t) => !others.some((o) => o.tags.includes(t)))
if (mark) DISPLAY_NAME.set(item.slug, `${item.name} (${mark})`)
```

## בדיקות ברמת הנתונים

ספרייה מייצרת עמודים, ולכן טעות בנתונים אינה באג בממשק אלא עמוד שבור
שגוגל סורק. הכל בלי דפדפן:

- slug ייחודי
- ההסבר ארוך מספיק לעמוד, מעל מאה ועשרים תווים
- כל קישור לפריט משלים מצביע על משהו שקיים. רשימה ידנית הופכת לקישור מת
  בשקט ברגע ששם משתנה

## כמה עמודים זה יותר מדי

**שישים ושניים פריטים בני מילה אחת אינם שישים ושניים עמודים.** עמוד שכל
תוכנו מילה ושורת הסבר הוא בדיוק התוכן הדליל שפוגע בדירוג של כל האתר.

פריט מקבל עמוד רק אם יש לו מה למלא אותו: הסבר, שימוש, מגבלות, וקישורים
החוצה. אחרת מקומו בעמוד אחד עשיר שאפשר לחפש בתוכו.

איך מתקינים

  1. צרו תיקייה בשם spa-page-factory בתוך ‎.claude/skills/‎ בפרויקט, או בתיקיית הבית לשימוש בכל הפרויקטים.
  2. שמרו בתוכה את הקובץ בשם SKILL.md.
  3. זהו. אין צורך להפעיל כלום, והוא ייטען לבד כשהמשימה תתאים לתיאור.