שני דברים ש-SEOK צריך מהשרת של האתר ושאי אפשר לעשות מבחוץ: קובץ מפתח של IndexNow בשורש הדומיין, ותגית אימות של Google Search Console ב-<head> של כל עמוד. שניהם מיושמים כפרמטרים ב-CRM_params שהמנוע כותב דרך system/seo_runner והליבה מגישה בלי שום קובץ פיזי. הקוד: system/libraries/SeoIndexNow.php, system/libraries/SeoVerification.php, system/collections/ROUTER.php, system/controllers/seo_indexnow.php.
system/ בעמוד הזה הוא תיקיית הליבה הפרוסה (api/core בריפו).
IndexNow: שני מפתחות#
| מפתח המנוע (hosted) | מפתח האתר (legacy) | |
|---|---|---|
| פרמטר | seo_indexnow_engine_key (עם סיומת __p<id> בפלטפורמה משנית) | seo_indexnow_key |
| מי מייצר | SEOK, ומוסר דרך seo_runner/indexnow {key} | SeoIndexNow::key() יוצר bin2hex(random_bytes(16)) בקריאה הראשונה |
| איפה מוגש | https://<host>/{key}.txt מהשורש, דרך ROUTER | https://<host>/system/seo_indexnow/key |
| מי מכריז | המנוע, את ה-diff הלילי של ה-sitemap | SeoIndexNow::cron() כל 15 דקות (מושבת כשהאתר hosted) |
פרוטוקול IndexNow מצמצם הגשה לתיקייה שבה יושב קובץ המפתח. מפתח שמוגש תחת /system/seo_indexnow/ מכסה רק כתובות תחת /system/, ולכן כל כתובת כתבה חזרה עם 422. המפתח של המנוע מוגש מהשורש בדיוק מהסיבה הזו.
הגשת /{key}.txt#
ב-ROUTER::parse_friendly_url(), לפני כל ניתוב אחר:
if (preg_match('/^([0-9a-fA-F]{32})\.txt$/', $url, $kf)) {
if (!class_exists('SeoIndexNow')) @require_once(CONFIG::$core_path . '/libraries/SeoIndexNow.php');
if (class_exists('SeoIndexNow')) {
try { SeoIndexNow::serve_engine_key($kf[1]); } catch (\Throwable $e) { /* never break routing over a key file */ }
}
}
SeoIndexNow::serve_engine_key()#
public static function serve_engine_key(string $key): void
עונה רק אם השם המבוקש שווה בדיוק למפתח של הפלטפורמה הנוכחית (engine_key()). אז: Content-Type: text/plain, Cache-Control: public, max-age=300, X-Robots-Tag: noindex, הגוף הוא המפתח בלבד, ו-die. כל שם אחר של .txt ממשיך לניתוב הרגיל. קובץ פיזי בשם הזה בשורש האתר יוגש על ידי שרת ה-web לפני ש-PHP רץ בכלל.
SeoIndexNow::set_engine_key()#
public static function set_engine_key(string $key): array
מקבל מחרוזת של 32 תווים הקסדצימליים (או "" לניקוי), שומר אותה בפרמטר, מנקה את מטמון params ומחזיר ["ok" => true, "key" => …, "key_url" => …]. כל צורה אחרת נדחית, כי מדובר בשם של קובץ בשורש הדומיין.
public static function engine_key(): string // "" עד שנמסר מפתח
public static function engine_key_url(): string // https://<host>/<key>.txt או ""
חוזה seo_runner/indexnow#
POST /system/seo_runner/indexnow
Authorization: Bearer <api_key>:<secret_key>
Content-Type: application/json
{"key": "0123456789abcdef0123456789abcdef", "ledger": 1}
- עם
key-set_engine_key()רץ קודם; אם הוא נכשל התשובה היא השגיאה שלו. - התשובה:
{ok:true, site_served:false, served:<יש מפתח>, engine_key, engine_key_url, key_url, known:<מספר כתובות שהאתר הכריז בעבר>, last:<תאריך>, by:"engine"}. - עם
ledger:1נוסףhashes[]- עד 20,000 ערכיsha1(url)מטבלתseo_indexnow_sent, כדי שהריצה הראשונה של המנוע לא תכריז את כל הארכיון מחדש.
ההכרזה במסלול הישן#
נשאר בקוד לאתר שעדיין לא עבר למנוע (SeoHosted::enabled() שקר). system/seo_indexnow/run רץ מהתזמון "כל 15 דקות" בקטגוריית המערכת ועובר על כל הפלטפורמות:
SeoIndexNow::inventory()קורא אתrobots.txtשל האתר ואוסף שורותSitemap:; אם אין,/sitemap.xml. נקראים עדMAX_FILES = 12קבצים (אינדקס נפרש רמה אחת), עדMAX_KNOWN = 20000כתובות.- הכתובות מושוות ל-
seo_indexnow_sentלפיsha1(url)בחתיכות של 500. - בריצה הראשונה כל הכתובות נרשמות בלי הכרזה (seeding). בריצות הבאות עד
MAX_SUBMIT = 500כתובות חדשות נשלחות. submit()מבצעPOST https://api.indexnow.org/indexnowעם{host, key, keyLocation, urlList},CURLOPT_TIMEOUT = 12(ללא connect timeout). 200 ו-202 הם הצלחה; 403 = המפתח המוגש לא תואם; 422 = כתובות מחוץ ל-host או מחוץ לתיקיית המפתח.- רק מה שהתקבל נרשם, כך שכישלון יחזור על עצמו בריצה הבאה.
public static function submit(array $urls): array // ["ok"=>bool, "submitted"=>n, "status"=>…] או ["ok"=>false, "error"=>…]
public static function cron(): array // ["ok", "seeded", "submitted", "backlog", "maps"] או שגיאה
הפלט של ה-controller לכל פלטפורמה: OK maps=2 submitted=14 backlog=0, או ERROR: …, ו-CronLogger שומר אותו.
אימות Search Console#
איך זה עובד#
SeoVerification שומר את הטוקן בפרמטר seo_gsc_verification (עם __p<id> בפלטפורמה משנית). SEO::tags_summary(), שנקרא מ-PAGE בראש כל עמוד, מוסיף את SeoVerification::meta_tag():
<meta name="google-site-verification" content="<token>">
גוגל בודק את הכתובת שנתנו לו, ודף הבית הוא העמוד שהכי סביר שיוגש מהמטמון; לכן התגית נמצאת בכל עמוד ולא רק בבית, ולכן set_token() מנקה את מטמון העמודים.
SeoVerification::set_token()#
public static function set_token(string $raw): array
| קלט | התנהגות |
|---|---|
תגית <meta ...> שלמה (מה שגוגל שם בלוח) | הערך של content נחלץ |
| טוקן בלבד | חייב להתאים ל-[A-Za-z0-9_\-]{20,128} |
"" | מנקה את הטוקן |
אחרי השמירה: cache_engine::remove("params"), ואז purge_pages() מרוקן את CONFIG::$cache_folder/<host>/pages ו-pages_mobile של הפלטפורמה (התוכן בלבד, התיקיות נשארות). מחזיר ["ok" => true, "token" => …, "pages_purged" => n] או ["ok" => false, "error" => …].
public static function token(): string // הטוקן השמור או ""
public static function meta_tag(): string // התגית או "" כשאין טוקן
public static function live(): array // מושך את דף הבית (timeout 12s) ובודק אם הטוקן מופיע ב-HTML
public static function status(): array // token_set, hint, service:false, data:false + הסברים
live() קיים בדיוק בגלל המטמון: "הטוקן נשמר" ו"הטוקן מוגש" הם שני דברים שונים, ו-live() מודד את השני.
איך מגדירים#
הדרך הרגילה היא דרך SEOK: המנוע קורא ל-POST /system/seo_runner/verification עם {token}, והתשובה כוללת tag ו-served:true. בלי token בגוף התשובה היא המצב: {ok:true, token, tag, live:{ok, served, checked, error}}.
מקוד של האתר (למשל סקריפט התקנה), אותה ספרייה:
require_once(CONFIG::$core_path . "/libraries/SeoVerification.php");
$r = SeoVerification::set_token('<meta name="google-site-verification" content="AbCdEf...">');
if (!$r["ok"]) error_log("GSC token refused: " . $r["error"]);
status() מפריד בכוונה בין "התגית במקום" לבין "נתוני Search Console זורמים". קריאת נתוני החיפוש דורשת credential ל-API, והיא נעשית במנוע (data תמיד false בליבה). אתר שאומת בעבר דרך הממשק של גוגל לא צריך את התגית בכלל ועדיין יכול להיות מחובר במלואו.מה לבדוק כשמשהו לא עובד#
| תסמין | סיבה סבירה | בדיקה |
|---|---|---|
/{key}.txt מחזיר 404 | המפתח שנשמר שונה מהשם המבוקש, או שנשמר לפלטפורמה אחרת | SeoIndexNow::engine_key() בהקשר של הפלטפורמה; ?platform=<id> ב-seo_runner/indexnow |
/{key}.txt מחזיר תוכן אחר | קובץ פיזי באותו שם בשורש, או כלל rewrite שמונע מ-PHP לקבל .txt | ראו ניתוב |
| IndexNow עונה 403 | הקובץ המוגש לא תואם למפתח שנשלח | curl https://<host>/<key>.txt |
| IndexNow עונה 422 | הכתובות מחוץ ל-host של המפתח, או מפתח תחת תיקייה | המפתח חייב להיות בשורש |
| גוגל לא רואה את התגית | העמוד מוגש ממטמון ישן, או ש-theme לא מדפיס את ה-<head> של PAGE | SeoVerification::live(); ראו מטמון עמודים מלא |
התזמון seo_indexnow/run כותב SKIP | האתר hosted או שאין טבלת seo_findings | זו ההתנהגות הצפויה |