web: Crawler-Abruf-Protokoll und Panel „Auslieferung" ohne Google-Link (Anhang 1: 1c.0, 1c.1, 1c.6)
Abrufe öffentlicher Meldungen durch bekannte Such- und KI-Dienste werden je Meldung mit Dienst, gekürztem User-Agent und Zeitpunkt protokolliert — keine IP. Dienste und Muster stehen in config/crawlers.php. Aufzeichnung wie der Aufruf-Zähler per defer() und fehlertolerant; Bereinigung hängt an press-releases:purge-events. VisibilityStats liefert die Abrufe je Dienst (letzter Abruf, Anzahl) und verliert den site:-Suchlink (Stufe 1 verspricht nichts, was von Dritten abhängt). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6446e19727
commit
7088d092d8
10 changed files with 440 additions and 7 deletions
|
|
@ -8,6 +8,7 @@ use App\Enums\PressReleaseStatus;
|
|||
use App\Models\Company;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseEvent;
|
||||
use App\Support\CrawlerIdentifier;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
|
|
@ -25,9 +26,14 @@ use Illuminate\Support\Collection;
|
|||
* Tageswert älter als die Aufbewahrung existiert nicht mehr — die
|
||||
* Verlaufsgrafik darf ihn nicht als „0 Aufrufe" ausgeben.
|
||||
*
|
||||
* Der Indexierungsstatus ist in dieser Stufe bewusst ohne Fremddienst
|
||||
* (Entscheidung 16.09.2026, Variante B): „in der Sitemap seit" plus ein
|
||||
* Link auf die Suche. Die Search-Console-Anbindung kommt als eigenes Paket.
|
||||
* Stufe 1 der Absenderansicht (Anhang 17.09.2026) verspricht nichts, was
|
||||
* von Dritten abhängt: Überschrift „Auslieferung", Daten aus eigener Hand —
|
||||
* veröffentlicht am, in der Sitemap seit, Aufrufe im Verlauf. Kein Link auf
|
||||
* eine `site:`-Suche (unvollständig, ohne Einordnung). Der Indexierungs-
|
||||
* status aus der Search Console (1c.4) läuft zunächst nur als Admin-
|
||||
* Auswertung; die Kundenanzeige hängt an config search_console.customer_visible
|
||||
* (E8). Abrufe durch Such- und KI-Dienste (1c.6) kommen aus dem
|
||||
* Crawler-Abruf-Protokoll (1c.0) — ein Abruf ist kein Zitat.
|
||||
*/
|
||||
class VisibilityStats
|
||||
{
|
||||
|
|
@ -124,9 +130,9 @@ class VisibilityStats
|
|||
}
|
||||
|
||||
/**
|
||||
* Einzelwerte einer Meldung für das Panel „Sichtbarkeit".
|
||||
* Einzelwerte einer Meldung für das Panel „Auslieferung".
|
||||
*
|
||||
* @return array{hits: int, first_view_at: Carbon|null, last_view_at: Carbon|null, shares: array<string, int>, shares_total: int, in_sitemap_since: Carbon|null, search_url: string|null}
|
||||
* @return array{hits: int, first_view_at: Carbon|null, last_view_at: Carbon|null, shares: array<string, int>, shares_total: int, in_sitemap_since: Carbon|null, crawlers: list<array{service: string, label: string, kind: string, fetches: int, last_at: Carbon}>, crawlers_since: Carbon|null}
|
||||
*/
|
||||
public function releaseSummary(PressRelease $pressRelease): array
|
||||
{
|
||||
|
|
@ -147,7 +153,6 @@ class VisibilityStats
|
|||
->sortDesc();
|
||||
|
||||
$links = $pressRelease->isVisibleInArchive() ? $pressRelease->publicPortalLinks() : [];
|
||||
$firstUrl = $links[0]['url'] ?? null;
|
||||
|
||||
return [
|
||||
'hits' => (int) $pressRelease->hits,
|
||||
|
|
@ -159,10 +164,44 @@ class VisibilityStats
|
|||
// Moment der Veröffentlichung. Das ist eine Aussage über uns, nicht
|
||||
// über Google.
|
||||
'in_sitemap_since' => $links !== [] && $pressRelease->published_at !== null ? Carbon::instance($pressRelease->published_at) : null,
|
||||
'search_url' => $firstUrl !== null ? 'https://www.google.com/search?q='.rawurlencode('site:'.preg_replace('#^https?://#', '', $firstUrl)) : null,
|
||||
'crawlers' => $this->crawlerFetches($pressRelease),
|
||||
'crawlers_since' => $this->crawlerHitsSince(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Abrufe durch Such- und KI-Dienste je Meldung (Roadmap 1c.6), zuletzt
|
||||
* abgerufener Dienst zuerst. Belegt nur den Abruf durch den Crawler des
|
||||
* Dienstes — nicht, dass die Meldung dort zitiert oder angezeigt wird.
|
||||
*
|
||||
* @return list<array{service: string, label: string, kind: string, fetches: int, last_at: Carbon}>
|
||||
*/
|
||||
public function crawlerFetches(PressRelease $pressRelease): array
|
||||
{
|
||||
return $pressRelease->crawlerHits()
|
||||
->selectRaw('service, count(*) as fetches, max(created_at) as last_at')
|
||||
->groupBy('service')
|
||||
->toBase()
|
||||
->get()
|
||||
->map(fn (object $row): array => [
|
||||
'service' => (string) $row->service,
|
||||
'label' => CrawlerIdentifier::label((string) $row->service),
|
||||
'kind' => CrawlerIdentifier::kind((string) $row->service),
|
||||
'fetches' => (int) $row->fetches,
|
||||
'last_at' => Carbon::parse((string) $row->last_at),
|
||||
])
|
||||
->sortByDesc(fn (array $row): int => $row['last_at']->getTimestamp())
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function crawlerHitsSince(): ?Carbon
|
||||
{
|
||||
$since = config('crawlers.recorded_since');
|
||||
|
||||
return $since ? Carbon::parse((string) $since) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ab wann es für diese Meldung überhaupt Tageswerte geben kann: der
|
||||
* spätere von Veröffentlichung und Beginn der Ereignisaufzeichnung.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace App\Services\Web;
|
|||
use App\Models\Contact;
|
||||
use App\Models\PressRelease;
|
||||
use App\Scopes\PortalScope;
|
||||
use App\Support\CrawlerIdentifier;
|
||||
use App\Support\WebPortalContext;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
|
|
@ -86,6 +87,15 @@ class ReleaseDetailPageData
|
|||
$releaseId = $release->getKey();
|
||||
defer(static fn () => PressRelease::recordDeferredView($releaseId));
|
||||
|
||||
// Crawler-Abruf-Protokoll (Roadmap 1c.0): nur bekannte Such- und
|
||||
// KI-Dienste, erkannt am User-Agent, ohne IP.
|
||||
$userAgent = (string) request()->userAgent();
|
||||
$crawler = CrawlerIdentifier::identify($userAgent);
|
||||
|
||||
if ($crawler !== null) {
|
||||
defer(static fn () => PressRelease::recordDeferredCrawlerHit($releaseId, $crawler, $userAgent));
|
||||
}
|
||||
|
||||
// Die Antwort zeigt den aktuellen Aufruf bereits an; die atomaren
|
||||
// Schreibzugriffe erfolgen nach dem Senden der Response.
|
||||
$release->hits = ($release->hits ?? 0) + 1;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue