חבילת ה-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" | SEOK | seo_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 mode | make_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_hits | track() (נקרא מ-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/migrate | enabled(), 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>, פילטר platform | all(), 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_findings | SeoFindings | ממצא אחד לכל finding_key; מצב, חומרה, ראיות, גרסאות כללים |
seo_occurrences | SeoFindings | העמודים של כל ממצא (UNIQUE(finding_key, url_hash)) |
seo_profile | SeoFindings | "עובדות" על האתר שנלמדו מתשובות המנהל (topic, fact, signature, source) |
seo_runs, seo_metrics | SeoFindings | היסטוריית ריצות ומשיכות GSC - לא נכתבות יותר בליבה, רק מיוצאות |
seo_changes | SeoContent | הצעות תוכן: before_value, after_value, state, approved_by, מדדי לפני/אחרי |
seo_bot_hits | SeoBots | day, bot, platform, hits עם UNIQUE(day, bot, platform) |
seo_indexnow_sent | SeoIndexNow | url_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_center | CRM_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}/Seo | CRM_seoUrl | כתובות ידידותיות: sysname (הכתובת החדשה), pageurl (הכתובת הפנימית), regularexp. שמירה מנקה את המטמון seoUrl_tbl. ראו כתובות ידידותיות |
SEO::tags_summary() (נקרא מ-PAGE בראש כל עמוד) מחפש ב-seo_center לפי הנתיב הנוכחי עם is_by_url = 1, ואם לא נמצא - לפי SEO::$page_key עם is_by_url = 0. ראו SEO למפתחי אתר.
הטופס מקודד את תוכן השדה ב-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/register | seo_ready / migrate | SeoHosted::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_at | SEOK /api_cms/import | פעם אחת במעבר | SeoHosted::export() |
| ממצאי ה-DB: כתובות, שמות תגיות, כותרות רשומות (A2.1), ספירות בוטים | SEOK (כתשובה ל-site_checks) | בכל סריקת מנוע | seo_runner::site_checks() |
| כותרות ו-meta קיימים, שאילתות GSC, position, ctr | WizzoAI (stream.ai.wizzo.media), במפתח wizzoai של האתר | propose* | SeoContent דרך wizzo_ai::json() |
הרשימה המלאה של המארחים נמצאת ב-מארחים חיצוניים.
SeoBots::track() מריץ INSERT ... ON DUPLICATE KEY UPDATE על seo_bot_hits בכל רינדור מלא (לא מהמטמון) שמבצע אחד מ-15 הבוטים המוכרים. זחלן אגרסיבי פירושו אלפי כתיבות בדקה על טבלה עם מפתח ייחודי. אין buffer; אם זה כואב, זה המקום לדגום.מה לקרוא הלאה#
- ממצאים, טיקטים וריצות - מודל הממצא, מכונת המצבים, זרימת ההצעות והאישור.
- IndexNow ואימות - קובץ המפתח בשורש, ההכרזה, תגית Search Console.
- SEO למפתחי אתר - מה קונטרולר ו-theme חייבים לעשות כדי שהמנוע יוכל לעבוד.
- תגיות SEO ו-Schema ו-SEO (רפרנס) - ה-API של
collections/SEO.php.