web/admin/pdf: gemeinsame Bildnachweis-Auflösung, Detailfenster am Artikelbild, Bestätigungsstand in Kunden- und Adminansicht, Wortwahl „automatisierte Prüfung" (Bildkennzeichnung Block D)

- App\Support\ImageCredit: Foto/Quelle-Zeile, KI-Hinweis mit Herkunft, Verantwortlich-Zeile nur mit Protokoll, sichere Links; PDF-Zeilen mit Bestätigungskennung
- Artikelseite: Overlay (eigenes i-Zeichen, Alpine ohne Plugin, Bottom-Sheet auf Mobil) mit Nachweis, Lizenz, Quelle, KI-Hinweis, Verantwortlich-Zeile, „Bild melden"
- Richtlinien 0.2: Prüfung als automatisiert beschrieben, Abschnitt „Bilder" (Decision-Update 3.5); Taglines, Suche, Newsrooms, Startseiten-CTA, Mails ohne „qualitätsgeprüft"
- Cache-Invalidierung bei Bildwechsel an veröffentlichten Meldungen; PHPStan-Baseline für das Resource bereinigt

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-22 09:44:34 +00:00
parent 24184f1dec
commit 9f44546082
26 changed files with 578 additions and 199 deletions

View file

@ -108,7 +108,7 @@
<a href="{{ $presseechoUrl }}" style="font-weight: bold; text-decoration: none;"><span style="color: #1B2417;">presse</span><span style="color: #345636;">echo</span></a>
und
<a href="{{ $businessportalUrl }}" style="font-weight: bold; text-decoration: none;"><span style="color: #232A33;">businessportal</span><span style="color: #C84A1E;">24</span></a>
— ein Konto, qualitätsgeprüft, auf beiden Portalen veröffentlichen.
— ein Konto, auf beiden Portalen veröffentlichen.
</td>
</tr>
</table>

View file

@ -8,7 +8,7 @@
'name' => 'businessportal',
'accent' => '24',
'tagline_short' => 'Pressemitteilungen · DACH',
'tagline_long' => 'Veröffentlichungs-Portal für qualitätsgeprüfte Pressemitteilungen aus Deutschland, Österreich und der Schweiz.',
'tagline_long' => 'Veröffentlichungs-Portal für Pressemitteilungen aus Deutschland, Österreich und der Schweiz.',
'footer_legal' => '© :year businessportal24 · Alle Rechte vorbehalten',
'about_label' => 'Über businessportal24',
]);

View file

@ -1,7 +1,7 @@
<x-emails.layout title="Ihre Pressemitteilung wurde veröffentlicht" preheader="Ihre Pressemitteilung wurde geprüft und veröffentlicht.">
<x-emails.layout title="Ihre Pressemitteilung wurde veröffentlicht" preheader="Ihre Pressemitteilung hat die automatisierte Prüfung bestanden und ist veröffentlicht.">
<p>{{ filled($recipientName) ? 'Hallo '.$recipientName.',' : 'Guten Tag,' }}</p>
<p>Ihre Pressemitteilung wurde geprüft und erfolgreich veröffentlicht.</p>
<p>Ihre Pressemitteilung hat unsere automatisierte Prüfung bestanden und ist jetzt veröffentlicht.</p>
<p class="title">{{ $pressRelease->title }}</p>

View file

@ -213,14 +213,25 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
@php
$adminTitleImage = $pr->images->sortByDesc('is_preview')->first();
@endphp
@if ($adminTitleImage && ($adminTitleImage->copyright || $adminTitleImage->is_ai_generated || $adminTitleImage->ai_declaration?->publicLabel()))
{{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act) --}}
@if ($adminTitleImage && ($adminCreditInfo = \App\Support\ImageCredit::for($adminTitleImage, $pr->company))->hasDetails())
{{-- Bildnachweis, KI-Kennzeichnung, Herkunft und Bestätigungsstand
(Bildkennzeichnung D.1/D.3) --}}
<div class="flex flex-wrap items-center gap-2 border-t border-[color:var(--color-bg-rule)] px-5 py-2.5 text-[12px] text-[color:var(--color-ink-3)]">
@if ($aiImageBadge = $adminTitleImage->ai_declaration?->publicLabel() ?? ($adminTitleImage->is_ai_generated ? __('KI-generiert') : null))
<span class="badge hub">{{ $aiImageBadge }}</span>
@if ($adminCreditInfo->aiLabel)
<span class="badge hub">{{ $adminCreditInfo->aiLabel }}</span>
@if ($adminCreditInfo->aiSourceLabel)
<span class="text-[color:var(--color-ink-4)]">({{ $adminCreditInfo->aiSourceLabel }})</span>
@endif
@endif
@if ($adminTitleImage->copyright)
<span>{{ $adminTitleImage->copyright }}</span>
@if ($adminCreditInfo->credit)
<span>{{ $adminCreditInfo->creditLabel }}: {{ $adminCreditInfo->credit }}</span>
@endif
@if ($adminCreditInfo->confirmation)
<span class="text-[color:var(--color-ink-4)]">· {{ __('Rechte bestätigt am :date (:key, AGB :agb, :channel)', ['date' => $adminCreditInfo->confirmation->confirmed_at->format('d.m.Y H:i'), 'key' => $adminCreditInfo->confirmation->legal_text_key, 'agb' => $adminCreditInfo->confirmation->agb_version ?? '–', 'channel' => $adminCreditInfo->confirmation->channel->value]) }}</span>
@elseif ($adminTitleImage->awaitsCustomerConfirmation())
<span class="badge">{{ __('wartet auf Kundenbestätigung') }}</span>
@elseif ($adminCreditInfo->isLegacy)
<span class="text-[color:var(--color-ink-4)]">· {{ __('Bestand ohne Bestätigungsprotokoll') }}</span>
@endif
</div>
@endif

View file

@ -1748,15 +1748,20 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
<flux:icon.photo variant="micro" class="size-3.5" />
<span>{{ __('Platzhalter-Titelbild — laden Sie im Editor ein eigenes Bild hoch.') }}</span>
</div>
@elseif ($titleImage && ($titleImage->copyright || $titleImage->is_ai_generated || $titleImage->ai_declaration?->publicLabel()))
{{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act): kurzes
Badge („KI-generiert" / „KI-bearbeitet"), kein Satz-Wortlaut. --}}
@elseif ($titleImage && ($imageCreditInfo = \App\Support\ImageCredit::for($titleImage, $pr->company))->hasDetails())
{{-- Bildnachweis, KI-Kennzeichnung und Bestätigungsstand aus der
gemeinsamen Auflösung (Bildkennzeichnung D.1/D.3). --}}
<div class="flex flex-wrap items-center gap-2 border-t border-[color:var(--color-bg-rule)] px-5 py-2.5 text-[12px] text-[color:var(--color-ink-3)]">
@if ($aiImageBadge = $titleImage->ai_declaration?->publicLabel() ?? ($titleImage->is_ai_generated ? __('KI-generiert') : null))
<span class="badge hub">{{ $aiImageBadge }}</span>
@if ($imageCreditInfo->aiLabel)
<span class="badge hub">{{ $imageCreditInfo->aiLabel }}</span>
@endif
@if ($titleImage->copyright)
<span>{{ $titleImage->copyright }}</span>
@if ($imageCreditInfo->credit)
<span>{{ $imageCreditInfo->creditLabel }}: {{ $imageCreditInfo->credit }}</span>
@endif
@if ($imageCreditInfo->confirmation)
<span class="text-[color:var(--color-ink-4)]">· {{ __('Rechte bestätigt am :date (:key, AGB :agb, :channel)', ['date' => $imageCreditInfo->confirmation->confirmed_at->format('d.m.Y H:i'), 'key' => $imageCreditInfo->confirmation->legal_text_key, 'agb' => $imageCreditInfo->confirmation->agb_version ?? '–', 'channel' => $imageCreditInfo->confirmation->channel->value]) }}</span>
@elseif ($titleImage->awaitsCustomerConfirmation())
<span class="badge">{{ __('wartet auf Ihre Bestätigung') }}</span>
@endif
</div>
@endif

View file

@ -40,7 +40,7 @@
? ($brand['meta_title'] ?? 'Aktuelle Pressemitteilungen aus Wirtschaft & Industrie – presseecho')
: __('web.meta.home_title', ['brand' => ($brand['name'] ?? 'presseecho').($brand['accent'] ?? '')]))
@section('meta_description', app()->getLocale() === 'de'
? ($brand['meta_description'] ?? 'Qualitätsgeprüfte Pressemitteilungen aus Wirtschaft und Industrie im DACH-Raum.')
? ($brand['meta_description'] ?? 'Pressemitteilungen aus Wirtschaft und Industrie im DACH-Raum — automatisiert geprüft, strukturiert distribuiert.')
: __('web.meta.home_description'))
@section('hreflang')

View file

@ -3,6 +3,7 @@
@php
use App\Enums\Portal;
use App\Services\PressRelease\CanonicalAssigner;
use App\Support\ImageCredit;
use App\Support\StructuredData;
use Illuminate\Support\Str;
@ -47,10 +48,13 @@
$signMedia = $isPreview ?? false;
$imageUrl = $image?->variantUrl('detail', $signMedia) ?? $image?->variantUrl('card', $signMedia) ?? $image?->url($signMedia);
$imageCaption = $image?->description ?: $image?->title;
$imageCredit = $image?->copyright ?: $image?->author;
// KI-Bildkennzeichnung (Art. 50 KI-VO): Badge direkt am Bild
// („KI-generiert" / „KI-bearbeitet"), null bei Originalfotos.
$imageAiBadge = $image?->ai_declaration?->publicLabel();
// Bildnachweis, KI-Kennzeichnung und Verantwortlich-Zeile aus einer
// Auflösung (Bildkennzeichnung D.1, Decision-Update 3.4). Die sichtbare
// Zeile unter dem Bild ist die Nennung, das Overlay die Erklärung.
$imageCreditInfo = $image ? ImageCredit::for($image, $company) : null;
$imageCredit = $imageCreditInfo?->credit;
$imageAiBadge = $imageCreditInfo?->aiLabel;
$imageReportUrl = route('melden', ['edition' => app()->getLocale()]);
// Ohne Pressefoto: die im User-Panel gewählte Platzhalter-Variante.
// Null bei Altbeständen ohne Variante — dann generischer Hero-Fallback.
@ -417,10 +421,65 @@
{{ $imageAiBadge }}
</span>
@endif
@if ($imageCreditInfo?->hasDetails())
{{-- Detailfenster zum Bildnachweis (Bildkennzeichnung D.2, E-B13):
eigenes „i"-Zeichen, kein „cr"-Logo. Alpine ohne Plugin: Fokus
wandert in den Dialog und beim Schließen zurück; Escape schließt.
Auf Mobil ein Bottom-Sheet, ab sm ein Popover am Icon. --}}
<div x-data="{ open: false, show() { this.open = true; this.$nextTick(() => this.$refs.close.focus()) }, hide() { this.open = false; this.$nextTick(() => this.$refs.trigger.focus()) } }"
@keydown.escape.window="if (open) hide()"
class="absolute bottom-3 left-3 z-10" data-image-credit>
<button type="button" x-ref="trigger" @click="open ? hide() : show()" :aria-expanded="open ? 'true' : 'false'"
aria-haspopup="dialog" aria-controls="image-credit-dialog"
class="flex size-7 items-center justify-center rounded-full bg-black/60 text-white ring-1 ring-white/50 backdrop-blur-[2px] hover:bg-black/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
title="{{ __('web.detail.image_credit_open') }}">
<span class="sr-only">{{ __('web.detail.image_credit_open') }}</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="9.5" stroke="currentColor" stroke-width="1.6" />
<path d="M12 10.5v6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
<circle cx="12" cy="7.6" r="1.15" fill="currentColor" />
</svg>
</button>
<div x-show="open" x-cloak @click.outside="hide()" id="image-credit-dialog" role="dialog" aria-modal="true"
aria-labelledby="image-credit-title"
class="fixed inset-x-0 bottom-0 z-40 max-h-[80vh] overflow-y-auto border-t border-bg-rule-strong bg-bg p-5 text-[13px] text-ink shadow-2xl sm:absolute sm:inset-auto sm:bottom-10 sm:left-0 sm:w-[22rem] sm:border sm:shadow-lg">
<div class="flex items-start justify-between gap-3">
<h2 id="image-credit-title" class="m-0 text-[11px] font-semibold uppercase tracking-[0.14em] text-ink-3">{{ __('web.detail.image_credit_title') }}</h2>
<button type="button" x-ref="close" @click="hide()" class="text-[12px] text-ink-3 underline-offset-2 hover:text-ink hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ink">
{{ __('web.detail.image_credit_close') }}
</button>
</div>
<ul class="m-0 mt-3 list-none space-y-2 p-0 leading-[1.5]">
@if ($imageCreditInfo->credit)
<li>{{ $imageCreditInfo->creditLabel }}: <span class="font-medium">{{ $imageCreditInfo->credit }}</span></li>
@endif
@if ($imageCreditInfo->licenseUrl)
<li>{{ __('web.detail.image_license') }}: <a href="{{ $imageCreditInfo->licenseUrl }}" rel="nofollow noopener" target="_blank" class="underline underline-offset-2">{{ \Illuminate\Support\Str::limit($imageCreditInfo->licenseUrl, 60) }}</a></li>
@endif
@if ($imageCreditInfo->sourceUrl)
<li>{{ __('web.detail.image_source_link') }}: <a href="{{ $imageCreditInfo->sourceUrl }}" rel="nofollow noopener" target="_blank" class="underline underline-offset-2">{{ \Illuminate\Support\Str::limit($imageCreditInfo->sourceUrl, 60) }}</a></li>
@endif
@if ($imageCreditInfo->aiLabel)
<li>
{{ $image->ai_declaration?->value === 'ai_edited' ? __('web.detail.image_ai_hint_edited') : __('web.detail.image_ai_hint_generated') }}
@if ($imageCreditInfo->aiSourceLabel)
<span class="block text-ink-3">{{ __('web.detail.image_ai_source', ['source' => $imageCreditInfo->aiSourceLabel]) }}</span>
@endif
</li>
@endif
@if ($imageCreditInfo->responsibleLine)
<li class="text-ink-3">{{ $imageCreditInfo->responsibleLine }}</li>
@endif
</ul>
<a href="{{ $imageReportUrl }}" class="mt-4 inline-block text-[12px] text-ink-3 underline underline-offset-2 hover:text-ink">{{ __('web.detail.image_report') }}</a>
</div>
</div>
@endif
</div>
@if ($imageCaption || $imageCredit)
<div class="text-[11.5px] text-ink-3 mt-2.5 italic max-w-[720px]">
{{ $imageCaption }}@if ($imageCaption && $imageCredit) · @endif@if ($imageCredit)<span class="not-italic">{{ __('web.detail.photo') }}: {{ $imageCredit }}</span>@endif
@if ($imageCaption || $imageCreditInfo?->line())
<div class="text-[11.5px] text-ink-3 mt-2.5 italic max-w-[720px]" data-image-credit-line>
{{ $imageCaption }}@if ($imageCaption && $imageCreditInfo?->line()) · @endif@if ($imageCreditInfo?->line())<span class="not-italic">{{ $imageCreditInfo->line() }}</span>@endif
</div>
@endif
</section>

View file

@ -33,9 +33,11 @@
<p>
Diese Richtlinien gelten für alle Pressemitteilungen auf presseecho.de und
businessportal24.com. Sie sind Bestandteil unserer <a href="{{ route('agb') }}">AGB</a>.
Unser Ziel ist einfach: Leser sollen sich darauf verlassen können, dass hier echte
Unternehmensnachrichten stehen — geprüft, den Richtlinien entsprechend und klar
gekennzeichnet.
Jede Mitteilung durchläuft vor der Veröffentlichung eine automatisierte Prüfung. Sie
sortiert Spam, reine Werbung und Beiträge aus, die keine Pressemitteilung sind, ordnet
die Kategorie zu und bewertet die formale Qualität des Textes. Eine Prüfung, ob Inhalte
richtig sind oder ob Sie die Rechte an Texten und Bildern besitzen, findet nicht statt.
Dafür sind Sie verantwortlich.
</p>
<h2>So funktioniert die Prüfung</h2>
@ -64,6 +66,19 @@
<li><strong>Passende Kategorie:</strong> Wählen Sie die Rubrik, die inhaltlich passt — nicht die mit der meisten Aufmerksamkeit.</li>
</ul>
<h2>Bilder</h2>
<p>
Zu jedem Bild gehört ein Bildnachweis; er erscheint öffentlich unter dem Bild. Laden Sie
nur Bilder hoch, für die Sie die Rechte haben, und achten Sie darauf, dass abgebildete
Personen mit der Veröffentlichung einverstanden sind. Bilder, die mit KI erzeugt oder
bearbeitet wurden, kennzeichnen Sie beim Hochladen; enthält die Datei signierte
Herkunftsdaten dazu, prüfen wir sie und zeigen die Kennzeichnung am Bild. Halten Sie eine
von uns gesetzte Kennzeichnung für falsch, schreiben Sie uns an
<a href="mailto:{{ $contactEmail }}">{{ $contactEmail }}</a>; wir prüfen das und
korrigieren Fehler. Bilder ohne geklärte Rechte entfernen wir; das
<a href="{{ route('melden') }}">Meldeverfahren</a> gilt auch für Bilder.
</p>
<h2>Ablehnungskategorien</h2>
</div>