PM-Detailseite: Direktlinks zur öffentlichen Seite je Portal

Veröffentlichte PMs zeigten im Backend nur den Status-Badge, ohne Weg
zur öffentlichen Seite. Im Seitenkopf stehen jetzt Direktlinks — bei
Portal::Both einer je Portal (QA-Befund 07.07.).

Die URL-Logik ist als PressRelease::publicPortalLinks() zentralisiert
(vorher doppelt in der Published-Mail und fehlerhaft im Nachweis-PDF:
dort fehlte das /de/press-release/-Segment, die dokumentierte URL lief
ins Leere; bei Both nennt der Nachweis jetzt beide URLs). Edition folgt
der PM-Sprache statt hart „de".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-07 15:00:40 +00:00
parent 930760a056
commit 28de92e18b
6 changed files with 128 additions and 13 deletions

View file

@ -16,6 +16,14 @@ class PressReleasePublished extends Mailable
public string $showUrl;
/**
* Öffentliche Detailseiten je Portal, auf dem die PM erschienen ist
* (bei Portal::Both beide Spokes).
*
* @var list<array{label: string, url: string}>
*/
public array $portalLinks;
public function __construct(
public readonly User $user,
public readonly PressRelease $pressRelease,
@ -23,6 +31,8 @@ class PressReleasePublished extends Mailable
$this->showUrl = $user->canAccessAdmin()
? route('admin.press-releases.show', $pressRelease->id)
: route('me.press-releases.show', $pressRelease->id);
$this->portalLinks = $pressRelease->publicPortalLinks();
}
public function envelope(): Envelope

View file

@ -7,6 +7,7 @@ use App\Enums\PressReleaseClassification;
use App\Enums\PressReleaseContentTier;
use App\Enums\PressReleasePlaceholder;
use App\Enums\PressReleaseStatus;
use App\Http\Middleware\SetEdition;
use App\Models\Concerns\HasUniqueSlug;
use App\Scopes\PortalScope;
use App\Services\PressRelease\PressReleaseHtmlSanitizer;
@ -165,6 +166,44 @@ class PressRelease extends Model
return $this->hasMany(PressReleaseStatusLog::class)->orderByDesc('created_at');
}
/**
* Öffentliche Detailseiten je Portal, auf dem die PM erscheint
* (bei Portal::Both beide Spokes). Zentrale Quelle für Mail,
* Nachweis-PDF und Detailansicht.
*
* @return list<array{portal: Portal, label: string, url: string}>
*/
public function publicPortalLinks(): array
{
if (! $this->slug) {
return [];
}
$portals = match ($this->portal) {
Portal::Both => [Portal::Presseecho, Portal::Businessportal24],
null => [],
default => [$this->portal],
};
$edition = in_array($this->language, SetEdition::EDITIONS, true)
? $this->language
: 'de';
return array_map(fn (Portal $portal): array => [
'portal' => $portal,
'label' => $portal->label(),
'url' => sprintf(
'%s/%s/press-release/%s',
rtrim((string) match ($portal) {
Portal::Presseecho => config('domains.domain_presseecho_url'),
default => config('domains.domain_businessportal_url'),
}, '/'),
$edition,
$this->slug,
),
], $portals);
}
public function kiAudits(): HasMany
{
return $this->hasMany(KiAudit::class)->orderByDesc('created_at');

View file

@ -90,25 +90,21 @@ class ProofPdfRenderer
*/
private function metaRows(PressRelease $pressRelease, string $portalLabel, string $dateLabel): array
{
// Bei Portal::Both dokumentiert der Nachweis beide öffentlichen URLs.
$links = $pressRelease->publicPortalLinks();
$urlRows = array_map(static fn (array $link): array => [
count($links) > 1 ? 'URL '.$link['label'] : 'URL',
$link['url'],
], $links);
return array_values(array_filter([
['Portal', $portalLabel],
['Veröffentlicht', $dateLabel],
['URL', $this->publicUrl($pressRelease)],
...$urlRows,
$pressRelease->category?->name ? ['Kategorie', (string) $pressRelease->category->name] : null,
]));
}
private function publicUrl(PressRelease $pressRelease): string
{
$base = match ($pressRelease->portal) {
Portal::Presseecho => config('domains.domain_presseecho_url'),
Portal::Businessportal24 => config('domains.domain_businessportal_url'),
default => config('app.url'),
};
return rtrim((string) $base, '/').'/'.ltrim((string) $pressRelease->slug, '/');
}
private function previewText(PressRelease $pressRelease): string
{
$plain = trim(html_entity_decode(strip_tags((string) $pressRelease->text)));

View file

@ -209,6 +209,9 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
return [
'pr' => $pr,
'publicLinks' => $pr->status === PressReleaseStatus::Published
? $pr->publicPortalLinks()
: [],
'creditBalance' => $user->creditBalance(),
'canBoost' => $boostService->canBoost($pr),
'isBoosted' => $boostService->isBoosted($pr),
@ -353,7 +356,17 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<div class="flex items-center gap-2 flex-shrink-0 flex-wrap justify-end">
{{-- Direktlinks zur öffentlichen Seite (QA-Befund 07.07.):
bei Portal::Both je Portal ein eigener Button. --}}
@foreach ($publicLinks as $link)
<flux:button variant="primary" icon="arrow-top-right-on-square"
href="{{ $link['url'] }}" target="_blank" rel="noopener">
{{ count($publicLinks) > 1
? __('Auf :portal ansehen', ['portal' => $link['label']])
: __('Öffentlich ansehen') }}
</flux:button>
@endforeach
@if ($canEdit)
<flux:button variant="filled" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
{{ __('Bearbeiten') }}

View file

@ -1,5 +1,6 @@
<?php
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Models\Category;
use App\Models\Company;
@ -127,6 +128,48 @@ test('customer show zeigt Boilerplate-Override nicht wenn leer', function () {
->assertDontSee('Eigener Abbinder (Boilerplate)');
});
test('customer show links to the public page of a published release', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = makeCustomerForShowPhase8a([
'status' => PressReleaseStatus::Published,
'published_at' => now()->subHour(),
'slug' => 'oeffentliche-pm',
'language' => 'de',
]);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Öffentlich ansehen')
->assertSee(config('domains.domain_presseecho_url').'/de/press-release/oeffentliche-pm', escape: false);
});
test('customer show links to both portals when the release is published on both', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = makeCustomerForShowPhase8a([
'status' => PressReleaseStatus::Published,
'published_at' => now()->subHour(),
'portal' => Portal::Both,
'slug' => 'beide-portale-pm',
'language' => 'de',
]);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Auf Presseecho ansehen')
->assertSee('Auf Businessportal24 ansehen')
->assertSee(config('domains.domain_presseecho_url').'/de/press-release/beide-portale-pm', escape: false)
->assertSee(config('domains.domain_businessportal_url').'/de/press-release/beide-portale-pm', escape: false);
});
test('customer show shows no public link for drafts', function () {
/** @var TestCase $this */
['customer' => $customer, 'pr' => $pr] = makeCustomerForShowPhase8a();
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertDontSee('Öffentlich ansehen');
});
test('admin show zeigt den Untertitel direkt unter dem Titel', function () {
/** @var TestCase $this */
$admin = makeAdminForShowPhase8a();

View file

@ -1,5 +1,6 @@
<?php
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Enums\SinglePurchaseStatus;
use App\Enums\SinglePurchaseType;
@ -87,6 +88,19 @@ test('the rendered proof is a non-trivial pdf document', function () {
expect(strlen($pdf))->toBeGreaterThan(800);
});
test('the rendered proof documents the real public url of the release', function () {
$pr = PressRelease::factory()->published()->create([
'portal' => Portal::Presseecho,
'slug' => 'nachweis-pm',
'language' => 'de',
]);
$pdf = app(ProofPdfRenderer::class)->render($pr);
// Vorher fehlte das /de/press-release/-Segment — die URL im Nachweis lief ins Leere.
expect($pdf)->toContain(config('domains.domain_presseecho_url').'/de/press-release/nachweis-pm');
});
test('the proof download requires a prior purchase', function () {
$user = customerWithRole();
$pr = PressRelease::factory()->published()->create(['user_id' => $user->id]);