יש באג מסוג מבלבל במיוחד: אתה פותח URL בדפדפן, האפליקציה נראית תקינה, הניווט עובד, אפילו התוכן מופיע, ואז בבדיקה פשוטה עם `curl` אתה מגלה שהשרת מחזיר HTTP 404.
זה קרה בפורטפוליו שלי. נתיבים כמו `/blog/`, `/blog/seo-discovery-is-product-work/`, עמודי case study, contact ו-privacy נראו כאילו הם עובדים כי React Router ידע לרנדר אותם בצד לקוח. אבל מבחינת השרת, crawler, preview של לינק או בדיקת SEO, חלק מהם עדיין היו לא באמת קיימים.
זאת נקודה קטנה בקוד, אבל נקודה גדולה באמון: URL ציבורי צריך להיות מוצר אמיתי, לא רק route ש-React מצליח להציל אחרי שהדף נטען.
הבעיה: SPA יכול להסתיר HTTP שבור
ב-Single Page Application קל מאוד להתבלבל. בזמן פיתוח, כל נתיב עובר דרך dev server שמחזיר את `index.html`, ואז ה-router עושה את שלו. גם בפרודקשן, אם נכנסת דרך עמוד הבית ולחצת על לינק פנימי, האפליקציה יכולה להציג את העמוד הנכון בלי שהשרת אי פעם נדרש להחזיר את הנתיב הזה ישירות.
אבל העולם לא תמיד מגיע דרך עמוד הבית.
משתמש יכול לפתוח קישור ישיר לפוסט. Google יכול לסרוק `/blog/some-post/`. LinkedIn יכול לנסות לשלוף Open Graph metadata. מישהו יכול לשלוח לינק ללקוח, והלקוח יכול לרענן את הדף בדיוק שם.
אם הנתיב מחזיר 404, גם אם ה-app shell בסוף נטען, יש כאן חוזה שבור.
האבחון: render תקין הוא לא אותו דבר כמו response תקין
הדבר החשוב היה להפריד בין שתי שאלות:
- האם React יודע לרנדר את הנתיב?
- האם השרת מחזיר response נכון לנתיב ציבורי?
הראשונה היא שאלת UI. השנייה היא שאלת מוצר, SEO ותשתית.
בבדיקות ראיתי שהדומיין הראשי והדומיין של GitHub Pages לא תמיד התנהגו אותו דבר, ושנתיבים פנימיים יכולים להחזיר 404 למרות שהאפליקציה עצמה עדיין נטענת. זה אומר שצריך לבדוק את ה-HTTP status, את ה-bundle, את ה-canonical, את metadata, ואת sitemap, לא רק "האם זה נראה בסדר בדפדפן".
הפתרון: גם fallback וגם static entries
התיקון התחיל ב-Cloudflare Pages עם `_redirects`:
`/* /index.html 200`
זה נותן ל-SPA fallback תקין כך שנתיבים פנימיים לא ייפלו לפני שהאפליקציה נטענת.
אבל לא עצרתי שם. הוספתי גם סקריפט build בשם `create-static-routes.mjs` שיוצר `index.html` פיזי לנתיבים ציבוריים חשובים: `/blog/`, כל פוסט בבלוג, case studies, `/contact/` ו-`/privacy/`. אותו סקריפט מייצר גם `sitemap.xml` ומזריק לכל נתיב title, description, canonical ו-Open Graph metadata שמתאימים לו.
המשמעות היא שהנתיבים האלה כבר לא תלויים רק ביכולת של React Router לרנדר אחרי טעינה. יש להם נוכחות סטטית בפרודקשן.
למה metadata הוא חלק מהתיקון
אפשר היה להסתפק ב-"עכשיו זה מחזיר 200". אבל URL ציבורי לא נמדד רק בקוד סטטוס.
כשמשתפים פוסט, ה-preview צריך להציג כותרת ותיאור נכונים. כשמנוע חיפוש קורא את הדף, ה-canonical צריך להצביע לדומיין הראשי `https://evyatarhazan.com/`, לא לדומיין המשני. כשמישהו פותח case study, הוא צריך לקבל metadata של אותו עמוד, לא metadata גנרי של הבית.
לכן הוספתי גם מקור נתונים מרכזי ל-SEO ב-`src/data/site.ts` ו-hook שמעדכן title, description, canonical, Open Graph ו-Twitter metadata בצד לקוח. ה-build נותן שכבה סטטית; ה-client שומר על עקביות בזמן ניווט.
איך אימתתי שזה באמת עובד
הבדיקה לא נגמרה ב-`npm run validate`. הרצתי גם בדיקות מול הפרודקשן.
בדקתי שנתיבים כמו `/blog/`, `/blog/seo-discovery-is-product-work/`, `/projects/nis_boutique/`, `/contact/`, `/privacy/`, `sitemap.xml`, `robots.txt` ו-`ads.txt` מחזירים HTTP 200 בדומיין הראשי. בדקתי שגם GitHub Pages מחזיר 200 לנתיבי blog/project שנבדקו. ובדקתי שה-canonical וה-social metadata כבר מצביעים לדומיין הממותג.
זה היה ההבדל בין "נראה שזה עובד" לבין "אפשר לשלוח את הקישור הזה בביטחון".
מה למדתי
ב-SPA, route שעובד אחרי hydration הוא לא בהכרח URL תקין.
אם הנתיב ציבורי, צריך להתייחס אליו כמו לנכס מוצרי: סטטוס HTTP תקין, metadata נכון, canonical נכון, sitemap, fallback, ובדיקה מול הדומיינים האמיתיים. אחרת האתר יכול להיראות תקין בעין אנושית ועדיין להיראות שבור לכלים שמחליטים אם הקישור אמין.
הלקח המעשי שלי הוא פשוט: לא מספיק לבדוק שהאפליקציה יודעת להגיע לעמוד. צריך לבדוק שהעולם החיצוני יודע להגיע אליו גם.