admin: Indexierungsstatus aus der Search Console, Kundenansicht vorbereitet und zu (Anhang 1: 1c.4, E8)
press-releases:check-index-status fragt täglich je Portal ein Kontingent veröffentlichter Meldungen über die URL Inspection API ab, nach Alter gestaffelt, nie geprüfte zuerst. Jede Antwort wird als Momentaufnahme in press_release_index_checks gespeichert; der reduzierte Befund (IndexState) trennt „noch nicht gecrawlt" von „gecrawlt, nicht indexiert". Client authentifiziert sich als Service-Konto über ein selbst signiertes JWT — kein Google-SDK. Admin ▸ Reports ▸ Indexierung zeigt die Verteilung je Portal getrennt nach Direkteinreichung und Syndikation (Status-Log source=api; Altbestand ohne Log zählt als direkt). Das Kundenpanel heißt jetzt „Auslieferung", zeigt Sitemap-Datum und die Abrufe durch Such- und KI-Dienste mit dem Satz „Ein Abruf ist kein Zitat"; der Google-Index-Block hängt an GSC_CUSTOMER_VISIBLE (false). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7088d092d8
commit
6c4618afed
17 changed files with 1324 additions and 30 deletions
91
app/Console/Commands/CheckIndexStatus.php
Normal file
91
app/Console/Commands/CheckIndexStatus.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\Portal;
|
||||
use App\Services\SearchConsole\IndexStatusChecker;
|
||||
use App\Services\SearchConsole\SearchConsoleClient;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Indexierungsstatus über die Search Console abfragen (Roadmap 1c.4).
|
||||
*
|
||||
* Je Portal ein Tageskontingent, nach Alter gestaffelt auf die jüngsten
|
||||
* Meldungen verteilt (config search_console.recheck_days). Jede Antwort
|
||||
* wird als Momentaufnahme gespeichert; der Lauf bewertet nichts und zeigt
|
||||
* dem Kunden nichts — das entscheidet die Verteilung (1c.5, E8).
|
||||
*/
|
||||
class CheckIndexStatus extends Command
|
||||
{
|
||||
protected $signature = 'press-releases:check-index-status
|
||||
{--portal= : Nur dieses Portal (presseecho|businessportal24)}
|
||||
{--budget= : Abfragen je Portal (Standard: config search_console.daily_budget)}
|
||||
{--dry-run : Nur auflisten, was geprüft würde}';
|
||||
|
||||
protected $description = 'Fragt den Indexierungsstatus veröffentlichter Meldungen bei der Google Search Console ab.';
|
||||
|
||||
public function handle(SearchConsoleClient $client, IndexStatusChecker $checker): int
|
||||
{
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
|
||||
if (! $dryRun && ! $client->isConfigured()) {
|
||||
$this->warn('Search Console nicht konfiguriert (GSC_ENABLED, GSC_SERVICE_ACCOUNT_FILE) — nichts zu tun.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$budget = max(1, (int) ($this->option('budget') ?: config('search_console.daily_budget', 300)));
|
||||
$portals = $checker->portals();
|
||||
|
||||
if ($this->option('portal')) {
|
||||
$wanted = Portal::tryFrom((string) $this->option('portal'));
|
||||
$portals = array_values(array_filter($portals, fn (Portal $portal): bool => $portal === $wanted));
|
||||
}
|
||||
|
||||
if ($portals === []) {
|
||||
$this->warn('Kein Portal mit eingetragener Property.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$checked = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($portals as $portal) {
|
||||
$candidates = $checker->candidates($portal, $budget);
|
||||
$this->info(sprintf('%s: %d fällige Meldungen (Kontingent %d).', $portal->label(), $candidates->count(), $budget));
|
||||
|
||||
foreach ($candidates as $release) {
|
||||
if ($dryRun) {
|
||||
$this->line(sprintf(' [%d] %s (veröffentlicht %s)', $release->id, $release->slug, $release->published_at?->format('d.m.Y') ?? '-'));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$check = $checker->check($release, $portal);
|
||||
$checked++;
|
||||
$this->line(sprintf(' [%d] %s → %s', $release->id, $release->slug, $check->state->label()));
|
||||
} catch (\Throwable $e) {
|
||||
$failed++;
|
||||
report($e);
|
||||
$this->error(sprintf(' [%d] %s: %s', $release->id, $release->slug, $e->getMessage()));
|
||||
|
||||
// Quote oder Auth kaputt: der Rest des Laufs würde nur
|
||||
// weitere Fehler produzieren.
|
||||
if ($failed >= 5 && $checked === 0) {
|
||||
$this->error('Abbruch nach fünf Fehlern ohne Treffer.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! $dryRun) {
|
||||
$this->info(sprintf('%d Prüfungen gespeichert, %d fehlgeschlagen.', $checked, $failed));
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue