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:
parent
65ac72ce05
commit
2124ffd1f2
26 changed files with 578 additions and 199 deletions
|
|
@ -21,6 +21,7 @@ use Illuminate\Http\Request;
|
|||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
use RuntimeException;
|
||||
|
||||
class PressReleaseImageController extends Controller
|
||||
|
|
@ -148,9 +149,15 @@ class PressReleaseImageController extends Controller
|
|||
noCrop: (bool) ($validated['no_crop'] ?? false),
|
||||
);
|
||||
|
||||
// Token-ID fürs Protokoll über den Bearer-Header auflösen: Sanctums
|
||||
// currentAccessToken() ist im Test ein TransientToken ohne Datensatz;
|
||||
// ohne Bearer bleibt die ID leer, der Kontoinhaber steht trotzdem drin.
|
||||
$bearer = $request->bearerToken();
|
||||
$token = $bearer !== null ? PersonalAccessToken::findToken($bearer) : null;
|
||||
|
||||
$actor = ImageUploadActor::api(
|
||||
$request->user(),
|
||||
$request->user()->currentAccessToken()?->getKey(),
|
||||
$token !== null ? (int) $token->getKey() : null,
|
||||
$request->ip(),
|
||||
$request->userAgent(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class StorePressReleaseImageRequest extends FormRequest
|
|||
if ($current === null || $given !== $current->key) {
|
||||
$validator->errors()->add(
|
||||
'confirm_text_version',
|
||||
sprintf('The confirmation text version is not current. Current version: %s.', $current?->key ?? 'none'),
|
||||
sprintf('The confirmation text version is not current. Current version: %s.', $current !== null ? $current->key : 'none'),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@
|
|||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\PressReleaseImage;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin PressReleaseImage
|
||||
*/
|
||||
class PressReleaseImageResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
|
|
@ -51,8 +55,8 @@ class PressReleaseImageResource extends JsonResource
|
|||
'rights_confirmation' => $this->whenLoaded('latestRightsConfirmation', fn () => $this->latestRightsConfirmation === null ? null : [
|
||||
'version' => $this->latestRightsConfirmation->legal_text_key,
|
||||
'agb_version' => $this->latestRightsConfirmation->agb_version,
|
||||
'confirmed_at' => $this->latestRightsConfirmation->confirmed_at?->toIso8601String(),
|
||||
'channel' => $this->latestRightsConfirmation->channel?->value,
|
||||
'confirmed_at' => $this->latestRightsConfirmation->confirmed_at->toIso8601String(),
|
||||
'channel' => $this->latestRightsConfirmation->channel->value,
|
||||
]),
|
||||
'warnings' => $this->warnings(),
|
||||
'is_preview' => $this->is_preview,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
/**
|
||||
|
|
@ -23,6 +24,55 @@ use Illuminate\Support\Facades\URL;
|
|||
* sagt, woher die Portalkennzeichnung stammt. `rules_version` NULL markiert
|
||||
* den Bestand vor der Umstellung; solche Bilder haben kein Beweisoriginal
|
||||
* und kein Bestätigungsprotokoll.
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $press_release_id
|
||||
* @property string $disk
|
||||
* @property string $path
|
||||
* @property array<string, mixed>|null $variants
|
||||
* @property string|null $title
|
||||
* @property string|null $description
|
||||
* @property string|null $copyright
|
||||
* @property string|null $author
|
||||
* @property ImageLicenseType|null $license_type
|
||||
* @property string|null $license_detail
|
||||
* @property string|null $license_url
|
||||
* @property string|null $source_url
|
||||
* @property bool $persons_consent
|
||||
* @property string|null $people_rights_status
|
||||
* @property string|null $property_rights_status
|
||||
* @property string|null $rights_notes
|
||||
* @property Carbon|null $rights_confirmed_at
|
||||
* @property bool $is_ai_generated
|
||||
* @property AiImageDeclaration|null $ai_declaration
|
||||
* @property AiImageDeclaration|null $ai_declaration_user
|
||||
* @property AiDeclarationSource|null $ai_declaration_source
|
||||
* @property bool $ai_declaration_locked
|
||||
* @property CreditSource|null $credit_source
|
||||
* @property ImageProvenanceStatus|null $provenance_status
|
||||
* @property array<string, mixed>|null $provenance
|
||||
* @property Carbon|null $provenance_checked_at
|
||||
* @property string|null $provenance_tool_version
|
||||
* @property string|null $original_disk
|
||||
* @property string|null $original_path
|
||||
* @property string|null $original_sha256
|
||||
* @property string|null $published_snapshot_path
|
||||
* @property string|null $published_snapshot_sha256
|
||||
* @property Carbon|null $published_ever_at
|
||||
* @property Carbon|null $customer_confirmation_requested_at
|
||||
* @property string|null $rules_version
|
||||
* @property int|null $focus_x
|
||||
* @property int|null $focus_y
|
||||
* @property bool $no_crop
|
||||
* @property bool $is_preview
|
||||
* @property int $sort_order
|
||||
* @property int|null $width
|
||||
* @property int|null $height
|
||||
* @property string|null $mime
|
||||
* @property string|null $legacy_portal
|
||||
* @property int|null $legacy_id
|
||||
* @property-read PressRelease|null $pressRelease
|
||||
* @property-read ImageRightsConfirmation|null $latestRightsConfirmation
|
||||
*/
|
||||
class PressReleaseImage extends Model
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace App\Services\PressRelease\Images;
|
|||
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Services\Image\ImageService;
|
||||
use App\Support\WebCaches;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ class PressReleaseImageRemover
|
|||
|
||||
if ($image->wasEverPublished() || $image->isLegacyRules()) {
|
||||
$image->delete();
|
||||
WebCaches::forgetPublicPages();
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use App\Services\Image\ImageProvenanceInspector;
|
|||
use App\Services\Image\ImageRightsTextRenderer;
|
||||
use App\Services\Image\ImageService;
|
||||
use App\Services\Image\ProvenanceResult;
|
||||
use App\Support\WebCaches;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
|
@ -115,7 +116,7 @@ class PressReleaseImageUploader
|
|||
noCrop: $submission->noCrop,
|
||||
);
|
||||
|
||||
return DB::transaction(function () use ($pressRelease, $submission, $actor, $legalText, $stored, $provenance, $sha256, $originalsDisk, $originalPath, $agbUrl): PressReleaseImage {
|
||||
$image = DB::transaction(function () use ($pressRelease, $submission, $actor, $legalText, $stored, $provenance, $sha256, $originalsDisk, $originalPath, $agbUrl): PressReleaseImage {
|
||||
$locked = PressRelease::query()
|
||||
->withoutGlobalScope(PortalScope::class)
|
||||
->whereKey($pressRelease->getKey())
|
||||
|
|
@ -169,6 +170,14 @@ class PressReleaseImageUploader
|
|||
|
||||
return $image;
|
||||
});
|
||||
|
||||
// Öffentliche Seiten-Caches nur verwerfen, wenn die PM schon
|
||||
// öffentlich ist (Admin-Bildtausch, D.5) — Entwürfe sind nicht sichtbar.
|
||||
if ($pressRelease->published_at !== null) {
|
||||
WebCaches::forgetPublicPages();
|
||||
}
|
||||
|
||||
return $image;
|
||||
} catch (Throwable $exception) {
|
||||
// Kein Rollback für Dateien: Was geschrieben wurde, wird
|
||||
// ausdrücklich wieder entfernt (Feedback F09).
|
||||
|
|
@ -202,12 +211,18 @@ class PressReleaseImageUploader
|
|||
throw new ImageUploadException(__('Dieses Bild kann nicht nachträglich bestätigt werden.'), 'rights_confirmed');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($image, $actor, $legalText, $agbUrl): ImageRightsConfirmation {
|
||||
$confirmation = DB::transaction(function () use ($image, $actor, $legalText, $agbUrl): ImageRightsConfirmation {
|
||||
$confirmation = $this->recordConfirmation($image, $image->pressRelease, $actor, $legalText, $agbUrl);
|
||||
$image->forceFill(['rights_confirmed_at' => now()])->save();
|
||||
|
||||
return $confirmation;
|
||||
});
|
||||
|
||||
if ($image->pressRelease?->published_at !== null) {
|
||||
WebCaches::forgetPublicPages();
|
||||
}
|
||||
|
||||
return $confirmation;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
namespace App\Services\PressRelease;
|
||||
|
||||
use App\Enums\AiImageDeclaration;
|
||||
use App\Enums\ImageLicenseType;
|
||||
use App\Enums\Portal;
|
||||
use App\Models\Company;
|
||||
use App\Models\Contact;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Support\ImageCredit;
|
||||
use GdImage;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
|
@ -313,36 +313,21 @@ class ProofPdfRenderer
|
|||
*/
|
||||
private function imageCreditLines(PressReleaseImage $image): array
|
||||
{
|
||||
$lines = [];
|
||||
// Gemeinsame Auflösung (Bildkennzeichnung D.1/D.4): Nachweis, Lizenz,
|
||||
// Quelle, KI-Kennzeichnung mit Herkunft, Bestätigung mit Kennung.
|
||||
$lines = ImageCredit::for($image, $image->pressRelease?->company)->pdfLines();
|
||||
|
||||
if (filled($image->copyright)) {
|
||||
$lines[] = 'Bildnachweis: '.$image->copyright;
|
||||
}
|
||||
// Bestandsbelege: die Felder des alten Formulars bleiben lesbar.
|
||||
if ($image->isLegacyRules()) {
|
||||
if (filled($image->author)
|
||||
&& (blank($image->copyright) || ! str_contains((string) $image->copyright, (string) $image->author))) {
|
||||
$lines[] = 'Urheber: '.$image->author;
|
||||
}
|
||||
|
||||
if (filled($image->author)
|
||||
&& (blank($image->copyright) || ! str_contains((string) $image->copyright, (string) $image->author))) {
|
||||
$lines[] = 'Urheber: '.$image->author;
|
||||
}
|
||||
|
||||
if ($image->license_type instanceof ImageLicenseType) {
|
||||
$detail = filled($image->license_detail) ? ' ('.$image->license_detail.')' : '';
|
||||
$lines[] = 'Lizenz: '.$image->license_type->label().$detail;
|
||||
}
|
||||
|
||||
if (filled($image->license_url)) {
|
||||
$lines[] = 'Lizenz-URL: '.$image->license_url;
|
||||
}
|
||||
|
||||
if (filled($image->source_url)) {
|
||||
$lines[] = 'Quelle: '.$image->source_url;
|
||||
}
|
||||
|
||||
$aiLabel = $image->ai_declaration instanceof AiImageDeclaration
|
||||
? $image->ai_declaration->publicLabel()
|
||||
: null;
|
||||
|
||||
if ($aiLabel !== null || $image->is_ai_generated) {
|
||||
$lines[] = 'KI-Kennzeichnung: '.($aiLabel ?? 'KI-generiert');
|
||||
if ($image->license_type instanceof ImageLicenseType) {
|
||||
$detail = filled($image->license_detail) ? ' ('.$image->license_detail.')' : '';
|
||||
$lines[] = 'Lizenz: '.$image->license_type->label().$detail;
|
||||
}
|
||||
}
|
||||
|
||||
return $lines;
|
||||
|
|
|
|||
159
app/Support/ImageCredit.php
Normal file
159
app/Support/ImageCredit.php
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Enums\AiDeclarationSource;
|
||||
use App\Models\Company;
|
||||
use App\Models\ImageRightsConfirmation;
|
||||
use App\Models\PressReleaseImage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Eine Auflösung des Bildnachweises für alle Ausgaben (Bildkennzeichnung
|
||||
* D.1): Artikelseite, Kunden- und Adminansicht, Beleg-PDF, Overlay.
|
||||
*
|
||||
* Regeln (Decision-Update 3.4):
|
||||
* - Nachweiszeile: „Foto: [copyright]", sonst „Foto: [author]", sonst im
|
||||
* Bestand „Quelle: [Firma]".
|
||||
* - KI-Zusatz aus der Portalkennzeichnung, Bestand-Fallback `is_ai_generated`.
|
||||
* - Verantwortlich-Zeile „Bild und Angaben: [Firma], eingereicht am [Datum]"
|
||||
* NUR mit Bestätigungsprotokoll — der Altbestand hat keins.
|
||||
* - Links nur mit http/https.
|
||||
*/
|
||||
final readonly class ImageCredit
|
||||
{
|
||||
private function __construct(
|
||||
public ?string $credit,
|
||||
public string $creditLabel,
|
||||
public ?string $aiLabel,
|
||||
public ?string $aiSourceLabel,
|
||||
public ?string $licenseUrl,
|
||||
public ?string $sourceUrl,
|
||||
public ?string $responsibleLine,
|
||||
public ?ImageRightsConfirmation $confirmation,
|
||||
public bool $isLegacy,
|
||||
) {}
|
||||
|
||||
public static function for(PressReleaseImage $image, ?Company $company = null): self
|
||||
{
|
||||
$company ??= $image->pressRelease?->company;
|
||||
$companyName = $company?->name;
|
||||
|
||||
$credit = self::clean($image->copyright) ?? self::clean($image->author);
|
||||
$creditLabel = __('web.detail.photo');
|
||||
|
||||
if ($credit === null && $image->isLegacyRules() && filled($companyName)) {
|
||||
$credit = $companyName;
|
||||
$creditLabel = __('web.detail.source');
|
||||
}
|
||||
|
||||
$aiLabel = $image->ai_declaration?->publicLabel() ?? ($image->is_ai_generated ? __('KI-generiert') : null);
|
||||
|
||||
$confirmation = $image->relationLoaded('latestRightsConfirmation')
|
||||
? $image->latestRightsConfirmation
|
||||
: $image->latestRightsConfirmation()->first();
|
||||
|
||||
$responsible = null;
|
||||
|
||||
if ($confirmation !== null && filled($companyName)) {
|
||||
$responsible = __('web.detail.image_responsible', [
|
||||
'company' => $companyName,
|
||||
'date' => $confirmation->confirmed_at->locale('de')->isoFormat('D. MMMM YYYY'),
|
||||
]);
|
||||
}
|
||||
|
||||
return new self(
|
||||
credit: $credit,
|
||||
creditLabel: $creditLabel,
|
||||
aiLabel: $aiLabel,
|
||||
aiSourceLabel: $aiLabel !== null ? $image->ai_declaration_source?->publicLabel() ?? ($image->isLegacyRules() ? null : AiDeclarationSource::Customer->publicLabel()) : null,
|
||||
licenseUrl: self::safeUrl($image->license_url),
|
||||
sourceUrl: self::safeUrl($image->source_url),
|
||||
responsibleLine: $responsible,
|
||||
confirmation: $confirmation,
|
||||
isLegacy: $image->isLegacyRules(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sichtbare Zeile unter dem Bild, z. B. „Foto: Musterfirma · KI-generiert".
|
||||
*/
|
||||
public function line(): ?string
|
||||
{
|
||||
if ($this->credit === null && $this->aiLabel === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
|
||||
if ($this->credit !== null) {
|
||||
$parts[] = $this->creditLabel.': '.$this->credit;
|
||||
}
|
||||
|
||||
if ($this->aiLabel !== null) {
|
||||
$parts[] = $this->aiLabel;
|
||||
}
|
||||
|
||||
return implode(' · ', $parts);
|
||||
}
|
||||
|
||||
public function hasDetails(): bool
|
||||
{
|
||||
return $this->credit !== null || $this->aiLabel !== null || $this->responsibleLine !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeilen für das Beleg-PDF (D.4).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function pdfLines(): array
|
||||
{
|
||||
$lines = [];
|
||||
|
||||
if ($this->credit !== null) {
|
||||
$lines[] = 'Bildnachweis: '.$this->credit;
|
||||
}
|
||||
|
||||
if ($this->licenseUrl !== null) {
|
||||
$lines[] = 'Lizenz: '.$this->licenseUrl;
|
||||
}
|
||||
|
||||
if ($this->sourceUrl !== null) {
|
||||
$lines[] = 'Quelle: '.$this->sourceUrl;
|
||||
}
|
||||
|
||||
if ($this->aiLabel !== null) {
|
||||
$lines[] = 'KI-Kennzeichnung: '.$this->aiLabel.($this->aiSourceLabel !== null ? ' ('.$this->aiSourceLabel.')' : '');
|
||||
}
|
||||
|
||||
if ($this->confirmation !== null) {
|
||||
$lines[] = sprintf(
|
||||
'Rechte bestätigt am %s (%s, AGB %s)',
|
||||
$this->confirmation->confirmed_at->format('d.m.Y H:i'),
|
||||
$this->confirmation->legal_text_key,
|
||||
$this->confirmation->agb_version ?? '–',
|
||||
);
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private static function clean(?string $value): ?string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
|
||||
return $value !== '' ? $value : null;
|
||||
}
|
||||
|
||||
private static function safeUrl(?string $url): ?string
|
||||
{
|
||||
$url = trim((string) $url);
|
||||
|
||||
if ($url === '' || ! Str::startsWith(strtolower($url), ['http://', 'https://'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filter_var($url, FILTER_VALIDATE_URL) !== false ? $url : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ class LegalDocumentSeeder extends Seeder
|
|||
[LegalDocument::TYPE_AGB, '0.6', '2026-07-03', 'B2B-Fassung (E2), anwaltsreif; externe Vorprüfung 11/11 bestätigt'],
|
||||
[LegalDocument::TYPE_DATENSCHUTZ, '0.3', '2026-07-03', 'B2B-angepasst; host-aware Bausteine Hub/Spokes'],
|
||||
[LegalDocument::TYPE_RICHTLINIEN, '0.1', '2026-07-03', 'Rahmentext; Kategorien aus rejection_categories'],
|
||||
[LegalDocument::TYPE_RICHTLINIEN, '0.2', '2026-09-22', 'Prüfung als „automatisiert“ beschrieben, Abschnitt „Bilder“ (Bildkennzeichnung D.6, Decision-Update 3.5)'],
|
||||
[LegalDocument::TYPE_MELDEVERFAHREN, '0.1', '2026-07-03', 'Drei Wege: DSA / DSGVO / Depublikation'],
|
||||
[LegalDocument::TYPE_IMPRESSUM, '0.1', '2026-07-03', 'DDG-korrigiert, Verbund-Absatz, DSA-Kontaktpunkt'],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -530,8 +530,8 @@ return [
|
|||
'trust_eyebrow' => 'Was uns ausmacht',
|
||||
'trust_heading' => 'Vier Prinzipien, die diese Plattform tragen',
|
||||
'trust_1_k' => 'Qualität',
|
||||
'trust_1_t' => 'Geprüfte Mitteilungen',
|
||||
'trust_1_d' => 'Jede Mitteilung durchläuft eine Qualitätsprüfung, bevor sie online geht. Keine SEO-Spam-Texte, keine reinen Werbeanzeigen.',
|
||||
'trust_1_t' => 'Automatisiert geprüfte Mitteilungen',
|
||||
'trust_1_d' => 'Jede Mitteilung durchläuft vor der Veröffentlichung eine automatisierte Prüfung auf Spam, reine Werbung, Kategorie und formale Qualität. Ob Inhalte richtig sind, prüfen wir nicht — dafür steht der Absender.',
|
||||
'trust_2_k' => 'Auffindbarkeit',
|
||||
'trust_2_t' => 'Dauerhaft im Archiv',
|
||||
'trust_2_d' => 'Pressemitteilungen bleiben im Archiv – auch nach Jahren. Mitteilungen aus mehr als einem Jahrzehnt sind weiterhin abrufbar.',
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ return [
|
|||
|
||||
'publish_band' => [
|
||||
'eyebrow' => 'Für Unternehmen',
|
||||
'title' => 'Ihre Pressemitteilung, qualitätsgeprüft veröffentlicht',
|
||||
'title' => 'Ihre Pressemitteilung, automatisiert geprüft und veröffentlicht',
|
||||
'subline' => 'Anmelden, Meldung einreichen, veröffentlichen — mit Reichweite im DACH-Raum, eigenem Newsroom und dauerhaft auffindbarem Archiv.',
|
||||
'cta_submit' => 'Pressemitteilung veröffentlichen',
|
||||
'cta_pricing' => 'Tarife & Pakete',
|
||||
|
|
@ -185,8 +185,8 @@ return [
|
|||
'imprint' => 'Impressum',
|
||||
'privacy' => 'Datenschutz',
|
||||
'tagline_short' => 'Pressemitteilungen · DACH',
|
||||
'tagline_long_presseecho' => 'Fachmeldungen aus Wirtschaft und Industrie. Qualitätsgeprüft. Strukturiert distribuiert.',
|
||||
'tagline_long_businessportal24' => 'Veröffentlichungs-Portal für qualitätsgeprüfte Pressemitteilungen aus Deutschland, Österreich und der Schweiz.',
|
||||
'tagline_long_presseecho' => 'Fachmeldungen aus Wirtschaft und Industrie. Automatisiert geprüft. Strukturiert distribuiert.',
|
||||
'tagline_long_businessportal24' => 'Veröffentlichungs-Portal für Pressemitteilungen aus Deutschland, Österreich und der Schweiz.',
|
||||
'rights' => '© :year :brand · Alle Rechte vorbehalten',
|
||||
'terms' => 'AGB',
|
||||
'guidelines' => 'Richtlinien',
|
||||
|
|
@ -228,9 +228,9 @@ return [
|
|||
|
||||
'search' => [
|
||||
'meta_title' => 'Pressemitteilungen durchsuchen – :brand',
|
||||
'meta_description' => 'Durchsuchen Sie das geprüfte Pressemitteilungs-Archiv nach Stichwort, Unternehmen und Branche – qualitätsgeprüft und dauerhaft auffindbar im DACH-Raum.',
|
||||
'meta_description' => 'Durchsuchen Sie das Pressemitteilungs-Archiv nach Stichwort, Unternehmen und Branche – strukturiert und dauerhaft auffindbar im DACH-Raum.',
|
||||
'eyebrow' => 'Archiv-Suche',
|
||||
'intro' => 'Stichwort, Unternehmen oder Branche – durchsuchen Sie das vollständige, qualitätsgeprüfte Archiv.',
|
||||
'intro' => 'Stichwort, Unternehmen oder Branche – durchsuchen Sie das vollständige Archiv.',
|
||||
'placeholder' => 'Suchbegriff, Unternehmen oder Branche…',
|
||||
'searching' => 'sucht…',
|
||||
'reset' => 'Suche zurücksetzen',
|
||||
|
|
@ -246,19 +246,19 @@ return [
|
|||
'previous_page' => 'Vorherige Seite',
|
||||
'next_page' => 'Nächste Seite',
|
||||
'discover_title' => 'Durchsuchen Sie das Archiv',
|
||||
'discover_text' => ':count geprüfte Pressemitteilungen — durchsuchbar nach Stichwort, Unternehmen und Branche. Geben Sie oben einen Suchbegriff ein oder wählen Sie eine Rubrik.',
|
||||
'discover_text' => ':count Pressemitteilungen — durchsuchbar nach Stichwort, Unternehmen und Branche. Geben Sie oben einen Suchbegriff ein oder wählen Sie eine Rubrik.',
|
||||
'popular_categories' => 'Beliebte Rubriken',
|
||||
],
|
||||
|
||||
'newsrooms_page' => [
|
||||
'meta_title' => 'Newsrooms – :brand',
|
||||
'meta_description' => 'Alle aktiven Unternehmens-Newsrooms im Überblick – qualitätsgeprüfte Pressemitteilungen je Unternehmen, gebündelt und dauerhaft auffindbar im DACH-Raum.',
|
||||
'meta_description' => 'Alle aktiven Unternehmens-Newsrooms im Überblick – Pressemitteilungen je Unternehmen, gebündelt und dauerhaft auffindbar im DACH-Raum.',
|
||||
'eyebrow' => 'Unternehmens-Newsrooms',
|
||||
'intro' => 'Alle aktiven Unternehmen mit ihren qualitätsgeprüften Pressemitteilungen – gebündelt an einem Ort.',
|
||||
'intro' => 'Alle aktiven Unternehmen mit ihren Pressemitteilungen – gebündelt an einem Ort.',
|
||||
'stat_total_sub' => 'mit veröffentlichten Meldungen',
|
||||
'stat_active' => 'Aktiv (30 Tage)',
|
||||
'stat_archive' => 'Meldungen im Archiv',
|
||||
'stat_archive_sub' => 'qualitätsgeprüft',
|
||||
'stat_archive_sub' => 'automatisiert geprüft',
|
||||
'search_label' => 'Newsroom suchen',
|
||||
'search_placeholder' => 'Unternehmen suchen…',
|
||||
'reset_filter' => 'Filter zurücksetzen',
|
||||
|
|
@ -269,11 +269,11 @@ return [
|
|||
'none_available' => 'Aktuell sind keine aktiven Newsrooms verfügbar.',
|
||||
'cta_eyebrow' => 'Eigener Newsroom',
|
||||
'cta_title' => 'Bündeln Sie Ihre Pressemitteilungen im eigenen Newsroom.',
|
||||
'cta_text' => 'Eigene Marken-URL · alle Meldungen zentral · SEO-Vorteil für Ihr Unternehmen · qualitätsgeprüft.',
|
||||
'cta_text' => 'Eigene Marken-URL · alle Meldungen zentral · SEO-Vorteil für Ihr Unternehmen.',
|
||||
],
|
||||
|
||||
'newsroom_page' => [
|
||||
'meta_description' => 'Newsroom von :company: alle qualitätsgeprüften Pressemitteilungen gebündelt an einem Ort.',
|
||||
'meta_description' => 'Newsroom von :company: alle Pressemitteilungen gebündelt an einem Ort.',
|
||||
'back_aria' => 'Zurück zu allen Newsrooms',
|
||||
'all_newsrooms' => 'Alle Newsrooms',
|
||||
'recent_stat' => ':count Meldungen in den letzten 30 Tagen',
|
||||
|
|
@ -295,7 +295,7 @@ return [
|
|||
|
||||
'category' => [
|
||||
'fallback_name' => 'Branche',
|
||||
'default_description' => 'Aktuelle Pressemitteilungen aus dem Bereich :category – qualitätsgeprüft und strukturiert distribuiert im DACH-Raum.',
|
||||
'default_description' => 'Aktuelle Pressemitteilungen aus dem Bereich :category – strukturiert distribuiert im DACH-Raum.',
|
||||
'meta_title' => 'Pressemitteilungen :category – :brand',
|
||||
'eyebrow' => 'Branche',
|
||||
'back_aria' => 'Zurück zu allen Meldungen',
|
||||
|
|
@ -365,6 +365,17 @@ return [
|
|||
'recommended' => 'Empfehlung',
|
||||
'recommended_hint' => 'Diese Pressemitteilung erreicht einen hohen Content-Score — basierend auf Quellenqualität, Verifizierungsstatus und inhaltlicher Relevanz.',
|
||||
'photo' => 'Foto',
|
||||
'source' => 'Quelle',
|
||||
'image_credit_title' => 'Bildnachweis',
|
||||
'image_credit_open' => 'Bildnachweis anzeigen',
|
||||
'image_credit_close' => 'Schließen',
|
||||
'image_license' => 'Lizenz',
|
||||
'image_source_link' => 'Quelle',
|
||||
'image_ai_hint_generated' => 'Dieses Bild wurde mit KI erstellt.',
|
||||
'image_ai_hint_edited' => 'Dieses Bild wurde mit KI bearbeitet.',
|
||||
'image_ai_source' => 'Kennzeichnung: :source',
|
||||
'image_responsible' => 'Bild und Angaben: :company, eingereicht am :date',
|
||||
'image_report' => 'Bild melden',
|
||||
'about_company' => 'Über das Unternehmen',
|
||||
'press_contact' => 'Pressekontakt',
|
||||
'verified_hint' => 'Verifizierter Publisher — Identität und Domain bestätigt',
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ return [
|
|||
|
||||
'meta' => [
|
||||
'home_title' => 'Latest press releases from business & industry – :brand',
|
||||
'home_description' => 'Press releases from Germany, Austria and Switzerland. Quality-checked. Distributed in a structured way.',
|
||||
'home_description' => 'Press releases from Germany, Austria and Switzerland. Automatically checked. Distributed in a structured way.',
|
||||
],
|
||||
|
||||
'shared' => [
|
||||
|
|
@ -132,7 +132,7 @@ return [
|
|||
|
||||
'publish_band' => [
|
||||
'eyebrow' => 'For companies',
|
||||
'title' => 'Your press release, quality-checked and published',
|
||||
'title' => 'Your press release, automatically checked and published',
|
||||
'subline' => 'Sign up, submit your release, publish — with reach across the DACH region, your own newsroom and a permanently searchable archive.',
|
||||
'cta_submit' => 'Publish a press release',
|
||||
'cta_pricing' => 'Plans & pricing',
|
||||
|
|
@ -180,8 +180,8 @@ return [
|
|||
'imprint' => 'Imprint',
|
||||
'privacy' => 'Privacy',
|
||||
'tagline_short' => 'Press releases · DACH',
|
||||
'tagline_long_presseecho' => 'Business and industry news. Quality-checked. Distributed in a structured way.',
|
||||
'tagline_long_businessportal24' => 'Publishing portal for quality-checked press releases from Germany, Austria and Switzerland.',
|
||||
'tagline_long_presseecho' => 'Business and industry news. Automatically checked. Distributed in a structured way.',
|
||||
'tagline_long_businessportal24' => 'Publishing portal for press releases from Germany, Austria and Switzerland.',
|
||||
'rights' => '© :year :brand · All rights reserved',
|
||||
'terms' => 'Terms',
|
||||
'guidelines' => 'Guidelines',
|
||||
|
|
@ -223,9 +223,9 @@ return [
|
|||
|
||||
'search' => [
|
||||
'meta_title' => 'Search press releases – :brand',
|
||||
'meta_description' => 'Search the quality-checked press release archive by keyword, company and industry – permanently discoverable across the DACH region.',
|
||||
'meta_description' => 'Search the press release archive by keyword, company and industry – permanently discoverable across the DACH region.',
|
||||
'eyebrow' => 'Archive search',
|
||||
'intro' => 'Keyword, company or industry – search the complete, quality-checked archive.',
|
||||
'intro' => 'Keyword, company or industry – search the complete archive.',
|
||||
'placeholder' => 'Keyword, company or industry…',
|
||||
'searching' => 'searching…',
|
||||
'reset' => 'Reset search',
|
||||
|
|
@ -247,13 +247,13 @@ return [
|
|||
|
||||
'newsrooms_page' => [
|
||||
'meta_title' => 'Newsrooms – :brand',
|
||||
'meta_description' => 'All active company newsrooms at a glance – quality-checked press releases per company, bundled and permanently discoverable across the DACH region.',
|
||||
'meta_description' => 'All active company newsrooms at a glance – press releases per company, bundled and permanently discoverable across the DACH region.',
|
||||
'eyebrow' => 'Company newsrooms',
|
||||
'intro' => 'All active companies with their quality-checked press releases – bundled in one place.',
|
||||
'intro' => 'All active companies with their press releases – bundled in one place.',
|
||||
'stat_total_sub' => 'with published releases',
|
||||
'stat_active' => 'Active (30 days)',
|
||||
'stat_archive' => 'Releases in the archive',
|
||||
'stat_archive_sub' => 'quality-checked',
|
||||
'stat_archive_sub' => 'automatically checked',
|
||||
'search_label' => 'Search newsrooms',
|
||||
'search_placeholder' => 'Search companies…',
|
||||
'reset_filter' => 'Reset filter',
|
||||
|
|
@ -264,11 +264,11 @@ return [
|
|||
'none_available' => 'There are currently no active newsrooms.',
|
||||
'cta_eyebrow' => 'Your own newsroom',
|
||||
'cta_title' => 'Bundle your press releases in your own newsroom.',
|
||||
'cta_text' => 'Your own branded URL · all releases in one place · SEO benefit for your company · quality-checked.',
|
||||
'cta_text' => 'Your own branded URL · all releases in one place · SEO benefit for your company.',
|
||||
],
|
||||
|
||||
'newsroom_page' => [
|
||||
'meta_description' => ':company newsroom: all quality-checked press releases bundled in one place.',
|
||||
'meta_description' => ':company newsroom: all press releases bundled in one place.',
|
||||
'back_aria' => 'Back to all newsrooms',
|
||||
'all_newsrooms' => 'All newsrooms',
|
||||
'recent_stat' => ':count releases in the last 30 days',
|
||||
|
|
@ -290,7 +290,7 @@ return [
|
|||
|
||||
'category' => [
|
||||
'fallback_name' => 'Industry',
|
||||
'default_description' => 'Latest press releases from the :category sector – quality-checked and distributed across the DACH region.',
|
||||
'default_description' => 'Latest press releases from the :category sector – and distributed across the DACH region.',
|
||||
'meta_title' => ':category press releases – :brand',
|
||||
'eyebrow' => 'Industry',
|
||||
'back_aria' => 'Back to all releases',
|
||||
|
|
@ -313,7 +313,7 @@ return [
|
|||
'related' => 'Related industries',
|
||||
'cta_eyebrow' => 'For companies in :category',
|
||||
'cta_title' => 'Publish your press release directly in :category.',
|
||||
'cta_text' => 'Reach via active newsrooms · Structured distribution across the DACH region · Quality-checked.',
|
||||
'cta_text' => 'Reach via active newsrooms · Structured distribution across the DACH region.',
|
||||
'cta_submit' => 'Submit press release',
|
||||
'cta_pricing' => 'View plans & industry packages',
|
||||
],
|
||||
|
|
@ -360,6 +360,17 @@ return [
|
|||
'recommended' => 'Recommended',
|
||||
'recommended_hint' => 'This press release achieves a high content score — based on source quality, verification status and content relevance.',
|
||||
'photo' => 'Photo',
|
||||
'source' => 'Source',
|
||||
'image_credit_title' => 'Image credit',
|
||||
'image_credit_open' => 'Show image credit',
|
||||
'image_credit_close' => 'Close',
|
||||
'image_license' => 'Licence',
|
||||
'image_source_link' => 'Source',
|
||||
'image_ai_hint_generated' => 'This image was created with AI.',
|
||||
'image_ai_hint_edited' => 'This image was edited with AI.',
|
||||
'image_ai_source' => 'Label: :source',
|
||||
'image_responsible' => 'Image and details: :company, submitted on :date',
|
||||
'image_report' => 'Report image',
|
||||
'about_company' => 'About the company',
|
||||
'press_contact' => 'Press contact',
|
||||
'verified_hint' => 'Verified publisher — identity and domain confirmed',
|
||||
|
|
|
|||
|
|
@ -264,108 +264,6 @@ parameters:
|
|||
count: 1
|
||||
path: app/Http/Resources/CompanyResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$copyright\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$created_at\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$description\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$disk\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$height\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$id\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$is_preview\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$mime\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$path\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$press_release_id\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$sort_order\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$title\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$updated_at\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$variants\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseImageResource\:\:\$width\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method App\\Http\\Resources\\PressReleaseImageResource\:\:url\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 2
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method App\\Http\\Resources\\PressReleaseImageResource\:\:variantUrl\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: app/Http/Resources/PressReleaseImageResource.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Http\\Resources\\PressReleaseResource\:\:\$backlink_url\.$#'
|
||||
identifier: property.notFound
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -76,9 +76,13 @@ test('an outdated text version is rejected with the current key in the message',
|
|||
|
||||
test('a valid upload stores the confirmation with the token and returns the new fields', function () {
|
||||
/** @var TestCase $this */
|
||||
['pressRelease' => $pressRelease, 'user' => $user] = rightsV2ApiDraft();
|
||||
// Echter Bearer-Token statt Sanctum::actingAs, damit die Token-ID im
|
||||
// Protokoll landet (TransientToken hat keinen Datensatz).
|
||||
$user = User::factory()->create();
|
||||
$pressRelease = PressRelease::factory()->create(['user_id' => $user->id, 'status' => PressReleaseStatus::Draft->value]);
|
||||
$plainToken = $user->createToken('connektar', ['press-releases:read', 'press-release-images:write'])->plainTextToken;
|
||||
|
||||
$response = $this->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [
|
||||
$response = $this->withToken($plainToken)->postJson("/api/v1/press-releases/{$pressRelease->id}/images", [
|
||||
'image' => rightsV2ApiFixture('xmp-ai-generated.jpg'),
|
||||
'ai_declaration' => 'ai_generated',
|
||||
'copyright' => 'KI-Bild / Musterfirma',
|
||||
|
|
@ -110,7 +114,7 @@ test('a valid upload stores the confirmation with the token and returns the new
|
|||
->and($image->focus_y)->toBe(80)
|
||||
->and($confirmation->channel)->toBe(ConfirmationChannel::Api)
|
||||
->and($confirmation->user_id)->toBe($user->id)
|
||||
->and($confirmation->personal_access_token_id)->not->toBeNull()
|
||||
->and($confirmation->personal_access_token_id)->toBe($user->tokens()->sole()->getKey())
|
||||
->and($confirmation->user_agent)->toBe('ConnektarBot/2.0')
|
||||
->and($response->json('data.urls.original'))->toBe(route('media.press-release-images.show', ['image' => $image->id]))
|
||||
->and($response->json('data.path'))->not->toContain('press-release-originals');
|
||||
|
|
|
|||
141
tests/Feature/Images/ImageCreditOutputTest.php
Normal file
141
tests/Feature/Images/ImageCreditOutputTest.php
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ConfirmationChannel;
|
||||
use App\Enums\Portal;
|
||||
use App\Models\Category;
|
||||
use App\Models\Company;
|
||||
use App\Models\ImageRightsConfirmation;
|
||||
use App\Models\LegalDocument;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Support\ImageCredit;
|
||||
use Database\Seeders\LegalDocumentSeeder;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Bildkennzeichnung Block D: eine Auflösung für Nachweiszeile, Overlay,
|
||||
* Kunden-/Adminansicht und PDF; Verantwortlich-Zeile nur mit Protokoll.
|
||||
*/
|
||||
function creditPublishedRelease(): array
|
||||
{
|
||||
$category = Category::factory()->create(['portal' => Portal::Presseecho]);
|
||||
$category->translations()->create(['locale' => 'de', 'name' => 'Energie', 'slug' => 'energie']);
|
||||
$company = Company::factory()->presseecho()->create(['name' => 'Beispiel AG', 'is_active' => true]);
|
||||
|
||||
$release = PressRelease::factory()
|
||||
->published()
|
||||
->forPortal(Portal::Presseecho)
|
||||
->for($category)
|
||||
->for($company)
|
||||
->create(['slug' => 'bild-nachweis-test', 'published_at' => now()->subDay()]);
|
||||
|
||||
return compact('category', 'company', 'release');
|
||||
}
|
||||
|
||||
function creditConfirmationFor(PressReleaseImage $image, string $channel = 'form'): ImageRightsConfirmation
|
||||
{
|
||||
return ImageRightsConfirmation::query()->create([
|
||||
'subject_type' => $image->getMorphClass(),
|
||||
'subject_id' => $image->getKey(),
|
||||
'press_release_id' => $image->press_release_id,
|
||||
'company_id' => $image->pressRelease->company_id,
|
||||
'user_id' => $image->pressRelease->user_id,
|
||||
'channel' => ConfirmationChannel::from($channel),
|
||||
'legal_text_key' => 'img-confirm-v2',
|
||||
'text_snapshot' => 'Ich bestätige …',
|
||||
'agb_version' => '0.6',
|
||||
'file_sha256' => $image->original_sha256,
|
||||
'declaration_snapshot' => [],
|
||||
'confirmed_at' => now()->setDate(2026, 9, 22)->setTime(10, 0),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
test('a formular v2 image renders credit line, ai hint with source and the responsible line', function () {
|
||||
/** @var TestCase $this */
|
||||
['release' => $release, 'company' => $company] = creditPublishedRelease();
|
||||
$image = PressReleaseImage::factory()->for($release)->rulesV2()->create([
|
||||
'copyright' => 'Anna Beispiel / Beispiel AG',
|
||||
'ai_declaration' => 'ai_edited',
|
||||
'ai_declaration_user' => 'ai_edited',
|
||||
'ai_declaration_source' => 'customer',
|
||||
'license_url' => 'https://example.com/lizenz',
|
||||
'source_url' => 'javascript:alert(1)',
|
||||
]);
|
||||
creditConfirmationFor($image);
|
||||
|
||||
$credit = ImageCredit::for($image->fresh(), $company);
|
||||
|
||||
expect($credit->line())->toBe('Foto: Anna Beispiel / Beispiel AG · KI-bearbeitet')
|
||||
->and($credit->responsibleLine)->toBe('Bild und Angaben: Beispiel AG, eingereicht am 22. September 2026')
|
||||
->and($credit->aiSourceLabel)->toBe('Angabe des Einreichers')
|
||||
->and($credit->licenseUrl)->toBe('https://example.com/lizenz')
|
||||
->and($credit->sourceUrl)->toBeNull()
|
||||
->and($credit->pdfLines())->toContain('Rechte bestätigt am 22.09.2026 10:00 (img-confirm-v2, AGB 0.6)');
|
||||
|
||||
$this->get('https://presseecho.test/de/press-release/bild-nachweis-test')
|
||||
->assertSuccessful()
|
||||
->assertSeeText('Foto: Anna Beispiel / Beispiel AG · KI-bearbeitet')
|
||||
->assertSee('data-image-credit', false)
|
||||
->assertSeeText('Dieses Bild wurde mit KI bearbeitet.')
|
||||
->assertSeeText('Kennzeichnung: Angabe des Einreichers')
|
||||
->assertSeeText('Bild und Angaben: Beispiel AG, eingereicht am 22. September 2026')
|
||||
->assertSeeText('Bild melden')
|
||||
->assertDontSee('javascript:alert', false);
|
||||
});
|
||||
|
||||
test('a legacy image without credit falls back to the company as source and shows no responsible line', function () {
|
||||
/** @var TestCase $this */
|
||||
['release' => $release, 'company' => $company] = creditPublishedRelease();
|
||||
$image = PressReleaseImage::factory()->for($release)->legacy()->create(['is_ai_generated' => true]);
|
||||
|
||||
$credit = ImageCredit::for($image, $company);
|
||||
|
||||
expect($credit->line())->toBe('Quelle: Beispiel AG · KI-generiert')
|
||||
->and($credit->responsibleLine)->toBeNull()
|
||||
->and($credit->aiSourceLabel)->toBeNull()
|
||||
->and($credit->isLegacy)->toBeTrue();
|
||||
|
||||
$this->get('https://presseecho.test/de/press-release/bild-nachweis-test')
|
||||
->assertSuccessful()
|
||||
->assertSeeText('Quelle: Beispiel AG · KI-generiert')
|
||||
->assertDontSeeText('Bild und Angaben');
|
||||
});
|
||||
|
||||
test('a legacy image with author but without copyright uses the author and the old fields stay in the pdf lines', function () {
|
||||
['release' => $release, 'company' => $company] = creditPublishedRelease();
|
||||
$image = PressReleaseImage::factory()->for($release)->legacy()->create([
|
||||
'author' => 'Max Mustermann',
|
||||
'license_type' => 'own',
|
||||
]);
|
||||
|
||||
$credit = ImageCredit::for($image, $company);
|
||||
|
||||
expect($credit->line())->toBe('Foto: Max Mustermann')
|
||||
->and($credit->pdfLines())->toBe(['Bildnachweis: Max Mustermann']);
|
||||
});
|
||||
|
||||
test('an image without any detail renders no line and no overlay', function () {
|
||||
/** @var TestCase $this */
|
||||
['release' => $release] = creditPublishedRelease();
|
||||
PressReleaseImage::factory()->for($release)->legacy()->create();
|
||||
$release->company()->update(['name' => '']);
|
||||
|
||||
$credit = ImageCredit::for($release->images()->first(), $release->company()->first());
|
||||
|
||||
expect($credit->line())->toBeNull()
|
||||
->and($credit->hasDetails())->toBeFalse();
|
||||
});
|
||||
|
||||
test('the guidelines page describes the automated check and the image rules under version 0.2', function () {
|
||||
/** @var TestCase $this */
|
||||
$this->seed(LegalDocumentSeeder::class);
|
||||
|
||||
expect(LegalDocument::current(LegalDocument::TYPE_RICHTLINIEN)?->version)->toBe('0.2');
|
||||
|
||||
$this->get('https://presseecho.test/de/richtlinien')
|
||||
->assertSuccessful()
|
||||
->assertSeeText('Jede Mitteilung durchläuft vor der Veröffentlichung eine automatisierte Prüfung.')
|
||||
->assertSeeText('Zu jedem Bild gehört ein Bildnachweis')
|
||||
->assertDontSeeText('geprüft, den Richtlinien entsprechend');
|
||||
});
|
||||
|
|
@ -21,12 +21,13 @@ test('the agb version falls back to the config while the table is empty', functi
|
|||
expect(LegalDocument::currentAgbVersion())->toBe('9.9');
|
||||
});
|
||||
|
||||
test('the seeder registers all five legal documents idempotently', function () {
|
||||
test('the seeder registers all legal documents idempotently', function () {
|
||||
/** @var TestCase $this */
|
||||
$this->seed(LegalDocumentSeeder::class);
|
||||
$this->seed(LegalDocumentSeeder::class); // Wiederholung darf nichts duplizieren.
|
||||
|
||||
expect(LegalDocument::query()->count())->toBe(5);
|
||||
expect(LegalDocument::query()->count())->toBe(6);
|
||||
expect(LegalDocument::current(LegalDocument::TYPE_RICHTLINIEN)?->version)->toBe('0.2');
|
||||
expect(LegalDocument::currentAgbVersion())->toBe('0.6');
|
||||
expect(LegalDocument::current(LegalDocument::TYPE_DATENSCHUTZ)?->version)->toBe('0.3');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ test('businessportal24 homepage renders the editorial shell with real data', fun
|
|||
->assertSeeText('Im Fokus')
|
||||
->assertSeeText('Aktive Newsrooms')
|
||||
->assertSeeText('Veröffentlichen Sie Ihre Pressemitteilung')
|
||||
->assertSeeText('Ihre Pressemitteilung, qualitätsgeprüft veröffentlicht')
|
||||
->assertSeeText('Ihre Pressemitteilung, automatisiert geprüft und veröffentlicht')
|
||||
->assertSeeText('Konto anlegen')
|
||||
->assertSeeText('Unser Qualitätsstandard')
|
||||
->assertDontSeeText('redaktionell geprüft')
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ test('presseecho homepage renders the editorial shell with real data', function
|
|||
->assertSeeText('Im Fokus')
|
||||
->assertSeeText('Aktive Newsrooms')
|
||||
->assertSeeText('Veröffentlichen Sie Ihre Pressemitteilung')
|
||||
->assertSeeText('Ihre Pressemitteilung, qualitätsgeprüft veröffentlicht')
|
||||
->assertSeeText('Ihre Pressemitteilung, automatisiert geprüft und veröffentlicht')
|
||||
->assertSeeText('Konto anlegen')
|
||||
->assertSeeText('Unser Qualitätsstandard')
|
||||
->assertDontSeeText('redaktionell geprüft')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue