IndexNow ואימות

לדעת איך האתר מגיש את קובץ המפתח של IndexNow מהשורש, איך עובדת ההכרזה במצב מאוחסן ובמסלול הישן, ואיך תגית האימות של Search Console מוזרקת לכל עמוד.

⏱ 6 דק' קריאה 1089 מילים ערוך דף זה ב-GitHub

שני דברים ש-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 מהשורש, דרך ROUTERhttps://<host>/system/seo_indexnow/key
מי מכריזהמנוע, את ה-diff הלילי של ה-sitemapSeoIndexNow::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 דקות" בקטגוריית המערכת ועובר על כל הפלטפורמות:

  1. SeoIndexNow::inventory() קורא את robots.txt של האתר ואוסף שורות Sitemap:; אם אין, /sitemap.xml. נקראים עד MAX_FILES = 12 קבצים (אינדקס נפרש רמה אחת), עד MAX_KNOWN = 20000 כתובות.
  2. הכתובות מושוות ל-seo_indexnow_sent לפי sha1(url) בחתיכות של 500.
  3. בריצה הראשונה כל הכתובות נרשמות בלי הכרזה (seeding). בריצות הבאות עד MAX_SUBMIT = 500 כתובות חדשות נשלחות.
  4. submit() מבצע POST https://api.indexnow.org/indexnow עם {host, key, keyLocation, urlList}, CURLOPT_TIMEOUT = 12 (ללא connect timeout). 200 ו-202 הם הצלחה; 403 = המפתח המוגש לא תואם; 422 = כתובות מחוץ ל-host או מחוץ לתיקיית המפתח.
  5. רק מה שהתקבל נרשם, כך שכישלון יחזור על עצמו בריצה הבאה.
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> של PAGESeoVerification::live(); ראו מטמון עמודים מלא
התזמון seo_indexnow/run כותב SKIPהאתר hosted או שאין טבלת seo_findingsזו ההתנהגות הצפויה
מצאתם טעות או חוסר? תקנו את הדף או פתחו Issue בריפו. התיעוד נכתב מתוך הקוד של ליבה 5.0.115.