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) @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 allein über die Kennung des Abrufers (User-Agent, config/crawlers.php), ohne Prüfung der Herkunft und ohne IP-Adressen; erkannte Crawler zählen nicht als Aufrufe.') }} @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