From b2aead1fbe2070b6ba8567e7390c6173c5741d8f Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sat, 21 Feb 2026 11:41:17 +0100 Subject: [PATCH] feat: Integrate DataForSEO for indexed pages tracking - Updated CRON documentation to include DataForSEO metrics synchronization. - Enhanced SettingsController to manage DataForSEO API credentials and settings. - Modified SiteController to handle DataForSEO domain input. - Updated Site model to accommodate DataForSEO data handling. - Added methods in SiteSeoMetric model for DataForSEO data retrieval and validation. - Implemented SiteSeoSyncService to synchronize SEO metrics from both SEMSTORM and DataForSEO. - Enhanced dashboard templates to display indexed pages data. - Updated settings and site creation/edit templates to include DataForSEO fields. - Created migration for adding DataForSEO related columns in the database. - Developed DataForSeoService to fetch indexed pages count from DataForSEO API. --- .env.example | 6 + .vscode/ftp-kr.sync.cache.json | 350 +++++++++----------- docs/CRON.md | 3 +- migrations/010_dataforseo_indexed_pages.sql | 7 + src/Controllers/SettingsController.php | 10 + src/Controllers/SiteController.php | 2 + src/Models/Site.php | 7 +- src/Models/SiteSeoMetric.php | 53 ++- src/Services/DataForSeoService.php | 118 +++++++ src/Services/SiteSeoSyncService.php | 117 +++++-- templates/dashboard/seo-stats.php | 11 +- templates/settings/index.php | 40 +++ templates/sites/create.php | 6 + templates/sites/edit.php | 6 + templates/sites/seo.php | 24 +- 15 files changed, 541 insertions(+), 219 deletions(-) create mode 100644 migrations/010_dataforseo_indexed_pages.sql create mode 100644 src/Services/DataForSeoService.php diff --git a/.env.example b/.env.example index 3623419..a18c412 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,12 @@ SEMSTORM_LOGIN= SEMSTORM_PASSWORD= SEMSTORM_API_BASE=https://api.semstorm.com SEMSTORM_TIMEOUT_SECONDS=30 +DATAFORSEO_LOGIN= +DATAFORSEO_PASSWORD= +DATAFORSEO_API_BASE=https://api.dataforseo.com +DATAFORSEO_TIMEOUT_SECONDS=30 +DATAFORSEO_LOCATION_CODE=2616 +DATAFORSEO_LANGUAGE_CODE=pl SEO_TRIGGER_TOKEN=change-this-to-long-random-token APP_URL=https://backpro.projectpro.pl diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index 1138374..158d262 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -5,127 +5,72 @@ "css": { "app.css": { "type": "-", -<<<<<<< HEAD "size": 1982, "lmtime": 1771375416079, -======= - "size": 1915, - "lmtime": 1771354445514, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false } }, "js": { "app.js": { "type": "-", -<<<<<<< HEAD "size": 12352, "lmtime": 1771375416080, -======= - "size": 12059, - "lmtime": 1771354440500, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false } }, "wp-theme-backpro-news": { "archive.php": { "type": "-", -<<<<<<< HEAD "size": 908, "lmtime": 1771375416081, -======= - "size": 880, - "lmtime": 1771352690031, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "footer.php": { "type": "-", -<<<<<<< HEAD "size": 324, "lmtime": 1771375416082, -======= - "size": 308, - "lmtime": 1771352651733, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "front-page.php": { "type": "-", -<<<<<<< HEAD "size": 4350, "lmtime": 1771375416082, -======= - "size": 4256, - "lmtime": 1771353361367, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "functions.php": { "type": "-", -<<<<<<< HEAD "size": 847, "lmtime": 1771375416083, -======= - "size": 811, - "lmtime": 1771352633840, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "header.php": { "type": "-", -<<<<<<< HEAD "size": 966, "lmtime": 1771375416084, -======= - "size": 937, - "lmtime": 1771352640810, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "index.php": { "type": "-", -<<<<<<< HEAD "size": 1029, "lmtime": 1771375416085, -======= - "size": 995, - "lmtime": 1771352675013, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "search.php": { "type": "-", -<<<<<<< HEAD "size": 683, "lmtime": 1771375416085, -======= - "size": 660, - "lmtime": 1771352696638, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "single.php": { "type": "-", -<<<<<<< HEAD "size": 6116, "lmtime": 1771375416086, -======= - "size": 5988, - "lmtime": 1771353631148, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "style.css": { "type": "-", -<<<<<<< HEAD "size": 10723, "lmtime": 1771375416087, -======= - "size": 10119, - "lmtime": 1771353528618, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false } } @@ -171,8 +116,8 @@ "config": { "routes.php": { "type": "-", - "size": 3489, - "lmtime": 1771375416088, + "size": 3968, + "lmtime": 1771627464051, "modified": false } }, @@ -182,6 +127,12 @@ "size": 1078, "lmtime": 1771149803282, "modified": false + }, + "semstorm.php": { + "type": "-", + "size": 1874, + "lmtime": 1771620180848, + "modified": false } }, "docs": { @@ -193,14 +144,14 @@ }, "CRON.md": { "type": "-", - "size": 4340, - "lmtime": 1771274893249, + "size": 4784, + "lmtime": 1771620216534, "modified": false }, "DATABASE.md": { "type": "-", - "size": 7265, - "lmtime": 1771274893237, + "size": 8354, + "lmtime": 1771620224191, "modified": false }, "PLAN.md": { @@ -212,14 +163,14 @@ }, ".env": { "type": "-", - "size": 330, - "lmtime": 1771446981282, + "size": 389, + "lmtime": 1771626264475, "modified": false }, ".env.example": { "type": "-", - "size": 285, - "lmtime": 1771275211060, + "size": 442, + "lmtime": 1771626490188, "modified": false }, ".htaccess": { @@ -282,6 +233,18 @@ "size": 333, "lmtime": 1771375416090, "modified": false + }, + "008_cron_logs.sql": { + "type": "-", + "size": 480, + "lmtime": 1771618047955, + "modified": false + }, + "009_site_seo_metrics.sql": { + "type": "-", + "size": 829, + "lmtime": 1771620035555, + "modified": false } }, "src": { @@ -306,8 +269,8 @@ }, "DashboardController.php": { "type": "-", - "size": 858, - "lmtime": 1771149691435, + "size": 1339, + "lmtime": 1771627595983, "modified": false }, "GlobalTopicController.php": { @@ -322,6 +285,12 @@ "lmtime": 1771270380033, "modified": false }, + "LogController.php": { + "type": "-", + "size": 2597, + "lmtime": 1771618047955, + "modified": false + }, "PublishController.php": { "type": "-", "size": 2668, @@ -330,20 +299,20 @@ }, "SettingsController.php": { "type": "-", - "size": 1645, - "lmtime": 1771274258130, + "size": 1868, + "lmtime": 1771626466574, "modified": false }, "SiteController.php": { "type": "-", - "size": 10788, - "lmtime": 1771375416092, + "size": 16907, + "lmtime": 1771628191087, "modified": false }, "TopicController.php": { "type": "-", - "size": 3446, - "lmtime": 1771271990709, + "size": 4009, + "lmtime": 1771628730441, "modified": false } }, @@ -400,8 +369,8 @@ "Helpers": { "Logger.php": { "type": "-", - "size": 2028, - "lmtime": 1771375416092, + "size": 3120, + "lmtime": 1771618047955, "modified": false }, "Validator.php": { @@ -414,8 +383,8 @@ "Models": { "Article.php": { "type": "-", - "size": 4960, - "lmtime": 1771375416093, + "size": 5674, + "lmtime": 1771617909267, "modified": false }, "GlobalTopic.php": { @@ -426,8 +395,14 @@ }, "Site.php": { "type": "-", - "size": 858, - "lmtime": 1771274660626, + "size": 1441, + "lmtime": 1771627194103, + "modified": false + }, + "SiteSeoMetric.php": { + "type": "-", + "size": 4817, + "lmtime": 1771627676032, "modified": false }, "Topic.php": { @@ -470,8 +445,20 @@ }, "PublisherService.php": { "type": "-", - "size": 8311, - "lmtime": 1771375416098, + "size": 11532, + "lmtime": 1771617909267, + "modified": false + }, + "SemstormService.php": { + "type": "-", + "size": 5897, + "lmtime": 1771627080134, + "modified": false + }, + "SiteSeoSyncService.php": { + "type": "-", + "size": 3146, + "lmtime": 1771620084332, "modified": false }, "TopicBalancer.php": { @@ -492,18 +479,12 @@ "logs": { "image_2026-02-17.log": { "type": "-", -<<<<<<< HEAD "size": 72, "lmtime": 1771375416100, -======= - "size": 71, - "lmtime": 1771350104612, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "installer_2026-02-17.log": { "type": "-", -<<<<<<< HEAD "size": 4379, "lmtime": 1771375416101, "modified": false @@ -512,21 +493,10 @@ "type": "-", "size": 118, "lmtime": 1771375416102, -======= - "size": 4436, - "lmtime": 1771350104684, - "modified": true - }, - "openai_2026-02-17.log": { - "type": "-", - "size": 117, - "lmtime": 1771350104754, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "publish_2026-02-17.log": { "type": "-", -<<<<<<< HEAD "size": 2539, "lmtime": 1771375416102, "modified": false @@ -535,21 +505,12 @@ "type": "-", "size": 702, "lmtime": 0, -======= - "size": 2508, - "lmtime": 1771350104824, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "wordpress_2026-02-17.log": { "type": "-", -<<<<<<< HEAD "size": 1471, "lmtime": 1771375416103, -======= - "size": 1459, - "lmtime": 1771350285395, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false } } @@ -558,13 +519,8 @@ "articles": { "index.php": { "type": "-", -<<<<<<< HEAD "size": 6036, "lmtime": 1771375416104, -======= - "size": 5926, - "lmtime": 1771354089325, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "show.php": { @@ -602,6 +558,12 @@ "size": 8067, "lmtime": 1771375416107, "modified": false + }, + "seo-stats.php": { + "type": "-", + "size": 7955, + "lmtime": 1771627707865, + "modified": false } }, "global-topics": { @@ -612,78 +574,6 @@ "modified": false } }, - "layout": { - "header.php": { - "type": "-", - "size": 623, -<<<<<<< HEAD - "lmtime": 1771375416109, -======= - "lmtime": 1771354088998, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 - "modified": false - }, - "main.php": { - "type": "-", -<<<<<<< HEAD - "size": 1821, - "lmtime": 1771375416109, - "modified": false -======= - "size": 1816, - "lmtime": 1771149813250, - "modified": true ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 - }, - "sidebar.php": { - "type": "-", - "size": 1894, - "lmtime": 1771375416110, - "modified": false - } - }, - "settings": { - "index.php": { - "type": "-", - "size": 5509, - "lmtime": 1771274394027, - "modified": false - } - }, - "sites": { - "create.php": { - "type": "-", - "size": 4883, - "lmtime": 1771274870419, - "modified": false - }, - "dashboard.php": { - "type": "-", - "size": 12823, - "lmtime": 1771375416111, - "modified": false - }, - "edit.php": { - "type": "-", - "size": 18800, - "lmtime": 1771375416112, - "modified": false - }, - "index.php": { - "type": "-", - "size": 4961, - "lmtime": 1771375416113, - "modified": false - } - }, - "topics": { - "index.php": { - "type": "-", - "size": 11477, - "lmtime": 1771375416114, - "modified": false - } - }, "installer": { "index.php": { "type": "-", @@ -691,16 +581,94 @@ "lmtime": 1771270472738, "modified": false } + }, + "layout": { + "header.php": { + "type": "-", + "size": 623, + "lmtime": 1771375416109, + "modified": false + }, + "main.php": { + "type": "-", + "size": 1821, + "lmtime": 1771375416109, + "modified": false + }, + "sidebar.php": { + "type": "-", + "size": 2261, + "lmtime": 1771627469556, + "modified": false + } + }, + "settings": { + "index.php": { + "type": "-", + "size": 7022, + "lmtime": 1771626482753, + "modified": false + } + }, + "sites": { + "create.php": { + "type": "-", + "size": 5295, + "lmtime": 1771620136407, + "modified": false + }, + "dashboard.php": { + "type": "-", + "size": 12963, + "lmtime": 1771625982185, + "modified": false + }, + "edit.php": { + "type": "-", + "size": 13837, + "lmtime": 1771628695879, + "modified": false + }, + "index.php": { + "type": "-", + "size": 5194, + "lmtime": 1771625987517, + "modified": false + }, + "seo.php": { + "type": "-", + "size": 7545, + "lmtime": 1771626686595, + "modified": false + } + }, + "topics": { + "index.php": { + "type": "-", + "size": 14943, + "lmtime": 1771628879393, + "modified": false + } + }, + "logs": { + "index.php": { + "type": "-", + "size": 3405, + "lmtime": 1771617998906, + "modified": false + } } }, + "tmp_debug_metrics.php": { + "type": "-", + "size": 393, + "lmtime": 1771626947881, + "modified": false + }, "TODO.md": { "type": "-", "size": 233, -<<<<<<< HEAD "lmtime": 1771373773622, -======= - "lmtime": 0, ->>>>>>> 2461cde97a6a75a38bee9437aae52847c968b5b5 "modified": false }, "vendor": { @@ -824,6 +792,12 @@ "lmtime": 1771150407034, "modified": false } + }, + "tmp_semstorm_test.php": { + "type": "-", + "size": 1027, + "lmtime": 1771627094790, + "modified": false } } }, diff --git a/docs/CRON.md b/docs/CRON.md index 6733c4a..458007e 100644 --- a/docs/CRON.md +++ b/docs/CRON.md @@ -117,7 +117,7 @@ Przycisk "Opublikuj teraz" na dashboardzie wywołuje `PublishController@run`, kt | 401 z WordPress | Złe dane API | Sprawdź api_user/api_token w konfiguracji strony | | 429 z OpenAI | Rate limit | Zwiększ interwał CRON lub poczekaj | -## Miesieczna synchronizacja metryk SEO (SEMSTORM) +## Miesieczna synchronizacja metryk SEO (SEMSTORM + DataForSEO) Dodaj osobne zadanie CRON uruchamiane raz w miesiacu: @@ -128,5 +128,6 @@ Dodaj osobne zadanie CRON uruchamiane raz w miesiacu: Skrypt: - pobiera aktywne strony (`sites.is_active = 1`), - pobiera metryki z SEMSTORM dla domeny strony, +- pobiera liczbe zaindeksowanych stron (zapytanie `site:domena`) z DataForSEO, - zapisuje dane do `site_seo_metrics` dla biezacego miesiaca, - pomija rekord, jesli miesiac jest juz zapisany (idempotencja). diff --git a/migrations/010_dataforseo_indexed_pages.sql b/migrations/010_dataforseo_indexed_pages.sql new file mode 100644 index 0000000..9eed8c2 --- /dev/null +++ b/migrations/010_dataforseo_indexed_pages.sql @@ -0,0 +1,7 @@ +-- BackPRO DataForSEO indexed pages integration +ALTER TABLE sites + ADD COLUMN dataforseo_domain VARCHAR(255) NULL AFTER semstorm_domain; + +ALTER TABLE site_seo_metrics + ADD COLUMN indexed_pages INT NOT NULL DEFAULT 0 AFTER traffic; + diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index 1b15d30..2d5b6ff 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -25,6 +25,12 @@ class SettingsController extends Controller 'semstorm_password', 'semstorm_api_base', 'semstorm_timeout_seconds', + 'dataforseo_login', + 'dataforseo_password', + 'dataforseo_api_base', + 'dataforseo_timeout_seconds', + 'dataforseo_location_code', + 'dataforseo_language_code', ]; private array $settingDefaults = [ @@ -36,6 +42,10 @@ class SettingsController extends Controller 'image_generation_prompt' => ImageService::DEFAULT_FREEPIK_PROMPT_TEMPLATE, 'semstorm_api_base' => 'https://api.semstorm.com', 'semstorm_timeout_seconds' => '30', + 'dataforseo_api_base' => 'https://api.dataforseo.com', + 'dataforseo_timeout_seconds' => '30', + 'dataforseo_location_code' => '2616', + 'dataforseo_language_code' => 'pl', ]; public function index(): void diff --git a/src/Controllers/SiteController.php b/src/Controllers/SiteController.php index 91d3a28..d6198b6 100644 --- a/src/Controllers/SiteController.php +++ b/src/Controllers/SiteController.php @@ -68,6 +68,7 @@ class SiteController extends Controller 'name' => $this->input('name'), 'url' => rtrim($this->input('url'), '/'), 'semstorm_domain' => $this->input('semstorm_domain') ?: null, + 'dataforseo_domain' => $this->input('dataforseo_domain') ?: null, 'api_user' => $this->input('api_user'), 'api_token' => $this->input('api_token'), 'publish_interval_hours' => (int) ($this->input('publish_interval_hours', 24)), @@ -149,6 +150,7 @@ class SiteController extends Controller 'name' => $this->input('name'), 'url' => rtrim($this->input('url'), '/'), 'semstorm_domain' => $this->input('semstorm_domain') ?: null, + 'dataforseo_domain' => $this->input('dataforseo_domain') ?: null, 'api_user' => $this->input('api_user'), 'api_token' => $this->input('api_token'), 'publish_interval_hours' => (int) ($this->input('publish_interval_hours', 24)), diff --git a/src/Models/Site.php b/src/Models/Site.php index 0ea245c..33a935c 100644 --- a/src/Models/Site.php +++ b/src/Models/Site.php @@ -39,7 +39,12 @@ class Site extends Model ON m.site_id = s.id AND m.metric_month = :metric_month WHERE s.is_active = 1 - AND m.id IS NULL + AND ( + m.id IS NULL + OR m.source_payload IS NULL + OR m.source_payload NOT LIKE '%\"dataforseo\"%' + OR m.source_payload LIKE '%\"dataforseo\":null%' + ) ORDER BY s.id ASC LIMIT 1"; diff --git a/src/Models/SiteSeoMetric.php b/src/Models/SiteSeoMetric.php index 0909063..b1bb6a4 100644 --- a/src/Models/SiteSeoMetric.php +++ b/src/Models/SiteSeoMetric.php @@ -21,19 +21,57 @@ class SiteSeoMetric extends Model return (bool) $stmt->fetchColumn(); } + public static function findForMonth(int $siteId, string $metricMonth): ?array + { + $stmt = self::db()->prepare( + 'SELECT id, metric_month, top3, top10, top20, top50, traffic, indexed_pages, source_payload, updated_at + FROM site_seo_metrics + WHERE site_id = :site_id AND metric_month = :metric_month + LIMIT 1' + ); + $stmt->execute([ + 'site_id' => $siteId, + 'metric_month' => $metricMonth, + ]); + + $row = $stmt->fetch(); + return $row ?: null; + } + + public static function hasDataforseoForMonth(int $siteId, string $metricMonth): bool + { + $row = self::findForMonth($siteId, $metricMonth); + if (!$row) { + return false; + } + + $payloadRaw = trim((string) ($row['source_payload'] ?? '')); + if ($payloadRaw === '') { + return false; + } + + $decoded = json_decode($payloadRaw, true); + if (!is_array($decoded) || !array_key_exists('dataforseo', $decoded)) { + return false; + } + + return is_string($decoded['dataforseo']) && trim($decoded['dataforseo']) !== ''; + } + public static function upsertMonthly(int $siteId, string $metricMonth, array $metrics, ?string $payload = null): void { $stmt = self::db()->prepare( 'INSERT INTO site_seo_metrics - (site_id, metric_month, top3, top10, top20, top50, traffic, source_payload) + (site_id, metric_month, top3, top10, top20, top50, traffic, indexed_pages, source_payload) VALUES - (:site_id, :metric_month, :top3, :top10, :top20, :top50, :traffic, :source_payload) + (:site_id, :metric_month, :top3, :top10, :top20, :top50, :traffic, :indexed_pages, :source_payload) ON DUPLICATE KEY UPDATE top3 = VALUES(top3), top10 = VALUES(top10), top20 = VALUES(top20), top50 = VALUES(top50), traffic = VALUES(traffic), + indexed_pages = VALUES(indexed_pages), source_payload = VALUES(source_payload), updated_at = CURRENT_TIMESTAMP' ); @@ -46,6 +84,7 @@ class SiteSeoMetric extends Model 'top20' => max(0, (int) ($metrics['top20'] ?? 0)), 'top50' => max(0, (int) ($metrics['top50'] ?? 0)), 'traffic' => max(0, (int) ($metrics['traffic'] ?? 0)), + 'indexed_pages' => max(0, (int) ($metrics['indexed_pages'] ?? 0)), 'source_payload' => $payload, ]); } @@ -53,7 +92,7 @@ class SiteSeoMetric extends Model public static function findBySite(int $siteId, int $limit = 12): array { $stmt = self::db()->prepare( - 'SELECT metric_month, top3, top10, top20, top50, traffic, created_at, updated_at + 'SELECT metric_month, top3, top10, top20, top50, traffic, indexed_pages, created_at, updated_at FROM site_seo_metrics WHERE site_id = :site_id ORDER BY metric_month DESC @@ -70,7 +109,7 @@ class SiteSeoMetric extends Model public static function latestForSite(int $siteId): ?array { $stmt = self::db()->prepare( - 'SELECT metric_month, top3, top10, top20, top50, traffic, updated_at + 'SELECT metric_month, top3, top10, top20, top50, traffic, indexed_pages, updated_at FROM site_seo_metrics WHERE site_id = :site_id ORDER BY metric_month DESC @@ -84,7 +123,7 @@ class SiteSeoMetric extends Model public static function latestForAllSites(string $sort = 'traffic', string $dir = 'desc'): array { - $allowedSort = ['top3', 'top10', 'top20', 'top50', 'traffic', 'metric_month', 'updated_at', 'site_name']; + $allowedSort = ['top3', 'top10', 'top20', 'top50', 'traffic', 'indexed_pages', 'metric_month', 'updated_at', 'site_name']; if (!in_array($sort, $allowedSort, true)) { $sort = 'traffic'; } @@ -109,6 +148,7 @@ class SiteSeoMetric extends Model m.top20, m.top50, m.traffic, + m.indexed_pages, m.updated_at, m.created_at, pm.metric_month AS prev_metric_month, @@ -116,7 +156,8 @@ class SiteSeoMetric extends Model pm.top10 AS prev_top10, pm.top20 AS prev_top20, pm.top50 AS prev_top50, - pm.traffic AS prev_traffic + pm.traffic AS prev_traffic, + pm.indexed_pages AS prev_indexed_pages FROM sites s LEFT JOIN site_seo_metrics m ON m.id = ( diff --git a/src/Services/DataForSeoService.php b/src/Services/DataForSeoService.php new file mode 100644 index 0000000..4abab63 --- /dev/null +++ b/src/Services/DataForSeoService.php @@ -0,0 +1,118 @@ +http = new Client([ + 'timeout' => max(5, $timeout), + 'verify' => false, + ]); + } + + public function fetchIndexedPagesCount(string $domain): array + { + $baseUrl = rtrim((string) Config::getDbSetting( + 'dataforseo_api_base', + Config::get('DATAFORSEO_API_BASE', 'https://api.dataforseo.com') + ), '/'); + $login = trim((string) Config::getDbSetting('dataforseo_login', Config::get('DATAFORSEO_LOGIN', ''))); + $password = trim((string) Config::getDbSetting('dataforseo_password', Config::get('DATAFORSEO_PASSWORD', ''))); + $locationCode = (int) Config::getDbSetting('dataforseo_location_code', Config::get('DATAFORSEO_LOCATION_CODE', '2616')); + $languageCode = strtolower(trim((string) Config::getDbSetting( + 'dataforseo_language_code', + Config::get('DATAFORSEO_LANGUAGE_CODE', 'pl') + ))); + + if ($login === '' || $password === '') { + throw new \RuntimeException('Brak danych logowania DataForSEO (login/haslo).'); + } + + if ($locationCode <= 0) { + $locationCode = 2616; + } + + if ($languageCode === '') { + $languageCode = 'pl'; + } + + $payload = [[ + 'keyword' => 'site:' . $domain, + 'location_code' => $locationCode, + 'language_code' => $languageCode, + 'device' => 'desktop', + 'os' => 'windows', + 'depth' => 10, + ]]; + + try { + $response = $this->http->post($baseUrl . '/v3/serp/google/organic/live/regular', [ + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'User-Agent' => 'BackPRO/1.0', + ], + 'auth' => [$login, $password], + 'json' => $payload, + ]); + } catch (GuzzleException $e) { + throw new \RuntimeException('Blad pobierania statystyk DataForSEO: ' . $e->getMessage(), 0, $e); + } + + $raw = (string) $response->getBody(); + $decoded = json_decode($raw, true); + if (!is_array($decoded)) { + throw new \RuntimeException('DataForSEO zwrocil niepoprawny JSON.'); + } + + return [ + 'indexed_pages' => $this->extractIndexedPages($decoded), + 'payload' => $raw, + ]; + } + + private function extractIndexedPages(array $payload): int + { + $tasks = $payload['tasks'] ?? null; + if (!is_array($tasks) || empty($tasks) || !is_array($tasks[0] ?? null)) { + return 0; + } + + $task = $tasks[0]; + $result = null; + if (is_array($task['result'] ?? null) && !empty($task['result'][0]) && is_array($task['result'][0])) { + $result = $task['result'][0]; + } + + if (!is_array($result)) { + return 0; + } + + // Prefer dedicated total counters from SERP response. + if (isset($result['se_results_count'])) { + return max(0, (int) $result['se_results_count']); + } + + if (isset($result['items_count'])) { + return max(0, (int) $result['items_count']); + } + + return 0; + } +} + diff --git a/src/Services/SiteSeoSyncService.php b/src/Services/SiteSeoSyncService.php index fbee587..976ab3d 100644 --- a/src/Services/SiteSeoSyncService.php +++ b/src/Services/SiteSeoSyncService.php @@ -10,10 +10,12 @@ use App\Models\SiteSeoMetric; class SiteSeoSyncService { private SemstormService $semstorm; + private DataForSeoService $dataforseo; public function __construct() { $this->semstorm = new SemstormService(); + $this->dataforseo = new DataForSeoService(); } public function syncSite(array $site, ?\DateTimeImmutable $month = null, bool $force = false): array @@ -24,53 +26,96 @@ class SiteSeoSyncService } $metricMonth = ($month ?? new \DateTimeImmutable('first day of this month'))->format('Y-m-01'); + $existing = SiteSeoMetric::findForMonth($siteId, $metricMonth); + $hasMonth = $existing !== null; + $hasDataforseo = SiteSeoMetric::hasDataforseoForMonth($siteId, $metricMonth); - if (!$force && SiteSeoMetric::existsForMonth($siteId, $metricMonth)) { + if (!$force && $hasMonth && $hasDataforseo) { return [ 'success' => true, 'status' => 'skipped', - 'message' => 'Dane SEO dla tego miesiaca juz istnieja.', + 'message' => 'Dane SEO dla tego miesiaca juz istnieja (SEMSTORM + DataForSEO).', 'metric_month' => $metricMonth, ]; } - $domain = $this->resolveDomain($site); - if ($domain === '') { - return ['success' => false, 'status' => 'error', 'message' => 'Brak domeny SEMSTORM dla strony.']; - } + $metricsToSave = [ + 'top3' => (int) ($existing['top3'] ?? 0), + 'top10' => (int) ($existing['top10'] ?? 0), + 'top20' => (int) ($existing['top20'] ?? 0), + 'top50' => (int) ($existing['top50'] ?? 0), + 'traffic' => (int) ($existing['traffic'] ?? 0), + 'indexed_pages' => (int) ($existing['indexed_pages'] ?? 0), + ]; + + $payload = $this->extractPayloadParts($existing['source_payload'] ?? null); + $semstormDomain = $this->resolveDomain($site); + $dataforseoDomain = $this->resolveDataforseoDomain($site, $semstormDomain); + $errors = []; + $didSync = false; try { - $metrics = $this->semstorm->fetchDomainMetrics($domain, new \DateTimeImmutable($metricMonth)); - SiteSeoMetric::upsertMonthly($siteId, $metricMonth, $metrics, $metrics['payload'] ?? null); + if ($force || !$hasMonth) { + if ($semstormDomain !== '') { + $semstormMetrics = $this->semstorm->fetchDomainMetrics($semstormDomain, new \DateTimeImmutable($metricMonth)); + $metricsToSave['top3'] = (int) ($semstormMetrics['top3'] ?? 0); + $metricsToSave['top10'] = (int) ($semstormMetrics['top10'] ?? 0); + $metricsToSave['top20'] = (int) ($semstormMetrics['top20'] ?? 0); + $metricsToSave['top50'] = (int) ($semstormMetrics['top50'] ?? 0); + $metricsToSave['traffic'] = (int) ($semstormMetrics['traffic'] ?? 0); + $payload['semstorm'] = is_string($semstormMetrics['payload'] ?? null) ? $semstormMetrics['payload'] : null; + $didSync = true; + } else { + $errors[] = 'Brak domeny SEMSTORM.'; + } + } + + if ($force || !$hasDataforseo) { + if ($dataforseoDomain !== '') { + $dataforseo = $this->dataforseo->fetchIndexedPagesCount($dataforseoDomain); + $metricsToSave['indexed_pages'] = max(0, (int) ($dataforseo['indexed_pages'] ?? 0)); + $payload['dataforseo'] = is_string($dataforseo['payload'] ?? null) ? $dataforseo['payload'] : null; + $didSync = true; + } else { + $errors[] = 'Brak domeny DataForSEO.'; + } + } + + if (!$didSync) { + throw new \RuntimeException(!empty($errors) ? implode(' ', $errors) : 'Brak danych do synchronizacji.'); + } + + $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + SiteSeoMetric::upsertMonthly($siteId, $metricMonth, $metricsToSave, is_string($payloadJson) ? $payloadJson : null); Logger::info( - "SEMSTORM sync OK: site_id={$siteId}, domain={$domain}, month={$metricMonth}", + "SEO sync OK: site_id={$siteId}, semstorm_domain={$semstormDomain}, dataforseo_domain={$dataforseoDomain}, month={$metricMonth}", 'semstorm' ); return [ 'success' => true, 'status' => 'saved', - 'message' => 'Zapisano dane SEO z SEMSTORM.', + 'message' => 'Zapisano/uzupelniono dane SEO (SEMSTORM + DataForSEO).', 'metric_month' => $metricMonth, - 'metrics' => [ - 'top3' => (int) ($metrics['top3'] ?? 0), - 'top10' => (int) ($metrics['top10'] ?? 0), - 'top20' => (int) ($metrics['top20'] ?? 0), - 'top50' => (int) ($metrics['top50'] ?? 0), - 'traffic' => (int) ($metrics['traffic'] ?? 0), - ], + 'metrics' => $metricsToSave, ]; } catch (\Throwable $e) { - Logger::error( - "SEMSTORM sync FAIL: site_id={$siteId}, domain={$domain}, month={$metricMonth}, error={$e->getMessage()}", + if (str_contains($e->getMessage(), 'DataForSEO')) { + Logger::warning( + "DataForSEO sync WARN: site_id={$siteId}, semstorm_domain={$semstormDomain}, dataforseo_domain={$dataforseoDomain}, month={$metricMonth}, error={$e->getMessage()}", + 'semstorm' + ); + } + Logger::info( + "SEO sync FAIL: site_id={$siteId}, semstorm_domain={$semstormDomain}, dataforseo_domain={$dataforseoDomain}, month={$metricMonth}, error={$e->getMessage()}", 'semstorm' ); return [ 'success' => false, 'status' => 'error', - 'message' => 'Blad pobierania SEMSTORM: ' . $e->getMessage(), + 'message' => 'Blad synchronizacji SEO: ' . $e->getMessage(), 'metric_month' => $metricMonth, ]; } @@ -95,4 +140,36 @@ class SiteSeoSyncService return strtolower($host); } + + private function resolveDataforseoDomain(array $site, string $fallbackDomain): string + { + $manual = trim((string) ($site['dataforseo_domain'] ?? '')); + if ($manual !== '') { + return strtolower($manual); + } + + return $fallbackDomain; + } + + private function extractPayloadParts($sourcePayload): array + { + $default = ['semstorm' => null, 'dataforseo' => null]; + $raw = trim((string) $sourcePayload); + if ($raw === '') { + return $default; + } + + $decoded = json_decode($raw, true); + if (!is_array($decoded)) { + return [ + 'semstorm' => $raw, + 'dataforseo' => null, + ]; + } + + return [ + 'semstorm' => is_string($decoded['semstorm'] ?? null) ? $decoded['semstorm'] : null, + 'dataforseo' => is_string($decoded['dataforseo'] ?? null) ? $decoded['dataforseo'] : null, + ]; + } } diff --git a/templates/dashboard/seo-stats.php b/templates/dashboard/seo-stats.php index 6c4679a..e301cec 100644 --- a/templates/dashboard/seo-stats.php +++ b/templates/dashboard/seo-stats.php @@ -52,6 +52,7 @@ TOP20 TOP50 Ruch + Zaindeksowane Aktualizacja @@ -59,7 +60,7 @@ - Brak danych SEO. + Brak danych SEO. @@ -128,6 +129,14 @@ - + + + + + + - + + diff --git a/templates/settings/index.php b/templates/settings/index.php index daa0f25..4450ae3 100644 --- a/templates/settings/index.php +++ b/templates/settings/index.php @@ -112,6 +112,46 @@ value="" min="5" max="120"> +
DataForSEO (indeksacja domeny)
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ diff --git a/templates/sites/create.php b/templates/sites/create.php index dfe8c07..838011c 100644 --- a/templates/sites/create.php +++ b/templates/sites/create.php @@ -22,6 +22,12 @@
Jeśli puste, system użyje hosta z URL WordPressa.
+
+ + +
Jesli puste, system uzyje domeny SEMSTORM lub hosta z URL.
+
+
diff --git a/templates/sites/edit.php b/templates/sites/edit.php index 906e6f4..7c62c28 100644 --- a/templates/sites/edit.php +++ b/templates/sites/edit.php @@ -36,6 +36,12 @@
Jeśli puste, system użyje hosta z URL WordPressa.
+
+ + +
Jesli puste, system uzyje domeny SEMSTORM lub hosta z URL.
+
+
diff --git a/templates/sites/seo.php b/templates/sites/seo.php index cbd232c..acefdb7 100644 --- a/templates/sites/seo.php +++ b/templates/sites/seo.php @@ -15,7 +15,7 @@
-
+
Widocznosc SEO (SEMSTORM)
@@ -25,7 +25,26 @@
- +
+ +
+
+
+ +
+
+
Zaindeksowane strony (DataForSEO)
+
+
+ +
+

+ Ostatnia aktualizacja miesieczna: + +

+ +

Brak danych o indeksacji. Uzyj przycisku "Synchronizuj teraz".

+
@@ -64,6 +83,7 @@

Ruch:

+

Zaindeksowane strony:

Ostatni zapis:
Miesiac: