feat: Add SEMSTORM domain input and SEO panel links
- Added optional SEMSTORM domain input field in site creation and editing forms. - Introduced SEO panel links in site dashboard and edit pages. - Created a new cron job for SEMSTORM data synchronization. - Implemented database migrations for cron logs and site SEO metrics. - Developed SiteSeoSyncService to handle SEMSTORM data fetching and storage. - Added logging functionality for cron events. - Created a new LogController to display cron logs with filtering options. - Added SEO statistics dashboard with visual representation of metrics. - Implemented site SEO metrics model for data retrieval and manipulation.
This commit is contained in:
@@ -3,13 +3,17 @@
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\Config;
|
||||
use App\Core\Controller;
|
||||
use App\Helpers\Logger;
|
||||
use App\Helpers\Validator;
|
||||
use App\Models\Article;
|
||||
use App\Models\Site;
|
||||
use App\Models\SiteSeoMetric;
|
||||
use App\Models\Topic;
|
||||
use App\Models\GlobalTopic;
|
||||
use App\Services\InstallerService;
|
||||
use App\Services\SiteSeoSyncService;
|
||||
use App\Services\WordPressService;
|
||||
|
||||
class SiteController extends Controller
|
||||
@@ -63,6 +67,7 @@ class SiteController extends Controller
|
||||
$siteId = Site::create([
|
||||
'name' => $this->input('name'),
|
||||
'url' => rtrim($this->input('url'), '/'),
|
||||
'semstorm_domain' => $this->input('semstorm_domain') ?: null,
|
||||
'api_user' => $this->input('api_user'),
|
||||
'api_token' => $this->input('api_token'),
|
||||
'publish_interval_hours' => (int) ($this->input('publish_interval_hours', 24)),
|
||||
@@ -135,6 +140,7 @@ class SiteController extends Controller
|
||||
Site::update((int) $id, [
|
||||
'name' => $this->input('name'),
|
||||
'url' => rtrim($this->input('url'), '/'),
|
||||
'semstorm_domain' => $this->input('semstorm_domain') ?: null,
|
||||
'api_user' => $this->input('api_user'),
|
||||
'api_token' => $this->input('api_token'),
|
||||
'publish_interval_hours' => (int) ($this->input('publish_interval_hours', 24)),
|
||||
@@ -207,6 +213,174 @@ class SiteController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function seoPanel(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$site = Site::find((int) $id);
|
||||
if (!$site) {
|
||||
$this->flash('danger', 'Strona nie znaleziona.');
|
||||
$this->redirect('/sites');
|
||||
return;
|
||||
}
|
||||
|
||||
$seoMetrics = SiteSeoMetric::findBySite((int) $id, 12);
|
||||
$seoLatest = SiteSeoMetric::latestForSite((int) $id);
|
||||
|
||||
$this->view('sites/seo', [
|
||||
'site' => $site,
|
||||
'seoMetrics' => $seoMetrics,
|
||||
'seoLatest' => $seoLatest,
|
||||
]);
|
||||
}
|
||||
|
||||
public function syncSeoMetrics(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$site = Site::find((int) $id);
|
||||
if (!$site) {
|
||||
$this->flash('danger', 'Strona nie znaleziona.');
|
||||
$this->redirect('/sites');
|
||||
return;
|
||||
}
|
||||
|
||||
$sync = new SiteSeoSyncService();
|
||||
$result = $sync->syncSite($site, null, true);
|
||||
|
||||
if (!empty($result['success'])) {
|
||||
$this->flash('success', (string) ($result['message'] ?? 'Pobrano dane SEO.'));
|
||||
} else {
|
||||
$this->flash('danger', (string) ($result['message'] ?? 'Nie udalo sie pobrac danych SEO.'));
|
||||
}
|
||||
|
||||
$this->redirect("/sites/{$id}/seo");
|
||||
}
|
||||
|
||||
public function syncSeoByToken(): void
|
||||
{
|
||||
$configuredToken = (string) Config::get('SEO_TRIGGER_TOKEN', '');
|
||||
$providedToken = (string) $this->input('token', '');
|
||||
|
||||
if ($providedToken === '') {
|
||||
$providedToken = (string) ($_SERVER['HTTP_X_SEO_TOKEN'] ?? '');
|
||||
}
|
||||
|
||||
if ($configuredToken === '') {
|
||||
Logger::warning('Token SEO trigger called, but SEO_TRIGGER_TOKEN is not configured.', 'semstorm');
|
||||
$this->json(['success' => false, 'message' => 'Token trigger is disabled.'], 503);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($providedToken === '' || !hash_equals($configuredToken, $providedToken)) {
|
||||
$ip = (string) ($_SERVER['REMOTE_ADDR'] ?? 'unknown');
|
||||
Logger::warning("Invalid SEO token attempt from {$ip}", 'semstorm');
|
||||
$this->json(['success' => false, 'message' => 'Forbidden'], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$force = ((int) $this->input('force', 0)) === 1;
|
||||
$siteId = (int) $this->input('site_id', 0);
|
||||
|
||||
$sync = new SiteSeoSyncService();
|
||||
$saved = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
$details = [];
|
||||
|
||||
if ($siteId > 0) {
|
||||
$site = Site::find($siteId);
|
||||
if (!$site) {
|
||||
$this->json(['success' => false, 'message' => 'Strona nie znaleziona.'], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $sync->syncSite($site, null, $force);
|
||||
$status = (string) ($result['status'] ?? 'error');
|
||||
|
||||
if ($status === 'saved') {
|
||||
$saved++;
|
||||
} elseif ($status === 'skipped') {
|
||||
$skipped++;
|
||||
} else {
|
||||
$failed++;
|
||||
}
|
||||
|
||||
$details[] = [
|
||||
'site_id' => (int) $site['id'],
|
||||
'site_name' => (string) $site['name'],
|
||||
'status' => $status,
|
||||
'message' => (string) ($result['message'] ?? ''),
|
||||
];
|
||||
} elseif (!$force) {
|
||||
$metricMonth = (new \DateTimeImmutable('first day of this month'))->format('Y-m-01');
|
||||
$site = Site::findNextDueForSeoSync($metricMonth);
|
||||
|
||||
if (!$site) {
|
||||
$this->json([
|
||||
'success' => true,
|
||||
'message' => 'SEMSTORM sync: brak stron do synchronizacji w tym miesiacu.',
|
||||
'saved' => 0,
|
||||
'skipped' => 0,
|
||||
'failed' => 0,
|
||||
'force' => false,
|
||||
'details' => [],
|
||||
], 200);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $sync->syncSite($site, null, false);
|
||||
$status = (string) ($result['status'] ?? 'error');
|
||||
|
||||
if ($status === 'saved') {
|
||||
$saved++;
|
||||
} elseif ($status === 'skipped') {
|
||||
$skipped++;
|
||||
} else {
|
||||
$failed++;
|
||||
}
|
||||
|
||||
$details[] = [
|
||||
'site_id' => (int) $site['id'],
|
||||
'site_name' => (string) $site['name'],
|
||||
'status' => $status,
|
||||
'message' => (string) ($result['message'] ?? ''),
|
||||
];
|
||||
} else {
|
||||
$sites = Site::findActive();
|
||||
|
||||
foreach ($sites as $site) {
|
||||
$result = $sync->syncSite($site, null, $force);
|
||||
$status = (string) ($result['status'] ?? 'error');
|
||||
|
||||
if ($status === 'saved') {
|
||||
$saved++;
|
||||
} elseif ($status === 'skipped') {
|
||||
$skipped++;
|
||||
} else {
|
||||
$failed++;
|
||||
}
|
||||
|
||||
$details[] = [
|
||||
'site_id' => (int) $site['id'],
|
||||
'site_name' => (string) $site['name'],
|
||||
'status' => $status,
|
||||
'message' => (string) ($result['message'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->json([
|
||||
'success' => $failed === 0,
|
||||
'message' => "SEMSTORM sync: saved={$saved}, skipped={$skipped}, failed={$failed}",
|
||||
'saved' => $saved,
|
||||
'skipped' => $skipped,
|
||||
'failed' => $failed,
|
||||
'force' => $force,
|
||||
'details' => $details,
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function enablePrettyPermalinks(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
Reference in New Issue
Block a user