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.
This commit is contained in:
@@ -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
|
||||
|
||||
350
.vscode/ftp-kr.sync.cache.json
vendored
350
.vscode/ftp-kr.sync.cache.json
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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).
|
||||
|
||||
7
migrations/010_dataforseo_indexed_pages.sql
Normal file
7
migrations/010_dataforseo_indexed_pages.sql
Normal file
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
118
src/Services/DataForSeoService.php
Normal file
118
src/Services/DataForSeoService.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Core\Config;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
|
||||
class DataForSeoService
|
||||
{
|
||||
private Client $http;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$timeout = (float) Config::getDbSetting(
|
||||
'dataforseo_timeout_seconds',
|
||||
Config::get('DATAFORSEO_TIMEOUT_SECONDS', '30')
|
||||
);
|
||||
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<th class="text-nowrap"><a class="link-dark text-decoration-none" href="<?= htmlspecialchars($sortLink('top20')) ?>">TOP20<?= htmlspecialchars($sortMark('top20')) ?></a></th>
|
||||
<th class="text-nowrap"><a class="link-dark text-decoration-none" href="<?= htmlspecialchars($sortLink('top50')) ?>">TOP50<?= htmlspecialchars($sortMark('top50')) ?></a></th>
|
||||
<th class="text-nowrap"><a class="link-dark text-decoration-none" href="<?= htmlspecialchars($sortLink('traffic')) ?>">Ruch<?= htmlspecialchars($sortMark('traffic')) ?></a></th>
|
||||
<th class="text-nowrap"><a class="link-dark text-decoration-none" href="<?= htmlspecialchars($sortLink('indexed_pages')) ?>">Zaindeksowane<?= htmlspecialchars($sortMark('indexed_pages')) ?></a></th>
|
||||
<th>Aktualizacja</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -59,7 +60,7 @@
|
||||
<tbody>
|
||||
<?php if (empty($rows)): ?>
|
||||
<tr>
|
||||
<td colspan="10" class="text-center text-muted py-4">Brak danych SEO.</td>
|
||||
<td colspan="11" class="text-center text-muted py-4">Brak danych SEO.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($rows as $row): ?>
|
||||
@@ -128,6 +129,14 @@
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
<?php if (!empty($row['metric_month'])): ?>
|
||||
<?= (int) ($row['indexed_pages'] ?? 0) ?>
|
||||
<small><?= $formatDelta($row['indexed_pages'] ?? 0, $row['prev_indexed_pages'] ?? null) ?></small>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
<?php if (!empty($row['updated_at'])): ?>
|
||||
<?= htmlspecialchars(date('d.m.Y H:i', strtotime((string) $row['updated_at']))) ?>
|
||||
|
||||
@@ -112,6 +112,46 @@
|
||||
value="<?= htmlspecialchars($settings['semstorm_timeout_seconds']) ?>" min="5" max="120">
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3 mt-4 border-bottom pb-2">DataForSEO (indeksacja domeny)</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="dataforseo_login" class="form-label">Login DataForSEO</label>
|
||||
<input type="text" class="form-control" id="dataforseo_login" name="dataforseo_login"
|
||||
value="<?= htmlspecialchars($settings['dataforseo_login']) ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="dataforseo_password" class="form-label">Haslo DataForSEO</label>
|
||||
<input type="password" class="form-control" id="dataforseo_password" name="dataforseo_password"
|
||||
value="<?= htmlspecialchars($settings['dataforseo_password']) ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="dataforseo_api_base" class="form-label">Bazowy URL API</label>
|
||||
<input type="text" class="form-control" id="dataforseo_api_base" name="dataforseo_api_base"
|
||||
value="<?= htmlspecialchars($settings['dataforseo_api_base']) ?>"
|
||||
placeholder="https://api.dataforseo.com">
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="dataforseo_location_code" class="form-label">Location code</label>
|
||||
<input type="number" class="form-control" id="dataforseo_location_code" name="dataforseo_location_code"
|
||||
value="<?= htmlspecialchars($settings['dataforseo_location_code']) ?>" min="1">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="dataforseo_language_code" class="form-label">Language code</label>
|
||||
<input type="text" class="form-control" id="dataforseo_language_code" name="dataforseo_language_code"
|
||||
value="<?= htmlspecialchars($settings['dataforseo_language_code']) ?>" placeholder="pl">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="dataforseo_timeout_seconds" class="form-label">Timeout API DataForSEO (sekundy)</label>
|
||||
<input type="number" class="form-control" id="dataforseo_timeout_seconds" name="dataforseo_timeout_seconds"
|
||||
value="<?= htmlspecialchars($settings['dataforseo_timeout_seconds']) ?>" min="5" max="120">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
<input type="text" class="form-control" id="semstorm_domain" name="semstorm_domain" placeholder="example.com">
|
||||
<div class="form-text">Jeśli puste, system użyje hosta z URL WordPressa.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dataforseo_domain" class="form-label">Domena DataForSEO (opcjonalnie)</label>
|
||||
<input type="text" class="form-control" id="dataforseo_domain" name="dataforseo_domain" placeholder="example.com">
|
||||
<div class="form-text">Jesli puste, system uzyje domeny SEMSTORM lub hosta z URL.</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="api_user" class="form-label">Użytkownik API (WordPress)</label>
|
||||
|
||||
@@ -36,6 +36,12 @@
|
||||
<input type="text" class="form-control" id="semstorm_domain" name="semstorm_domain" value="<?= htmlspecialchars($site['semstorm_domain'] ?? '') ?>" placeholder="example.com">
|
||||
<div class="form-text">Jeśli puste, system użyje hosta z URL WordPressa.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dataforseo_domain" class="form-label">Domena DataForSEO (opcjonalnie)</label>
|
||||
<input type="text" class="form-control" id="dataforseo_domain" name="dataforseo_domain" value="<?= htmlspecialchars($site['dataforseo_domain'] ?? '') ?>" placeholder="example.com">
|
||||
<div class="form-text">Jesli puste, system uzyje domeny SEMSTORM lub hosta z URL.</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="api_user" class="form-label">Użytkownik API</label>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Widocznosc SEO (SEMSTORM)</h5>
|
||||
<form method="post" action="/sites/<?= (int) $site['id'] ?>/seo/sync" data-confirm="Pobrac i nadpisac dane SEO dla biezacego miesiaca?">
|
||||
@@ -25,7 +25,26 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="seoVisibilityChart" height="320"></canvas>
|
||||
<div class="position-relative" style="height: 340px;">
|
||||
<canvas id="seoVisibilityChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4 border-secondary-subtle">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Zaindeksowane strony (DataForSEO)</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!empty($seoLatest)): ?>
|
||||
<div class="display-6 fw-semibold mb-1"><?= (int) ($seoLatest['indexed_pages'] ?? 0) ?></div>
|
||||
<p class="small text-muted mb-0">
|
||||
Ostatnia aktualizacja miesieczna:
|
||||
<?= htmlspecialchars(date('m.Y', strtotime((string) $seoLatest['metric_month']))) ?>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<p class="text-muted small mb-0">Brak danych o indeksacji. Uzyj przycisku "Synchronizuj teraz".</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,6 +83,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="small mb-2"><strong>Ruch:</strong> <?= (int) $seoLatest['traffic'] ?></p>
|
||||
<p class="small mb-2"><strong>Zaindeksowane strony:</strong> <?= (int) ($seoLatest['indexed_pages'] ?? 0) ?></p>
|
||||
<p class="small text-muted mb-0">
|
||||
Ostatni zapis: <?= htmlspecialchars(date('d.m.Y H:i', strtotime((string) $seoLatest['updated_at']))) ?><br>
|
||||
Miesiac: <?= htmlspecialchars(date('m.Y', strtotime((string) $seoLatest['metric_month']))) ?>
|
||||
|
||||
Reference in New Issue
Block a user