תזמון ותצוגה מקדימה

איך רשומה מתוזמנת עולה לאוויר ויורדת ממנו בזמן, איך נוצרים קישורי תצוגה מקדימה חתומים, ומה תבנית או בקר של אתר צריכים לדעת כדי שהתצוגה תעבוד.

⏱ 12 דק' קריאה 2083 מילים

אחרי שלרשומה יש סטטוס (סטטוס וטיוטות), צריך לענות על שתי שאלות: מי מעביר רשומה מתוזמנת ל"מפורסם" בדיוק בזמן, ואיך רואים טיוטה על האתר האמיתי לפני שהיא פתוחה לציבור. העמוד הזה מסביר את המתזמן ואת קישור התצוגה המקדימה, ואת הכללים שתבנית או בקר של אתר צריכים לכבד. הם זמינים מגרסת הליבה 5.0.155 (בדקו ב-system/core/version.txt).

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

המתזמן#

מה שמגיע זמנו רץ במקום אחד, CONTENT_WORKFLOW::run_due(), ומופעל משתי דלתות. מה שהמתזמן עושה בכל ריצה:

  1. רשומות בסטטוס scheduled ש-wz_publish_at שלהן הגיע (עד 200 לטבלה בריצה) הופכות ל-published.
  2. רשומות published ש-wz_unpublish_at שלהן הגיע (עד 200 לטבלה) הופכות ל-hidden.
  3. טיוטות מתוזמנות של רשומות שבאוויר (עד 50 בריצה) מתפרסמות, כשהליבה מריצה את הטופס בשם המנהל שתזמן אותן, כך שההרשאות והמעקב ביומן נשארים שלו.

כל מעבר שולח אירוע status עם source = schedule (אירועי תוכן, יומן ביקורת) ומנקה את המטמון של הרשומה. טיוטה שאי אפשר לפרסם (מישהו שמר גרסה חיה חדשה יותר, שגיאה בטופס, או מנהל שנמחק מאז) חוזרת לסטטוס review ונרשמת ב-error_log, כדי שאדם יראה אותה ב"ממתינים ומתוזמנים" ולא ינסו לפרסם אותה בכל דקה מחדש.

כל הזמנים הם לפי השעון של PHP (date()), שהוא אותו שעון שבו המנהל הקליד את התאריך, ולא NOW() של שרת מסד נתונים שיושב אולי באזור זמן אחר.

הדלת הראשונה: משימת cron#

במסך משימות מתוזמנות קיימת משימה מערכת בשם "תוכן: פרסום והסתרה מתוזמנים" (בקטגוריית "מערכת", שמסונכרנת מהמאסטר בכל עדכון גרסה):

שדהערך
urlsystem/content_workflow_cron/run
crontime* * * * * (כל דקה)

הבקר content_workflow_cron דורש אחד משלושה: הרצה משורת פקודה (CLI), הטוקן tk (ש-cron_runner מוסיף לכל משימה), או מנהל מחובר. פלט הריצה:

OK published=2 hidden=0 drafts=1 drafts_failed=0 next_due=2026-11-01 08:00:00

כשריצה אחרת כבר פעילה, הפלט הוא SKIP: another run is in progress. הנעילה היא נעילת MySQL אחת לכל מסד ולכל קידומת טבלאות (GET_LOCK), כך שגם שתי דלתות שנפתחות יחד לא מפרסמות פעמיים.

הדלת השנייה: הבדיקה העצלה#

לא בכל אתר ה-cron רץ. לכן core.php קורא ל-CONTENT_WORKFLOW::boot() בראש כל בקשה, לפני מטמון העמודים, והוא עושה בדיקה אחת בלבד: משווה את הזמן הנוכחי לזמן הקרוב ביותר שמשהו מחכה לו. כשהזמן עבר, הוא מפעיל את אותה ריצה ברקע (MISC::ping("system/content_workflow_cron/run")) ולא ממתין לה, כך שהמבקר לא מרגיש.

הזמן הקרוב ביותר שמור כזמן השינוי של קובץ אחד, content_next_due בתיקיית המטמון של האתר (CONFIG::$cache_folder), ולא בפרמטר. זה מכוון: הבדיקה רצה לפני שהפרמטרים נטענים ולפני שמטמון העמודים עונה, וכתיבה לטבלת הפרמטרים בכל תזמון הייתה מרוקנת את מטמון הפרמטרים. העלות בבקשה היא קריאת stat אחת.

פרטים שחשוב להכיר:

  • בקובץ שחסר, הבקשה הראשונה מריצה פעם אחת וכותבת אותו (רחוק בעתיד אם שום דבר לא מחכה).
  • הבקשה שמפעילה "תופסת" את הקובץ לשתי דקות, כדי שבקשות שמגיעות מיד אחריה לא יפעילו גם הן. הריצה כותבת את הזמן האמיתי בסופה.
  • אתר שאין בו תיקיית מטמון כתיבה, אין לו דלת עצלה, והתזמון תלוי ב-cron.
  • בדלת העצלה הדיוק הוא של הביקור הראשון אחרי הזמן. אתר עם cron פעיל מדויק עד הדקה.
  • בקשת CLI (משימת cron, ping) לא מפעילה את הבדיקה.

כשמתזמנים (דרך הטופס, set או שמירת טיוטה מתוזמנת), CONTENT_WORKFLOW::note_due() מקרבת את הזמן בקובץ אם התאריך החדש קרוב יותר.

מה נוקה כשרשומה משנה נראות#

פרסום, הסתרה ופרסום טיוטה מנקים מטמון, ללא תלות בדלת. בלי זה עמוד שהוסתר היה נשאר באוויר ב-מטמון העמודים עד שהוא פג. CONTENT_WORKFLOW::clear_cache() מסירה:

  • את כתובת הרשומה, מתוך preview_url של סוג התוכן (pages/{id} נהיה pages/12), ואת הכתובת הידידותית שלה ב-seoUrl. רשומה שהאתר כבר לא מציג נמחקת מיד, בלי להגיש עותק ישן בזמן שהעמוד נבנה מחדש.
  • כשהרשומה נכנסה לאתר או יצאה ממנו: גם את דף הבית ואת sitemap.xml, כי הם מפרטים אותה.

עמוד רשימה שלכם (אינדקס בלוג, קטגוריה) לא נמחק. הוא מתרענן לפי חיי המטמון שלו עצמו. אם הוא חייב להתעדכן מיד, נקו אותו בעצמכם עם cache_engine.

תזמון נראה כך מהקוד

אין שיטה נפרדת לתזמון. מתזמנים עם CONTENT_WORKFLOW::set_status($table, $ids, "scheduled", ["publish_at" => "2026-11-01 08:00:00"]), או בהסתרה עתידית עם ["unpublish_at" => ...] על רשומה מפורסמת. דרך ה-API זו המתודה set של content_status.

תצוגה מקדימה#

תצוגה מקדימה היא כתובת האתר הרגילה של הרשומה עם הפרמטר wz_preview=<טוקן>. הטוקן חתום (MISC::sign, ראו הצפנה וקידוד) ונושא את הטבלה, מזהה הרשומה, השפה, תוקף, ואם זה קישור שיתוף. אי אפשר לשנות אותו ולהפנות לרשומה אחרת.

סוגמי פותחתוקףאיך יוצרים
קישור מנהלמנהל מחובר בלבד (אחרת הקישור לא עושה דבר)24 שעותcontent_status/preview
קישור שיתוףכל מי שמחזיק בקישור48 שעותcontent_status/share, בלחיצה מפורשת של מנהל עם הרשאת עריכה

מה קורה בבקשה עם טוקן תקף:

  1. הסינון נפתח לרשומה אחת. CONTENT_WORKFLOW::query_filter() מוסיף ל-DB::query על אותה טבלה את התנאי OR id = <הרשומה>, כך שטיוטה, רשומה ממתינה או מוסתרת נראית. שאר הרשומות נשארות מסוננות.
  2. הטיוטה מונחת על השורה החיה. get_row ו-get_all של DB::query על הרשומה הזאת מחזירים אותה עם הטיוטה הפתוחה שלה מעליה: עמודות הטבלה הראשית תמיד, ועמודות התוכן המתורגם כשהשפה של השאילתה היא שפת הטיוטה. בקר של האתר שקורא DB::get_val("pages", $id) מקבל את הטיוטה, בלי לשנות בו שורה.
  3. העמוד לא נשמר במטמון. PAGE::$cache_this_page נהיה false, ובדיקת המטמון בתחילת הבקשה מדלגת על כל כתובת עם wz_preview, כך שטיוטה לא תוגש אחר כך לאחרים.
  4. העמוד לא ייסרק. נשלחת הכותרת X-Robots-Tag: noindex, nofollow ונוסף <meta name="robots" content="noindex,nofollow"> בראש ה-<head>.
  5. פס תצוגה מקדימה נוסף בראש ה-<body>: סימון "תצוגה מקדימה" (או "תצוגה מקדימה, לא פורסם"), מצב הרשומה ("טיוטה שלא פורסמה", "מתוזמן ל-..."), ובקישור שיתוף גם עד מתי הוא בתוקף. הכפתורים: חזרה לעריכה ופרסום (למנהל מחובר, והפרסום רק כשהוא רשאי והרשומה עוד לא באוויר), והעתקת קישור (מנהל מקבל קישור שיתוף חדש ל-48 שעות, ואחר מעתיק את הכתובת שבה הוא נמצא). הפס מצויר ב-Shadow DOM, כך שה-CSS של התבנית לא פוגע בו והוא לא פוגע בתבנית.
מה קורה בטוקן לא תקף

טוקן שפג, שנחתם אחרת, או של טבלה שאינה משתתפת לא פותח כלום: הרשומה נשארת מסוננת והעמוד הוא העמוד הרגיל. עם זאת, כל בקשה שנושאת את הפרמטר wz_preview, תקף או לא, לא נשמרת במטמון ומקבלת noindex.

מה קישור התצוגה לא מכסה#

התצוגה המקדימה מונחת על DB::query בלבד. שני דברים בעקבות זה:

  • SQL גולמי (גם עם CONTENT::visible_where()) לא רואה את הרשומה ולא את הטיוטה.
  • קלטים שכותבים לטבלאות אחרות מאשר טבלת הרשומה (למשל גלריות ותגיות) לא נכנסים לטיוטה כערכים, ולכן הם מופיעים רק אחרי הפרסום.

מה תבנית או בקר של אתר צריכים לעשות#

רוב האתרים לא צריכים לעשות כלום. כדי שהתצוגה תעבוד בלי הפתעות:

  • קראו תוכן דרך DB::query / DB::get_val, ולא ב-SQL גולמי. כך הסינון והטיוטה עובדים לבד.
  • אל תשמרו עותק של הרשומה במטמון משלכם (cache_engine::set לפי מזהה רשומה, למשל) בבקשה שיש בה $_GET["wz_preview"]: טיוטה תיכנס לשם ותוגש לכולם. דלגו על המטמון המקומי כשהפרמטר קיים.
  • אל תפנו (redirect) לכתובת "נקייה" בלי הפרמטר. הפניה שמורידה את wz_preview משמיטה את התצוגה ומראה את הגרסה החיה.
  • בנו כתובת canonical מהכתובת הנקייה של הרשומה, לא מ-REQUEST_URI הגולמי. הליבה לא מדפיסה canonical בעצמה (תגיות SEO), ואם התבנית שלכם עושה זאת מה-URI של הבקשה, ה-canonical יצביע על כתובת התצוגה.
  • אל תנסו לעקוף את ה-noindex. PAGE::$robots ממשיך לעבוד כרגיל, ובתצוגה נוספת אליו עוד תגית. אין צורך לשנות אותו.
  • אל תשתמשו בתוכן התצוגה לשום דבר שנשמר (שליחה במייל, ייצוא, עדכון נתון נגזר). זה תוכן שעוד לא אושר.
  • אל תניחו שחלק עליון של הדף פנוי. הפס קבוע בראש הדף ודוחף את התוכן למטה בגובה שלו. אלמנט position: fixed בראש התבנית שלכם עלול להיבלע מאחוריו.
  • בקריאות AJAX של התבנית ל-API משלה (שמביאות תוכן אחרי הטעינה) הפרמטר לא עובר לבד. אם הן צריכות להראות את התצוגה, העבירו אותו אליהן.

ה-API של התזמון והתצוגה#

אלה מתודות של המודול content_status ({admin}/content_status/<method>?pmode=empg), עם אותם כללי הרשאה וחוזה כמו שבעמוד סטטוס וטיוטות. כשל נראה כך: {"success": false, "error": "<code>"}.

preview#

GET {admin}/content_status/preview?tbl=pages&id=12&lang=1&pmode=empg

קישור מנהל, ל-24 שעות. דורש גישה לפאנל של סוג התוכן.

{ "success": true, "url": "https://example.com/about?wz_preview=...", "expires_at": "2026-10-07 14:30:00", "share": false }

הכתובת נבנית מ-CONFIG::$site_url ומהכתובת הידידותית של הרשומה (אם יש לה כתובת ב-seoUrl), אחרת מ-preview_url של סוג התוכן. שגיאות: not_found, no_preview_url (לסוג התוכן אין preview_url), forbidden, type_without_workflow.

share#

POST {admin}/content_status/share   {"tbl": "pages", "id": 12, "lang": 1, "csrf": "..."}

קישור שיתוף, ל-48 שעות, למי שמחזיק בו. דורש הרשאת edit על הפאנל, וטוקן CSRF (מביאים עם csrf). התשובה בצורה של preview, עם "share": true. שיתוף הוא פעולה מפורשת של מנהל: הקישור נוצר רק בלחיצה, ואין קישור שיתוף קבוע.

schedule#

GET {admin}/content_status/schedule?pmode=empg

מתי המתזמן יבדוק שוב, ומצב משימת ה-cron:

{ "success": true, "next_due": "2026-11-01 08:00:00", "cron": { "active": true, "last_run": "2026-10-06 12:01:00", "next_run": "2026-10-06 12:02:00" } }

next_due הוא null כשהקובץ לא נוצר עדיין, ו-cron הוא null כשהמשימה לא קיימת באתר. אם cron.active שווה false או ש-last_run ישן, התזמון עובד רק דרך הדלת העצלה.

סימון הסטטוס ב-MCP#

בכלי list_rows של שרת ה-MCP של האתר (קטלוג כלי ה-MCP), כל שורה בפאנל של סוג תוכן עם סטטוס מקבלת את wz_status (או expired לרשומה שתאריך ההסתרה שלה עבר), את status_label בעברית, את publish_at לרשומה מתוזמנת, את unpublish_at לרשומה מפורסמת שמתוזמנת להיעלם, ואת has_draft כשיש טיוטה פתוחה. הסימון נוסף בעמוד הראשון של התוצאות ללא סינון עמוד. כך סוכן שקורא רשימה לא מניח שטיוטה היא תוכן חי.

ריפרנס: CONTENT_PREVIEW#

המחלקה נמצאת ב-system/collections/CONTENT_PREVIEW.php. ההפעלה של רוב השיטות היא של הליבה (DB, CONTENT_WORKFLOW, core.php), ואתר צריך לקרוא בעיקר ל-token() ול-open_id().

קבועים#

קבועערךמשמעות
CONTENT_PREVIEW::PARAM"wz_preview"שם הפרמטר בכתובת
CONTENT_PREVIEW::ADMIN_TTL86400תוקף קישור מנהל, בשניות
CONTENT_PREVIEW::SHARE_TTL172800תוקף קישור שיתוף, בשניות
public static function link($table, $id, $lang, $share = false): ?array

בונה קישור תצוגה לרשומה. מחזירה ["url" => ..., "expires_at" => "Y-m-d H:i:s", "share" => bool], או null כשלסוג התוכן אין preview_url. הקורא אחראי לבדוק הרשאות (ה-API עושה זאת).

$link = CONTENT_PREVIEW::link("pages", 12, CONFIG::$lang, false);
echo $link["url"];

CONTENT_PREVIEW::token()#

public static function token(): array|false

הטוקן התקף של הבקשה הנוכחית, או false. מחזירה מערך עם המפתחות t (טבלה), i (מזהה הרשומה), l (שפה), e (תפוגה כ-timestamp) ו-s (שיתוף, bool). קישור מנהל מחזיר טוקן רק כשיש מנהל מחובר. בניהול (system_type = admin) היא תמיד false. התשובה נשמרת לבקשה.

if (CONTENT_PREVIEW::token()) {
    // בקשת תצוגה מקדימה: אל תכתבו למטמון משלכם
}

CONTENT_PREVIEW::open_id()#

public static function open_id($table): int

מזהה הרשומה שהסינון פותח לטבלה הזאת בתצוגה תקפה, או 0.

CONTENT_PREVIEW::overlay_row()#

public static function overlay_row($table, $row, $lang): array

מניחה את הטיוטה הפתוחה על שורה של הרשומה שבתצוגה. DB_QUERY_OBJECT קורא לה בעצמו. שורה של רשומה אחרת חוזרת כמו שהיא.

CONTENT_PREVIEW::page()#

public static function page($html)

הפעולה האחרונה על העמוד לפני ההגשה (core.php): מבטלת שמירה במטמון, שולחת noindex, ומוסיפה את הפס. לא נקראת מקוד אתר.

ריפרנס: שיטות המתזמן ב-CONTENT_WORKFLOW#

שאר השיטות של המחלקה מתוארות בסטטוס וטיוטות.

CONTENT_WORKFLOW::run_due()#

public static function run_due(): array

מבצעת את כל מה שהגיע זמנו (ראו "המתזמן"). נועלת, כך שרצה אחת בכל רגע. מחזירה:

["published" => 0, "hidden" => 0, "drafts" => 0, "drafts_failed" => 0, "locked" => false, "next_due" => null]

כש-locked הוא true, ריצה אחרת פעילה ולא נעשה דבר.

CONTENT_WORKFLOW::boot()#

public static function boot(): void

הבדיקה העצלה, שנקראת מ-core.php בכל בקשה. לא זורקת חריגה, לא ממתינה, ולא עושה דבר ב-CLI.

CONTENT_WORKFLOW::refresh_next_due(), next_due() ו-note_due()#

public static function refresh_next_due(): ?string
public static function next_due(): ?string
public static function note_due(...$times): void

refresh_next_due מחשבת מחדש את הזמן הקרוב ביותר מכל הטבלאות והטיוטות וכותבת אותו לקובץ (בלי תזמון: עוד יום). next_due קוראת אותו ("Y-m-d H:i:s", או null כשלא חושב). note_due מקרבת אותו כשנכתב תזמון קרוב יותר, והיא נקראת מתוך set_status, שמירת הטופס ושמירת טיוטה מתוזמנת.

CONTENT_WORKFLOW::workflow_tables()#

public static function workflow_tables(): array

שמות הטבלאות (בלי CRM_) של סוגי התוכן המשתתפים באתר.

CONTENT_WORKFLOW::clear_cache()#

public static function clear_cache($table, $id, $lists = false): void

מסירה את עותקי המטמון של רשומה ושל הכתובת הידידותית שלה. עם $lists = true גם את דף הבית ו-sitemap.xml. נקראת מ-set_status, מפרסום טיוטה ומשמירת הטופס, ולא צריך לקרוא לה ידנית, אלא אם כתבתם מסלול כתיבה משלכם שמשנה wz_status ישירות ב-SQL.

התיעוד נכתב מתוך הקוד של ליבה 5.0.185.