latest(2000);
$checker = app(IndexStatusChecker::class);
$name = sprintf('indexierung-%s.csv', now()->format('Y-m-d'));
return response()->streamDownload(function () use ($rows, $checker): void {
$handle = fopen('php://output', 'wb');
fputcsv($handle, ['Geprüft', 'Portal', 'ID', 'Titel', 'URL', 'Befund', 'Google-Status', 'Zuletzt gecrawlt', 'Google führt', 'Kanonisch (Google)'], ';');
foreach ($rows as $check) {
fputcsv($handle, [
$check->checked_at->format('d.m.Y H:i'),
$check->portal->label(),
$check->press_release_id,
$check->pressRelease?->title ?? '',
$check->url,
$check->state->label(),
$check->coverage_state ?? '',
$check->last_crawl_time?->format('d.m.Y') ?? '',
$this->sourceLabel($checker->sourceVerdict($check)['kind']),
$check->google_canonical ?? '',
], ';');
}
fclose($handle);
}, $name, ['Content-Type' => 'text/csv; charset=UTF-8']);
}
public function downloadCrawlers(): StreamedResponse
{
$rows = app(CrawlerHitReport::class)->byService($this->days);
$name = sprintf('crawler-abrufe-%s.csv', now()->format('Y-m-d'));
return response()->streamDownload(function () use ($rows): void {
$handle = fopen('php://output', 'wb');
fputcsv($handle, ['Dienst', 'Art', sprintf('Abrufe %d Tage', $this->days), sprintf('Meldungen %d Tage', $this->days), 'Abrufe gesamt', 'Letzter Abruf'], ';');
foreach ($rows as $row) {
fputcsv($handle, [$row['label'], $row['kind'] === 'search' ? 'Suche' : 'KI', $row['fetches'], $row['releases'], $row['fetches_total'], $row['last_at']->format('d.m.Y H:i')], ';');
}
fclose($handle);
}, $name, ['Content-Type' => 'text/csv; charset=UTF-8']);
}
public function sourceLabel(string $kind): string
{
return match ($kind) {
'self' => __('diese Adresse'),
'own_other' => __('eigene Fassung, anderes Portal'),
'foreign' => __('fremdes Portal'),
default => __('keine Angabe'),
};
}
public function with(): array
{
$report = app(IndexStatusReport::class);
$checker = app(IndexStatusChecker::class);
$crawlers = app(CrawlerHitReport::class);
$feedFetches = app(FeedFetchReport::class);
$days = in_array($this->days, [7, 30, 90], true) ? $this->days : 30;
return [
'configured' => app(SearchConsoleClient::class)->isConfigured(),
'customerVisible' => (bool) config('search_console.customer_visible'),
'canonicalActive' => (bool) config('canonical.enabled'),
'distribution' => $report->distribution(),
'sources' => $report->sourceSummary($checker),
'latest' => $report->latest(100),
'lastCheckedAt' => $report->lastCheckedAt(),
'states' => IndexState::cases(),
'portals' => [Portal::Presseecho, Portal::Businessportal24],
'checker' => $checker,
'crawlerServices' => $crawlers->byService($days),
'crawlerDaily' => $crawlers->daily(14),
'crawlerTop' => $crawlers->topReleases($days),
'crawlersSince' => config('crawlers.recorded_since') ? \Illuminate\Support\Carbon::parse((string) config('crawlers.recorded_since')) : null,
'feedTotals' => $feedFetches->totals($days),
'feedReaders' => $feedFetches->byReader($days),
'feedTop' => $feedFetches->topFeeds($days),
'feedsSince' => config('feeds.recorded_since') ? \Illuminate\Support\Carbon::parse((string) config('feeds.recorded_since')) : null,
'days' => $days,
];
}
}; ?>
{{ $configured ? __('täglicher Lauf 05:10 Uhr') : __('GSC_ENABLED und Schlüsseldatei setzen') }}
{{ __('Momentaufnahmen, jede Antwort eine Zeile') }}
{{ __('Kopien verweisen auf die Erstquelle (1c.8, E9)') }}
{{ __('Entscheidung E8 — GSC_CUSTOMER_VISIBLE') }}
@foreach ($portals as $portal)
{{ $portal->label() }} · {{ __('Google-Index') }}
{{ __('im Archiv sichtbare Meldungen, Stand der jeweils jüngsten Prüfung') }}
{{ __('Herkunft · Rolle') }}
{{ __('Meldungen') }}
@foreach ($states as $state)
{{ ucfirst($state->label()) }}
@endforeach
{{ __('Ungeprüft') }}
@foreach (['direct' => __('Direkt eingereicht'), 'api' => __('Syndikation (API)')] as $origin => $originLabel)
@foreach (['source' => __('Erstquelle'), 'copy' => __('Kopie mit Verweis')] as $role => $roleLabel)
@php
$row = $distribution[$portal->value][$origin][$role] ?? [];
$checkedTotal = max(0, ($row['total'] ?? 0) - ($row['unchecked'] ?? 0));
@endphp
@continue(($row['total'] ?? 0) === 0 && $role === 'copy')
{{ $originLabel }}
{{ $roleLabel }}
{{ number_format($row['total'] ?? 0, 0, ',', '.') }}
@foreach ($states as $state)
@php
$count = (int) ($row[$state->value] ?? 0);
$expected = $role === 'copy' ? $state === IndexState::Excluded : $state === IndexState::Indexed;
@endphp
$expected && $count > 0, 'text-[color:var(--color-ink-2)]' => ! ($expected && $count > 0)])>{{ number_format($count, 0, ',', '.') }}
@if ($checkedTotal > 0)
{{ number_format($count / $checkedTotal * 100, 1, ',', '.') }} %
@endif
@endforeach
{{ number_format($row['unchecked'] ?? 0, 0, ',', '.') }}
@endforeach
@endforeach
{{ __('Prozentwerte beziehen sich auf die geprüften Meldungen der Zeile. Fett: der Befund, der für die Rolle erwartet wird — Erstquelle indexiert, Kopie ausgeschlossen zugunsten der Erstquelle.') }}
@endforeach
{{-- Quellenbefund (1c.9): welche Adresse Google als maßgeblich führt.
Nur Erstquellen — bei Kopien ist die Abweichung gewollt. --}}
{{ __('Quellenbefund') }}
{{ __('Erstquellen mit Prüfung: welche Adresse Google als maßgeblich führt') }}
{{ __('Portal') }}
{{ __('Geprüft') }}
{{ __('Diese Adresse') }}
{{ __('Eigene Fassung, anderes Portal') }}
{{ __('Fremdes Portal') }}
{{ __('Keine Angabe') }}
@foreach ($portals as $portal)
@php $row = $sources[$portal->value]; @endphp
{{ $portal->label() }}
{{ number_format($row['total'], 0, ',', '.') }}
@foreach (['self', 'own_other', 'foreign', 'unknown'] as $kind)
{{ number_format($row[$kind], 0, ',', '.') }}
@if ($row['total'] > 0)
{{ number_format($row[$kind] / $row['total'] * 100, 1, ',', '.') }} %
@endif
@if ($kind === 'foreign' && $row['foreign_hosts'] !== [])
{{ collect($row['foreign_hosts'])->map(fn ($n, $host) => $host.' '.$n)->implode(' · ') }}
@endif
@endforeach
@endforeach
{{ __('„Keine Angabe“ heißt: Google nennt keine kanonische Adresse — meist, weil die Seite noch nicht gecrawlt oder nicht indexiert ist. Warum Google eine bestimmte Fassung wählt, ist nicht bekannt.') }}
{{-- Crawler-Abruf-Protokoll (1c.0): Abrufe, keine Zitate. --}}
{{ __('Abrufe durch Such- und KI-Dienste') }}
@foreach ([7, 30, 90] as $range)
$days === $range,
'text-[color:var(--color-ink-3)] hover:bg-[color:var(--color-bg-elev)]' => $days !== $range,
])>{{ __(':days Tage', ['days' => $range]) }}
@endforeach
@if ($crawlerServices !== [])
{{ __('CSV') }}
@endif
@php
$dailyPeak = max(1, (int) collect($crawlerDaily)->max(fn ($d) => $d['search'] + $d['ai']));
@endphp
{{ __('Abrufe je Tag, letzte 14 Tage') }}
{{ __('KI') }}
{{ __('Suche') }}
· {{ __('Spitze :n', ['n' => number_format($dailyPeak, 0, ',', '.')]) }}
@foreach ($crawlerDaily as $day)
@php $total = $day['search'] + $day['ai']; @endphp
@endforeach
{{ $crawlerDaily[0]['date']->format('d.m.') }}
{{ __('heute') }}
{{ __('Dienst') }}
{{ __('Art') }}
{{ __('Abrufe :days Tage', ['days' => $days]) }}
{{ __('Meldungen :days Tage', ['days' => $days]) }}
{{ __('Abrufe gesamt') }}
{{ __('Letzter Abruf') }}
@forelse ($crawlerServices as $service)
{{ $service['label'] }}
{{ $service['kind'] === 'search' ? __('Suche') : __('KI') }}
{{ number_format($service['fetches'], 0, ',', '.') }}
{{ number_format($service['releases'], 0, ',', '.') }}
{{ number_format($service['fetches_total'], 0, ',', '.') }}
{{ $service['last_at']->format('d.m.Y H:i') }}
@empty
{{ __('Noch kein Abruf protokolliert.') }}
@if ($crawlersSince)
{{ __('Erfasst seit :date.', ['date' => $crawlersSince->format('d.m.Y')]) }}
@endif
@endforelse
@if ($crawlerTop !== [])
{{ __('Am häufigsten abgerufen, :days Tage', ['days' => $days]) }}
@foreach ($crawlerTop as $top)
{{ $top['title'] }}
{{ trans_choice(':n Abruf|:n Abrufe', $top['fetches'], ['n' => $top['fetches']]) }} · {{ trans_choice(':n Dienst|:n Dienste', $top['services'], ['n' => $top['services']]) }}
@endforeach
@endif
{{ __('Ein Abruf belegt, dass der Crawler des Dienstes die Seite geholt hat — nicht, dass sie dort angezeigt oder zitiert wird. Erkennung über den User-Agent (config/crawlers.php), keine IP-Adressen.') }}
@if ($crawlersSince)
{{ __('Protokoll seit :date.', ['date' => $crawlersSince->format('d.m.Y')]) }}
@endif
{{-- Feed-Abruf-Protokoll (3.1): Abrufe der RSS-Feeds, gemeldete Abonnenten. --}}
{{ __('Abrufe der RSS-Feeds') }}
{{ __(':n Abrufe in :days Tagen', ['n' => number_format($feedTotals['total'], 0, ',', '.'), 'days' => $days]) }}
· {{ __('Portal :n', ['n' => number_format($feedTotals['portal'], 0, ',', '.')]) }}
· {{ __('Branchen :n', ['n' => number_format($feedTotals['category'], 0, ',', '.')]) }}
· {{ __('Newsrooms :n', ['n' => number_format($feedTotals['newsroom'], 0, ',', '.')]) }}
{{ __('Reader-Dienst') }}
{{ __('Abrufe :days Tage', ['days' => $days]) }}
{{ __('Feeds') }}
{{ __('Gemeldete Abonnenten') }}
{{ __('Letzter Abruf') }}
@forelse ($feedReaders as $reader)
{{ $reader['label'] }}
{{ number_format($reader['fetches'], 0, ',', '.') }}
{{ number_format($reader['feeds'], 0, ',', '.') }}
{{ $reader['subscribers'] === null ? '—' : number_format($reader['subscribers'], 0, ',', '.') }}
{{ $reader['last_at']->format('d.m.Y H:i') }}
@empty
{{ __('Noch kein Feed-Abruf protokolliert.') }}
@if ($feedsSince)
{{ __('Erfasst seit :date.', ['date' => $feedsSince->format('d.m.Y')]) }}
@endif
@endforelse
@if ($feedTop !== [])
{{ __('Am häufigsten abgerufene Feeds, :days Tage', ['days' => $days]) }}
@foreach ($feedTop as $top)
{{ $top['label'] }}
{{ trans_choice(':n Abruf|:n Abrufe', $top['fetches'], ['n' => $top['fetches']]) }} · {{ trans_choice(':n Reader|:n Reader', $top['readers'], ['n' => $top['readers']]) }}@if ($top['subscribers'] !== null) · {{ __(':n Abonnenten gemeldet', ['n' => $top['subscribers']]) }}@endif
@endforeach
@endif
{{ __('Gezählt werden Abrufe der Feed-Adressen; ein Reader-Dienst holt einen Feed einmal für alle seine Nutzer. „Gemeldete Abonnenten" ist die Zahl, die Dienste wie Feedly oder Inoreader selbst im User-Agent nennen — je Feed das Maximum, summiert. Erkennung über den User-Agent (config/feeds.php), keine IP-Adressen.') }}
@if ($feedsSince)
{{ __('Protokoll seit :date.', ['date' => $feedsSince->format('d.m.Y')]) }}
@endif
{{ __('Jüngste Prüfungen') }}
@if ($latest->isNotEmpty())
{{ __('CSV') }}
@endif
{{ __('Geprüft') }}
{{ __('Meldung') }}
{{ __('Portal') }}
{{ __('Befund') }}
{{ __('Google führt') }}
{{ __('Zuletzt gecrawlt') }}
@forelse ($latest as $check)
@php $source = $checker->sourceVerdict($check); @endphp
{{ $check->checked_at->format('d.m.Y H:i') }}
{{ $check->pressRelease?->title ?? '#'.$check->press_release_id }}
{{ __('veröffentlicht :date', ['date' => $check->pressRelease?->published_at?->format('d.m.Y') ?? '—']) }}@if ($check->pressRelease?->canonical_press_release_id) · {{ __('Kopie mit Verweis') }}@endif
{{ $check->portal->label() }}
$check->state === IndexState::Indexed, 'warn' => in_array($check->state, [IndexState::CrawledNotIndexed, IndexState::NotCrawled], true), 'err' => $check->state === IndexState::Excluded, 'muted' => $check->state === IndexState::Unknown])>{{ $check->state->label() }}
@if ($check->coverage_state)
{{ $check->coverage_state }}
@endif
{{ $this->sourceLabel($source['kind']) }}
@if (in_array($source['kind'], ['own_other', 'foreign'], true))
{{ $source['host'] }}
@endif
{{ $check->last_crawl_time?->format('d.m.Y') ?? '—' }}
@empty
{{ __('Noch keine Prüfung gespeichert. Erster Lauf: php artisan press-releases:check-index-status') }}
@endforelse