חבילת ה-SEO (SEOK)

להבין מה נשאר מחבילת ה-SEO בליבה אחרי המעבר למנוע המאוחסן SEOK, מה כל ספריית Seo* עושה, אילו טבלאות ותזמונים קיימים ואילו נתונים יוצאים מהאתר.

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

חבילת ה-SEO של WIZZO CMS נקראת SEOK. מאז ליבה 5.0.59 (ובאופן מלא מ-5.0.64) המנוע עצמו - זחילה, קריאת Search Console, אנליסט ה-AI, הדוחות, התזמונים, הדשבורד וכלי ה-MCP - רץ בשרת מאוחסן בכתובת https://seok.wizzo.market. באתר נשאר "סוכן אתר" (system/seo_runner) שעושה רק את מה שרק השרת של האתר יכול לעשות: בדיקות שדורשות את מסד הנתונים, הצעות תוכן ואישורן, טיקטים, תגית אימות ל-Search Console וקובץ המפתח של IndexNow.

בעמוד הזה system/ הוא תיקיית הליבה הפרוסה (= api/core בריפו) ו-{admin} הוא CONFIG::$admin_url של האתר.

המפרט בשורש הריפו מיושן

הקובץ SEO_COLLECTOR_SPEC.md מתאר את הארכיטקטורה שלפני המעבר ("המוצר נבנה בליבה"). מודל הממצא, החומרה והמצבים שבו עדיין מדויקים ומיושמים ב-SeoFindings, אבל המחלקות SeoCollector, SeoSearchConsole, SeoAnalyst, SeoNetwork ו-SeoCitations שהוא מתאר אינן קיימות ב-api/core. אל תחפשו אותן.

מאוחסן מול מקומי#

המתג הוא פרמטר אחד: CRM_params.seo_engine. כשהערך הוא "hosted", SeoHosted::enabled() מחזירה true וכל הקוד מתנהג כסוכן של המנוע. הערך נקבע רק על ידי SeoHosted::migrate(), אחרי שהאתר נרשם במנוע וייצא אליו את ההיסטוריה המקומית. המעבר מופעל בפעם הראשונה שפותחים את SEOK ב-Wizzo Market עבור האתר (המרקט קורא ל-POST /system/connector/seo_ready).

// מה המצב של האתר הזה?
require_once(CONFIG::$core_path . "/libraries/SeoHosted.php");
if (SeoHosted::enabled()) { /* המנוע המאוחסן מחזיק את הממצאים */ }
$keys = SeoHosted::keys();   // ["api_key"=>…, "secret_key"=>…] או null כשאין מפתחות מהמרקט
$base = SeoHosted::base();   // additional_data.dashboard_url (https בלבד) או https://seok.wizzo.market
מצבseo_engineמי מחזיק ממצאיםמה רץ באתר
hosted"hosted"SEOKseo_runner עונה למנוע; SeoFindings::upsert() ב-capture mode מחזיר ממצאים במקום לשמור
core (לפני מעבר)ריקטבלאות CRM_seo_* מקומיותרק הספריות שבטבלה למטה. שאר המנוע המקומי כבר נמחק מהליבה, כך שבפועל "core mode" הוא מצב מעבר בלבד

הזהות בשני הכיוונים היא מפתחות SEOK של האתר מהמרקט (market_service::settings("seoadvisor")): המנוע קורא לאתר עם Authorization: Bearer <api_key>:<secret_key> (SeoHosted::request_ok()), והאתר קורא למנוע עם אותה כותרת (SeoHosted::call()). ראו Wizzo Market.

הספריות#

כולן ב-system/libraries/, כולן סטטיות, ונטענות עם require_once או $this->load->library("SeoX"). חלקן יורשות wz_model וחלקן לא - אין לכך משמעות מעשית.

ספרייהתפקידנקודות כניסה ציבוריות
SeoFindingsמודל הממצאים: מפתח, חומרה, מצבים, טבלאות, capture modemake_key(), severity(), fix_for(), capture(), upsert(), close_missing_for_checks(), platform_filter(). ראו ממצאים, טיקטים וריצות
SeoContentסריקת כותרות של פאנל תוכן (A2.1/A2.2), הצעות AI ל-meta_title/meta_description, אישור/החלה/ביטול, מדידת השפעהpanel(), set_panel(), columns(), scan(), propose(), propose_query(), propose_pair(), pending(), apply(), revert(), reject(), applied(), impact_summary()
SeoSiteChecksבדיקות שדורשות DB בלבד: טבלת ההפניות CRM_redirections (K6.5 לולאות, K6.6 שרשראות, K6.7 יעד ריק/יחסי)site_only(): array
SeoCannibalכפילויות טקסונומיה (D8) מתוך פאנלי תגיות/קטגוריות; D1.x דרך KAMA נשאר בקוד אך לא נקראtaxonomy_check(), taxonomy_dupes(), norm_taxonomy_name()
SeoBotsטלמטריית זחלנים: 15 UA ידועים נספרים יומית ב-seo_bot_hitstrack() (נקרא מ-PAGE בכל רינדור מלא בצד הלקוח), summary(int $days = 28)
SeoGeoניתוח AI crawlers: ממצא G4 מתוך ספירות הבוטים, פענוח חסימות ב-robots.txt (G3)analyze(array $d), parse_crawlers(string $robots), fetch_files()
SeoTrafficשאילתות אנליטיקה דרך KAMA (MCP sql_query, 200 שורות לקריאה)available(), call(), site_total(), top_pages()
SeoTicketsפתיחת טיקט ב-TODO לממצאavailable(), project_id(), open(string $finding_key, int $project = 0)
SeoVerificationתגית אימות Google Search Console מפרמטר, מוזרקת לכל עמודtoken(), set_token(), meta_tag(), live(), status(). ראו IndexNow ואימות
SeoIndexNowמפתח IndexNow של המנוע מוגש מהשורש כ-/{key}.txt; המסלול המקומי הישן (sitemaps, api.indexnow.org)engine_key(), set_engine_key(), serve_engine_key(), key(), submit(), cron()
SeoHostedהקישור למנוע: מפתחות, בסיס, אימות קריאה נכנסת, קריאות יוצאות, register/export/migrateenabled(), keys(), base(), request_ok(), call(), mcp_call(), register(), export(), migrate()
SeoMcpכלי ה-SEO על דלתות ה-MCP של האתר: רק seo_changes (קריאה בלבד) מקומי, כל seo_* אחר מועבר למנועmanifest(), run(string $tool, array $args, string $who = "MCP")
SeoPlatformאתר רב-דומיינים: בחירת פלטפורמה, שמות פרמטרים עם סיומת __p<id>, פילטר platformall(), multi(), current(), adopt(), focus(), base_url(), param(), key_seed(), filter()

SeoPlatform בקצרה#

באתר עם פלטפורמה אחת כל המתודות הן no-op. באתר רב-פלטפורמות: adopt() קורא ?platform=<id> (רק בפאנל וב-seo_runner, לעולם לא בצד הלקוח), param("seo_gsc_verification") מחזיר seo_gsc_verification__p3 לפלטפורמה 3, key_seed() מחזיר "p3|" ונכנס לחישוב finding_key, ו-filter() מחזיר " AND platform = 3" (לפלטפורמה הראשית: = N OR = 0). ensure_column($table) מוסיף עמודת platform בעצלנות לטבלה שאין לה.

הטבלאות#

כולן עם הקידומת CONFIG::$db_fullprefix (בדרך כלל CRM_), ונוצרות עצמאית בקריאה הראשונה (SeoFindings::ensure_tables(), SeoContent::ensure_table() וכו'), לא דרך סנכרון הסכמה של עדכון הליבה.

טבלהנוצרת ב-תוכן
seo_findingsSeoFindingsממצא אחד לכל finding_key; מצב, חומרה, ראיות, גרסאות כללים
seo_occurrencesSeoFindingsהעמודים של כל ממצא (UNIQUE(finding_key, url_hash))
seo_profileSeoFindings"עובדות" על האתר שנלמדו מתשובות המנהל (topic, fact, signature, source)
seo_runs, seo_metricsSeoFindingsהיסטוריית ריצות ומשיכות GSC - לא נכתבות יותר בליבה, רק מיוצאות
seo_changesSeoContentהצעות תוכן: before_value, after_value, state, approved_by, מדדי לפני/אחרי
seo_bot_hitsSeoBotsday, bot, platform, hits עם UNIQUE(day, bot, platform)
seo_indexnow_sentSeoIndexNowurl_hash, first_sent, platform - מה האתר הכריז בעבר ב-IndexNow

מבנה העמודות המלא נמצא ב-ממצאים, טיקטים וריצות.

חוזה ה-API של system/seo_runner#

הקובץ system/controllers/seo_runner.php. הגוף הוא JSON או $_POST; התשובה תמיד {ok:true,...} או {ok:false,error}; 401 כשאין מפתח מנוע, 403 ל-"admin or engine only". פרמטר ?platform=<id> נבחר ב-constructor דרך SeoPlatform::adopt().

פעולהאימותגוףמה קורה
catalogמנוע-מה האתר מציע: cms_url, core, platforms[], content_panel, panels[], todo, bots, redirects, indexnow, verification, site_checks
site_checksמנוע-מריץ SeoSiteChecks::site_only(), SeoContent::scan() (אם נבחר פאנל), SeoCannibal::taxonomy_check(), SeoGeo::analyze() תחת SeoFindings::capture(true) ומחזיר findings[], checks_run[], log[]
proposeמנוע{finding?, occurrences?, key, offset?}משקף (mirror) את הממצא לטבלאות המקומיות ואז SeoContent::propose*
changesמנוע{key}pending[], applied[]
apply_changes / revert_changes / reject_changesמנוע{key, ids}SeoContent::apply/revert/reject
content_panelמנוע{panel}probe_panel() ואז set_panel()
impactמנוע-SeoContent::impact_summary()
todo_meta / ticketמנוע{key, project?}SeoTickets::open()
agent / agent_runsמנוע{key}שיגור סוכן תיקונים (ai_agents_lib::dispatch_chat_job, slug seo_fixes, approve_writes)
verificationמנוע{token?}SeoVerification::set_token() או מצב נוכחי
indexnowמנוע{key?, ledger?}SeoIndexNow::set_engine_key() ומצב
exportמנוע-SeoHosted::export()
register / migrate / statusמנהל או מנוע-רישום, מעבר, `{engine:"hosted"
המנוע כותב רשומות בזהות מנהל-בוט

כש-seo_runner טוען את SeoContent בקריאת מנוע ללא סשן, הוא קורא ל-SeoContent::set_admin(admin_mcp_tools::ensure_bot_admin()). אם אין מנהל בשם mcp_agent הוא נוצר, עם קבוצה "MCP Agent" בעלת perms = -1. כל כתיבה דרך apply_changes נעשית בזהות הזו, ובטבלת seo_changes העמודה approved_by מקבלת ADMIN::get_id() = 0. מי שמחזיק את api_key:secret_key של SEOK (המנוע, או מי שקרא את system/market_settings.json) יכול להפעיל את כל הפעולות שמסומנות "מנוע". אין פרמטר באתר שמכבה את הדלת הזו מלבד ביטול המוצר במרקט.

תזמונים (cron)#

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

URLתדירותמה קורה היום
system/seo_cron/weeklyשבועימחזיר SKIP: the SEO engine is hosted (SEOK); nothing runs on the site
system/seo_indexnow/runכל 15 דקותSKIP אם אין טבלת seo_findings (ה-SEO מעולם לא היה בשימוש) או אם האתר hosted; אחרת המסלול הישן של הכרזת sitemaps

שתיהן דורשות ?tk=<cron_runner_token> או סשן מנהל, בדיוק כמו שאר משימות המערכת. הן נשארות בליבה כי מחיקת ה-controller הייתה צובעת את השורה באדום בכל אתר בצי.

פאנלי הניהול#

פאנלכתובתטבלהמה עורכים
seo_center/{admin}/seo_centerCRM_seo_centerרשומת מטא לעמוד לפי page_key: is_by_url (1 = הנתיב, 0 = מפתח מ-SEO::set_key), title_include_sitename, meta_title, meta_description, meta_keywords, og_title, og_description, og_image, scripts
Seo/{admin}/SeoCRM_seoUrlכתובות ידידותיות: sysname (הכתובת החדשה), pageurl (הכתובת הפנימית), regularexp. שמירה מנקה את המטמון seoUrl_tbl. ראו כתובות ידידותיות

SEO::tags_summary() (נקרא מ-PAGE בראש כל עמוד) מחפש ב-seo_center לפי הנתיב הנוכחי עם is_by_url = 1, ואם לא נמצא - לפי SEO::$page_key עם is_by_url = 0. ראו SEO למפתחי אתר.

שדה &quot;סקריפטים&quot; ב-seo_center

הטופס מקודד את תוכן השדה ב-btoa() בצד הלקוח ומפענח base64_decode תמיד בצד השרת. btoa נכשל על תווים מחוץ ל-Latin1 (עברית בתוך הסקריפט), ושמירה שלא עברה דרך ה-JS של הטופס (למשל דרך MCP) תעבור base64_decode על טקסט רגיל ותושחת.

הפאנל הישן seo_advisor נמחק מהליבה. שורה שלו עדיין עשויה להופיע ב-adminPanel_panels של אתרים ותיקים; התפריט מפנה אותה ל-/{admin}/wizzo_market?tool=seoadvisor דרך market_service::LEGACY_TOOLS.

MCP#

באתר hosted, SeoMcp::manifest() מפרסם כלי אחד, seo_changes (state: proposed|applied, key?, platform?), קריאה בלבד. כל כלי seo_* אחר שלקוח ישן עדיין קורא בשמו מועבר ל-SeoHosted::mcp_call(). כלי המנוע עצמם מגיעים דרך ה-gateway של המרקט כ-seok_* (external_mcp::EXCLUDED_PRODUCTS מכיל seok, וההחרגה מבוטלת כשהאתר hosted). ראו MCP ב-WIZZO CMS.

מה יוצא מהאתר#

מה נשלחלאןמתיקוד
cms_url (= CONFIG::$env_url), מפתח WizzoAI של האתר, site_name, גרסת ליבהSEOK /api_cms/registerseo_ready / migrateSeoHosted::register()
כל טבלאות seo_* (occurrences עד 50,000, changes עד 2,000) + הפרמטרים seo_gsc_property, seo_gsc_verification, seo_title_suffix, seo_content_panel, seo_brand_terms, seo_gsc_pulled_atSEOK /api_cms/importפעם אחת במעברSeoHosted::export()
ממצאי ה-DB: כתובות, שמות תגיות, כותרות רשומות (A2.1), ספירות בוטיםSEOK (כתשובה ל-site_checks)בכל סריקת מנועseo_runner::site_checks()
כותרות ו-meta קיימים, שאילתות GSC, position, ctrWizzoAI (stream.ai.wizzo.media), במפתח wizzoai של האתרpropose*SeoContent דרך wizzo_ai::json()

הרשימה המלאה של המארחים נמצאת ב-מארחים חיצוניים.

טלמטריית בוטים כותבת ל-DB בכל עמוד
SeoBots::track() מריץ INSERT ... ON DUPLICATE KEY UPDATE על seo_bot_hits בכל רינדור מלא (לא מהמטמון) שמבצע אחד מ-15 הבוטים המוכרים. זחלן אגרסיבי פירושו אלפי כתיבות בדקה על טבלה עם מפתח ייחודי. אין buffer; אם זה כואב, זה המקום לדגום.

מה לקרוא הלאה#

מצאתם טעות או חוסר? תקנו את הדף או פתחו Issue בריפו. התיעוד נכתב מתוך הקוד של ליבה 5.0.115.