web: Jahresarchiv je Branche, Kategoriebeschreibungen, Portalname im Titel (Roadmap 2.3)
- /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>
This commit is contained in:
parent
b74f881ac8
commit
84ea617107
15 changed files with 793 additions and 13 deletions
150
app/Services/Web/CategoryArchivePageData.php
Normal file
150
app/Services/Web/CategoryArchivePageData.php
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<?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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -293,6 +293,8 @@ class CategoryPageData
|
|||
'topNewsrooms' => $topNewsrooms,
|
||||
'subCategories' => $subCategories,
|
||||
'relatedCategories' => $relatedCategories,
|
||||
// Jahresleiste ins Archiv (2.3) — eine gruppierte Query.
|
||||
'archiveYears' => CategoryArchivePageData::yearsWithCounts($categoryIds, $portalValues, $language),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue