- /de/category/{slug}/archiv/{jahr} (CategoryArchivePageData,
CategoryArchiveController, View kategorie-archiv): 30 Meldungen je
Seite, neueste zuerst, Jahresleiste mit Zahlen; ohne Jahr das juengste
Jahr mit Meldungen. Canonical mit ?page=N auf Folgeseiten, hreflang
nur auf Seite 1, CollectionPage + BreadcrumbList. Gecacht bis Seite 3.
Leere Jahre, unbekannte Kategorien, leere Folgeseiten: 404.
- Branchenseite: Jahresleiste unter dem Feed (archiveYears, eine
gruppierte Query); Sitemap listet alle Jahresarchive.
- categories:seed-descriptions {--dry-run} {--force}: fuellt leere DE/EN
Beschreibungen aus database/seeders/data/category-descriptions.php
(14 Kategorien, sachlich, ohne Werbeaussagen), leert den Seiten-Cache.
- Branchenseite: Titel trug config('app.name') ("– pressekonto") statt
des Portalnamens — jetzt presseecho/businessportal24.
- Protokoll Teil 5 (2.3 mit Befund: 0 von 14 Beschreibungen, Sonstiges =
28 % des Bestands), Roadmap 2.3 abgehakt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
150 lines
5.2 KiB
PHP
150 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Web;
|
|
|
|
use App\Models\Category;
|
|
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\Facades\DB;
|
|
|
|
/**
|
|
* Jahresarchiv einer Branche (Roadmap 2.3): `/category/{slug}/archiv/{jahr}`.
|
|
* Die Branchenseite zeigt nur die jüngsten ~15 Meldungen; das Archiv macht
|
|
* den Bestand (34.000 Meldungen seit 2008) als stabile, verlinkte Seiten
|
|
* erreichbar. Jahr statt fortlaufender Seitenzahl, weil eine Jahresseite
|
|
* ihren Inhalt behält, während `?page=2` bei jeder neuen Meldung verrutscht.
|
|
*/
|
|
class CategoryArchivePageData
|
|
{
|
|
public const PerPage = 30;
|
|
|
|
/** Nur die ersten Seiten je Jahr werden gecacht (siehe NewsroomsPageData). */
|
|
private const MaxCachedPage = 3;
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function build(WebPortalContext $context, string $slug, ?int $year): array
|
|
{
|
|
$page = Paginator::resolveCurrentPage();
|
|
|
|
$build = fn (): array => $this->buildFresh($context, $slug, $year, $page);
|
|
|
|
if ($page > self::MaxCachedPage) {
|
|
return $build();
|
|
}
|
|
|
|
return WebPageCache::remember(
|
|
WebPageCache::key('category-archive', [$context->portal->value, $context->language, $slug, (string) $year, 'page', $page]),
|
|
$build
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Jahre mit Meldungen einer Branche (inkl. Unterkategorien), absteigend
|
|
* — auch für die Jahresleiste auf der Branchenseite.
|
|
*
|
|
* @param list<int> $categoryIds
|
|
* @param list<string> $portalValues
|
|
* @return array<int, int> Jahr => Anzahl
|
|
*/
|
|
public static function yearsWithCounts(array $categoryIds, array $portalValues, string $language): array
|
|
{
|
|
$yearExpression = DB::connection()->getDriverName() === 'sqlite'
|
|
? "CAST(strftime('%Y', published_at) AS INTEGER)"
|
|
: 'YEAR(published_at)';
|
|
|
|
return PressRelease::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->forPortalValues($portalValues)
|
|
->forLanguage($language)
|
|
->visibleInArchive()
|
|
->whereIn('category_id', $categoryIds)
|
|
->selectRaw($yearExpression.' as year, COUNT(*) as releases_count')
|
|
->groupBy('year')
|
|
->orderByDesc('year')
|
|
->pluck('releases_count', 'year')
|
|
->map(fn ($count): int => (int) $count)
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function buildFresh(WebPortalContext $context, string $slug, ?int $year, int $page): array
|
|
{
|
|
$portalValues = $context->portalValues;
|
|
$language = $context->language;
|
|
|
|
$category = Category::query()
|
|
->whereIn('portal', $portalValues)
|
|
->where('is_active', true)
|
|
->whereHas('translations', fn (Builder $query) => $query->where('locale', $language)->where('slug', $slug))
|
|
->with([
|
|
'translations' => fn ($query) => $query->where('locale', $language),
|
|
'parent.translations' => fn ($query) => $query->where('locale', $language),
|
|
])
|
|
->first();
|
|
|
|
if (! $category) {
|
|
abort(404);
|
|
}
|
|
|
|
$categoryIds = Category::query()
|
|
->where('parent_id', $category->id)
|
|
->where('is_active', true)
|
|
->pluck('id')
|
|
->prepend($category->id)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$years = self::yearsWithCounts($categoryIds, $portalValues, $language);
|
|
|
|
if ($years === []) {
|
|
abort(404);
|
|
}
|
|
|
|
// Ohne Jahr: das jüngste Jahr mit Meldungen (nicht das Kalenderjahr —
|
|
// eine Branche ohne Meldung im laufenden Jahr hätte sonst eine leere Seite).
|
|
$year ??= (int) array_key_first($years);
|
|
|
|
if (! isset($years[$year])) {
|
|
abort(404);
|
|
}
|
|
|
|
$releases = PressRelease::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->forPortalValues($portalValues)
|
|
->forLanguage($language)
|
|
->visibleInArchive()
|
|
->whereIn('category_id', $categoryIds)
|
|
->whereBetween('published_at', [
|
|
now()->setDate($year, 1, 1)->startOfDay(),
|
|
now()->setDate($year, 12, 31)->endOfDay(),
|
|
])
|
|
->with([
|
|
'company:id,name,slug',
|
|
'category.translations' => fn ($query) => $query->where('locale', $language),
|
|
])
|
|
->orderByDesc('published_at')
|
|
->paginate(self::PerPage, ['id', 'portal', 'language', 'slug', 'title', 'category_id', 'company_id', 'published_at', 'content_score'], 'page', $page);
|
|
|
|
if ($page > 1 && $releases->isEmpty()) {
|
|
abort(404);
|
|
}
|
|
|
|
return [
|
|
'category' => $category,
|
|
'categoryAlternateSlugs' => $category->translations()->whereIn('locale', ['de', 'en'])->pluck('slug', 'locale')->all(),
|
|
'year' => $year,
|
|
'years' => $years,
|
|
'releases' => $releases,
|
|
'totalReleases' => array_sum($years),
|
|
];
|
|
}
|
|
}
|