array('Overview', 'Genel bakış'), 'menu_titles' => array('SEO titles', 'SEO başlıkları'), 'menu_alerts' => array('Alerts', 'Uyarılar'), 'menu_sync' => array('Live sync', 'Canlı senkron'), 'menu_settings' => array('Settings', 'Ayarlar'), 'not_connected' => array('This WordPress is not connected to SEOLOK yet. Paste the site ID and token from SEOLOK under Settings.', 'Bu WordPress henüz SEOLOK\'a bağlı değil. SEOLOK\'taki site kimliğini ve token\'ı Ayarlar\'a yapıştırın.'), 'open_settings' => array('Open settings', 'Ayarları aç'), 'open_in_seolok' => array('Open in SEOLOK', 'SEOLOK\'ta aç'), 'failed' => array('SEOLOK could not be reached. Try again in a minute.', 'SEOLOK\'a ulaşılamadı. Bir dakika sonra yeniden deneyin.'), 'forbidden' => array('You are not allowed to do this.', 'Bunu yapmaya yetkiniz yok.'), // SEO titles 'titles_title' => array('SEO titles', 'SEO başlıkları'), 'titles_strip' => array('SEO titles and descriptions suggested by SEOLOK', 'SEOLOK\'un önerdiği SEO başlıkları ve açıklamaları'), 'titles_lead' => array('SEOLOK picks pages that show up on Google but get few clicks and writes a new title and description for them. Review, approve and apply them here.', 'SEOLOK, Google\'da görünen ama az tıklanan sayfaları seçer ve onlar için yeni başlık ve açıklama yazar. Burada inceleyin, onaylayın ve sitede uygulayın.'), 'titles_empty' => array('No SEO title suggestions for this site yet. SEOLOK writes them from your Search Console data; they appear here as soon as there are some.', 'Bu site için henüz SEO başlığı önerisi yok. SEOLOK bunları Search Console verinize göre hazırlar; öneri gelince burada görünür.'), 'detail' => array('Suggestion', 'Öneri detayları'), 'page' => array('Page', 'Sayfa'), 'current_title' => array('Current title', 'Mevcut başlık'), 'current_desc' => array('Current description', 'Mevcut açıklama'), 'current_note' => array('as SEOLOK read it at the last scan', 'SEOLOK\'un son taramada okuduğu'), 'new_title' => array('Suggested SEO title', 'Önerilen SEO başlığı'), 'new_desc' => array('Suggested meta description', 'Önerilen meta açıklama'), 'approval' => array('Status', 'Onay durumu'), 'chars' => array('%d characters', '%d karakter'), 'none' => array('(none)', '(yok)'), 'waiting_text' => array('SEOLOK has not written the text for this page yet; it appears here when it is ready.', 'SEOLOK bu sayfanın metnini henüz yazmadı; hazır olunca burada görünür.'), 'st_proposed' => array('Waiting for approval', 'Onay bekliyor'), 'st_approved' => array('Approved · on the to-do list', 'Onaylandı · yapılacaklar listende'), 'st_applied' => array('Applied on the site', 'Sitede uygulandı'), 'st_rejected' => array('Rejected', 'Reddedildi'), 'st_waiting' => array('Text on its way', 'Metin bekleniyor'), 'all' => array('All', 'Tümü'), 'b_approve' => array('Approve', 'Onayla'), 'b_reject' => array('Reject', 'Reddet'), 'b_reopen' => array('Reopen', 'Yeniden aç'), 'b_apply' => array('Apply on the site', 'Sitede uygula'), 'b_undo' => array('Undo', 'Geri al'), 'serp' => array('Search result preview', 'SERP önizleme'), 'desktop' => array('Desktop', 'Masaüstü'), 'mobile' => array('Mobile', 'Mobil'), 'serp_note' => array('Approximate: Google may shorten or rewrite titles and descriptions.', 'Yaklaşık görünüm: Google başlığı ve açıklamayı kısaltabilir ya da değiştirebilir.'), 'analysis' => array('Checks', 'SEO analizi'), 'c_title_ok' => array('Title length is fine', 'Başlık uzunluğu uygun'), 'c_title_long' => array('Title may be cut off on Google', 'Başlık Google\'da kesilebilir'), 'c_title_short' => array('Title is short', 'Başlık kısa'), 'c_desc_ok' => array('Description length is fine', 'Açıklama uzunluğu uygun'), 'c_desc_long' => array('Description may be cut off', 'Açıklama kesilebilir'), 'c_desc_short' => array('Description is short', 'Açıklama kısa'), 'c_kw_ok' => array('Search term is in the title', 'Arama terimi başlıkta var'), 'c_kw_missing' => array('Search term is not in the title word for word', 'Arama terimi başlıkta birebir yok'), 'c_kw_start' => array('Search term is at the start of the title', 'Arama terimi başlığın başında'), 'c_kw_late' => array('Search term is in the second half of the title', 'Arama terimi başlığın ikinci yarısında'), 'c_kw_desc_ok' => array('Search term is in the description', 'Arama terimi açıklamada var'), 'c_kw_desc_missing' => array('Search term is not in the description word for word', 'Arama terimi açıklamada birebir yok'), 'all_pages' => array('All pages', 'Tüm sayfalar'), 'search_pages' => array('Search pages…', 'Sayfa ara…'), 'search' => array('Search', 'Ara'), 'col_action' => array('Action', 'İşlem'), 'col_updated' => array('Last update', 'Son güncelleme'), 'col_status' => array('Status', 'Durum'), 'no_match' => array('No page matches.', 'Eşleşen sayfa yok.'), 'showing' => array('Showing the first %d.', 'İlk %d tanesi gösteriliyor.'), 'bulk_all' => array('Select all', 'Tümünü seç'), 'menu_fresh' => array('Old posts', 'Eski yazılar'), 'fresh_strip' => array('Old posts: still current?', 'Eski yazılar: hâlâ güncel mi?'), 'fresh_title' => array('Old posts', 'Eski yazılar'), 'fresh_lead' => array('Posts and pages not updated for 18 months. SEOLOK does not decide that a post is current: it reads the page and lists what a person should check (old figures and dates, missing sections, a sharper angle). Mark a post current, or ignore it.', '18 aydır güncellenmeyen yazılar ve sayfalar. SEOLOK bir yazının güncel olduğuna karar vermez: sayfayı okur ve kontrol edilmesi gerekenleri listeler (eski rakamlar ve tarihler, eksik bölümler, daha net bir açı). Yazıyı güncel diye işaretleyin ya da yoksayın.'), 'fresh_left' => array('%d checks left today.', 'Bugün %d kontrol hakkı kaldı.'), 'fresh_empty' => array('No post waits: everything was updated or marked current in the last 18 months.', 'Bekleyen yazı yok: hepsi son 18 ayda güncellendi ya da güncel diye işaretlendi.'), 'fresh_waiting' => array('Waiting', 'Bekleyen'), 'fresh_checked' => array('Checked', 'Kontrol edildi'), 'fresh_ignored' => array('Ignored', 'Yoksayılan'), 'col_post' => array('Post', 'Yazı'), 'col_age' => array('Last updated', 'Son güncelleme'), 'col_signals' => array('Signs', 'İşaretler'), 'fresh_months' => array('%d months ago', '%d ay önce'), 'fresh_year_title' => array('%s in the title', 'Başlıkta %s'), 'fresh_year_text' => array('Text mentions %s', 'Metinde %s'), 'fresh_checking' => array('Being checked…', 'Kontrol ediliyor…'), 'fresh_check_failed' => array('The check could not be written; try again.', 'Kontrol yazılamadı; yeniden deneyin.'), 'fresh_facts' => array('To check and update', 'Kontrol edilip güncellenmesi gerekenler'), 'fresh_sections' => array('Sections worth adding', 'Eklenebilecek bölümler'), 'fresh_angle' => array('A sharper angle', 'Daha net bir açı'), 'fresh_checked_on' => array('checked %s', '%s kontrol edildi'), 'b_fresh_check' => array('Is it still current? Check', 'Güncel mi? Kontrol et'), 'b_fresh_ok' => array('Current, mark it', 'Güncel, işaretle'), 'b_fresh_ignore' => array('Ignore', 'Yoksay'), 'b_fresh_unignore' => array('Bring back', 'Geri getir'), 'r_fresh_started' => array('The check started: the result shows here in a few minutes.', 'Kontrol başladı: sonuç birkaç dakika içinde burada görünür.'), 'r_fresh_ready' => array('This post was checked in the last 30 days; the result is below.', 'Bu yazı son 30 günde kontrol edildi; sonuç aşağıda.'), 'r_fresh_cap' => array('Today\'s checks are used up; try again tomorrow.', 'Bugünkü kontrol hakkı doldu; yarın yeniden deneyin.'), 'r_fresh_ok' => array('Marked current: it comes back in 18 months.', 'Güncel diye işaretlendi: 18 ay sonra yeniden listelenir.'), 'r_fresh_ignored' => array('Ignored: it is under "Ignored" now.', 'Yoksayıldı: artık "Yoksayılan" altında.'), 'r_fresh_back' => array('Back in the list.', 'Yeniden listede.'), 'dash_fresh' => array('Old posts waiting', 'Bekleyen eski yazı'), 'dash_title' => array('SEOLOK: site status', 'SEOLOK: site durumu'), 'dash_open' => array('Open', 'Aç'), 'dash_titles' => array('SEO titles', 'SEO başlıkları'), 'dash_waiting' => array('Waiting for approval', 'Onay bekliyor'), 'dash_ready' => array('Approved, not applied yet', 'Onaylandı, uygulanmadı'), 'dash_applied' => array('Applied', 'Sitede uygulandı'), 'dash_titles_error' => array('SEO titles could not be read from SEOLOK right now.', 'SEO başlıkları şu an SEOLOK\'tan okunamadı.'), 'dash_links' => array('Broken links waiting', 'Bekleyen kırık bağlantı'), 'dash_alerts' => array('Open alerts', 'Açık uyarı'), 'dash_sync' => array('Live sync', 'Canlı senkron'), 'dash_on' => array('On', 'Açık'), 'dash_off' => array('Off', 'Kapalı'), 'dash_state' => array('State', 'Durum'), 'bulk_pick' => array('Select this suggestion', 'Bu öneriyi seç'), 'bulk_with' => array('With the selected:', 'Seçilenlerle:'), 'bulk_count' => array('%d selected', '%d seçili'), 'b_bulk_approve' => array('Approve', 'Onayla'), 'b_bulk_apply' => array('Approve and apply on site', 'Onayla ve sitede uygula'), 'b_bulk_reject' => array('Reject', 'Reddet'), 'bulk_none' => array('Select at least one suggestion first.', 'Önce en az bir öneri seç.'), 'bulk_done' => array('%d suggestion(s) done.', '%d öneri yapıldı.'), 'bulk_same_page' => array('%d skipped: another selected suggestion is for the same page (one title per page).', '%d atlandı: aynı sayfa için seçilmiş başka bir öneri var (bir sayfaya tek başlık).'), 'bulk_state' => array('%d skipped: not ready for this (waiting for text, already applied or rejected).', '%d atlandı: bu işlem için hazır değil (metin bekliyor, zaten uygulanmış ya da reddedilmiş).'), 'bulk_failed' => array('%d could not be done:', '%d yapılamadı:'), 'bulk_cap' => array('%d were not queued (at most %d at a time); select them again when this run is over.', '%d öneri sıraya alınmadı (bir seferde en çok %d); bu işlem bitince yeniden seç.'), 'bulk_queued' => array('%d suggestion(s) queued. They are done one by one, not all at once.', '%d öneri sıraya alındı. Hepsi bir anda değil, tek tek yapılıyor.'), 'bulk_busy' => array('A bulk run is still going. Wait until it is over or stop it first.', 'Bir toplu işlem sürüyor. Bitmesini bekle ya da önce durdur.'), 'bulk_running' => array('Bulk run in progress', 'Toplu işlem sürüyor'), 'bulk_progress' => array('%1$d of %2$d handled: %3$d done, %4$d could not be done', '%2$d öneriden %1$d tanesi ele alındı: %3$d yapıldı, %4$d yapılamadı'), 'bulk_now' => array('Next: %s', 'Sıradaki: %s'), 'bulk_stop' => array('Stop', 'Durdur'), 'bulk_stop_done' => array('The run was stopped. What was done stays done; the rest was left as it was.', 'İşlem durduruldu. Yapılanlar yapıldı olarak kalır; kalanlara dokunulmadı.'), 'bulk_stopped' => array('Stopped: %d suggestion(s) were left untouched.', 'Durduruldu: %d öneriye dokunulmadı.'), 'bulk_note' => array('One suggestion at a time, while this page is open. If you close it, WordPress\'s scheduled jobs (WP-Cron) carry on slowly, one at a time, as your site gets visits.', 'Bu sayfa açıkken öneriler tek tek yapılır. Sayfayı kapatırsan WordPress\'in zamanlı işleri (WP-Cron) sitene girildikçe, yine tek tek devam ettirir.'), 'bulk_js_error' => array('The connection was lost. The run goes on in the background; reload this page to see how far it is.', 'Bağlantı koptu. İşlem arka planda sürer; ne kadar ilerlediğini görmek için sayfayı yenile.'), 'bulk_js_retry' => array('Carry on here', 'Burada devam et'), 'prev' => array('Previous', 'Önceki'), 'next' => array('Next', 'Sonraki'), 'r_done_approve' => array('Approved. SEOLOK has the same decision.', 'Onaylandı. SEOLOK\'ta da aynı karar kayıtlı.'), 'r_done_reject' => array('Rejected.', 'Reddedildi.'), 'r_done_reopen' => array('Back in the list, waiting for approval.', 'Yeniden onay bekleyenler arasında.'), 'r_done_applied' => array('Applied: the page now uses the new title and description.', 'Uygulandı: sayfa artık yeni başlığı ve açıklamayı kullanıyor.'), 'r_next' => array('The next suggestion that waits for you is open.', 'Sıradaki bekleyen öneri açıldı.'), 'r_all_done' => array('No other suggestion waits for approval or applying.', 'Onay ya da uygulama bekleyen başka öneri yok.'), 'r_done_undone' => array('Undone: the page\'s previous title and description are back.', 'Geri alındı: sayfanın önceki başlığı ve açıklaması geri geldi.'), 'r_not_found' => array('This suggestion is no longer in SEOLOK.', 'Bu öneri artık SEOLOK\'ta yok.'), 'r_state' => array('This suggestion changed in the meantime; the list is refreshed.', 'Bu öneri bu arada değişmiş; liste yenilendi.'), 'r_no_text' => array('There is no text to approve yet.', 'Onaylanacak bir metin henüz yok.'), 'r_page' => array('No post, page or category of this WordPress site answers to this address.', 'Bu adrese cevap veren bir yazı, sayfa ya da kategori bu WordPress sitesinde bulunamadı.'), 'r_report' => array('The change was made but SEOLOK could not be told, so it was taken back. Try again in a minute.', 'Değişiklik yapıldı ama SEOLOK\'a bildirilemedi; bu yüzden geri alındı. Bir dakika sonra yeniden deneyin.'), // Alerts 'alerts_title' => array('Alerts', 'Uyarılar'), 'alerts_strip' => array('Warnings when search engines get blocked', 'Arama motorları engellenince uyarı'), 'alerts_lead' => array('Problems that keep your site out of Google, checked here in WordPress.', 'Sitenizi Google\'ın dışında bırakan sorunlar, burada WordPress içinde kontrol edilir.'), 'crit_badge' => array('Critical', 'Kritik uyarı'), 'crit_title' => array('Search engines are blocked right now', 'Arama motoru görünürlüğünüz şu anda engelleniyor'), 'crit_text' => array('Your site tells search engines not to show it. While this lasts your pages can drop out of Google.', 'Siteniz arama motorlarına kendisini göstermemelerini söylüyor. Bu sürdükçe sayfalarınız Google\'dan düşebilir.'), 'what_title' => array('What does this mean?', 'Bu ne anlama geliyor?'), 'what_text' => array('A setting, a robots.txt rule or a "noindex" tag tells Google to stay away. It is often left behind from the time the site was built. Fix the red cards below; the fix shows on Google within days.', 'Bir ayar, bir robots.txt kuralı ya da "noindex" etiketi Google\'a uzak durmasını söylüyor. Çoğu zaman sitenin yapım aşamasından kalır. Aşağıdaki kırmızı kartları düzeltin; düzeltme birkaç gün içinde Google\'a yansır.'), 'all_clear' => array('No blocking found: search engines may visit and index your site.', 'Engelleme bulunmadı: arama motorları sitenizi tarayabilir ve dizine ekleyebilir.'), 'b_allow' => array('Allow search engines', 'Aramaya izin ver'), 'b_recheck' => array('Check again', 'Tekrar kontrol et'), 'b_check_pages' => array('Check the pages', 'Sayfaları kontrol et'), 'b_ignore' => array('Ignore this warning', 'Uyarıyı yoksay'), 'b_unignore' => array('Show again', 'Yeniden göster'), 'ignored' => array('Ignored', 'Yoksayıldı'), 'blocked' => array('Blocking', 'Engelleniyor'), 'clear' => array('Fine', 'Sorun yok'), 'unknown' => array('Not checked', 'Kontrol edilmedi'), 'notice' => array('Check', 'Bakın'), 'robots' => array('robots.txt', 'Robots.txt'), 'robots_bad' => array('robots.txt tells search engines not to crawl the site.', 'robots.txt dosyanız arama motorlarının siteyi taramasını engelliyor.'), 'robots_ok' => array('robots.txt lets search engines in.', 'robots.txt arama motorlarına izin veriyor.'), 'robots_file' => array('From the robots.txt file on the server', 'Sunucudaki robots.txt dosyasından'), 'robots_virtual' => array('Written by WordPress (no robots.txt file on the server)', 'WordPress\'in yazdığı (sunucuda robots.txt dosyası yok)'), 'robots_view' => array('View robots.txt', 'robots.txt dosyasını görüntüle'), 'robots_fix_file' => array('Remove the "Disallow: /" line from the robots.txt file on your server (hosting file manager or FTP).', 'Sunucudaki robots.txt dosyasından "Disallow: /" satırını silin (barındırma dosya yöneticisi ya da FTP).'), 'meta' => array('Meta robots', 'Meta robots'), 'meta_bad' => array('The home page carries a "noindex" tag.', 'Ana sayfada "noindex" etiketi var.'), 'meta_warn' => array('Some key pages carry a "noindex" tag.', 'Bazı önemli sayfalarda "noindex" etiketi var.'), 'meta_ok' => array('No "noindex" on the pages checked.', 'Kontrol edilen sayfalarda "noindex" yok.'), 'meta_never' => array('The pages have not been checked yet. The check opens them like a visitor (a few seconds).', 'Sayfalar henüz kontrol edilmedi. Kontrol, sayfaları bir ziyaretçi gibi açar (birkaç saniye).'), 'meta_failed' => array('The pages could not be opened from inside WordPress (%s). Some hosts block this; check again later.', 'Sayfalar WordPress içinden açılamadı (%s). Bazı barındırmalar buna izin vermez; sonra yeniden deneyin.'), 'checked_at' => array('Checked %s', 'Kontrol: %s'), 'visibility' => array('Site visibility', 'Site görünürlüğü'), 'vis_bad' => array('"Discourage search engines from indexing this site" is ticked in Settings → Reading.', 'Ayarlar → Okuma\'da "Arama motorlarının siteyi dizine eklemesini engelle" işaretli.'), 'vis_ok' => array('Search engines may index the site (Settings → Reading).', 'Arama motorları siteyi dizine ekleyebilir (Ayarlar → Okuma).'), 'setting' => array('Setting', 'Ayar durumu'), 'open_reading' => array('Open Settings → Reading', 'Okuma ayarlarını aç'), 'other_checks' => array('Other technical checks', 'Diğer teknik kontroller'), 'oc_https' => array('The site address is not https.', 'Site adresi https değil.'), 'oc_permalinks' => array('Plain permalinks (?p=123): readable addresses help search.', 'Düz kalıcı bağlantılar (?p=123): okunur adresler aramaya yardım eder.'), 'oc_cron' => array('Scheduled tasks (WP-Cron) are %d minutes late.', 'Zamanlanmış görevler (WP-Cron) %d dakika gecikmiş.'), 'oc_ok' => array('https, permalinks and scheduled tasks are fine.', 'https, kalıcı bağlantılar ve zamanlanmış görevler sorunsuz.'), 'timeline' => array('Alert timeline', 'Uyarı zaman çizelgesi'), 'timeline_empty' => array('Nothing recorded yet. Changes found by the checks on this site appear here.', 'Henüz kayıt yok. Bu sitedeki kontrollerin bulduğu değişiklikler burada görünür.'), 'affected' => array('Affected pages', 'Etkilenen sayfalar'), 'affected_lead' => array('These pages tell Google not to index them.', 'Bu sayfalar Google\'a dizine eklenmemesini söylüyor.'), 'affected_none' => array('None of the pages checked is blocked.', 'Kontrol edilen sayfalardan hiçbiri engelli değil.'), 'not_indexed' => array('Not indexed', 'Dizine eklenmiyor'), 'set_by' => array('Set by', 'Kaynağı'), 'src_yoast' => array('Yoast SEO', 'Yoast SEO'), 'src_rankmath' => array('Rank Math', 'Rank Math'), 'src_seolok' => array('SEOLOK (applied action)', 'SEOLOK (uygulanan iş)'), 'src_reading' => array('WordPress reading setting', 'WordPress okuma ayarı'), 'src_header' => array('Server header (X-Robots-Tag)', 'Sunucu başlığı (X-Robots-Tag)'), 'src_unknown' => array('Theme or another plugin', 'Tema ya da başka bir eklenti'), 'edit_page' => array('Edit', 'Düzenle'), 'n_pages' => array('%d pages', '%d sayfa'), 'ev_vis_bad' => array('Search engines blocked in Settings → Reading', 'Ayarlar → Okuma\'da arama motorları engellendi'), 'ev_vis_ok' => array('Search engines allowed again in Settings → Reading', 'Ayarlar → Okuma\'da arama motorlarına yeniden izin verildi'), 'ev_robots_bad' => array('robots.txt started blocking search engines', 'robots.txt arama motorlarını engellemeye başladı'), 'ev_robots_ok' => array('robots.txt no longer blocks search engines', 'robots.txt artık arama motorlarını engellemiyor'), 'ev_meta_bad' => array('"noindex" found on the home page', 'Ana sayfada "noindex" bulundu'), 'ev_meta_warn' => array('"noindex" found on key pages', 'Önemli sayfalarda "noindex" bulundu'), 'ev_meta_ok' => array('No "noindex" on the pages checked any more', 'Kontrol edilen sayfalarda artık "noindex" yok'), 'ev_by' => array('by %s', '%s tarafından'), 'r_rechecked' => array('Checked again.', 'Yeniden kontrol edildi.'), 'r_ignored' => array('This warning is ignored. You can show it again on its card.', 'Bu uyarı yoksayıldı. Kartından yeniden gösterebilirsiniz.'), 'r_unignored' => array('The warning shows again.', 'Uyarı yeniden gösteriliyor.'), 'nf_s_waiting' => array('Not found', 'Bulunamayan adres'), 'nf_s_hits' => array('%s tries in all', 'toplam %s deneme'), 'nf_s_recent' => array('Seen this week', 'Bu hafta görülen'), 'nf_s_recent_sub' => array('addresses asked for in the last 7 days', 'son 7 günde istenen adres'), 'nf_s_linked' => array('Linked from your pages', 'Kendi sayfanızdan link'), 'nf_s_linked_sub' => array('a page of the site points to them', 'sitenizdeki bir sayfa bu adreslere link veriyor'), 'nf_s_done_sub' => array('sent on with a 301', '301 ile yönlendirildi'), 'nf_tries' => array('%s tries', '%s deneme'), 'nf_from' => array('Came from', 'Nereden geldi'), 'nf_from_unknown' => array('Not known yet (counted before 0.6.8)', 'Henüz bilinmiyor (0.6.8 öncesi sayıldı)'), 'nf_direct' => array('%d typed or bookmarked', '%d doğrudan (yazılarak / yer imi)'), 'nf_linked_by' => array('Linked from (SEOLOK scan)', 'Link veren sayfalar (SEOLOK taraması)'), 'hm_title' => array('SEOLOK', 'SEOLOK'), 'hm_lead' => array('Your site\'s SEO health, jobs and opportunities from SEOLOK, right on the Dashboard.', 'Sitenizin SEO sağlığı, yapılacaklar ve fırsatlar; SEOLOK\'tan, doğrudan panoda.'), 'hm_cta_text' => array('Better rankings: SEOLOK keeps an eye on your site.', 'Daha iyi sıralamalar için SEOLOK sitenizi izliyor.'), 'hm_cta' => array('Open SEOLOK', 'SEOLOK panelini aç'), 'hm_status' => array('Site status', 'Site durumu'), 'hm_state_ok' => array('All is well', 'Genel durum iyi'), 'hm_state_bad' => array('Search engines are blocked', 'Arama motorları engelleniyor'), 'hm_state_bad_sub' => array('See Alerts.', 'Uyarılar\'a bakın.'), 'hm_search_ok' => array('Search engines can reach the site;', 'Arama motorları erişebiliyor;'), 'hm_sync_on' => array('live sync is on.', 'canlı senkron açık.'), 'hm_sync_off' => array('live sync is off.', 'canlı senkron kapalı.'), 'hm_open_overview' => array('Open Overview', 'Genel bakışı aç'), 'hm_details' => array('Details', 'Detayları gör'), 'hm_links' => array('broken links waiting', 'bekleyen kırık bağlantı'), 'hm_alerts' => array('open alerts', 'açık uyarı'), 'hm_fresh' => array('old posts waiting', 'bekleyen eski yazı'), 'hm_sent' => array('Latest sync events', 'Son senkronizasyon etkinlikleri'), 'hm_jobs' => array('Today\'s jobs', 'Bugünkü görevler'), 'hm_jobs_count' => array('%1$d done, %2$d left', '%1$d tamamlandı, %2$d kaldı'), 'hm_job_sync' => array('Turn live sync on', 'Canlı senkronu aç'), 'hm_job_links' => array('Fix %d broken links', '%d kırık bağlantıyı düzelt'), 'hm_job_links_done' => array('No broken link waiting', 'Bekleyen kırık bağlantı yok'), 'hm_job_titles' => array('Review %d SEO title suggestions', '%d SEO başlığı önerisini incele'), 'hm_job_titles_done' => array('No SEO title waiting for approval', 'Onay bekleyen SEO başlığı yok'), 'hm_job_titles_unknown' => array('SEO titles could not be read from SEOLOK', 'SEO başlıkları SEOLOK\'tan okunamadı'), 'hm_job_links_unknown' => array('Broken links: open Overview once to load them', 'Kırık bağlantılar: yüklemek için Genel bakış\'ı bir kez açın'), 'hm_job_alerts' => array('Fix %d search engine blocks', '%d arama motoru engelini kaldır'), 'hm_job_alerts_done' => array('Search engines are not blocked', 'Arama motorları engellenmiyor'), 'hm_job_fresh' => array('Plan updates for %d old posts', '%d eski yazı için güncelleme planla'), 'hm_job_fresh_done' => array('No old post waiting', 'Bekleyen eski yazı yok'), 'hm_health' => array('Technical health', 'Teknik sağlık'), 'hm_health_ok' => array('No important technical problem found.', 'Önemli bir teknik sorun bulunmadı.'), 'hm_health_bad' => array('Some checks need a look: open Alerts.', 'Bazı kontrollere bakmak gerekiyor: Uyarılar\'ı açın.'), 'hm_q_check' => array('Check the site', 'Siteyi kontrol et'), 'hm_q_check_s' => array('Search engine blocks and technical checks.', 'Arama motoru engelleri ve teknik kontroller.'), 'hm_q_links_s' => array('Fix broken links on every page at once.', 'Kırık bağlantıları tüm sayfalarda tek seferde düzeltin.'), 'hm_q_titles' => array('Open SEO titles', 'SEO başlıklarını aç'), 'hm_q_titles_s' => array('Suggestions waiting for approval.', 'Onay bekleyen öneriler.'), 'hm_q_sync' => array('Turn live sync on', 'Canlı senkronu aç'), 'hm_q_sync_on' => array('Live sync', 'Canlı senkron'), 'hm_q_sync_s' => array('Tell SEOLOK about page changes at once.', 'Sayfa değişikliklerini SEOLOK\'a hemen bildirin.'), 'hm_opps_wait' => array('Shown once SEOLOK\'s overview has loaded (open SEOLOK → Overview).', 'SEOLOK\'un genel bakışı yüklenince görünür (SEOLOK → Genel bakış\'ı açın).'), 'hm_hide' => array('Hide this panel (the small SEOLOK box comes back)', 'Bu paneli gizle (küçük SEOLOK kutusu geri gelir)'), 'hm_show' => array('Show the full SEOLOK panel again', 'Tam SEOLOK panelini yeniden göster'), 'ov_from_scan' => array('From SEOLOK\'s last scan', 'SEOLOK\'un son taramasından'), 'ov_ask' => array('Ask a question', 'Soru sor'), 'ov_go_jobs' => array('See the %d jobs', '%d işe bak'), 'menu_audit' => array('Site analysis', 'Site analizi'), 'menu_opportunities' => array('Content opportunities', 'İçerik fırsatları'), 'menu_keywords' => array('Keywords', 'Anahtar kelimeler'), 'menu_competitors' => array('Competitors', 'Rakip analizi'), 'menu_backlinks' => array('Backlinks', 'Backlink takibi'), 'menu_reports' => array('Reports', 'Raporlar'), 'in_open' => array('Open in SEOLOK', 'SEOLOK\'ta aç'), 'in_lead_audit' => array('SEOLOK\'s scan of your site: every problem, how many pages it touches and how to fix it.', 'SEOLOK\'un sitenizi taraması: her sorun, kaç sayfayı etkilediği ve nasıl düzeltileceği.'), 'in_lead_opportunities' => array('Searches where your site is close to more visits, found from your Search Console data.', 'Sitenizin daha fazla ziyarete yakın olduğu aramalar; Search Console verinizden bulunur.'), 'in_lead_keywords' => array('The searches SEOLOK tracks for you every day: your Google rank, its change and monthly searches.', 'SEOLOK\'un sizin için her gün takip ettiği aramalar: Google sıranız, değişimi ve aylık arama.'), 'in_lead_competitors' => array('The sites you meet most on your tracked searches, and who is ahead where.', 'Takip ettiğiniz aramalarda en sık karşılaştığınız siteler ve kimin nerede önde olduğu.'), 'in_lead_backlinks' => array('Links from other sites to yours and SEOLOK\'s domain score.', 'Diğer sitelerden sitenize gelen bağlantılar ve SEOLOK alan adı puanı.'), 'in_lead_reports' => array('Your monthly SEO reports prepared by SEOLOK.', 'SEOLOK\'un hazırladığı aylık SEO raporlarınız.'), 'in_audit_never' => array('SEOLOK has not scanned this site yet. The first scan starts after the site is added in SEOLOK.', 'SEOLOK bu siteyi henüz taramadı. İlk tarama site SEOLOK\'a eklenince başlar.'), 'in_healthy' => array('%s pages without a problem', '%s sayfada sorun yok'), 'in_critical' => array('Critical issues', 'Kritik sorunlar'), 'in_critical_sub' => array('pages to fix first', 'önce düzeltilecek sayfa'), 'in_warnings' => array('Warnings', 'Uyarılar'), 'in_info' => array('%s small notes', '%s küçük not'), 'in_issues' => array('Issues found', 'Bulunan sorunlar'), 'in_issues_lead' => array('Most important first. Open a row for why it matters, how to fix it and the pages.', 'En önemlisi önce. Satırı açınca neden önemli olduğu, nasıl düzeltileceği ve sayfalar görünür.'), 'in_no_issues' => array('No issue found in the last scan.', 'Son taramada sorun bulunmadı.'), 'in_more_pages' => array('…and %d more pages in SEOLOK.', '…ve SEOLOK\'ta %d sayfa daha.'), 'in_scanned' => array('Last scan: %s', 'Son tarama: %s'), 'in_col_search' => array('Search', 'Arama'), 'in_col_kind' => array('Kind', 'Tür'), 'in_in_progress' => array('In progress', 'Üzerinde çalışılıyor'), 'in_tracked' => array('Tracked searches', 'Takip edilen arama'), 'in_cap' => array('of %s in your plan', 'planınızda %s'), 'in_top3' => array('In the top 3', 'İlk 3\'te'), 'in_top10' => array('On page one', 'İlk sayfada'), 'in_moves' => array('Moves', 'Hareket'), 'in_moves_sub' => array('against the reading before', 'bir önceki ölçüme göre'), 'in_kw_lead' => array('Google rank of each tracked search, mobile or desktop, with Search Console\'s average position.', 'Takip edilen her aramanın Google sırası (mobil ya da masaüstü) ve Search Console ortalama pozisyonu.'), 'in_kw_none' => array('No tracked searches yet. Add them in SEOLOK → Keywords.', 'Henüz takip edilen arama yok. SEOLOK → Anahtar kelimeler\'den ekleyin.'), 'in_col_keyword' => array('Search', 'Arama'), 'in_col_rank' => array('Rank', 'Sıra'), 'in_col_change' => array('Change', 'Değişim'), 'in_col_volume' => array('Monthly searches', 'Aylık arama'), 'in_col_gsc' => array('Search Console avg.', 'Search Console ort.'), 'in_desktop' => array('desktop', 'masaüstü'), 'in_mobile' => array('mobile', 'mobil'), 'in_unverified' => array('Unverified', 'Doğrulanamadı'), 'in_not_found' => array('Not in top 100', 'İlk 100\'de yok'), 'in_comp_lead' => array('Counted on your tracked searches.', 'Takip ettiğiniz aramalar üzerinden sayılır.'), 'in_comp_none' => array('No competitor yet: SEOLOK finds them once searches are tracked.', 'Henüz rakip yok: aramalar takip edilince SEOLOK bulur.'), 'in_col_site' => array('Site', 'Site'), 'in_col_shared' => array('Shared searches', 'Ortak arama'), 'in_col_top10' => array('On page one', 'İlk sayfada'), 'in_col_avg' => array('Avg. position', 'Ort. sıra'), 'in_col_wins' => array('Who is ahead', 'Kim önde'), 'in_added' => array('Added by you', 'Siz eklediniz'), 'in_you_ahead' => array('you ahead in %d', '%d aramada siz'), 'in_they_ahead' => array('they in %d', '%d aramada onlar'), 'in_domain_score' => array('SEOLOK domain score', 'SEOLOK alan adı puanı'), 'in_checked' => array('Checked: %s', 'Kontrol: %s'), 'in_bl_never' => array('Not measured yet', 'Henüz ölçülmedi'), 'in_ref_domains' => array('Linking sites', 'Bağlantı veren site'), 'in_backlinks' => array('Backlinks', 'Geri bağlantı'), 'in_since_last' => array('%s since the last reading', 'son ölçümden bu yana %s'), 'in_lost' => array('Lost (30 days)', 'Kaybedilen (30 gün)'), 'in_lost_sub' => array('links that disappeared', 'kaybolan bağlantı'), 'in_bl_list' => array('Latest backlinks', 'Son geri bağlantılar'), 'in_bl_lead' => array('Linking sites over time, and the links SEOLOK knows.', 'Zaman içinde bağlantı veren siteler ve SEOLOK\'un bildiği bağlantılar.'), 'in_bl_locked' => array('SEOLOK measures incoming links for sites on the Agency and Agency Pro plans.', 'SEOLOK gelen bağlantıları Ajans ve Ajans Pro planındaki siteler için ölçer.'), 'in_bl_none' => array('SEOLOK has no backlink list for this site yet.', 'SEOLOK\'ta bu site için henüz geri bağlantı listesi yok.'), 'in_col_from' => array('From', 'Nereden'), 'in_col_anchor' => array('Link text', 'Bağlantı metni'), 'in_col_strength' => array('Strength', 'Güç'), 'in_col_first' => array('First seen', 'İlk görülme'), 'in_rep_lead' => array('Open a report in SEOLOK to read it or download the PDF.', 'Okumak ya da PDF indirmek için raporu SEOLOK\'ta açın.'), 'in_rep_none' => array('No report yet. SEOLOK prepares one at the start of each month.', 'Henüz rapor yok. SEOLOK her ayın başında bir rapor hazırlar.'), 'in_rep_title' => array('SEO report · %s', 'SEO raporu · %s'), 'in_ready' => array('Ready', 'Hazır'), 'in_preparing' => array('Being prepared', 'Hazırlanıyor'), 'in_rep_open' => array('Reports open in SEOLOK (sign-in needed): use "Open in SEOLOK" above.', 'Raporlar SEOLOK\'ta açılır (giriş gerekir): yukarıdaki "SEOLOK\'ta aç" düğmesini kullanın.'), 'ov_tagline' => array('Better rankings, more visitors.', 'Daha iyi sıralamalar, daha fazla ziyaretçi.'), 'ov_refresh' => array('Refresh', 'Yenile'), 'ov_lead' => array('Your site\'s SEO health, jobs and opportunities in one place.', 'Sitenizin SEO sağlığı, yapılacaklar ve fırsatlar tek yerde.'), 'ov_broken' => array('Broken links', 'Kırık bağlantılar'), 'ov_broken_sub' => array('waiting to be fixed', 'düzeltilmeyi bekliyor'), 'ov_broken_none' => array('None found in the last scan', 'Son taramada bulunmadı'), 'ov_score' => array('Site health score', 'Site sağlık puanı'), 'ov_score_sub' => array('Last scan: %s', 'Son tarama: %s'), 'ov_score_never' => array('Shown after SEOLOK\'s first scan.', 'SEOLOK\'un ilk taramasından sonra görünür.'), 'ov_tasks' => array('Today\'s jobs', 'Bugünün görevleri'), 'ov_tasks_sub' => array('picked from your Google data', 'Google verinize göre seçildi'), 'ov_tasks_none' => array('Nothing urgent today', 'Bugün acil iş yok'), 'ov_issues' => array('Technical issues', 'Teknik sorunlar'), 'ov_critical' => array('%s critical', '%s kritik'), 'ov_warning' => array('%s warnings', '%s uyarı'), 'ov_todo' => array('To do', 'Yapılacaklar'), 'ov_todo_lead' => array('Steps that improve your site, most important first.', 'Sitenizi iyileştiren adımlar, en önemlisi önce.'), 'ov_all' => array('See all', 'Tümünü gör'), 'ov_summary' => array('Site summary', 'Site özeti'), 'ov_summary_lead' => array('Health score and Google numbers.', 'Sağlık puanı ve Google rakamları.'), 'ov_detail' => array('Detailed analysis', 'Detaylı analiz'), 'ov_pages' => array('Pages scanned', 'Taranan sayfa'), 'ov_clicks' => array('Visits from Google', 'Google\'dan ziyaret'), 'ov_position' => array('Average position', 'Ortalama pozisyon'), 'ov_impressions' => array('Seen on Google', 'Google\'da görülme'), 'ov_no_gsc' => array('Connect Search Console in SEOLOK to see it.', 'Görmek için SEOLOK\'ta Search Console\'u bağlayın.'), 'ov_window' => array('Search Console, last 28 days against the 28 before.', 'Search Console, son 28 gün; önceki 28 günle karşılaştırma.'), 'ov_activity' => array('Recent activity', 'Son etkinlikler'), 'ov_activity_lead' => array('What changed on your site lately.', 'Sitenizde son değişenler.'), 'ov_activity_none' => array('Nothing recorded yet. Fixes and page changes appear here.', 'Henüz kayıt yok. Düzeltmeler ve sayfa değişiklikleri burada görünür.'), 'ov_quick' => array('Quick actions', 'Hızlı işlemler'), 'ov_quick_lead' => array('The tools you use most.', 'En sık kullandığınız araçlar.'), 'ov_q_alerts' => array('Check search engine blocks', 'Arama motoru engellerini kontrol et'), 'ov_q_links' => array('Check broken links', 'Kırık bağlantıları kontrol et'), 'ov_q_competitors' => array('Competitor analysis', 'Rakip analizi yap'), 'ov_q_rank' => array('Find my Google rank', 'Google sıramı bul'), 'ov_q_report' => array('Create a report', 'Rapor oluştur'), 'ov_opps' => array('Content opportunities', 'İçerik fırsatları'), 'ov_opps_lead' => array('Searches that can bring more visits.', 'Daha fazla ziyaret getirebilecek aramalar.'), 'ov_opps_none' => array('No open opportunity yet. SEOLOK finds them from your Search Console data.', 'Henüz açık fırsat yok. SEOLOK bunları Search Console verinizden bulur.'), 'ov_opp_seen' => array('seen %s times', '%s kez görüldü'), 'ov_opp_pos' => array('position %s', '%s. sıra'), 'ov_look' => array('Review', 'İncele'), 'oc_https_ok' => array('The site opens over https.', 'Site https üzerinden açılıyor.'), 'oc_permalinks_ok' => array('Readable addresses (permalinks) are on.', 'Okunur adresler (kalıcı bağlantılar) açık.'), 'oc_cron_ok' => array('Scheduled tasks run on time.', 'Zamanlanmış görevler zamanında çalışıyor.'), 'oc_cron_real' => array('WP-Cron is run by the server (DISABLE_WP_CRON) and is on time.', 'WP-Cron sunucu tarafından çalıştırılıyor (DISABLE_WP_CRON) ve zamanında.'), 'oc_sitemap_ok' => array('An XML sitemap is published.', 'XML site haritası yayında.'), 'oc_sitemap' => array('No XML sitemap found: turn on WordPress\'s own sitemap or your SEO plugin\'s.', 'XML site haritası bulunamadı: WordPress\'in ya da SEO eklentinizin site haritasını açın.'), 'oc_canonical_ok' => array('Every page checked has a canonical tag.', 'Kontrol edilen her sayfada canonical etiketi var.'), 'oc_canonical' => array('%1$d of %2$d pages checked have no canonical tag.', 'Kontrol edilen %2$d sayfanın %1$d tanesinde canonical etiketi yok.'), 'oc_canonical_never' => array('Shown after the pages are checked.', 'Sayfalar kontrol edilince görünür.'), 'oc_t_https' => array('HTTPS', 'HTTPS kullanımı'), 'oc_t_permalinks' => array('Permalinks', 'Kalıcı bağlantılar'), 'oc_t_cron' => array('Scheduled tasks (WP-Cron)', 'Zamanlanmış görevler (WP-Cron)'), 'oc_t_sitemap' => array('XML sitemap', 'XML site haritası'), 'oc_t_canonical' => array('Canonical tags', 'Canonical etiketler'), 'al_docs' => array('Documentation', 'Dokümantasyon'), 'al_support' => array('Get support', 'Destek alın'), 'al_health' => array('Site health', 'Site sağlık durumu'), 'al_crawlable' => array('Site can be crawled', 'Site taranabilir'), 'al_blocked' => array('Search engines are blocked', 'Arama motorları engelleniyor'), 'al_no_block' => array('No critical block', 'Kritik engel yok'), 'al_n_blocks' => array('%d critical', '%d kritik engel'), 'al_blocked_lead' => array('Your site tells search engines to stay away. Fix the red cards below.', 'Siteniz arama motorlarına uzak durmasını söylüyor. Aşağıdaki kırmızı kartları düzeltin.'), 'al_last' => array('Last check', 'Son kontrol'), 'al_daily' => array('Checked automatically every 24 hours.', 'Otomatik kontrol her 24 saatte bir yapılır.'), 'al_tech' => array('Technical checks', 'Teknik kontroller'), 'al_tech_ok' => array('The basic technical settings work.', 'Temel teknik ayarlar düzgün çalışıyor.'), 'al_tech_bad' => array('%d settings need a look.', '%d ayara bakmak gerekiyor.'), 'al_n_checks' => array('%d checks', '%d kontrol'), 'al_meta_short_never' => array('Pages not checked yet.', 'Sayfalar henüz kontrol edilmedi.'), 'al_vis_short_ok' => array('Search engines may index your site.', 'Arama motorları sitenizi dizine ekleyebilir.'), 'al_vis_short_bad' => array('Search engines are told not to index your site.', 'Arama motorlarına sitenizi dizine eklememeleri söyleniyor.'), 'al_vis_fix' => array('Untick it in Settings → Reading, or press "Allow search engines" above.', 'Ayarlar → Okuma\'da işareti kaldırın ya da yukarıdaki "Aramaya izin ver" düğmesine basın.'), 'al_important' => array('Important alerts', 'Önemli uyarılar'), 'al_counts' => array('%1$d critical · %2$d to check', '%1$d kritik · %2$d dikkat'), 'al_none_lead' => array('There is no critical problem right now. You will see it here as soon as one shows up.', 'Şu anda kritik bir sorun bulunmuyor. Bir sorun çıkarsa burada hemen görürsünüz.'), 'al_all_good' => array('All good.', 'Her şey yolunda.'), 'al_all_good_sub' => array('The checks run every day; this page warns you when something changes.', 'Kontroller her gün yapılır; bir şey değişirse bu sayfa sizi uyarır.'), 'al_robots_lead' => array('The file that tells search engines which addresses they may crawl.', 'Arama motorlarına hangi adresleri tarayabileceğini söyleyen dosya.'), 'al_meta_lead' => array('The "noindex" tag in your key pages\' code.', 'Önemli sayfalarınızın kodundaki "noindex" etiketi.'), 'al_vis_lead' => array('WordPress\'s own "discourage search engines" setting.', 'WordPress\'in kendi "arama motorlarını engelle" ayarı.'), 'al_pages_checked' => array('Pages checked', 'Kontrol edilen sayfa'), 'al_pages_noindex' => array('Pages with noindex', 'noindex içeren sayfa'), 'al_col_page' => array('Page', 'Sayfa'), 'al_col_checked' => array('Last check', 'Son kontrol'), 'al_no_affected' => array('No affected pages.', 'Etkilenen sayfa bulunmuyor.'), 'al_no_affected_sub' => array('Every page checked looks normal.', 'Kontrol edilen tüm sayfalar normal görünüyor.'), 'al_passed' => array('%1$d / %2$d passed', '%1$d / %2$d başarılı'), 'al_ev_check' => array('Site check finished', 'Site kontrolü tamamlandı'), 'al_ev_check_failed' => array('Pages could not be opened during the check', 'Kontrolde sayfalar açılamadı'), // Live sync 'sync_title' => array('Live sync', 'Canlı senkron'), 'sync_strip' => array('Live sync: off by default', 'Canlı senkron: varsayılan kapalı'), 'sync_lead' => array('Tells SEOLOK when pages are published, updated or deleted and how the site is doing, so SEOLOK can re-check changed pages soon and warn you at once. It does not change your SEO titles or your content.', 'Sayfalar yayınlandığında, güncellendiğinde ya da silindiğinde ve sitenin durumunu SEOLOK\'a bildirir; böylece SEOLOK değişen sayfaları kısa sürede yeniden kontrol eder ve sorun olursa hemen uyarır. SEO başlıklarınızı ya da içeriğinizi değiştirmez.'), 'sync_is_off' => array('Live sync is off', 'Canlı senkron şu anda kapalı'), 'sync_is_on' => array('Live sync is on', 'Canlı senkron açık'), 'sync_off_text' => array('Off by default: nothing about your site is sent until an administrator switches it on.', 'Varsayılan olarak kapalıdır: bir yönetici açmadıkça siteniz hakkında hiçbir şey gönderilmez.'), 'sync_on_text' => array('SEOLOK hears about published, updated and deleted pages within a minute and gets an hourly report of the site\'s state. Page content, visitors and users are never sent.', 'SEOLOK yayınlanan, güncellenen ve silinen sayfaları bir dakika içinde, sitenin durumunu saatte bir öğrenir. Sayfa içeriği, ziyaretçiler ve kullanıcılar asla gönderilmez.'), 'switch_off' => array('OFF', 'KAPALI'), 'switch_on' => array('ON', 'AÇIK'), 'b_sync_on' => array('Turn on live sync', 'Canlı senkronu aç'), 'b_sync_off' => array('Turn off', 'Kapat'), 'b_sync_now' => array('Send now', 'Şimdi gönder'), 'sync_on_note' => array('When on, new changes are sent within a minute; turning it off deletes what waits.', 'Açıkken yeni değişiklikler bir dakika içinde gönderilir; kapatınca bekleyenler silinir.'), 'sync_on_text_cron' => array('Live sync is on, but WordPress\' scheduled tasks (WP-Cron) are not running, so changes wait: they are sent when WP-Cron runs again, when an administrator opens the admin, or with "Send now". Page content, visitors and users are never sent.', 'Canlı senkron açık, ancak WordPress\'in zamanlı işleri (WP-Cron) çalışmıyor; bu yüzden değişiklikler bekliyor: WP-Cron yeniden çalıştığında, bir yönetici panele girdiğinde ya da "Şimdi gönder" ile gönderilir. Sayfa içeriği, ziyaretçiler ve kullanıcılar asla gönderilmez.'), 'sync_on_note_cron' => array('No "within a minute" promise while WP-Cron is late; turning it off deletes what waits.', 'WP-Cron gecikirken "bir dakika içinde" sözü verilmez; kapatınca bekleyenler silinir.'), 'cron_late' => array('WordPress\' scheduled tasks have not run for %d minutes (hosting: enable a real cron or let WP-Cron run).', 'WordPress\'in zamanlı işleri %d dakikadır çalışmıyor (barındırma: gerçek bir cron tanımla ya da WP-Cron\'un çalışmasına izin ver).'), 'cron_run' => array('Run scheduled tasks now', 'Zamanlı işleri şimdi çalıştır'), 'cron_run_hint' => array('Runs WordPress\' scheduler (wp-cron.php) once. Works even when WP-Cron is switched off in wp-config.php. Lasting fix: a real cron at your host.', 'WordPress\'in zamanlayıcısını (wp-cron.php) bir kez çalıştırır; wp-config.php\'de WP-Cron kapalı olsa da çalışır. Kalıcı çözüm: barındırmada gerçek bir cron.'), 'cron_help_title' => array('Lasting fix: how to set up a real cron', 'Kalıcı çözüm: gerçek cron nasıl kurulur?'), 'cron_help_lead' => array('The goal is one thing: have this address opened every 5 minutes (any way that opens it works, on any server):', 'Amaç tek: şu adresin 5 dakikada bir açılması (açan her yol işe yarar, sunucu türü fark etmez):'), 'cron_help_linux' => array('cPanel, DirectAdmin and other Linux panels → Cron Jobs: schedule "*/5 * * * *", command:', 'cPanel, DirectAdmin ve diğer Linux panelleri → Cron İşleri: zamanlama "*/5 * * * *", komut:'), 'cron_help_plesk' => array('Plesk → Scheduled Tasks → type "Fetch a URL", the address above, every 5 minutes.', 'Plesk → Zamanlanmış Görevler → tür "URL getir", adres yukarıdaki, her 5 dakika.'), 'cron_help_windows' => array('Windows server (IIS) → Task Scheduler → repeat every 5 minutes, command:', 'Windows sunucu (IIS) → Görev Zamanlayıcı → her 5 dakikada bir, komut:'), 'cron_help_external' => array('No access to a panel? Ask a free service such as cron-job.org or UptimeRobot to open the same address every 5 minutes.', 'Panele erişimin yoksa: cron-job.org ya da UptimeRobot gibi ücretsiz bir servise aynı adresi 5 dakikada bir açtır.'), 'cron_help_config' => array('Optional, once a real cron works: add define( \'DISABLE_WP_CRON\', true ); to wp-config.php so visits stop starting it too (the plugin never edits that file for you).', 'İsteğe bağlı, gerçek cron çalışınca: ziyaretlerin de başlatmasını durdurmak için wp-config.php\'ye define( \'DISABLE_WP_CRON\', true ); ekle (eklenti bu dosyayı senin yerine düzenlemez).'), 'cron_ran' => array('Scheduled tasks were run. The delay is gone.', 'Zamanlı işler çalıştırıldı. Gecikme kapandı.'), 'cron_ran_still' => array('Scheduled tasks were run, but a delay of %d minutes still shows: a long task (a backup, for example) may still be running, or a plugin keeps postponing one. Look again in a few minutes.', 'Zamanlı işler çalıştırıldı, ama %d dakikalık gecikme hâlâ görünüyor: uzun süren bir iş (örneğin yedek) sürüyor olabilir ya da bir eklenti bir işi sürekli erteliyor. Birkaç dakika sonra yeniden bak.'), 'cron_started' => array('Scheduled tasks were started; a long one (a backup, for example) is still running in the background. Look again in a few minutes.', 'Zamanlı işler başlatıldı; uzun süren bir iş (örneğin yedek) arka planda sürüyor. Birkaç dakika sonra yeniden bak.'), 'cron_failed' => array('WordPress could not reach its own scheduler (%s). A firewall, password protection (Basic Auth) or a cache/security plugin may be blocking it. Set up a real cron at your host.', 'WordPress kendi zamanlayıcısına bağlanamadı (%s). Güvenlik duvarı, şifre koruması (Basic Auth) ya da bir önbellek/güvenlik eklentisi engelliyor olabilir. Barındırmada gerçek bir cron tanımla.'), 'cron_failed_own' => array('SEOLOK\'s waiting changes were sent from here instead.', 'SEOLOK\'un bekleyen değişiklikleri bunun yerine buradan gönderildi.'), 'queue_oldest' => array('Oldest waiting: %s', 'En eski bekleyen: %s önce'), 'queue_next' => array('Next try: %s', 'Sonraki deneme: %s'), 'k_next' => array('Next try', 'Sonraki deneme'), 'next_backoff' => array('%s (waiting after the last failure)', '%s (son hatadan sonra bekleme)'), 'next_cron_late' => array('WP-Cron is late: sent when an administrator opens the admin, or with "Send now"', 'WP-Cron gecikmiş: bir yönetici panele girince ya da "Şimdi gönder" ile gönderilir'), 'next_none' => array('not scheduled yet', 'henüz planlanmadı'), 'err_http_request_failed' => array('SEOLOK could not be reached (network error or timeout)', 'SEOLOK\'a ulaşılamadı (ağ hatası ya da zaman aşımı)'), 'err_seolok_unauthorized' => array('SEOLOK did not accept the key: check the token under Settings → SEOLOK', 'SEOLOK anahtarı kabul etmedi: Ayarlar → SEOLOK\'taki anahtarı kontrol et'), 'err_seolok_remote_error' => array('SEOLOK gave an unexpected answer; it is tried again', 'SEOLOK beklenmeyen bir cevap verdi; yeniden denenir'), 'err_seolok_not_configured' => array('Site ID or token missing', 'Site kimliği ya da anahtar eksik'), 'err_other' => array('Sending failed (%s)', 'Gönderim başarısız (%s)'), 'k_queue' => array('Waiting to be sent', 'Gönderilmeyi bekleyen'), 'k_queue_sub' => array('changes in the queue', 'kuyruktaki değişiklik'), 'k_today' => array('Sent today', 'Bugün gönderilen'), 'k_today_sub' => array('changes', 'değişiklik'), 'k_total' => array('Sent in total', 'Toplam gönderilen'), 'k_total_sub' => array('since plugin 0.6.0', 'eklenti 0.6.0\'dan beri'), 'k_last' => array('Last successful send', 'Son başarılı gönderim'), 'never' => array('never', 'hiç'), 'ago' => array('%s ago', '%s önce'), 'last_error' => array('Last error (%1$s): %2$s', 'Son hata (%1$s): %2$s'), 'queue' => array('Queue', 'Senkron kuyruğu'), 'queue_empty' => array('Nothing waits to be sent. New changes appear here until they are sent.', 'Gönderilmeyi bekleyen bir şey yok. Yeni değişiklikler gönderilene kadar burada görünür.'), 'col_content' => array('Content', 'İçerik'), 'col_change' => array('Change', 'Değişiklik'), 'col_added' => array('Added', 'Eklenme zamanı'), 'col_time' => array('Time', 'Zaman'), 'col_event' => array('Event', 'Olay'), 'col_details' => array('Details', 'Ayrıntı'), 'types' => array('Content types', 'Seçilen içerik türleri'), 'types_lead' => array('Changes of these types are told to SEOLOK. Media files, menus, templates, forms, form entries, visitors and users are never sent and are not listed here.', 'Bu türlerdeki değişiklikler SEOLOK\'a bildirilir. Ortam dosyaları, menüler, şablonlar, formlar, form kayıtları, ziyaretçiler ve kullanıcılar hiç gönderilmez; burada da listelenmez.'), 'b_save' => array('Save', 'Kaydet'), 'type_on' => array('On', 'Etkin'), 'type_off' => array('Off', 'Devre dışı'), 'recent' => array('Recently sent', 'Son senkronizasyon etkinlikleri'), 'recent_empty' => array('Nothing sent yet.', 'Henüz bir şey gönderilmedi.'), 'sent' => array('Sent', 'Gönderildi'), 'whole_site' => array('Site setting', 'Site ayarı'), 'audit' => array('Audit log', 'Denetim günlüğü'), 'audit_empty' => array('No entries yet.', 'Henüz kayıt yok.'), 'l_on' => array('Live sync switched on', 'Canlı senkron açıldı'), 'l_off' => array('Live sync switched off', 'Canlı senkron kapatıldı'), 'l_deactivated' => array('Plugin deactivated: SEOLOK was told sync is off', 'Eklenti devre dışı bırakıldı: SEOLOK\'a senkronun kapandığı bildirildi'), 'l_reset' => array('Connection changed: sync switched off', 'Bağlantı değişti: senkron kapatıldı'), 'l_error' => array('Sending failed', 'Gönderim hatası'), 'l_types' => array('Content types changed', 'İçerik türleri değişti'), 'l_manual' => array('Sent by hand', 'Elle gönderildi'), 'ev_post_published' => array('Published', 'Yayınlandı'), 'ev_post_updated' => array('Updated', 'Güncellendi'), 'ev_post_unpublished'=> array('Taken offline', 'Yayından kalktı'), 'ev_post_deleted' => array('Deleted', 'Silindi'), 'ev_url_changed' => array('Address changed', 'Adresi değişti'), 'ev_search_visibility' => array('Search visibility changed', 'Arama görünürlüğü değişti'), 'ev_permalinks_changed' => array('Permalinks changed', 'Kalıcı bağlantılar değişti'), 'ev_plugin_activated' => array('Plugin activated', 'Eklenti etkinleştirildi'), 'ev_plugin_deactivated' => array('Plugin deactivated', 'Eklenti devre dışı bırakıldı'), 'ev_theme_switched' => array('Theme switched', 'Tema değişti'), 'r_types_saved' => array('Content types saved.', 'İçerik türleri kaydedildi.'), 'r_types_none' => array('Pick at least one content type (or turn live sync off).', 'En az bir içerik türü seçin (ya da canlı senkronu kapatın).'), 'r_changed_since' => array('Not applied: the page\'s title was changed after this suggestion was written. SEOLOK was told; its next text run writes the text again from the current title.', 'Uygulanmadı: bu öneri yazıldıktan sonra sayfanın başlığı değiştirilmiş. SEOLOK\'a bildirildi; bir sonraki metin yazımında güncel başlıktan yeniden yazılır.'), 'r_sent' => array('Sent to SEOLOK.', 'SEOLOK\'a gönderildi.'), 'menu_404' => array('404 addresses', '404 adresleri'), 'nf_strip' => array('Addresses visitors could not find', 'Ziyaretçilerin bulamadığı adresler'), 'nf_title' => array('404 addresses', '404 adresleri'), 'nf_lead' => array('Addresses on your site that visitors asked for and that answered "not found" (404). Send each one to the page that replaced it with a 301, or ignore it.', 'Sitende ziyaretçilerin açmaya çalıştığı ve "bulunamadı" (404) cevabı veren adresler. Her birini yerine geçen sayfaya 301 ile gönder ya da yok say.'), 'nf_privacy' => array('Kept: the address, how often it was asked for (first and last day) and where visitors came from: a page of this site by its address, another site by its domain only (google.com). No IP address, browser, search words or query string; where visitors came from stays in WordPress and is not sent to SEOLOK. Logged-in staff, WordPress and scanner addresses (wp-admin, .php) and files are not counted. At most 500 addresses.', 'Tutulanlar: adres, kaç kez açılmaya çalışıldığı (ilk ve son günüyle) ve ziyaretçinin nereden geldiği: sitenizdeki bir sayfaysa o sayfanın adresi, başka bir siteyse yalnız alan adı (google.com). IP adresi, tarayıcı, arama kelimesi ya da adres parametreleri tutulmaz; nereden gelindiği WordPress\'te kalır, SEOLOK\'a gönderilmez. Giriş yapmış ekip, WordPress ve tarayıcı botu adresleri (wp-admin, .php) ve dosyalar sayılmaz. En çok 500 adres.'), 'nf_sync_on' => array('Live sync is on: the 30 most-asked addresses go to SEOLOK with the hourly report, and SEOLOK suggests a similar page of your site for the 301.', 'Canlı senkron açık: en çok açılmaya çalışılan 30 adres saatlik raporla SEOLOK\'a gider, SEOLOK de 301 için sitenden benzer bir sayfa önerir.'), 'nf_sync_off' => array('Live sync is off: this list stays in WordPress. Switch live sync on if SEOLOK should suggest a page for each address.', 'Canlı senkron kapalı: bu liste WordPress\'te kalır. SEOLOK\'un her adres için sayfa önermesini istersen canlı senkronu aç.'), 'nf_waiting' => array('Not found', 'Bulunamayan'), 'nf_redirected' => array('Redirected', 'Yönlendirilen'), 'nf_ignored' => array('Ignored', 'Yok sayılan'), 'col_address' => array('Address', 'Adres'), 'col_hits' => array('Hits', 'Kaç kez'), 'col_first' => array('First seen', 'İlk görülme'), 'col_last' => array('Last seen', 'Son görülme'), 'col_to' => array('Sends visitors to', 'Yönlendiği sayfa'), 'nf_empty_waiting' => array('No 404 address recorded. Addresses appear here as soon as a visitor asks for one that does not exist.', 'Kayıtlı 404 adresi yok. Bir ziyaretçi olmayan bir adresi açmaya çalışınca burada görünür.'), 'nf_empty_redirected' => array('No address was redirected from this list yet.', 'Bu listeden henüz yönlendirilen adres yok.'), 'nf_empty_ignored' => array('No ignored address.', 'Yok sayılan adres yok.'), 'nf_placeholder' => array('Page to send visitors to, e.g. /services/', 'Gönderilecek sayfa, örn. /hizmetler/'), 'nf_suggested' => array('SEOLOK suggests:', 'SEOLOK önerisi:'), 'nf_page_exists' => array('A page answers at this address now', 'Bu adreste artık bir sayfa var'), 'nf_by_seolok' => array('Done from SEOLOK: undo it in SEOLOK (or on Overview).', 'SEOLOK\'tan yapıldı: geri alma SEOLOK\'ta (ya da Genel bakış\'ta).'), 'b_nf_redirect' => array('301 redirect', '301 yönlendir'), 'b_nf_ignore' => array('Ignore', 'Yok say'), 'b_nf_unignore' => array('Bring back', 'Geri getir'), 'b_nf_undo' => array('Undo', 'Geri al'), 'r_nf_redirected' => array('Done: %1$s now sends visitors to %2$s with a 301.', 'Tamam: %1$s artık ziyaretçileri 301 ile %2$s adresine gönderiyor.'), 'r_nf_bad_target' => array('Pick a published page of this site (not the home page and not the address itself).', 'Bu sitenin yayında olan bir sayfasını seç (ana sayfa ya da adresin kendisi olamaz).'), 'r_nf_loop' => array('Not done: this 301 would go round in circles, the chosen page already redirects back here.', 'Yapılmadı: bu 301 döngü olur, seçilen sayfa zaten buraya yönleniyor.'), 'r_nf_unknown' => array('This address is no longer in the list.', 'Bu adres artık listede yok.'), 'r_nf_ignored' => array('Ignored: it is not counted any more.', 'Yok sayıldı: artık sayılmıyor.'), 'r_nf_back' => array('Back: it is counted again from the next visit.', 'Geri geldi: bir sonraki ziyaretten itibaren yeniden sayılır.'), 'r_nf_undone' => array('Undone: the 301 was removed.', 'Geri alındı: 301 kaldırıldı.'), 'r_nf_changed' => array('Not undone: this 301 was changed in the meantime, so it was left as it is.', 'Geri alınmadı: bu 301 bu arada değişmiş, olduğu gibi bırakıldı.'), ); } if (!isset($s[$key])) { return $key; } return $s[$key][seolok_is_tr() ? 1 : 0]; } /* ------------------------------------------------------------------ shared */ /** The SEOLOK mark (the icon of the plugin's page on WordPress.org). */ function seolok_brand() { return 'seolok'; } /** * The head of every SEOLOK screen (0.6.8): the same white bar as Overview and Alerts (mark › title, the screen's button on the * right), then one sentence. `$strip` is kept for older callers and no longer drawn. */ function seolok_screen_head($strip, $title, $lead, $button = '') { unset($strip); $o = '
' . seolok_brand() . '

' . esc_html($title) . '

' . ($button !== '' ? '' : '') . '
'; return $o . '

' . esc_html($lead) . '

'; } function seolok_screen_url($page, $args = array()) { return add_query_arg(array_merge(array('page' => $page), $args), admin_url('admin.php')); } /** A small form posting one operation of a screen to admin-post.php (nonce, capability and the screen are checked there). */ function seolok_screen_form($screen, $op, $label, $class = 'button', $fields = array()) { $o = '
'; $o .= ''; foreach ($fields as $name => $value) { $o .= ''; } $o .= wp_nonce_field('seolok_screen', '_wpnonce', true, false); $o .= '
'; return $o; } function seolok_pill($tone, $text) { $tone = in_array($tone, array('ok', 'info', 'warn', 'bad', 'muted'), true) ? $tone : 'muted'; return '' . esc_html($text) . ''; } function seolok_when($ts) { $ts = (int) $ts; return $ts > 0 ? wp_date('j M Y, H:i', $ts) : ''; } function seolok_len($text) { return function_exists('mb_strlen') ? mb_strlen((string) $text, 'UTF-8') : strlen((string) $text); } /** Keeps the newest $max entries of a log option (newest first). */ function seolok_log_push($option, $entry, $max = SEOLOK_LOG_MAX) { $log = get_option($option, array()); $log = is_array($log) ? $log : array(); array_unshift($log, $entry); update_option($option, array_slice($log, 0, $max), false); } /* ------------------------------------------------------------------ menu */ add_action('admin_menu', function () { $GLOBALS['seolok_screen_hooks'] = array(); $GLOBALS['seolok_screen_hooks'][] = add_menu_page('SEOLOK', 'SEOLOK' . seolok_menu_badge(), 'manage_options', SEOLOK_LOKMAN_PAGE, 'seolok_lokman_page', 'dashicons-heart', 58); add_submenu_page(SEOLOK_LOKMAN_PAGE, seolok_s('menu_overview'), seolok_s('menu_overview'), 'manage_options', SEOLOK_LOKMAN_PAGE, 'seolok_lokman_page'); // SEOLOK's own data (includes/insights.php): site analysis, opportunities, keywords, competitors, backlinks, reports foreach (SEOLOK_INSIGHT_SCREENS as $seolok_screen) { $GLOBALS['seolok_screen_hooks'][] = add_submenu_page(SEOLOK_LOKMAN_PAGE, seolok_s('menu_' . $seolok_screen), seolok_s('menu_' . $seolok_screen), 'manage_options', seolok_insight_slug($seolok_screen), function () use ($seolok_screen) { seolok_insight_page($seolok_screen); }); } $GLOBALS['seolok_screen_hooks'][] = add_submenu_page(SEOLOK_LOKMAN_PAGE, seolok_s('menu_titles'), seolok_s('menu_titles'), 'manage_options', SEOLOK_TITLES_PAGE, 'seolok_titles_page'); $alerts = seolok_alert_count(); $GLOBALS['seolok_screen_hooks'][] = add_submenu_page(SEOLOK_LOKMAN_PAGE, seolok_s('menu_alerts'), seolok_s('menu_alerts') . ($alerts > 0 ? ' ' . esc_html((string) $alerts) . '' : ''), 'manage_options', SEOLOK_ALERTS_PAGE, 'seolok_alerts_page'); $GLOBALS['seolok_screen_hooks'][] = add_submenu_page(SEOLOK_LOKMAN_PAGE, seolok_s('menu_fresh'), seolok_s('menu_fresh'), 'manage_options', SEOLOK_FRESH_PAGE, 'seolok_fresh_page'); $GLOBALS['seolok_screen_hooks'][] = add_submenu_page(SEOLOK_LOKMAN_PAGE, seolok_s('menu_404'), seolok_s('menu_404'), 'manage_options', SEOLOK_404_PAGE, 'seolok_404_page'); $GLOBALS['seolok_screen_hooks'][] = add_submenu_page(SEOLOK_LOKMAN_PAGE, seolok_s('menu_sync'), seolok_s('menu_sync'), 'manage_options', SEOLOK_SYNC_PAGE, 'seolok_sync_page'); add_submenu_page(SEOLOK_LOKMAN_PAGE, seolok_s('menu_settings'), seolok_s('menu_settings'), 'manage_options', 'options-general.php?page=seolok'); }); add_action('admin_enqueue_scripts', function ($hook) { $hooks = isset($GLOBALS['seolok_screen_hooks']) && is_array($GLOBALS['seolok_screen_hooks']) ? $GLOBALS['seolok_screen_hooks'] : array(); if (in_array($hook, $hooks, true) || $hook === 'settings_page_seolok') { wp_enqueue_style('seolok-admin', plugins_url('assets/seolok-admin.css', SEOLOK_FILE), array(), SEOLOK_VERSION); wp_enqueue_script('seolok-admin', plugins_url('assets/seolok-admin.js', SEOLOK_FILE), array(), SEOLOK_VERSION, true); wp_localize_script('seolok-admin', 'SEOLOK_BULK', array('ajax' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('seolok_screen'), 'delay' => SEOLOK_BULK_PAUSE_MS, 'error' => seolok_s('bulk_js_error'))); } }); /** Opens a screen: the flash notice of the last click, then the screen's own content. */ function seolok_screen_open() { $o = '
'; $flash = get_transient(seolok_flash_key()); if ($flash !== false) { delete_transient(seolok_flash_key()); $o .= seolok_render_flash($flash); } return $o; } function seolok_screen_not_connected() { return '

' . esc_html(seolok_s('not_connected')) . '

' . esc_html(seolok_s('open_settings')) . '

'; } /* ------------------------------------------------------------- the handler */ function seolok_handle_screen() { if (!current_user_can('manage_options')) { wp_die(esc_html(seolok_s('forbidden')), '', array('response' => 403)); } check_admin_referer('seolok_screen'); $screen = isset($_POST['screen']) ? sanitize_key(wp_unslash($_POST['screen'])) : ''; $op = isset($_POST['op']) ? sanitize_key(wp_unslash($_POST['op'])) : ''; $id = isset($_POST['id']) ? preg_replace('/[^0-9a-fA-F-]/', '', sanitize_text_field(wp_unslash($_POST['id']))) : ''; $back = array(); if ($screen === 'titles' && $op === 'bulk_stop') { seolok_bulk_stop(); $result = array('tone' => 'success', 'lines' => array(seolok_s('bulk_stop_done'))); $back = array(); $page = SEOLOK_TITLES_PAGE; } elseif ($screen === 'titles' && in_array($op, array('bulk_approve', 'bulk_apply', 'bulk_reject'), true)) { $ids = isset($_POST['ids']) && is_array($_POST['ids']) ? preg_replace('/[^0-9a-fA-F-]/', '', array_map('sanitize_text_field', wp_unslash($_POST['ids']))) : array(); $result = seolok_titles_bulk_start(substr($op, 5), array_values(array_unique(array_filter($ids)))); $back = array(); foreach (array('status', 's') as $keep) { if (isset($_POST[$keep]) && $_POST[$keep] !== '') { $back[$keep] = sanitize_text_field(wp_unslash($_POST[$keep])); } } $page = SEOLOK_TITLES_PAGE; } elseif ($screen === 'titles') { $result = seolok_titles_op($op, $id); // after "apply" or "reject" the screen moves on to the next suggestion that still waits; after "approve" it stays (apply comes next) $move = $id !== '' && in_array($op, array('apply', 'reject'), true) && isset($result['tone']) && $result['tone'] === 'success'; $back = $id !== '' ? array($move ? 'after' : 'item' => $id) : array(); foreach (array('status', 's') as $keep) { if (isset($_POST[$keep]) && $_POST[$keep] !== '') { $back[$keep] = sanitize_text_field(wp_unslash($_POST[$keep])); } } $page = SEOLOK_TITLES_PAGE; } elseif ($screen === 'fresh') { $post_id = isset($_POST['post']) ? absint(wp_unslash($_POST['post'])) : 0; $result = seolok_fresh_op($op, $post_id); $back = array(); if (isset($_POST['view']) && $_POST['view'] !== '') { $back['view'] = sanitize_key(wp_unslash($_POST['view'])); } $page = SEOLOK_FRESH_PAGE; } elseif ($screen === 'notfound') { // a path is compared with the stored ones only (never printed unescaped); sanitize_text_field() would drop %-encoded letters $path = isset($_POST['path']) && is_string($_POST['path']) ? seolok_404_path(wp_unslash($_POST['path'])) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $to = isset($_POST['to']) && is_string($_POST['to']) ? substr(preg_replace('/[\x00-\x1F\x7F]/', '', wp_unslash($_POST['to'])), 0, 2000) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $result = seolok_404_op($op, $path, $to); if (isset($_POST['view']) && $_POST['view'] !== '') { $back['view'] = sanitize_key(wp_unslash($_POST['view'])); } $page = SEOLOK_404_PAGE; } elseif ($screen === 'alerts') { $key = isset($_POST['key']) ? sanitize_key(wp_unslash($_POST['key'])) : ''; $result = seolok_alerts_op($op, $key); $page = SEOLOK_ALERTS_PAGE; } elseif ($screen === 'sync') { $types = isset($_POST['types']) && is_array($_POST['types']) ? array_map('sanitize_key', wp_unslash($_POST['types'])) : array(); $result = seolok_sync_screen_op($op, $types); $page = SEOLOK_SYNC_PAGE; } else { $result = array('tone' => 'warning', 'lines' => array(seolok_s('forbidden'))); $page = SEOLOK_LOKMAN_PAGE; } set_transient(seolok_flash_key(), $result, 120); wp_safe_redirect(seolok_screen_url($page, $back)); exit; } add_action('admin_post_seolok_screen', 'seolok_handle_screen'); /* ------------------------------------------------------------- SEO titles */ /** @return array|WP_Error SEOLOK's list (cached two minutes) */ function seolok_titles_data($force = false) { $key = 'seolok_titles_' . seolok_lokman_locale(); if (!$force) { $cached = get_transient($key); if (is_array($cached)) { return $cached; } } $data = seolok_remote('GET', '/api/wp/titles?locale=' . seolok_lokman_locale()); if (is_wp_error($data)) { return $data; } if (!isset($data['v'], $data['items']) || (int) $data['v'] !== 1 || !is_array($data['items'])) { return new WP_Error('seolok_remote_error', 'Unexpected answer'); } set_transient($key, $data, 30); // short: a decision taken in SEOLOK shows here within half a minute return $data; } function seolok_titles_forget() { delete_transient('seolok_titles_tr'); delete_transient('seolok_titles_en'); } /** One button of the screen. Approve / reject / reopen are SEOLOK's; apply and undo are written here, then reported. */ function seolok_titles_op($op, $id) { if (!in_array($op, array('approve', 'reject', 'reopen', 'apply', 'undo'), true) || $id === '') { return array('tone' => 'warning', 'lines' => array(seolok_s('forbidden'))); } seolok_titles_forget(); $answer = seolok_remote('POST', '/api/wp/titles', array('op' => $op, 'id' => $id)); if (is_wp_error($answer)) { return array('tone' => 'warning', 'lines' => array($answer->get_error_code() === 'seolok_unauthorized' ? seolok_t('bad_token') : seolok_s('failed'))); } $code = isset($answer['code']) ? sanitize_key((string) $answer['code']) : ''; if (empty($answer['ok'])) { return array('tone' => 'warning', 'lines' => array(seolok_s(in_array($code, array('not_found', 'state', 'no_text'), true) ? 'r_' . $code : 'failed'))); } if ($code === 'write' || $code === 'restore') { return seolok_titles_write($code, $id, $answer); } return array('tone' => 'success', 'lines' => array(seolok_s(in_array($code, array('done_approve', 'done_reject', 'done_reopen'), true) ? 'r_' . $code : 'failed'))); } /** * Bulk buttons of the SEO titles table (approve, approve + apply on site, reject). Pressing one does NOT do everything at once: * the selected suggestions are put in a queue (the option "seolok_bulk") and handled ONE AT A TIME — by the open page (a small AJAX * call per suggestion, with a progress bar and a Stop button) and, if the page is closed, by WP-Cron. Each suggestion goes through the * same path as its single button, so SEOLOK sees every decision as it happens. A short lock keeps the page and WP-Cron from working * on the queue at the same moment. */ define('SEOLOK_BULK_MAX', 200); // the screen lists at most this many suggestions define('SEOLOK_BULK_PAUSE_MS', 700); // the open page waits this long between two suggestions (the site is not hammered) define('SEOLOK_BULK_CRON_HOOK', 'seolok_bulk_tick'); define('SEOLOK_BULK_LOCK_SECONDS', 90); /** The option as it is in the database now (a page that waits must not work from a copy WordPress cached earlier). */ function seolok_bulk_state() { wp_cache_delete('seolok_bulk', 'options'); $state = get_option('seolok_bulk', array()); return is_array($state) && !empty($state['queue']) && is_array($state['queue']) ? $state : null; } function seolok_bulk_lock() { $now = time(); if (add_option('seolok_bulk_lock', $now, '', 'no')) { return true; } wp_cache_delete('seolok_bulk_lock', 'options'); if ($now - (int) get_option('seolok_bulk_lock', 0) > SEOLOK_BULK_LOCK_SECONDS) { delete_option('seolok_bulk_lock'); // the holder died (a fatal error, a timeout): take over return add_option('seolok_bulk_lock', $now, '', 'no'); } return false; } function seolok_bulk_unlock() { delete_option('seolok_bulk_lock'); } function seolok_bulk_schedule() { if (!wp_next_scheduled(SEOLOK_BULK_CRON_HOOK)) { wp_schedule_single_event(time() + 20, SEOLOK_BULK_CRON_HOOK); } } /** Puts the selected suggestions in the queue. @return array{tone:string,lines:array} the notice to show */ function seolok_titles_bulk_start($what, $ids) { if (!in_array($what, array('approve', 'apply', 'reject'), true) || !$ids) { return array('tone' => 'warning', 'lines' => array(seolok_s('bulk_none'))); } if (seolok_bulk_state()) { return array('tone' => 'warning', 'lines' => array(seolok_s('bulk_busy'))); } $data = seolok_titles_data(true); $known = array(); if (is_array($data)) { foreach (seolok_arr($data['items']) as $row) { if (is_array($row) && isset($row['id'])) { $known[seolok_str($row['id'])] = $row; } } } $queue = array(); $state_skipped = 0; $same_page = 0; $pages = array(); foreach ($ids as $id) { $row = isset($known[$id]) ? $known[$id] : null; $status = $row ? seolok_str($row['status']) : ''; $waiting = $row && !empty($row['waiting']); if ($what === 'reject') { $ok_state = in_array($status, array('proposed', 'approved'), true); } elseif ($what === 'approve') { $ok_state = $status === 'proposed' && !$waiting; } else { $ok_state = ($status === 'proposed' && !$waiting) || $status === 'approved'; } if (!$ok_state) { $state_skipped++; continue; } // only one title per page is applied (a second one would overwrite the first) $page = strtolower(untrailingslashit(seolok_str($row['url']))); if ($what === 'apply') { if (isset($pages[$page])) { $same_page++; continue; } $pages[$page] = true; } $queue[] = array('id' => $id, 'status' => $status, 'path' => substr(seolok_str($row['path']), 0, 200)); } $over = max(0, count($queue) - SEOLOK_BULK_MAX); $queue = array_slice($queue, 0, SEOLOK_BULK_MAX); $state = array('what' => $what, 'queue' => $queue, 'total' => count($queue), 'done' => 0, 'failed' => 0, 'state' => $state_skipped, 'same_page' => $same_page, 'over' => $over, 'reason' => '', 'stopped' => 0, 'at' => time()); if (!$queue) { return array('tone' => 'warning', 'lines' => seolok_bulk_lines($state, false)); } update_option('seolok_bulk', $state, false); seolok_bulk_schedule(); return array('tone' => 'success', 'lines' => array(sprintf(seolok_s('bulk_queued'), count($queue)))); } /** The sentences that tell how a run went (what was done, skipped, could not be done). */ function seolok_bulk_lines($state, $stopped) { $lines = array(); if (!empty($state['done'])) { $lines[] = sprintf(seolok_s('bulk_done'), (int) $state['done']); } if (!empty($state['same_page'])) { $lines[] = sprintf(seolok_s('bulk_same_page'), (int) $state['same_page']); } if (!empty($state['state'])) { $lines[] = sprintf(seolok_s('bulk_state'), (int) $state['state']); } if (!empty($state['failed'])) { $lines[] = sprintf(seolok_s('bulk_failed'), (int) $state['failed']); // which pages and why, so a failure is never a number alone foreach (array_slice(isset($state['results']) && is_array($state['results']) ? array_values(array_filter($state['results'], function ($r) { return empty($r['ok']); })) : array(), 0, 12) as $r) { $lines[] = '• ' . seolok_str($r['path']) . ' — ' . seolok_str($r['line']); } } if (!empty($state['over'])) { $lines[] = sprintf(seolok_s('bulk_cap'), (int) $state['over'], SEOLOK_BULK_MAX); } if ($stopped) { $lines[] = sprintf(seolok_s('bulk_stopped'), (int) $state['stopped']); } return $lines ? $lines : array(seolok_s('bulk_none')); } /** The run is over (finished or stopped): the queue goes, the summary waits for the next time the screen is opened. */ function seolok_bulk_finish($state, $stopped) { $ok = !empty($state['done']) && empty($state['failed']); update_option('seolok_bulk_done', array('tone' => $ok ? 'success' : 'warning', 'lines' => seolok_bulk_lines($state, $stopped), 'at' => time()), false); delete_option('seolok_bulk'); wp_clear_scheduled_hook(SEOLOK_BULK_CRON_HOOK); seolok_titles_forget(); } /** One suggestion through the same path as its single button(s). Counts what happened in $state. */ function seolok_bulk_one(&$state, $item) { $id = seolok_str($item['id']); $status = seolok_str($item['status']); $what = $state['what']; $steps = $what === 'apply' ? ($status === 'proposed' ? array('approve', 'apply') : array('apply')) : array($what); if (!isset($state['results']) || !is_array($state['results'])) { $state['results'] = array(); } $path = isset($item['path']) ? seolok_str($item['path']) : ''; foreach ($steps as $step) { $r = seolok_titles_op($step, $id); if (!isset($r['tone']) || $r['tone'] !== 'success') { $line = !empty($r['lines'][0]) ? seolok_str($r['lines'][0]) : ''; if ($line === seolok_s('r_state')) { $state['state']++; // somebody decided it meanwhile: not a failure return; } $state['failed']++; if ($state['reason'] === '' && $line !== '') { $state['reason'] = $line; } $state['results'][] = array('path' => $path, 'ok' => false, 'line' => $line !== '' ? $line : seolok_s('failed')); return; } } $state['done']++; $state['results'][] = array('path' => $path, 'ok' => true, 'line' => ''); } /** What the screen and the page's script show about the run. */ function seolok_bulk_progress($state, $finished = false, $busy = false) { if (!$state) { return array('active' => false, 'finished' => true, 'busy' => false, 'total' => 0, 'handled' => 0, 'left' => 0, 'text' => '', 'now' => ''); } $left = count($state['queue']); $handled = max(0, (int) $state['total'] - $left); $next = $left ? seolok_str($state['queue'][0]['path']) : ''; return array( 'active' => !$finished, 'finished' => $finished, 'busy' => $busy, 'total' => (int) $state['total'], 'handled' => $handled, 'left' => $left, 'text' => sprintf(seolok_s('bulk_progress'), $handled, (int) $state['total'], (int) $state['done'], (int) $state['failed']), 'now' => $next !== '' ? sprintf(seolok_s('bulk_now'), $next) : '', ); } /** * Handles exactly ONE suggestion of the queue. Called by the open page (AJAX) and by WP-Cron; the lock keeps them apart. * @return array the progress after this step */ function seolok_bulk_step() { $state = seolok_bulk_state(); if (!$state) { return seolok_bulk_progress(null, true); } if (!seolok_bulk_lock()) { return seolok_bulk_progress($state, false, true); } if (function_exists('set_time_limit')) { set_time_limit(60); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- up to three short calls to SEOLOK for one suggestion } try { $state = seolok_bulk_state(); // read again under the lock: a Stop may have come meanwhile if (!$state) { return seolok_bulk_progress(null, true); } $item = array_shift($state['queue']); seolok_bulk_one($state, $item); if (!seolok_bulk_state()) { return seolok_bulk_progress(null, true); // Stop came while this suggestion was being handled: the run stays stopped } if ($state['queue']) { update_option('seolok_bulk', $state, false); return seolok_bulk_progress($state); } $progress = seolok_bulk_progress($state, true); seolok_bulk_finish($state, false); return $progress; } finally { seolok_bulk_unlock(); } } /** Stop: what was not handled yet is left as it is; the summary says how far the run got. */ function seolok_bulk_stop() { $state = seolok_bulk_state(); if (!$state) { return seolok_bulk_progress(null, true); } // a suggestion that is being handled right now is let finish (a few seconds at most); after that the run stops either way for ($try = 0; $try < 4 && !seolok_bulk_lock(); $try++) { usleep(300000); } $state = seolok_bulk_state(); if ($state) { $state['stopped'] = count($state['queue']); $progress = seolok_bulk_progress($state, true); seolok_bulk_finish($state, true); } else { $progress = seolok_bulk_progress(null, true); } seolok_bulk_unlock(); return $progress; } // WP-Cron: one suggestion per tick while a run waits (the open page does the work faster; this is for a closed page) add_action(SEOLOK_BULK_CRON_HOOK, function () { if (!seolok_bulk_state()) { return; } seolok_bulk_step(); if (seolok_bulk_state()) { seolok_bulk_schedule(); } }); function seolok_bulk_ajax($what) { if (!current_user_can('manage_options')) { wp_send_json(array('error' => 'forbidden'), 403); } check_ajax_referer('seolok_screen', 'nonce'); wp_send_json($what === 'stop' ? seolok_bulk_stop() : seolok_bulk_step()); } add_action('wp_ajax_seolok_bulk_step', function () { seolok_bulk_ajax('step'); }); add_action('wp_ajax_seolok_bulk_stop', function () { seolok_bulk_ajax('stop'); }); /** The progress card above the table while a run is on (drawn from the option; the page's script keeps it up to date). */ function seolok_bulk_panel() { $state = seolok_bulk_state(); if (!$state) { return ''; } $p = seolok_bulk_progress($state); $percent = $p['total'] > 0 ? min(100, (int) floor(100 * $p['handled'] / $p['total'])) : 0; $o = '

' . esc_html(seolok_s('bulk_running')) . '

'; $o .= '
' . wp_nonce_field('seolok_screen', '_wpnonce', true, false) . '
'; $o .= '
'; $o .= '

' . esc_html($p['text']) . '

' . esc_html($p['now']) . '

'; $o .= '

' . esc_html(seolok_s('bulk_note')) . '

'; $o .= '
'; // shown by the page's script when a call fails return $o; } /** The summary of the last finished run, once (it may have ended while nobody looked: WP-Cron finished it). */ function seolok_bulk_summary() { wp_cache_delete('seolok_bulk_done', 'options'); $done = get_option('seolok_bulk_done', null); if (!is_array($done) || empty($done['lines']) || !is_array($done['lines'])) { return ''; } delete_option('seolok_bulk_done'); return seolok_render_flash(array('tone' => isset($done['tone']) && $done['tone'] === 'success' ? 'success' : 'warning', 'lines' => array_map('seolok_str', array_slice($done['lines'], 0, 20)))); } /** Writes SEOLOK's approved text on the page (or puts the previous values back) and reports it; a report SEOLOK did not take is undone here too. */ function seolok_titles_write($code, $id, $answer) { $url = isset($answer['url']) ? esc_url_raw((string) $answer['url']) : ''; $target = $url !== '' && seolok_same_host($url) ? seolok_resolve_target($url) : new WP_Error('seolok_foreign_url', 'not this site'); if (is_wp_error($target)) { // SEOLOK's card and history say that WordPress could not write this page (a bulk run would otherwise fail in silence there) seolok_remote('POST', '/api/wp/titles', array('op' => 'failed', 'id' => $id, 'code' => 'page')); return array('tone' => 'warning', 'lines' => array(seolok_s('r_page'))); } $before = array('title' => (string) seolok_get_value($target, 'title'), 'metaDescription' => (string) seolok_get_value($target, 'metadesc')); if ($code === 'write') { $title = sanitize_text_field(isset($answer['title']) ? (string) $answer['title'] : ''); $desc = sanitize_text_field(isset($answer['description']) ? (string) $answer['description'] : ''); if ($title === '' || $desc === '') { return array('tone' => 'warning', 'lines' => array(seolok_s('r_no_text'))); } // written for another title than the page has now (an editor changed it since): not applied, SEOLOK writes it again from the new title if (isset($answer['expectedTitle']) && seolok_title_changed_since($target, (string) $answer['expectedTitle'])) { // SEOLOK learns the page's title now: the text is counted as "to write" there and written again; the suggestion keeps its status seolok_remote('POST', '/api/wp/titles', array('op' => 'stale', 'id' => $id, 'currentTitle' => seolok_effective_title($target))); seolok_titles_forget(); return array('tone' => 'warning', 'lines' => array(seolok_s('r_changed_since'))); } seolok_set_value($target, 'title', $title); seolok_set_value($target, 'metadesc', $desc); seolok_title_extras_set($target, sanitize_text_field(isset($answer['keyword']) ? (string) $answer['keyword'] : ''), $desc); $report = seolok_remote('POST', '/api/wp/titles', array('op' => 'applied', 'id' => $id, 'previous' => $before, 'via' => seolok_detect_seo_plugin() ?: 'core')); $done = 'r_done_applied'; } else { $previous = isset($answer['previous']) && is_array($answer['previous']) ? $answer['previous'] : array(); seolok_restore_value($target, 'title', sanitize_text_field(isset($previous['title']) ? (string) $previous['title'] : '')); seolok_restore_value($target, 'metadesc', sanitize_text_field(isset($previous['metaDescription']) ? (string) $previous['metaDescription'] : '')); seolok_title_extras_undo($target); $report = seolok_remote('POST', '/api/wp/titles', array('op' => 'undone', 'id' => $id)); $done = 'r_done_undone'; } if (is_wp_error($report) || empty($report['ok'])) { // SEOLOK did not hear it: the page goes back to what it was, so the two sides never disagree seolok_restore_value($target, 'title', $before['title']); seolok_restore_value($target, 'metadesc', $before['metaDescription']); if ($code === 'write') { seolok_title_extras_undo($target); } return array('tone' => 'warning', 'lines' => array(seolok_s('r_report'))); } return array('tone' => 'success', 'lines' => array(seolok_s($done))); } /** Title / description / search-term checks of one suggestion: array of array(ok, label, value). */ function seolok_title_checks($title, $desc, $keyword) { $checks = array(); $t = seolok_len($title); $checks[] = array($t >= 30 && $t <= 60, seolok_s($t > 60 ? 'c_title_long' : ($t < 30 ? 'c_title_short' : 'c_title_ok')), $t . '/60'); $d = seolok_len($desc); $checks[] = array($d >= 70 && $d <= 160, seolok_s($d > 160 ? 'c_desc_long' : ($d < 70 ? 'c_desc_short' : 'c_desc_ok')), $d . '/160'); if ($keyword !== '') { $kw = seolok_kw_fold($keyword); $t_fold = seolok_kw_fold($title); $at = seolok_kw_at($t_fold, $kw); $checks[] = array($at >= 0, seolok_s($at >= 0 ? 'c_kw_ok' : 'c_kw_missing'), $keyword); if ($at >= 0) { $start = $at <= seolok_len($t_fold) / 2; $checks[] = array($start, seolok_s($start ? 'c_kw_start' : 'c_kw_late'), ''); } $in_desc = seolok_kw_at(seolok_kw_fold($desc), $kw) >= 0; $checks[] = array($in_desc, seolok_s($in_desc ? 'c_kw_desc_ok' : 'c_kw_desc_missing'), ''); } return $checks; } /** Lower case, every i/İ/ı/I the same letter, single spaces: the focus keyword is matched word for word, as Rank Math / Yoast do (SEOLOK's lib/actions/focus-keyword is the same rule). */ function seolok_kw_fold($text) { $text = str_replace(array('İ', 'I', 'ı'), 'i', (string) $text); $text = function_exists('mb_strtolower') ? mb_strtolower($text, 'UTF-8') : strtolower($text); return trim((string) preg_replace('/\s+/u', ' ', $text)); } /** Where $phrase stands in $text as whole words (characters, not bytes); -1 = not there. */ function seolok_kw_at($text, $phrase) { if ($phrase === '' || !preg_match('/(? 'warn', 'approved' => 'info', 'applied' => 'ok', 'rejected' => 'bad'); $status = isset($item['status']) && isset($tones[$item['status']]) ? $item['status'] : 'proposed'; return seolok_pill($tones[$status], seolok_s('st_' . $status)); } /** The buttons a suggestion offers in its state. $primary_only: the table's single button. */ function seolok_title_buttons($item, $keep, $primary_only = false) { $id = seolok_str($item['id']); $status = seolok_str($item['status']); $f = array_merge(array('id' => $id), $keep); if ($status === 'proposed' && empty($item['waiting'])) { return seolok_screen_form('titles', 'approve', seolok_s('b_approve'), 'button button-primary', $f) . ($primary_only ? '' : seolok_screen_form('titles', 'reject', seolok_s('b_reject'), 'button', $f)); } if ($status === 'approved') { return seolok_screen_form('titles', 'apply', seolok_s('b_apply'), 'button button-primary', $f) . ($primary_only ? '' : seolok_screen_form('titles', 'reject', seolok_s('b_reject'), 'button', $f)); } if ($status === 'applied' && !empty($item['canUndo'])) { return seolok_screen_form('titles', 'undo', seolok_s('b_undo'), 'button', $f); } if ($status === 'rejected') { return seolok_screen_form('titles', 'reopen', seolok_s('b_reopen'), 'button', $f); } return ''; } function seolok_serp($url, $title, $desc, $kind) { $host = (string) wp_parse_url($url, PHP_URL_HOST); $path = trim((string) wp_parse_url($url, PHP_URL_PATH), '/'); $crumb = $host . ($path !== '' ? ' › ' . str_replace('/', ' › ', $path) : ''); return '

' . esc_html(seolok_s($kind)) . '

' . esc_html($crumb) . '

' . esc_html($title) . '

' . esc_html($desc) . '

'; } /** Index of the next suggestion after $from that still waits for a click (approve or apply), going round the list; null when none. */ function seolok_next_waiting($items, $from) { $n = count($items); for ($step = 1; $step < $n; $step++) { $i = ($from + $step) % $n; $status = seolok_str(isset($items[$i]['status']) ? $items[$i]['status'] : ''); if (($status === 'proposed' && empty($items[$i]['waiting'])) || $status === 'approved') { return $i; } } return null; } function seolok_titles_page() { if (!current_user_can('manage_options')) { return; } $o = seolok_screen_open(); $data = seolok_is_configured() ? seolok_titles_data(isset($_GET['seolok_refresh'])) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a read-only "fetch again" flag $panel = is_array($data) && seolok_panel_url(isset($data['panelUrl']) ? $data['panelUrl'] : '') !== '' ? '' . esc_html(seolok_s('open_in_seolok')) . '' : ''; $o .= seolok_screen_head(seolok_s('titles_strip'), seolok_s('titles_title'), seolok_s('titles_lead'), $panel); if ($data === null) { echo wp_kses($o . seolok_screen_not_connected() . '
', seolok_allowed_html()); return; } if (is_wp_error($data)) { echo wp_kses($o . seolok_render_problem($data) . '', seolok_allowed_html()); return; } $o .= seolok_bulk_summary() . seolok_bulk_panel(); // a run that ended while nobody looked, or the progress of the one that is going $items = array_values(array_filter(seolok_arr($data['items']), 'is_array')); if (!$items) { echo wp_kses($o . '

' . esc_html(seolok_s('titles_empty')) . '

', seolok_allowed_html()); return; } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- filters of a read-only list $status = isset($_GET['status']) ? sanitize_key(wp_unslash($_GET['status'])) : ''; $search = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : ''; $want = isset($_GET['item']) ? sanitize_text_field(wp_unslash($_GET['item'])) : ''; $after = isset($_GET['after']) ? sanitize_text_field(wp_unslash($_GET['after'])) : ''; // phpcs:enable $keep = array_filter(array('status' => $status, 's' => $search)); // the detail card: the chosen suggestion, or the first one $index = 0; foreach ($items as $i => $item) { if (seolok_str($item['id']) === $want || ($want === '' && seolok_str($item['id']) === $after)) { $index = $i; } } if ($want === '' && $after !== '') { $next = seolok_next_waiting($items, $index); if ($next !== null) { $index = $next; } $o .= seolok_render_flash(array('tone' => 'success', 'lines' => array(seolok_s($next !== null ? 'r_next' : 'r_all_done')))); } $item = $items[$index]; $title = seolok_str(isset($item['title']) ? $item['title'] : ''); $desc = seolok_str(isset($item['description']) ? $item['description'] : ''); $cur_title = seolok_str(isset($item['currentTitle']) ? $item['currentTitle'] : ''); $cur_desc = seolok_str(isset($item['currentDescription']) ? $item['currentDescription'] : ''); $url = seolok_str($item['url']); $count = function ($text, $suggested) { return $text === '' ? '' : '' . esc_html(sprintf(seolok_s('chars'), seolok_len($text))) . ''; }; $field = function ($label, $text, $note = '', $suggested = false) use ($count) { return '
' . esc_html($label) . ($note !== '' ? '' . esc_html($note) . '' : '') . '
' . ($text !== '' ? esc_html($text) : '' . esc_html(seolok_s('none')) . '') . '
' . $count($text, $suggested) . '
'; }; $nav = '' . esc_html(($index + 1) . ' / ' . count($items)) . ''; if ($index > 0) { $nav .= '‹'; } if ($index < count($items) - 1) { $nav .= '›'; } $o .= '

' . esc_html(seolok_s('detail')) . '

' . $nav . '
'; $o .= '
' . esc_html(seolok_s('page')) . '
'; $o .= $field(seolok_s('current_title'), $cur_title, seolok_s('current_note')); $o .= $field(seolok_s('current_desc'), $cur_desc); if (!empty($item['waiting'])) { $o .= '

' . esc_html(seolok_s('waiting_text')) . '

'; } else { $o .= $field(seolok_s('new_title'), $title, '', true); $o .= $field(seolok_s('new_desc'), $desc, '', true); } $o .= '
' . esc_html(seolok_s('approval')) . '
' . seolok_status_pill($item) . '
' . esc_html(seolok_when(strtotime(seolok_str($item['at'])))) . '
'; $o .= '
' . seolok_title_buttons($item, $keep) . '
'; // preview and checks of what the page would show $show_title = $title !== '' ? $title : $cur_title; $show_desc = $desc !== '' ? $desc : $cur_desc; $o .= '

' . esc_html(seolok_s('serp')) . '

' . seolok_serp($url, $show_title, $show_desc, 'desktop') . seolok_serp($url, $show_title, $show_desc, 'mobile') . '

' . esc_html(seolok_s('serp_note')) . '

'; if ($title !== '') { $o .= '

' . esc_html(seolok_s('analysis')) . '

    '; foreach (seolok_title_checks($title, $desc, seolok_str(isset($item['keyword']) ? $item['keyword'] : '')) as $check) { $o .= '
  • ' . esc_html($check[1]) . '' . esc_html($check[2]) . '
  • '; } $o .= '
'; } $o .= '
'; // the table: status filters (with counts), a search, one button per row $counts = array('' => count($items), 'proposed' => 0, 'approved' => 0, 'applied' => 0, 'rejected' => 0); foreach ($items as $row) { $st = seolok_str($row['status']); if (isset($counts[$st])) { $counts[$st]++; } } $o .= '

' . esc_html(seolok_s('all_pages')) . '

'; $o .= '
'; $o .= ''; $rows = array_values(array_filter($items, function ($row) use ($status, $search) { if ($status !== '' && seolok_str($row['status']) !== $status) { return false; } if ($search === '') { return true; } $hay = seolok_str($row['path']) . ' ' . seolok_str(isset($row['title']) ? $row['title'] : '') . ' ' . seolok_str(isset($row['currentTitle']) ? $row['currentTitle'] : ''); return function_exists('mb_stripos') ? mb_stripos($hay, $search, 0, 'UTF-8') !== false : stripos($hay, $search) !== false; })); if (!$rows) { $o .= '

' . esc_html(seolok_s('no_match')) . '

'; } else { // bulk bar: the row boxes belong to this form through their form="" attribute (each row keeps its own single-button form) $o .= '
'; foreach ($keep as $name => $value) { $o .= ''; } $o .= wp_nonce_field('seolok_screen', '_wpnonce', true, false); $o .= '' . esc_html(seolok_s('bulk_with')) . ' ' . esc_html(sprintf(seolok_s('bulk_count'), 0)) . ' '; $o .= ' '; $o .= ' '; $o .= '
'; $o .= '
'; foreach (array_slice($rows, 0, 50) as $row) { $row_status = seolok_str($row['status']); $pickable = ($row_status === 'proposed' && empty($row['waiting'])) || $row_status === 'approved'; $o .= ''; $o .= ''; $o .= ''; $o .= ''; } $o .= '
' . esc_html(seolok_s('page')) . '' . esc_html(seolok_s('current_title')) . '' . esc_html(seolok_s('new_title')) . '' . esc_html(seolok_s('new_desc')) . '' . esc_html(seolok_s('col_status')) . '' . esc_html(seolok_s('col_action')) . '' . esc_html(seolok_s('col_updated')) . '
' . ($pickable ? '' : '') . '' . esc_html(seolok_str($row['path'])) . '' . esc_html(seolok_str(isset($row['currentTitle']) ? $row['currentTitle'] : '')) . '' . esc_html(seolok_str(isset($row['title']) ? $row['title'] : '')) . '' . esc_html(seolok_str(isset($row['description']) ? $row['description'] : '')) . '' . seolok_status_pill($row) . '' . seolok_title_buttons($row, $keep, true) . '' . esc_html(seolok_when(strtotime(seolok_str($row['at'])))) . '
'; if (count($rows) > 50) { $o .= '

' . esc_html(sprintf(seolok_s('showing'), 50)) . '

'; } } $o .= '
'; echo wp_kses($o, seolok_allowed_html()); } /* ------------------------------------------------------------------ Alerts */ /** robots.txt as search engines get it: the file on the server, or the one WordPress writes. @return array{source:string,text:string} */ function seolok_robots_txt() { $file = ABSPATH . 'robots.txt'; if (is_readable($file)) { $text = file_get_contents($file); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- a local file, read only return array('source' => 'file', 'text' => is_string($text) ? substr($text, 0, 20000) : ''); } // what WordPress' do_robots() writes, through the same filter SEO plugins use $public = (string) get_option('blog_public', '1'); $output = "User-agent: *\n"; if ($public === '0') { $output .= "Disallow: /\n"; } else { $path = (string) wp_parse_url(site_url(), PHP_URL_PATH); $output .= 'Disallow: ' . $path . "/wp-admin/\nAllow: " . $path . "/wp-admin/admin-ajax.php\n"; } return array('source' => 'virtual', 'text' => (string) apply_filters('robots_txt', $output, $public)); } /** * Whether robots.txt keeps Google out of the whole site: the group for Googlebot (or, without one, for every robot) disallows "/" * and does not allow it back. @return array the lines that block (empty = not blocked) */ function seolok_robots_blocking($text) { $groups = array(); $current = null; $last_was_agent = false; foreach (preg_split('/\r\n|\r|\n/', (string) $text) as $line) { $line = trim(preg_replace('/#.*$/', '', $line)); if ($line === '' || strpos($line, ':') === false) { continue; } list($field, $value) = array_map('trim', explode(':', $line, 2)); $field = strtolower($field); if ($field === 'user-agent') { if (!$last_was_agent || $current === null) { $groups[] = array('agents' => array(), 'rules' => array()); $current = count($groups) - 1; } $groups[$current]['agents'][] = strtolower($value); $last_was_agent = true; continue; } $last_was_agent = false; if ($current !== null && ($field === 'disallow' || $field === 'allow')) { $groups[$current]['rules'][] = array($field, $value); } } $pick = function ($agent) use ($groups) { $found = array(); foreach ($groups as $g) { if (in_array($agent, $g['agents'], true)) { $found[] = $g; } } return $found; }; $use = $pick('googlebot'); $agent = 'googlebot'; if (!$use) { $use = $pick('*'); $agent = '*'; } $blocking = array(); foreach ($use as $g) { $all = false; $back = false; foreach ($g['rules'] as $rule) { if ($rule[0] === 'disallow' && ($rule[1] === '/' || $rule[1] === '/*')) { $all = true; } if ($rule[0] === 'allow' && ($rule[1] === '/' || $rule[1] === '/*')) { $back = true; } } if ($all && !$back) { $blocking = array('User-agent: ' . ($agent === '*' ? '*' : 'Googlebot'), 'Disallow: /'); } } return $blocking; } /** Who put "noindex" on a page of this site, as far as WordPress can tell. */ function seolok_noindex_source($url) { if ((string) get_option('blog_public', '1') === '0') { return 'reading'; } $target = seolok_resolve_target($url); if (is_wp_error($target)) { return 'unknown'; } if (seolok_get_value($target, 'noindex') === '1') { return 'seolok'; } if ($target['kind'] === 'post') { if (get_post_meta($target['id'], '_yoast_wpseo_meta-robots-noindex', true) === '1') { return 'yoast'; } $rm = get_post_meta($target['id'], 'rank_math_robots', true); if (is_array($rm) && in_array('noindex', $rm, true)) { return 'rankmath'; } } return 'unknown'; } /** The home page and a few key pages (posts page, latest pages and posts), at most 8. */ function seolok_key_pages() { $urls = array(home_url('/')); $posts_page = (int) get_option('page_for_posts'); if ($posts_page > 0) { $urls[] = get_permalink($posts_page); } foreach (array('page' => 4, 'post' => 3) as $type => $n) { foreach (get_posts(array('post_type' => $type, 'post_status' => 'publish', 'numberposts' => $n, 'orderby' => 'modified', 'order' => 'DESC')) as $p) { $urls[] = get_permalink($p); } } return array_slice(array_values(array_unique(array_filter($urls, 'is_string'))), 0, 8); } /** Opens each key page like a visitor and reads its robots tag and X-Robots-Tag header. */ function seolok_check_pages() { $pages = array(); $errors = 0; $last_error = ''; foreach (seolok_key_pages() as $url) { $response = wp_remote_get($url, array('timeout' => 8, 'redirection' => 3, 'sslverify' => apply_filters('https_local_ssl_verify', false), 'headers' => array('Cache-Control' => 'no-cache'), 'user-agent' => 'SEOLOK-Check/' . SEOLOK_VERSION . '; ' . home_url('/'))); if (is_wp_error($response)) { $errors++; $last_error = $response->get_error_message(); $pages[] = array('url' => $url, 'code' => 0, 'noindex' => false, 'how' => '', 'tag' => '', 'source' => ''); continue; } $code = (int) wp_remote_retrieve_response_code($response); $header = (string) wp_remote_retrieve_header($response, 'x-robots-tag'); $body = substr((string) wp_remote_retrieve_body($response), 0, 300000); $tag = ''; $how = ''; if (preg_match('/\b(noindex|none)\b/i', $header)) { $how = 'header'; $tag = 'X-Robots-Tag: ' . $header; } elseif (preg_match_all('/]*>/i', $body, $metas)) { foreach ($metas[0] as $meta) { if (preg_match('/\bname\s*=\s*["\']?(robots|googlebot)\b/i', $meta) && preg_match('/\bcontent\s*=\s*["\']([^"\']*)/i', $meta, $content) && preg_match('/\b(noindex|none)\b/i', $content[1])) { $how = 'meta'; $tag = trim($meta); break; } } } // a in the page, or a Link header with rel=canonical (0.6.8, "Other technical checks") $canonical = (bool) preg_match('/rel\s*=\s*["\']?canonical\b/i', (string) wp_remote_retrieve_header($response, 'link')); if (!$canonical && preg_match_all('/]*>/i', $body, $links)) { foreach ($links[0] as $link) { if (preg_match('/\brel\s*=\s*["\']?canonical\b/i', $link)) { $canonical = true; break; } } } $pages[] = array('url' => $url, 'code' => $code, 'noindex' => $how !== '', 'how' => $how, 'tag' => substr($tag, 0, 300), 'source' => $how === 'header' ? 'header' : ($how === 'meta' ? seolok_noindex_source($url) : ''), 'canonical' => $canonical); } return array('at' => time(), 'pages' => $pages, 'failed' => $pages && $errors === count($pages), 'error' => substr($last_error, 0, 200)); } /** The state of every check: bad | warn | ok | unknown, with what it found. Local checks are cheap and run on every view. */ function seolok_checks() { $robots = seolok_robots_txt(); $block = seolok_robots_blocking($robots['text']); $pages = get_option('seolok_page_check', array()); $pages = is_array($pages) ? $pages : array(); $meta = 'unknown'; if (!empty($pages['pages']) && empty($pages['failed'])) { $meta = 'ok'; foreach ($pages['pages'] as $p) { if (!empty($p['noindex'])) { $meta = untrailingslashit((string) $p['url']) === untrailingslashit(home_url('/')) ? 'bad' : ($meta === 'bad' ? 'bad' : 'warn'); } } } return array( 'visibility' => array('state' => (string) get_option('blog_public', '1') === '0' ? 'bad' : 'ok'), 'robots' => array('state' => $block ? 'bad' : 'ok', 'source' => $robots['source'], 'lines' => $block, 'text' => $robots['text']), 'meta' => array('state' => $meta, 'pages' => $pages), ); } /** Notes every change of a check's state in the timeline (who did it, when WordPress knows). */ function seolok_checks_record($checks, $by = '') { $states = get_option('seolok_check_states', array()); $states = is_array($states) ? $states : array(); $changed = false; foreach ($checks as $key => $check) { $state = $check['state']; if ($state === 'unknown') { continue; } $before = isset($states[$key]) ? $states[$key] : null; if ($before !== $state) { // the first look at a healthy site is not news if (!($before === null && $state === 'ok')) { seolok_log_push('seolok_check_log', array('at' => time(), 'key' => $key, 'state' => $state, 'by' => $by)); } $states[$key] = $state; $changed = true; } } if ($changed) { update_option('seolok_check_states', $states, false); } } /** Warnings shown in red: blocking checks that are not ignored. */ function seolok_alert_count() { $states = get_option('seolok_check_states', array()); $ignored = get_option('seolok_ignored', array()); $n = 0; foreach (is_array($states) ? $states : array() as $key => $state) { if ($state === 'bad' && !(is_array($ignored) && isset($ignored[$key]))) { $n++; } } return $n; } // the reading setting changed (by anyone, anywhere): recorded in the timeline with the user, whatever live sync says add_action('update_option_blog_public', function ($old, $new) { $user = wp_get_current_user(); seolok_checks_record(array('visibility' => array('state' => (string) $new === '0' ? 'bad' : 'ok')), $user && $user->exists() ? $user->display_name : ''); }, 20, 2); function seolok_alerts_op($op, $key) { if ($op === 'search_on') { $result = seolok_search_allow(); seolok_checks_record(seolok_checks()); return $result; } if ($op === 'recheck') { update_option('seolok_page_check', seolok_check_pages(), false); seolok_checks_record(seolok_checks()); return array('tone' => 'success', 'lines' => array(seolok_s('r_rechecked'))); } if (($op === 'ignore' || $op === 'unignore') && in_array($key, array('visibility', 'robots', 'meta'), true)) { $ignored = get_option('seolok_ignored', array()); $ignored = is_array($ignored) ? $ignored : array(); if ($op === 'ignore') { $ignored[$key] = time(); } else { unset($ignored[$key]); } update_option('seolok_ignored', $ignored, false); return array('tone' => 'success', 'lines' => array(seolok_s($op === 'ignore' ? 'r_ignored' : 'r_unignored'))); } return array('tone' => 'warning', 'lines' => array(seolok_s('forbidden'))); } /** The daily check (0.6.8): WP-Cron opens the key pages and records the three checks once a day, so the timeline moves without a click. */ define('SEOLOK_DAILY_CHECK_HOOK', 'seolok_daily_check'); add_action(SEOLOK_DAILY_CHECK_HOOK, function () { update_option('seolok_page_check', seolok_check_pages(), false); seolok_checks_record(seolok_checks()); }); add_action('admin_init', function () { if (!wp_next_scheduled(SEOLOK_DAILY_CHECK_HOOK)) { wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', SEOLOK_DAILY_CHECK_HOOK); } }); /** One check's state as a pill tone and label (bad / warn / ok / not checked). */ function seolok_ui_tone($state) { return $state === 'bad' ? 'bad' : ($state === 'warn' ? 'warn' : ($state === 'ok' ? 'ok' : 'muted')); } function seolok_ui_label($state) { return $state === 'bad' ? seolok_s('blocked') : ($state === 'warn' ? seolok_s('notice') : ($state === 'ok' ? seolok_s('clear') : seolok_s('unknown'))); } /** A small line icon (stroke, currentColor) for the alerts screen. */ function seolok_ui_icon($name) { $paths = array( 'shield' => '', 'file' => '', 'code' => '', 'eye' => '', 'gear' => '', 'bell' => '', 'cal' => '', 'pages' => '', 'clock' => '', 'check' => '', 'warn' => '', 'arrow' => '', 'chev' => '', 'book' => '', 'help' => '', 'link' => '', 'chain' => '', 'chart' => '', 'list' => '', 'search' => '', 'bolt' => '', 'star' => '', 'refresh'=> '', ); $d = isset($paths[$name]) ? $paths[$name] : ''; return ''; } /** * The checks the plugin sees from the inside, besides the three blocking ones (alerts screen, 0.6.8): https, permalinks, WP-Cron, * an XML sitemap and canonical tags on the pages checked. Each: ok | warn | unknown, with one sentence. */ function seolok_other_checks($robots_text, $page_check) { $out = array(); $https = wp_parse_url(home_url('/'), PHP_URL_SCHEME) === 'https'; $out['https'] = array('state' => $https ? 'ok' : 'warn', 'text' => seolok_s($https ? 'oc_https_ok' : 'oc_https')); $pretty = (string) get_option('permalink_structure', '') !== ''; $out['permalinks'] = array('state' => $pretty ? 'ok' : 'warn', 'text' => seolok_s($pretty ? 'oc_permalinks_ok' : 'oc_permalinks')); $late = seolok_cron_overdue_minutes(); $off = defined('DISABLE_WP_CRON') && DISABLE_WP_CRON; $out['cron'] = array('state' => $late >= 120 ? 'warn' : 'ok', 'text' => $late >= 120 ? sprintf(seolok_s('oc_cron'), $late) : seolok_s($off ? 'oc_cron_real' : 'oc_cron_ok')); $core = function_exists('wp_sitemaps_get_server') && wp_sitemaps_get_server() && method_exists(wp_sitemaps_get_server(), 'sitemaps_enabled') && wp_sitemaps_get_server()->sitemaps_enabled(); $declared = (bool) preg_match('/^\s*sitemap\s*:/im', (string) $robots_text); $out['sitemap'] = array('state' => $core || $declared ? 'ok' : 'warn', 'text' => seolok_s($core || $declared ? 'oc_sitemap_ok' : 'oc_sitemap')); $pages = !empty($page_check['pages']) && empty($page_check['failed']) ? $page_check['pages'] : array(); if (!$pages) { $out['canonical'] = array('state' => 'unknown', 'text' => seolok_s('oc_canonical_never')); } else { $opened = array_filter($pages, function ($p) { return isset($p['code']) && (int) $p['code'] === 200 && empty($p['noindex']); // a page kept out of Google needs no canonical }); $with = array_filter($opened, function ($p) { return !empty($p['canonical']); }); $known = array_filter($opened, function ($p) { return array_key_exists('canonical', $p); }); $out['canonical'] = !$known ? array('state' => 'unknown', 'text' => seolok_s('oc_canonical_never')) : (count($with) === count($known) ? array('state' => 'ok', 'text' => seolok_s('oc_canonical_ok')) : array('state' => 'warn', 'text' => sprintf(seolok_s('oc_canonical'), count($known) - count($with), count($known)))); } return $out; } /** A detail card of one blocking check: its own header, the body, and the ignore button when it warns. */ function seolok_alert_card($key, $icon, $title, $state, $ignored, $body, $lead = '') { $tone = $ignored ? 'muted' : seolok_ui_tone($state); $o = '
' . seolok_ui_icon($icon) . '

' . esc_html($title) . '

' . ($lead !== '' ? '

' . esc_html($lead) . '

' : '') . '
' . ($ignored ? seolok_pill('muted', seolok_s('ignored')) : seolok_pill($tone, seolok_ui_label($state))) . '
' . $body . '
'; if ($state === 'bad' || $state === 'warn' || $ignored) { $o .= '
' . seolok_screen_form('alerts', $ignored ? 'unignore' : 'ignore', seolok_s($ignored ? 'b_unignore' : 'b_ignore'), 'button seolok-button-quiet', array('key' => $key)) . '
'; } return $o . '
'; } /** A green / red / amber note inside a card. */ function seolok_ui_note($tone, $text, $sub = '') { return '
' . seolok_ui_icon($tone === 'ok' ? 'check' : 'warn') . '

' . esc_html($text) . '

' . ($sub !== '' ? '

' . esc_html($sub) . '

' : '') . '
'; } function seolok_alerts_page() { if (!current_user_can('manage_options')) { return; } $checks = seolok_checks(); seolok_checks_record($checks); $ignored = get_option('seolok_ignored', array()); $ignored = is_array($ignored) ? $ignored : array(); $page_check = $checks['meta']['pages']; $other = seolok_other_checks($checks['robots']['text'], $page_check); $tr = seolok_is_tr(); $help = 'https://seolok.com/' . ($tr ? 'tr' : 'en') . '/help/wordpress'; $support = 'https://seolok.com/' . ($tr ? 'tr' : 'en') . '/contact'; $critical = array(); $attention = 0; foreach (array('visibility', 'robots', 'meta') as $key) { if (isset($ignored[$key])) { continue; } if ($checks[$key]['state'] === 'bad') { $critical[] = $key; } elseif ($checks[$key]['state'] === 'warn') { $attention++; } } foreach ($other as $c) { if ($c['state'] === 'warn') { $attention++; } } $last = !empty($page_check['at']) ? (int) $page_check['at'] : 0; $o = seolok_screen_open(); // top bar: the mark, "› Alerts", help and support $o .= ''; // health hero $ok = !$critical; $o .= '
' . seolok_ui_icon($ok ? 'shield' : 'warn') . '

' . esc_html(seolok_s('al_health')) . '

' . esc_html(seolok_s($ok ? 'al_crawlable' : 'al_blocked')) . ' ' . seolok_pill($ok ? 'ok' : 'bad', $ok ? seolok_s('al_no_block') : sprintf(seolok_s('al_n_blocks'), count($critical))) . '

' . esc_html(seolok_s($ok ? 'all_clear' : 'al_blocked_lead')) . '

'; $o .= '
' . seolok_ui_icon('cal') . '

' . esc_html(seolok_s('al_last')) . '

' . esc_html($last ? seolok_when($last) : seolok_s('unknown')) . '

' . esc_html(seolok_s('al_daily')) . '

'; $o .= '
' . seolok_screen_form('alerts', 'recheck', seolok_s('b_recheck'), 'button button-primary seolok-ui-primary') . '
'; if ($critical) { $o .= '
' . esc_html(seolok_s('crit_badge')) . '

' . esc_html(seolok_s('crit_title')) . '

' . esc_html(seolok_s('crit_text')) . '

'; if (in_array('visibility', $critical, true)) { $o .= seolok_screen_form('alerts', 'search_on', seolok_s('b_allow'), 'button seolok-button-danger'); } $o .= seolok_screen_form('alerts', 'recheck', seolok_s('b_recheck'), 'button') . '
'; } // four summary cards $other_bad = count(array_filter($other, function ($c) { return $c['state'] === 'warn'; })); $other_state = $other_bad ? 'warn' : 'ok'; $summary = array( array('robots', 'file', seolok_s('robots'), $checks['robots']['state'], 1, seolok_s($checks['robots']['state'] === 'bad' ? 'robots_bad' : 'robots_ok')), array('meta', 'code', seolok_s('meta'), $checks['meta']['state'], 1, $checks['meta']['state'] === 'unknown' ? seolok_s('al_meta_short_never') : seolok_s($checks['meta']['state'] === 'bad' ? 'meta_bad' : ($checks['meta']['state'] === 'warn' ? 'meta_warn' : 'meta_ok'))), array('visibility', 'eye', seolok_s('visibility'), $checks['visibility']['state'], 1, seolok_s($checks['visibility']['state'] === 'bad' ? 'al_vis_short_bad' : 'al_vis_short_ok')), array('other', 'gear', seolok_s('al_tech'), $other_state, count($other), $other_bad ? sprintf(seolok_s('al_tech_bad'), $other_bad) : seolok_s('al_tech_ok')), ); $o .= ''; // important alerts $o .= '
' . seolok_ui_icon('bell') . '

' . esc_html(seolok_s('al_important')) . '

' . seolok_pill('muted', sprintf(seolok_s('al_counts'), count($critical), $attention)) . '
'; if (!$critical && !$attention) { $o .= '

' . esc_html(seolok_s('al_none_lead')) . '

' . seolok_ui_note('ok', seolok_s('al_all_good'), seolok_s('al_all_good_sub')); } else { $o .= ''; } $o .= '
'; // three detail cards $r = $checks['robots']; $body = '

' . esc_html(seolok_s($r['state'] === 'bad' ? 'robots_bad' : 'robots_ok')) . '

' . esc_html(seolok_s($r['source'] === 'file' ? 'robots_file' : 'robots_virtual')) . '

'; $lines = $r['lines'] ? $r['lines'] : array_slice(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $r['text']))), 0, 6); $body .= '
' . esc_html(implode("\n", $lines)) . '
'; if ($r['state'] === 'bad' && $r['source'] === 'file') { $body .= '

' . esc_html(seolok_s('robots_fix_file')) . '

'; } $body .= '

' . seolok_ui_icon('link') . esc_html(seolok_s('robots_view')) . '

'; $cards = seolok_alert_card('robots', 'file', seolok_s('robots'), $r['state'], isset($ignored['robots']), $body, seolok_s('al_robots_lead')); $m = $checks['meta']; if ($m['state'] === 'unknown') { $text = !empty($page_check['failed']) ? sprintf(seolok_s('meta_failed'), seolok_str(isset($page_check['error']) ? $page_check['error'] : '')) : seolok_s('meta_never'); $body = '

' . esc_html($text) . '

'; } else { $body = seolok_ui_note($m['state'] === 'ok' ? 'ok' : 'bad', seolok_s($m['state'] === 'bad' ? 'meta_bad' : ($m['state'] === 'warn' ? 'meta_warn' : 'meta_ok'))); $noindex = 0; foreach ($page_check['pages'] as $p) { if (!empty($p['noindex'])) { $noindex++; } } foreach ($page_check['pages'] as $p) { if (!empty($p['noindex']) && !empty($p['tag'])) { $body .= '
' . esc_html($p['tag']) . '
'; break; } } $body .= '
' . esc_html((string) count($page_check['pages'])) . '' . esc_html(seolok_s('al_pages_checked')) . '
' . esc_html((string) $noindex) . '' . esc_html(seolok_s('al_pages_noindex')) . '
'; } if (!empty($page_check['at'])) { $body .= '

' . seolok_ui_icon('clock') . esc_html(sprintf(seolok_s('checked_at'), seolok_when($page_check['at']))) . '

'; } $body .= '
' . seolok_screen_form('alerts', 'recheck', seolok_s('b_check_pages'), 'button seolok-ui-outline') . '
'; $cards .= seolok_alert_card('meta', 'code', seolok_s('meta'), $m['state'], isset($ignored['meta']), $body, seolok_s('al_meta_lead')); $v = $checks['visibility']; $body = seolok_ui_note($v['state'] === 'bad' ? 'bad' : 'ok', seolok_s($v['state'] === 'bad' ? 'vis_bad' : 'al_vis_short_ok'), seolok_s($v['state'] === 'bad' ? 'al_vis_fix' : 'vis_ok')); $body .= ''; $cards .= seolok_alert_card('visibility', 'eye', seolok_s('visibility'), $v['state'], isset($ignored['visibility']), $body, seolok_s('al_vis_lead')); $o .= '
' . $cards . '
'; // bottom row: affected pages, timeline, other technical checks $affected = array(); foreach (!empty($page_check['pages']) && is_array($page_check['pages']) ? $page_check['pages'] : array() as $p) { if (!empty($p['noindex'])) { $affected[] = $p; } } $o .= '
' . seolok_ui_icon('pages') . '

' . esc_html(seolok_s('affected')) . '

' . seolok_pill('muted', sprintf(seolok_s('n_pages'), count($affected))) . '
'; if ($m['state'] === 'unknown') { $o .= '

' . esc_html(seolok_s('meta_never')) . '

'; } else { $o .= '

' . esc_html(seolok_s($affected ? 'affected_lead' : 'affected_none')) . '

'; if (!$affected) { $o .= ''; } foreach ($affected as $p) { $url = (string) $p['url']; $src = in_array($p['source'], array('yoast', 'rankmath', 'seolok', 'reading', 'header'), true) ? $p['source'] : 'unknown'; $target = seolok_resolve_target($url); $edit = !is_wp_error($target) && $target['kind'] === 'post' ? get_edit_post_link($target['id'], 'raw') : ''; $o .= ''; } $o .= '
' . esc_html(seolok_s('al_col_page')) . '' . esc_html(seolok_s('col_status')) . '' . esc_html(seolok_s('set_by')) . '' . esc_html(seolok_s('al_col_checked')) . '
' . seolok_ui_icon('file') . '' . esc_html(seolok_s('al_no_affected')) . '' . esc_html(seolok_s('al_no_affected_sub')) . '
' . esc_html(wp_parse_url($url, PHP_URL_PATH) ? wp_parse_url($url, PHP_URL_PATH) : $url) . '' . ($edit ? ' · ' . esc_html(seolok_s('edit_page')) . '' : '') . '' . seolok_pill('bad', seolok_s('not_indexed')) . '' . esc_html(seolok_s('src_' . $src)) . '' . esc_html(seolok_when($page_check['at'])) . '
'; } $o .= '
'; $log = get_option('seolok_check_log', array()); $o .= '
' . seolok_ui_icon('clock') . '

' . esc_html(seolok_s('timeline')) . '

'; $items = array(); if (!empty($page_check['at'])) { $items[] = array('at' => (int) $page_check['at'], 'state' => !empty($page_check['failed']) ? 'bad' : 'ok', 'text' => seolok_s(!empty($page_check['failed']) ? 'al_ev_check_failed' : 'al_ev_check'), 'by' => ''); } foreach (is_array($log) ? array_slice($log, 0, 8) : array() as $entry) { $key = isset($entry['key']) ? (string) $entry['key'] : ''; $state = isset($entry['state']) ? (string) $entry['state'] : ''; $label = array('visibility' => 'ev_vis_', 'robots' => 'ev_robots_', 'meta' => 'ev_meta_'); if (!isset($label[$key]) || !in_array($state, array('bad', 'warn', 'ok'), true)) { continue; } $items[] = array('at' => isset($entry['at']) ? (int) $entry['at'] : 0, 'state' => $state === 'ok' ? 'ok' : 'bad', 'text' => seolok_s($label[$key] . ($key === 'meta' ? $state : ($state === 'ok' ? 'ok' : 'bad'))), 'by' => !empty($entry['by']) ? sprintf(seolok_s('ev_by'), (string) $entry['by']) : ''); } if (!$items) { $o .= '

' . esc_html(seolok_s('timeline_empty')) . '

'; } else { $o .= '
    '; foreach (array_slice($items, 0, 8) as $it) { $o .= '
  1. ' . esc_html($it['text']) . ($it['by'] !== '' ? '' . esc_html($it['by']) . '' : '') . '' . esc_html(seolok_when($it['at'])) . '
  2. '; } $o .= '
'; } $o .= '
'; $passed = count(array_filter($other, function ($c) { return $c['state'] === 'ok'; })); $o .= '
' . seolok_ui_icon('gear') . '

' . esc_html(seolok_s('other_checks')) . '

' . seolok_pill($passed === count($other) ? 'ok' : 'warn', sprintf(seolok_s('al_passed'), $passed, count($other))) . '
    '; foreach ($other as $key => $c) { $o .= '
  • ' . seolok_ui_icon($c['state'] === 'ok' ? 'check' : 'warn') . '' . esc_html(seolok_s('oc_t_' . $key)) . '' . esc_html($c['text']) . '' . seolok_pill(seolok_ui_tone($c['state']), seolok_ui_label($c['state'] === 'warn' ? 'warn' : $c['state'])) . '
  • '; } $o .= '
'; $o .= ''; echo wp_kses($o, seolok_allowed_html()); } /* --------------------------------------------------------------- Live sync */ /** Post types the live sync may tell about (public, not media); with WooCommerce this includes "product". */ function seolok_sync_type_choices() { $out = array(); foreach (get_post_types(array('public' => true), 'objects') as $name => $type) { // templates, forms, form entries and other "public in name only" types are never sent, so they are not offered either (seolok_sync_type_allowed) if (!seolok_sync_type_allowed($name)) { continue; } $out[$name] = isset($type->labels->name) ? (string) $type->labels->name : (string) $name; } return $out; } /** A failed report's code in the owner's words (no address, no token, no server text). */ function seolok_sync_error_text($code) { $code = sanitize_key((string) $code); $known = array('http_request_failed', 'seolok_unauthorized', 'seolok_remote_error', 'seolok_not_configured'); return in_array($code, $known, true) ? seolok_s('err_' . $code) : sprintf(seolok_s('err_other'), $code); } /** Whether WP-Cron is keeping up (it runs the flush and the hourly report): late by more than five minutes, or switched off, means no promise of "within a minute". */ function seolok_cron_late() { return (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) || seolok_cron_overdue_minutes() >= 5; } /** The next try, as a sentence: a scheduled moment, the backoff after a failure, or "WP-Cron is late" (then an administrator's visit or "Send now"). */ function seolok_sync_next_text() { $q = seolok_sync_queue_state(); if ($q['backoffUntil'] > time()) { return sprintf(seolok_s('next_backoff'), seolok_when($q['backoffUntil'])); } if (seolok_cron_late() || ($q['nextAt'] > 0 && $q['nextAt'] < time() - MINUTE_IN_SECONDS)) { return seolok_s('next_cron_late'); } return $q['nextAt'] > 0 ? seolok_when($q['nextAt']) : seolok_s('next_none'); } /** The types the owner picked; null = all of them (the default). */ function seolok_sync_types() { $types = get_option('seolok_sync_types', null); return is_array($types) ? array_values(array_filter($types, 'is_string')) : null; } /** Counters of what was sent (kept by this plugin): array(total, day, today). */ function seolok_sync_note_sent($events) { $n = count($events); $stats = get_option('seolok_sync_stats', array()); $stats = is_array($stats) ? $stats : array(); $day = wp_date('Y-m-d'); $stats['total'] = (isset($stats['total']) ? (int) $stats['total'] : 0) + $n; $stats['today'] = (isset($stats['day']) && $stats['day'] === $day ? (int) $stats['today'] : 0) + $n; $stats['day'] = $day; $stats['last_ok'] = time(); unset($stats['error']); update_option('seolok_sync_stats', $stats, false); if (!$n) { return; } $sent = get_option('seolok_sync_sent', array()); $sent = is_array($sent) ? $sent : array(); foreach ($events as $event) { $data = isset($event['data']) && is_array($event['data']) ? $event['data'] : array(); array_unshift($sent, array('at' => time(), 'type' => (string) $event['type'], 'title' => isset($data['title']) ? (string) $data['title'] : '', 'url' => isset($data['url']) ? (string) $data['url'] : (isset($data['to']) ? (string) $data['to'] : ''))); } update_option('seolok_sync_sent', array_slice($sent, 0, SEOLOK_SENT_MAX), false); } function seolok_sync_note_error($error) { $code = is_wp_error($error) ? $error->get_error_code() : 'error'; $stats = get_option('seolok_sync_stats', array()); $stats = is_array($stats) ? $stats : array(); $stats['error'] = array('at' => time(), 'code' => $code); update_option('seolok_sync_stats', $stats, false); seolok_log_push('seolok_sync_log', array('at' => time(), 'kind' => 'error', 'detail' => $code)); } /** A switch, a reset or a setting: the audit log (with the user, when there is one). */ function seolok_sync_note($kind, $detail = '') { $user = function_exists('wp_get_current_user') ? wp_get_current_user() : null; seolok_log_push('seolok_sync_log', array('at' => time(), 'kind' => $kind, 'detail' => $detail, 'by' => $user && $user->exists() ? $user->display_name : '')); } /** * Runs WordPress's own scheduler once, the way a server cron does: wp-cron.php without the key WordPress adds to its own start takes the * lock itself and runs everything that is due. DISABLE_WP_CRON only stops the automatic start on page loads (`wp_cron()`), not this file, * which is why a real cron can call it on a site that has WP-Cron switched off. A long task (a backup) may outlast the wait: then the * answer is "started", not an error, because the other process goes on (ignore_user_abort). * * @return array state ran|started|failed, late (minutes still overdue after a run), detail (why a try failed) */ function seolok_cron_run_now() { $response = wp_remote_get(site_url('wp-cron.php') . '?seolok_run=' . time(), array('timeout' => 25, 'sslverify' => apply_filters('https_local_ssl_verify', false), 'headers' => array('Cache-Control' => 'no-cache'))); if (is_wp_error($response)) { $message = (string) $response->get_error_message(); if (stripos($message, 'timed out') !== false || stripos($message, 'timeout') !== false || stripos($message, 'curl error 28') !== false) { return array('state' => 'started', 'late' => 0, 'detail' => ''); } return array('state' => 'failed', 'late' => 0, 'detail' => substr(sanitize_text_field($message), 0, 120)); } $code = (int) wp_remote_retrieve_response_code($response); if ($code < 200 || $code >= 300) { return array('state' => 'failed', 'late' => 0, 'detail' => 'HTTP ' . $code); } // the other process changed the schedule: this request must not read its own cached copy wp_cache_delete('cron', 'options'); wp_cache_delete('alloptions', 'options'); return array('state' => 'ran', 'late' => seolok_cron_overdue_minutes(), 'detail' => ''); } /** How to set up a real cron, for any host: the address is the whole job, the rest is where each kind of panel keeps its scheduler. */ function seolok_cron_help() { $url = site_url('wp-cron.php'); $code = function ($text) { return '' . esc_html($text) . ''; }; $o = '
' . esc_html(seolok_s('cron_help_title')) . '
'; $o .= '

' . esc_html(seolok_s('cron_help_lead')) . '

' . $code($url) . '

'; $o .= '

' . esc_html(seolok_s('cron_help_linux')) . '

' . $code('curl -s "' . $url . '" >/dev/null 2>&1') . '

'; $o .= '

' . esc_html(seolok_s('cron_help_plesk')) . '

'; $o .= '

' . esc_html(seolok_s('cron_help_windows')) . '

' . $code('powershell -NoProfile -Command "(Invoke-WebRequest -UseBasicParsing \'' . $url . '\').StatusCode"') . '

'; $o .= '

' . esc_html(seolok_s('cron_help_external')) . '

' . esc_html(seolok_s('cron_help_config')) . '

'; return $o . '
'; } /** The button's answer: what happened in words; when WordPress cannot reach its scheduler, SEOLOK's own waiting changes are still sent from here. */ function seolok_cron_run_screen() { $r = seolok_cron_run_now(); if ($r['state'] === 'started') { return array('tone' => 'success', 'lines' => array(seolok_s('cron_started'))); } if ($r['state'] === 'ran') { return $r['late'] < 5 ? array('tone' => 'success', 'lines' => array(seolok_s('cron_ran'))) : array('tone' => 'warning', 'lines' => array(sprintf(seolok_s('cron_ran_still'), (int) $r['late']))); } $lines = array(sprintf(seolok_s('cron_failed'), $r['detail'])); if (seolok_sync_enabled()) { $answer = seolok_sync_beat(10, true); if (!is_wp_error($answer)) { seolok_sync_note('manual'); $lines[] = seolok_s('cron_failed_own'); } } return array('tone' => 'warning', 'lines' => $lines); } function seolok_sync_screen_op($op, $types) { if ($op === 'sync_on' || $op === 'sync_off') { return seolok_sync_set($op === 'sync_on'); } if ($op === 'cron_run') { return seolok_cron_run_screen(); } if ($op === 'sync_now' && seolok_sync_enabled()) { $answer = seolok_sync_beat(10, true); if (!is_wp_error($answer)) { seolok_sync_note('manual'); } return array('tone' => is_wp_error($answer) ? 'warning' : 'success', 'lines' => array(is_wp_error($answer) ? seolok_s('failed') : seolok_s('r_sent'))); } if ($op === 'types') { $choices = seolok_sync_type_choices(); $picked = array_values(array_intersect(array_keys($choices), $types)); if (!$picked) { return array('tone' => 'warning', 'lines' => array(seolok_s('r_types_none'))); } if (count($picked) === count($choices)) { delete_option('seolok_sync_types'); // all of them: new public types are told too } else { update_option('seolok_sync_types', $picked, false); } seolok_sync_note('types', implode(', ', array_map(function ($t) use ($choices) { return $choices[$t]; }, $picked))); return array('tone' => 'success', 'lines' => array(seolok_s('r_types_saved'))); } return array('tone' => 'warning', 'lines' => array(seolok_s('forbidden'))); } function seolok_sync_event_label($type) { $key = 'ev_' . sanitize_key($type); $label = seolok_s($key); return $label === $key ? (string) $type : $label; } function seolok_kpi($icon, $label, $big, $sub, $date = false) { return '

' . esc_html($label) . '

' . esc_html($big) . '

' . esc_html($sub) . '

'; } function seolok_sync_page() { if (!current_user_can('manage_options')) { return; } $o = seolok_screen_open(); $o .= seolok_screen_head(seolok_s('sync_strip'), seolok_s('sync_title'), seolok_s('sync_lead')); if (!seolok_is_configured()) { echo wp_kses($o . seolok_screen_not_connected() . '', seolok_allowed_html()); return; } $on = seolok_sync_enabled(); $stats = get_option('seolok_sync_stats', array()); $stats = is_array($stats) ? $stats : array(); $outbox = seolok_sync_outbox(); $last = isset($stats['last_ok']) ? (int) $stats['last_ok'] : (int) get_option('seolok_sync_last', 0); // the switch $cron_late = $on && seolok_cron_late(); $o .= '

' . esc_html(seolok_s($on ? 'sync_is_on' : 'sync_is_off')) . '

' . esc_html(seolok_s($on ? ($cron_late ? 'sync_on_text_cron' : 'sync_on_text') : 'sync_off_text')) . '

'; // scheduled posts and backups wait too, whether or not live sync is on: the notice and the button do not depend on the switch if (seolok_cron_late()) { $o .= '

' . esc_html(sprintf(seolok_s('cron_late'), seolok_cron_overdue_minutes())) . '

'; $o .= '
' . seolok_screen_form('sync', 'cron_run', seolok_s('cron_run'), 'button') . '

' . esc_html(seolok_s('cron_run_hint')) . '

' . seolok_cron_help() . '
'; } if (!empty($stats['error']['at'])) { $o .= '

' . esc_html(sprintf(seolok_s('last_error'), seolok_when($stats['error']['at']), seolok_sync_error_text($stats['error']['code']))) . '

'; } $o .= '
' . seolok_screen_form('sync', $on ? 'sync_off' : 'sync_on', seolok_s($on ? 'switch_on' : 'switch_off'), 'seolok-switch seolok-switch--' . ($on ? 'on' : 'off')); $o .= '
' . seolok_screen_form('sync', $on ? 'sync_off' : 'sync_on', seolok_s($on ? 'b_sync_off' : 'b_sync_on'), $on ? 'button' : 'button button-primary seolok-button-big') . '

' . esc_html(seolok_s($cron_late ? 'sync_on_note_cron' : 'sync_on_note')) . '

'; // counters $today = isset($stats['day']) && $stats['day'] === wp_date('Y-m-d') ? (int) $stats['today'] : 0; $o .= '
' . seolok_kpi('file', seolok_s('k_queue'), (string) count($outbox), seolok_s('k_queue_sub')) . seolok_kpi('clock', seolok_s('k_today'), (string) $today, seolok_s('k_today_sub')); $o .= seolok_kpi('check', seolok_s('k_total'), (string) (isset($stats['total']) ? (int) $stats['total'] : 0), seolok_s('k_total_sub')); $o .= seolok_kpi('calendar', seolok_s('k_last'), $last > 0 ? seolok_when($last) : seolok_s('never'), $last > 0 ? sprintf(seolok_s('ago'), human_time_diff($last, time())) : '', $last > 0) . '
'; // queue + content types $o .= '

' . esc_html(seolok_s('queue')) . ' ' . seolok_pill('muted', (string) count($outbox)) . '

' . ($on && $outbox ? seolok_screen_form('sync', 'sync_now', seolok_s('b_sync_now'), 'button') : '') . '
'; if (!$outbox) { $o .= '

' . esc_html(seolok_s('queue_empty')) . '

'; } else { $q = seolok_sync_queue_state(); $o .= '

' . esc_html(sprintf(seolok_s('queue_oldest'), $q['oldestAt'] > 0 ? human_time_diff($q['oldestAt'], time()) : '—')) . ' · ' . esc_html(sprintf(seolok_s('queue_next'), seolok_sync_next_text())) . '

'; $o .= '
'; foreach (array_slice($outbox, 0, 20) as $event) { $data = isset($event['data']) && is_array($event['data']) ? $event['data'] : array(); $name = isset($data['title']) && $data['title'] !== '' ? (string) $data['title'] : (isset($data['url']) ? (string) $data['url'] : (isset($data['slug']) ? (string) $data['slug'] : seolok_s('whole_site'))); $o .= ''; } $o .= '
' . esc_html(seolok_s('col_content')) . '' . esc_html(seolok_s('col_change')) . '' . esc_html(seolok_s('col_added')) . '
' . esc_html($name) . '' . esc_html(seolok_sync_event_label(isset($event['type']) ? $event['type'] : '')) . '' . esc_html(seolok_when(isset($event['at']) ? $event['at'] : 0)) . '
'; } $o .= '

' . esc_html(seolok_s('types')) . '

' . esc_html(seolok_s('types_lead')) . '

'; $picked = seolok_sync_types(); $o .= '
' . wp_nonce_field('seolok_screen', '_wpnonce', true, false) . '
    '; foreach (seolok_sync_type_choices() as $name => $label) { $checked = $picked === null || in_array($name, $picked, true); $o .= '
  • ' . seolok_pill($checked ? 'ok' : 'muted', seolok_s($checked ? 'type_on' : 'type_off')) . '
  • '; } $o .= '
'; // sent + audit log $sent = get_option('seolok_sync_sent', array()); $o .= '

' . esc_html(seolok_s('recent')) . '

'; if (!is_array($sent) || !$sent) { $o .= '

' . esc_html(seolok_s('recent_empty')) . '

'; } else { $o .= '
'; foreach (array_slice($sent, 0, 10) as $row) { $path = isset($row['url']) && $row['url'] !== '' ? (string) wp_parse_url($row['url'], PHP_URL_PATH) : ''; $name = (string) $row['title'] !== '' ? (string) $row['title'] : ($path !== '' ? $path : seolok_s('whole_site')); $o .= ''; } $o .= '
' . esc_html(seolok_s('col_time')) . '' . esc_html(seolok_s('col_content')) . '' . esc_html(seolok_s('col_change')) . '' . esc_html(seolok_s('col_status')) . '
' . esc_html(seolok_when($row['at'])) . '' . esc_html($name) . '' . ($path !== '' ? '
' . esc_html($path) . '' : '') . '
' . esc_html(seolok_sync_event_label($row['type'])) . '' . seolok_pill('ok', seolok_s('sent')) . '
'; } $log = get_option('seolok_sync_log', array()); $o .= '

' . esc_html(seolok_s('audit')) . '

'; if (!is_array($log) || !$log) { $o .= '

' . esc_html(seolok_s('audit_empty')) . '

'; } else { $o .= '
'; foreach (array_slice($log, 0, 10) as $row) { $kind = isset($row['kind']) ? sanitize_key($row['kind']) : ''; $tone = $kind === 'error' ? 'bad' : ($kind === 'on' || $kind === 'manual' ? 'ok' : 'muted'); $detail = isset($row['detail']) ? (string) $row['detail'] : ''; $details = array_filter(array($kind === 'error' && $detail !== '' ? seolok_sync_error_text($detail) : $detail, !empty($row['by']) ? sprintf(seolok_s('ev_by'), (string) $row['by']) : '')); $o .= ''; } $o .= '
' . esc_html(seolok_s('col_time')) . '' . esc_html(seolok_s('col_event')) . '' . esc_html(seolok_s('col_details')) . '
' . esc_html(seolok_when(isset($row['at']) ? $row['at'] : 0)) . '' . esc_html(seolok_s('l_' . $kind)) . '' . esc_html(implode(' · ', $details)) . '
'; } $o .= '
'; echo wp_kses($o, seolok_allowed_html()); } /* -------------------------------------------------------- Dashboard box */ /** * WordPress "Dashboard" box (0.6.1): the site's SEOLOK state at a glance: SEO title suggestions by state, broken links and alerts * waiting, live sync (switch, queue, today, total, last send, last error) and the last things sent. Administrators only. Everything is * read from what the plugin already keeps; only the title counts come from SEOLOK (the same two-minute cache as the SEO titles screen). */ add_action('wp_dashboard_setup', function () { // the full-width panel on top (includes/home.php) replaces this box; the box comes back when a user hides the panel if (current_user_can('manage_options') && seolok_home_hidden()) { wp_add_dashboard_widget('seolok_status', seolok_s('dash_title'), 'seolok_dashboard_widget'); } }); add_action('admin_enqueue_scripts', function ($hook) { if ($hook === 'index.php' && current_user_can('manage_options')) { wp_enqueue_style('seolok-admin', plugins_url('assets/seolok-admin.css', SEOLOK_FILE), array(), SEOLOK_VERSION); } }); function seolok_dashboard_widget() { if (!current_user_can('manage_options')) { return; } $o = '

' . esc_html(seolok_s('hm_show')) . '

'; if (!seolok_is_configured()) { echo wp_kses($o . seolok_screen_not_connected() . '
', seolok_allowed_html()); return; } $row = function ($label, $value, $url = '', $tone = '') { $cell = $tone !== '' ? seolok_pill($tone, (string) $value) : '' . esc_html((string) $value) . ''; return '' . ($url !== '' ? '' . esc_html($label) . '' : esc_html($label)) . '' . $cell . ''; }; $table = function ($title, $rows, $url) { return '

' . esc_html($title) . ' ' . esc_html(seolok_s('dash_open')) . ' →

' . $rows . '
'; }; $titles_url = admin_url('admin.php?page=' . SEOLOK_TITLES_PAGE); // state of the site $alerts = seolok_alert_count(); $links = (int) get_option('seolok_badge', 0); $rows = $row(seolok_s('dash_links'), $links, admin_url('admin.php?page=' . SEOLOK_LOKMAN_PAGE), $links > 0 ? 'warn' : 'ok'); $rows .= $row(seolok_s('dash_alerts'), $alerts, admin_url('admin.php?page=' . SEOLOK_ALERTS_PAGE), $alerts > 0 ? 'bad' : 'ok'); $fresh = seolok_fresh_count(); $rows .= $row(seolok_s('dash_fresh'), $fresh, admin_url('admin.php?page=' . SEOLOK_FRESH_PAGE), $fresh > 0 ? 'warn' : 'ok'); $o .= $table(seolok_s('menu_overview'), $rows, admin_url('admin.php?page=' . SEOLOK_LOKMAN_PAGE)); // SEO titles by state $data = seolok_titles_data(); if (is_wp_error($data) || !is_array($data)) { $o .= '

' . esc_html(seolok_s('dash_titles')) . '

' . esc_html(seolok_s('dash_titles_error')) . '

'; } else { $n = array('proposed' => 0, 'approved' => 0, 'applied' => 0); foreach (seolok_arr($data['items']) as $item) { $st = is_array($item) ? seolok_str($item['status']) : ''; if (isset($n[$st]) && !($st === 'proposed' && !empty($item['waiting']))) { $n[$st]++; } } $rows = $row(seolok_s('dash_waiting'), $n['proposed'], seolok_screen_url(SEOLOK_TITLES_PAGE, array('status' => 'proposed')), $n['proposed'] > 0 ? 'warn' : 'muted'); $rows .= $row(seolok_s('dash_ready'), $n['approved'], seolok_screen_url(SEOLOK_TITLES_PAGE, array('status' => 'approved')), $n['approved'] > 0 ? 'info' : 'muted'); $rows .= $row(seolok_s('dash_applied'), $n['applied'], seolok_screen_url(SEOLOK_TITLES_PAGE, array('status' => 'applied')), 'ok'); $o .= $table(seolok_s('dash_titles'), $rows, $titles_url); } // live sync $on = seolok_sync_enabled(); $stats = get_option('seolok_sync_stats', array()); $stats = is_array($stats) ? $stats : array(); $last = isset($stats['last_ok']) ? (int) $stats['last_ok'] : (int) get_option('seolok_sync_last', 0); $today = isset($stats['day']) && $stats['day'] === wp_date('Y-m-d') ? (int) $stats['today'] : 0; $rows = $row(seolok_s('dash_state'), seolok_s($on ? 'dash_on' : 'dash_off'), '', $on ? 'ok' : 'muted'); $rows .= $row(seolok_s('k_queue'), count(seolok_sync_outbox())); $rows .= $row(seolok_s('k_today'), $today); $rows .= $row(seolok_s('k_total'), isset($stats['total']) ? (int) $stats['total'] : 0); $rows .= $row(seolok_s('k_last'), $last > 0 ? seolok_when($last) : seolok_s('never')); if ($on && seolok_sync_outbox()) { $rows .= $row(seolok_s('k_next'), seolok_sync_next_text()); } if (!empty($stats['error']['at'])) { $rows .= '' . esc_html(sprintf(seolok_s('last_error'), seolok_when($stats['error']['at']), seolok_sync_error_text($stats['error']['code']))) . ''; } $o .= $table(seolok_s('dash_sync'), $rows, admin_url('admin.php?page=' . SEOLOK_SYNC_PAGE)); // the last things sent $sent = get_option('seolok_sync_sent', array()); if (is_array($sent) && $sent) { $rows = ''; foreach (array_slice($sent, 0, 5) as $item) { $path = isset($item['url']) && $item['url'] !== '' ? (string) wp_parse_url($item['url'], PHP_URL_PATH) : ''; $name = isset($item['title']) && (string) $item['title'] !== '' ? (string) $item['title'] : ($path !== '' ? $path : seolok_s('whole_site')); $rows .= '' . esc_html($name) . '
' . esc_html(seolok_sync_event_label(isset($item['type']) ? $item['type'] : '')) . '' . esc_html(seolok_when(isset($item['at']) ? $item['at'] : 0)) . ''; } $o .= $table(seolok_s('recent'), $rows, admin_url('admin.php?page=' . SEOLOK_SYNC_PAGE)); } echo wp_kses($o . '', seolok_allowed_html()); } /* ------------------------------------------------------------- Old posts */ /** When the post was last updated or marked current (unix time). */ function seolok_fresh_touched($post) { $modified = strtotime((string) $post->post_modified_gmt . ' UTC'); $ok = (int) get_post_meta($post->ID, '_seolok_fresh_ok', true); return max($modified ? $modified : 0, $ok); } /** Years before the current one that a text mentions ("2024 güncel tarifesi"), newest first. Counted in code, nothing guessed. */ function seolok_fresh_years($text) { $now = (int) gmdate('Y'); preg_match_all('/\b(20[0-9]{2})\b/', (string) $text, $m); $years = array_values(array_unique(array_filter(array_map('intval', $m[1]), function ($y) use ($now) { return $y >= 2000 && $y < $now; }))); rsort($years); return array_slice($years, 0, 3); } /** This site's published posts and pages not updated (nor marked current) for 18 months, oldest first: array of rows. */ function seolok_fresh_posts() { $types = array_values(array_filter(array('post', 'page'), 'post_type_exists')); $ids = get_posts(array('post_type' => $types, 'post_status' => 'publish', 'numberposts' => 400, 'orderby' => 'modified', 'order' => 'ASC', 'fields' => 'ids', 'no_found_rows' => true, 'suppress_filters' => false)); $limit = time() - SEOLOK_FRESH_DAYS * DAY_IN_SECONDS; $rows = array(); foreach (is_array($ids) ? $ids : array() as $id) { $post = get_post($id); if (!$post) { continue; } $touched = seolok_fresh_touched($post); if ($touched > $limit) { continue; } $url = (string) get_permalink($id); $rows[] = array( 'id' => (int) $id, 'title' => wp_strip_all_tags((string) get_the_title($id)), 'url' => $url, 'path' => (string) wp_parse_url($url, PHP_URL_PATH), 'touched' => $touched, 'modified' => (string) $post->post_modified_gmt, 'ignored' => (string) get_post_meta($id, '_seolok_fresh_ignore', true) === '1', 'yTitle' => seolok_fresh_years(get_the_title($id)), 'yText' => seolok_fresh_years(substr(wp_strip_all_tags((string) (isset($post->post_content) ? $post->post_content : '')), 0, 20000)), ); } return $rows; } /** SEOLOK's checks for this site (cached two minutes): array('left' => n, 'byPath' => path => item) or WP_Error. */ function seolok_fresh_remote($force = false) { $key = 'seolok_fresh'; if (!$force) { $cached = get_transient($key); if (is_array($cached)) { return $cached; } } $data = seolok_remote('GET', '/api/wp/freshness'); if (is_wp_error($data)) { return $data; } if (!isset($data['v'], $data['items']) || (int) $data['v'] !== 1 || !is_array($data['items'])) { return new WP_Error('seolok_remote_error', 'Unexpected answer'); } $by = array(); foreach ($data['items'] as $item) { if (is_array($item) && isset($item['path'])) { $by[untrailingslashit(seolok_str($item['path']))] = $item; } } $out = array('left' => isset($data['left']) ? (int) $data['left'] : 0, 'byPath' => $by); set_transient($key, $out, MINUTE_IN_SECONDS); // short: a link fixed in SEOLOK leaves this list within a minute return $out; } /** A button of the screen: the check goes to SEOLOK; "current", "ignore" and "bring back" are kept here. */ function seolok_fresh_op($op, $post_id) { $post = $post_id ? get_post($post_id) : null; if (!$post || !in_array($op, array('fresh_check', 'fresh_ok', 'fresh_ignore', 'fresh_unignore'), true)) { return array('tone' => 'warning', 'lines' => array(seolok_s('forbidden'))); } if ($op === 'fresh_ok') { update_post_meta($post_id, '_seolok_fresh_ok', time()); delete_post_meta($post_id, '_seolok_fresh_ignore'); return array('tone' => 'success', 'lines' => array(seolok_s('r_fresh_ok'))); } if ($op === 'fresh_ignore') { update_post_meta($post_id, '_seolok_fresh_ignore', '1'); return array('tone' => 'success', 'lines' => array(seolok_s('r_fresh_ignored'))); } if ($op === 'fresh_unignore') { delete_post_meta($post_id, '_seolok_fresh_ignore'); return array('tone' => 'success', 'lines' => array(seolok_s('r_fresh_back'))); } delete_transient('seolok_fresh'); $modified = strtotime((string) $post->post_modified_gmt . ' UTC'); $answer = seolok_remote('POST', '/api/wp/freshness', array('url' => (string) get_permalink($post_id), 'modified' => $modified ? gmdate('c', $modified) : '')); if (is_wp_error($answer)) { return array('tone' => 'warning', 'lines' => array($answer->get_error_code() === 'seolok_unauthorized' ? seolok_t('bad_token') : seolok_s('failed'))); } $code = isset($answer['code']) ? sanitize_key((string) $answer['code']) : ''; if (!empty($answer['ok']) && in_array($code, array('started', 'ready'), true)) { return array('tone' => 'success', 'lines' => array(seolok_s('r_fresh_' . $code))); } return array('tone' => 'warning', 'lines' => array($code === 'cap' ? seolok_s('r_fresh_cap') : seolok_s('failed'))); } /** How many posts wait under "Eski yazılar" (not ignored): the Dashboard box and the menu read it. */ function seolok_fresh_count() { $n = 0; foreach (seolok_fresh_posts() as $row) { if (!$row['ignored']) { $n++; } } return $n; } function seolok_fresh_page() { if (!current_user_can('manage_options')) { return; } $o = seolok_screen_open(); $o .= seolok_screen_head(seolok_s('fresh_strip'), seolok_s('fresh_title'), seolok_s('fresh_lead')); if (!seolok_is_configured()) { echo wp_kses($o . seolok_screen_not_connected() . '', seolok_allowed_html()); return; } $view = isset($_GET['view']) ? sanitize_key(wp_unslash($_GET['view'])) : 'waiting'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a read-only filter $view = in_array($view, array('waiting', 'checked', 'ignored'), true) ? $view : 'waiting'; $remote = seolok_fresh_remote(isset($_GET['seolok_refresh'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a read-only "fetch again" flag $by = is_array($remote) ? $remote['byPath'] : array(); $rows = seolok_fresh_posts(); $groups = array('waiting' => array(), 'checked' => array(), 'ignored' => array()); foreach ($rows as $row) { $check = isset($by[untrailingslashit($row['path'])]) ? $by[untrailingslashit($row['path'])] : null; $row['check'] = $check; $groups[$row['ignored'] ? 'ignored' : ($check && !empty($check['brief']) ? 'checked' : 'waiting')][] = $row; } if (is_wp_error($remote)) { $o .= seolok_render_problem($remote); } else { $o .= '

' . esc_html(sprintf(seolok_s('fresh_left'), (int) $remote['left'])) . '

'; } $o .= '
'; $list = $groups[$view]; if (!$list) { $o .= '

' . esc_html(seolok_s('fresh_empty')) . '

'; echo wp_kses($o, seolok_allowed_html()); return; } $o .= '
'; foreach (array_slice($list, 0, 100) as $row) { $f = array('post' => (string) $row['id'], 'view' => $view); $months = (int) floor((time() - $row['touched']) / (30.44 * DAY_IN_SECONDS)); $signs = ''; if ($row['yTitle']) { $signs .= seolok_pill('warn', sprintf(seolok_s('fresh_year_title'), implode(', ', $row['yTitle']))) . ' '; } if ($row['yText']) { $signs .= seolok_pill('muted', sprintf(seolok_s('fresh_year_text'), implode(', ', $row['yText']))) . ' '; } $check = $row['check']; if ($check && !empty($check['checking'])) { $signs .= seolok_pill('info', seolok_s('fresh_checking')); } elseif ($check && !empty($check['failed'])) { $signs .= seolok_pill('bad', seolok_s('fresh_check_failed')); } $edit = get_edit_post_link($row['id'], 'raw'); $o .= ''; } $o .= '
' . esc_html(seolok_s('col_post')) . '' . esc_html(seolok_s('col_age')) . '' . esc_html(seolok_s('col_signals')) . '' . esc_html(seolok_s('col_action')) . '
' . esc_html($row['title'] !== '' ? $row['title'] : $row['path']) . '
' . esc_html($row['path']) . ''; if ($check && !empty($check['brief']) && is_array($check['brief'])) { $brief = $check['brief']; $o .= '
' . esc_html(sprintf(seolok_s('fresh_checked_on'), seolok_when(strtotime(seolok_str(isset($check['checkedAt']) ? $check['checkedAt'] : ''))))) . ''; foreach (array('updateFacts' => 'fresh_facts', 'addSections' => 'fresh_sections') as $field => $label) { $items = isset($brief[$field]) && is_array($brief[$field]) ? array_slice($brief[$field], 0, 8) : array(); if ($items) { $o .= '

' . esc_html(seolok_s($label)) . '

    '; foreach ($items as $line) { $o .= '
  • ' . esc_html(seolok_str($line)) . '
  • '; } $o .= '
'; } } if (!empty($brief['angle'])) { $o .= '

' . esc_html(seolok_s('fresh_angle')) . ': ' . esc_html(seolok_str($brief['angle'])) . '

'; } $o .= '
'; } $o .= '
' . esc_html(sprintf(seolok_s('fresh_months'), $months)) . '' . $signs . ''; if ($view === 'ignored') { $o .= seolok_screen_form('fresh', 'fresh_unignore', seolok_s('b_fresh_unignore'), 'button', $f); } else { if (!($check && !empty($check['checking']))) { $o .= seolok_screen_form('fresh', 'fresh_check', seolok_s('b_fresh_check'), 'button button-primary', $f); } $o .= seolok_screen_form('fresh', 'fresh_ok', seolok_s('b_fresh_ok'), 'button', $f) . seolok_screen_form('fresh', 'fresh_ignore', seolok_s('b_fresh_ignore'), 'button-link', $f); } $o .= '
'; echo wp_kses($o, seolok_allowed_html()); } /* ------------------------------------------------------------- 404 addresses (0.6.7) */ /** * The page a typed or suggested target means: a path ("/hizmetler/") or a full address of this site, that a published page answers * (not the home page). @return string the full address, or '' when it is not acceptable */ function seolok_404_target($to) { $to = trim((string) $to); if ($to === '') { return ''; } if (strpos($to, '/') === 0 && strpos($to, '//') !== 0) { $to = seolok_path_url($to); } $to = esc_url_raw($to); if ($to === '' || !seolok_same_host($to)) { return ''; } $path = seolok_normalize_path($to); if ($path === seolok_normalize_path(home_url('/')) || !seolok_path_has_page($path)) { return ''; } return $to; } /** One button of the screen; everything is kept here, in WordPress (the 301 is the plugin's own, the one SEOLOK's fixes use). */ function seolok_404_op($op, $path, $to) { $ok = function ($line) { return array('tone' => 'success', 'lines' => array($line)); }; $no = function ($line) { return array('tone' => 'warning', 'lines' => array($line)); }; if ($path === '' || !in_array($op, array('nf_redirect', 'nf_ignore', 'nf_unignore', 'nf_undo'), true)) { return $no(seolok_s('forbidden')); } $log = seolok_404_log(); if ($op === 'nf_redirect') { if (!isset($log[$path])) { return $no(seolok_s('r_nf_unknown')); } $target = seolok_404_target($to); if ($target === '' || seolok_normalize_path($target) === $path) { return $no(seolok_s('r_nf_bad_target')); } $stored = seolok_store_redirect($path, $target, 'wp'); if (is_wp_error($stored)) { return $no(seolok_s(strpos($stored->get_error_message(), 'loop') !== false ? 'r_nf_loop' : 'r_nf_bad_target')); } return $ok(sprintf(seolok_s('r_nf_redirected'), rawurldecode($path), rawurldecode(seolok_normalize_path($target)))); } if ($op === 'nf_ignore') { if (!isset($log[$path])) { return $no(seolok_s('r_nf_unknown')); } unset($log[$path]); update_option('seolok_404_log', $log, false); $ignored = array($path => gmdate('Y-m-d')) + seolok_404_get('seolok_404_ignored'); update_option('seolok_404_ignored', array_slice($ignored, 0, SEOLOK_404_IGNORED_MAX, true), false); return $ok(seolok_s('r_nf_ignored')); } if ($op === 'nf_unignore') { $ignored = seolok_404_get('seolok_404_ignored'); if (!isset($ignored[$path])) { return $no(seolok_s('r_nf_unknown')); } unset($ignored[$path]); update_option('seolok_404_ignored', $ignored, false); return $ok(seolok_s('r_nf_back')); } // undo of a 301 made on this screen: only while the stored 301 is still the one it made (one made from SEOLOK is undone there, with its history) $done = seolok_404_get('seolok_404_done'); $entry = isset($done[$path]) && is_array($done[$path]) ? $done[$path] : null; if (!$entry) { return $no(seolok_s('r_nf_unknown')); } if (isset($entry['by']) && $entry['by'] === 'seolok') { return $no(seolok_s('nf_by_seolok')); } $redirects = seolok_get_redirects(); if (!isset($redirects[$path]) || $redirects[$path] !== (isset($entry['to']) ? $entry['to'] : '')) { seolok_404_unredirected($path); return $no(seolok_s('r_nf_changed')); } unset($redirects[$path]); update_option('seolok_redirects', $redirects, true); seolok_404_unredirected($path); seolok_purge_cached_urls(array(seolok_path_url($path))); return $ok(seolok_s('r_nf_undone')); } /** * SEOLOK's suggested page per 404 path (path => address), read from the Overview page's data (cached five minutes): SEOLOK has the * list only while live sync is on, and picks the page with the same rules as for broken links. Nothing when it cannot be read. */ function seolok_404_suggestions() { return seolok_404_seolok_rows()['to']; } /** SEOLOK's view of the 404 list (Overview data, cached five minutes; live sync on only): to = path => suggested page, refs = path => pages of its crawl that link to it. */ function seolok_404_seolok_rows() { $out = array('to' => array(), 'refs' => array()); if (!seolok_sync_enabled()) { return $out; } $data = seolok_lokman_data(); foreach (is_array($data) && isset($data['cards']) ? seolok_arr($data['cards']) : array() as $card) { if (!is_array($card) || !isset($card['id']) || $card['id'] !== 'notfound') { continue; } foreach (seolok_arr(isset($card['rows']) ? $card['rows'] : null) as $row) { $hidden = is_array($row) && isset($row['form']['hidden']) && is_array($row['form']['hidden']) ? $row['form']['hidden'] : array(); if (!empty($hidden['target']) && !empty($hidden['to']) && is_string($hidden['target']) && is_string($hidden['to'])) { $out['to'][seolok_normalize_path($hidden['target'])] = $hidden['to']; } $title = is_array($row) && isset($row['title']) && is_string($row['title']) ? $row['title'] : ''; if ($title !== '' && !empty($row['refs']) && is_array($row['refs'])) { $out['refs'][seolok_normalize_path($title)] = array_slice(array_values(array_filter($row['refs'], 'is_string')), 0, 5); } } } return $out; } /** The site address of a stored path, safe in an attribute: quotes, angle brackets and spaces are %-coded first (a 404 path is whatever was asked for). */ function seolok_404_url($path) { return home_url(str_replace(array('"', "'", '<', '>', ' ', '`'), array('%22', '%27', '%3C', '%3E', '%20', '%60'), (string) $path)); } /** A page of this site as a short link with "Edit" when WordPress knows the post (where the dead link sits). */ function seolok_404_page_link($path_or_url, $count = null) { $path = (string) (strpos((string) $path_or_url, 'http') === 0 ? wp_parse_url($path_or_url, PHP_URL_PATH) : $path_or_url); $path = $path === '' ? '/' : $path; $url = seolok_404_url($path); $id = function_exists('url_to_postid') ? (int) url_to_postid($url) : 0; $edit = $id > 0 ? get_edit_post_link($id, 'raw') : ''; return '' . esc_html(rawurldecode($path)) . '' . ($count !== null ? ' ×' . esc_html((string) (int) $count) . '' : '') . ($edit ? ' ' . esc_html(seolok_s('edit_page')) . '' : '') . ''; } function seolok_404_day($day) { $ts = is_string($day) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $day) ? strtotime($day . ' 12:00:00 UTC') : false; return $ts ? wp_date('j M Y', $ts) : ''; } /** A small form of one row (path and view travel along; the nonce and the capability are checked in seolok_handle_screen). */ function seolok_404_form($op, $label, $class, $path, $view, $input = null) { $f = '
'; $f .= ''; $f .= '' . wp_nonce_field('seolok_screen', '_wpnonce', true, false); if ($input !== null) { $f .= ' '; } return $f . '
'; } function seolok_404_page() { if (!current_user_can('manage_options')) { return; } $o = seolok_screen_open(); $o .= seolok_screen_head(seolok_s('nf_strip'), seolok_s('nf_title'), seolok_s('nf_lead')); $view = isset($_GET['view']) ? sanitize_key(wp_unslash($_GET['view'])) : 'waiting'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a read-only filter $view = in_array($view, array('waiting', 'redirected', 'ignored'), true) ? $view : 'waiting'; $waiting = seolok_404_top(SEOLOK_404_MAX); $done = seolok_404_get('seolok_404_done'); $ignored = seolok_404_get('seolok_404_ignored'); $counts = array('waiting' => count($waiting), 'redirected' => count($done), 'ignored' => count($ignored)); $hits = 0; $recent = 0; $linked = 0; $week = gmdate('Y-m-d', time() - 7 * DAY_IN_SECONDS); foreach ($waiting as $row) { $hits += $row['hits']; $recent += strcmp($row['last'], $week) >= 0 ? 1 : 0; $linked += $row['pages'] ? 1 : 0; } $o .= '
'; $o .= seolok_ui_stat('warn', $counts['waiting'] > 0 ? 'warn' : 'ok', seolok_s('nf_s_waiting'), seolok_num($counts['waiting']), sprintf(seolok_s('nf_s_hits'), seolok_num($hits))); $o .= seolok_ui_stat('clock', $recent > 0 ? 'warn' : 'ok', seolok_s('nf_s_recent'), seolok_num($recent), seolok_s('nf_s_recent_sub')); $o .= seolok_ui_stat('chain', $linked > 0 ? 'bad' : 'ok', seolok_s('nf_s_linked'), seolok_num($linked), seolok_s('nf_s_linked_sub')); $o .= seolok_ui_stat('check', 'ok', seolok_s('nf_redirected'), seolok_num($counts['redirected']), seolok_s('nf_s_done_sub')); $o .= '
'; $o .= seolok_ui_panel_open('list', seolok_s('nf_title'), seolok_s(seolok_sync_enabled() ? 'nf_sync_on' : 'nf_sync_off')) . ''; if ($counts[$view] === 0) { $o .= seolok_ui_empty(seolok_s('nf_empty_' . $view)) . '

' . esc_html(seolok_s('nf_privacy')) . '

'; echo wp_kses($o, seolok_allowed_html()); return; } if ($view === 'waiting') { $seolok = seolok_404_seolok_rows(); $o .= '
    '; foreach (array_slice($waiting, 0, 100) as $row) { $path = $row['path']; $exists = seolok_path_has_page($path); $suggestion = !$exists && isset($seolok['to'][$path]) ? (string) $seolok['to'][$path] : ''; $o .= '
  • ' . esc_html(rawurldecode($path)) . ' ' . seolok_ui_icon('link') . '' . seolok_pill($row['hits'] > 1 ? 'warn' : 'muted', sprintf(seolok_s('nf_tries'), seolok_num($row['hits']))) . '' . esc_html($row['first'] === $row['last'] ? seolok_404_day($row['last']) : seolok_404_day($row['first']) . ' – ' . seolok_404_day($row['last'])) . '' . ($exists ? seolok_pill('ok', seolok_s('nf_page_exists')) : '') . '
    '; // where the dead link sits / where visitors came from $src = array(); foreach ($row['pages'] as $from => $n) { $src[] = seolok_404_page_link((string) $from, (int) $n); } foreach ($row['hosts'] as $host => $n) { $src[] = '' . esc_html((string) $host) . ' ×' . esc_html((string) (int) $n) . ''; } if ($row['direct'] > 0) { $src[] = '' . esc_html(sprintf(seolok_s('nf_direct'), $row['direct'])) . ''; } $o .= '
    ' . esc_html(seolok_s('nf_from')) . '' . ($src ? implode('', $src) : '' . esc_html(seolok_s('nf_from_unknown')) . '') . '
    '; $refs = isset($seolok['refs'][$path]) ? $seolok['refs'][$path] : array(); if ($refs) { $o .= '
    ' . esc_html(seolok_s('nf_linked_by')) . ''; foreach ($refs as $ref) { $o .= seolok_404_page_link($ref); } $o .= '
    '; } if ($suggestion !== '') { $o .= '
    ' . esc_html(seolok_s('nf_suggested')) . '' . esc_html(rawurldecode(seolok_normalize_path($suggestion))) . '
    '; } $o .= '
    '; if (!$exists) { $o .= seolok_404_form('nf_redirect', seolok_s('b_nf_redirect'), 'button button-primary seolok-ui-primary', $path, $view, $suggestion !== '' ? (string) wp_parse_url($suggestion, PHP_URL_PATH) : '') . seolok_pages_datalist(); } $o .= seolok_404_form('nf_ignore', seolok_s('b_nf_ignore'), 'button-link', $path, $view) . '
  • '; } $o .= '
'; } elseif ($view === 'redirected') { $o .= '
'; foreach ($done as $path => $entry) { $entry = is_array($entry) ? $entry : array(); $to = isset($entry['to']) ? (string) $entry['to'] : ''; $o .= ''; $o .= ''; } $o .= '
' . esc_html(seolok_s('col_address')) . '' . esc_html(seolok_s('col_to')) . '' . esc_html(seolok_s('col_hits')) . '' . esc_html(seolok_s('col_action')) . '
' . esc_html(rawurldecode((string) $path)) . '
' . esc_html(seolok_when(isset($entry['at']) ? $entry['at'] : 0)) . '
' . esc_html(rawurldecode(seolok_normalize_path($to))) . '' . esc_html((string) (isset($entry['c']) ? (int) $entry['c'] : 0)) . ''; $o .= isset($entry['by']) && $entry['by'] === 'seolok' ? '' . esc_html(seolok_s('nf_by_seolok')) . '' : seolok_404_form('nf_undo', seolok_s('b_nf_undo'), 'button', (string) $path, $view); $o .= '
'; } else { $o .= '
'; foreach ($ignored as $path => $day) { $o .= ''; } $o .= '
' . esc_html(seolok_s('col_address')) . '' . esc_html(seolok_s('col_last')) . '' . esc_html(seolok_s('col_action')) . '
' . esc_html(rawurldecode((string) $path)) . '' . esc_html(seolok_404_day($day)) . '' . seolok_404_form('nf_unignore', seolok_s('b_nf_unignore'), 'button', (string) $path, $view) . '
'; } $o .= '

' . esc_html(seolok_s('nf_privacy')) . '

'; echo wp_kses($o, seolok_allowed_html()); }