performance: Caching, deferred Tracking & externe Timeouts (H7, M12, M13)

- Navigations-Chrome (CategoryNavigation), Suche- und Newsroom-Seitendaten
  300s gecacht; gemeinsamer WebPageCache-Helper (eine TTL/Key-Konvention).
- Cache-Poisoning über ?page= geschlossen (Key an Paginator ausgerichtet,
  kein withQueryString im Cache); Newsrooms cachet nur den ungefilterten
  Fall und die ersten Seiten (kein unbegrenzter Key-Raum).
- Detailseiten schreiben Hits/View-Events deferred nach der Response
  (M12); OpenAI-/Markt-/USt-ID-Aufrufe mit Connect-Timeouts und
  begrenzten Retries; Admin-Memo-Caches konsistent invalidiert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-14 13:49:47 +00:00
parent 5a492926dd
commit 23cbd0bfe2
18 changed files with 419 additions and 39 deletions

View file

@ -6,8 +6,10 @@ use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use App\Scopes\PortalScope;
use App\Support\WebPageCache;
use App\Support\WebPortalContext;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Carbon;
/**
@ -22,6 +24,31 @@ class NewsroomDetailPageData
* @return array<string, mixed>
*/
public function build(WebPortalContext $context, string $slug): array
{
// Seitennummer exakt so auflösen wie der Paginator (Paginator::
// resolveCurrentPage validiert und fällt bei ungültigen Werten auf 1
// zurück). Sonst landet Seite-1-Inhalt unter einem anderen Seiten-Key
// (Cache-Poisoning über z. B. ?page=2.5).
$page = Paginator::resolveCurrentPage();
$cacheKey = WebPageCache::key('newsroom', [
implode('-', $context->portalValues),
$context->language,
$slug,
'page',
$page,
]);
return WebPageCache::remember(
$cacheKey,
fn (): array => $this->buildUncached($context, $slug),
);
}
/**
* @return array<string, mixed>
*/
private function buildUncached(WebPortalContext $context, string $slug): array
{
$portalValues = $context->portalValues;
$language = $context->language;
@ -66,12 +93,14 @@ class NewsroomDetailPageData
$leadRelease = $base()->with($with)->orderByDesc('published_at')->first();
// Nur den Seiten-Parameter an die Pagination-Links hängen (kein
// ->withQueryString()): sonst backt der erste Besucher seine beliebigen
// Query-Parameter (utm_* o. Ä.) in die für alle gecachten Links.
$releases = $base()
->with($with)
->when($leadRelease, fn (Builder $query) => $query->whereKeyNot($leadRelease->getKey()))
->orderByDesc('published_at')
->paginate(10)
->withQueryString();
->paginate(10);
$mostReadReleases = $base()
->orderByDesc('hits')