From 01014887cfdf2437c800e15bc78123da0b0ba933 Mon Sep 17 00:00:00 2001
From: Kevin Adametz
Date: Tue, 15 Sep 2026 11:05:53 +0000
Subject: [PATCH] =?UTF-8?q?newsroom:=20Hinweis=20=E2=80=9EDiesen=20Newsroo?=
=?UTF-8?q?m=20verwalten=E2=80=9C=20auf=20der=20Seite=20und=20in=20der=20M?=
=?UTF-8?q?ail=20(Roadmap=201b.4,=20E4)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
E4 (15.09.2026): Die Veröffentlichungsmail trägt den Hinweis am Ende, als
Hinweis, nicht als Angebot. Auf der Newsroom-Seite eine Zeile unter dem
Profil. Der Mail-Link ist mit src=mail_publish getaggt; die Claim-Seite
reicht die Herkunft bis auf das Token durch, sodass die Leiter „aus der
Mail beansprucht“ von „auf der Seite entdeckt“ unterscheidet.
Co-Authored-By: Claude Opus 5 (1M context)
---
.../Web/NewsroomClaimController.php | 17 ++++++-
app/Mail/PressReleasePublished.php | 27 ++++++++++
app/Services/Company/NewsroomClaimService.php | 4 +-
dev/2026-09-01/roadmap-2026.md | 4 +-
dev/2026-09-01/umsetzung-protokoll.md | 22 +++++++++
lang/de/web.php | 2 +
lang/en/web.php | 2 +
.../emails/press-release/published.blade.php | 10 ++++
resources/views/web/newsroom-claim.blade.php | 2 +
resources/views/web/newsroom.blade.php | 7 +++
tests/Feature/Web/NewsroomClaimTest.php | 49 +++++++++++++++++++
11 files changed, 141 insertions(+), 5 deletions(-)
diff --git a/app/Http/Controllers/Web/NewsroomClaimController.php b/app/Http/Controllers/Web/NewsroomClaimController.php
index c7aaa7d3..01082de7 100644
--- a/app/Http/Controllers/Web/NewsroomClaimController.php
+++ b/app/Http/Controllers/Web/NewsroomClaimController.php
@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Web;
+use App\Enums\ConversionSource;
use App\Http\Controllers\Controller;
use App\Models\Company;
use App\Scopes\PortalScope;
@@ -68,7 +69,7 @@ class NewsroomClaimController extends Controller
// Enumeration-sicher: dieselbe Meldung, ob ein Konto deaktiviert ist
// oder nicht — die Adresse ist ohnehin nur maskiert sichtbar.
- $this->claims->request($company, $request->ip());
+ $this->claims->request($company, $request->ip(), $this->sourceOf($request));
return back()->with('newsroom-claim-status', __('static.newsroom_claim.flash_sent'));
}
@@ -101,6 +102,20 @@ class NewsroomClaimController extends Controller
abort(404);
}
+ /**
+ * Kam der Besucher aus einer Mail (`src` aus dem Link, per Formular
+ * weitergereicht), trägt das Token diese Herkunft — sonst ist es der
+ * Hinweis auf der Newsroom-Seite.
+ */
+ private function sourceOf(Request $request): ConversionSource
+ {
+ $source = ConversionSource::parse($request->filled('src') ? (string) $request->input('src') : null);
+
+ return in_array($source, [ConversionSource::MailPublish, ConversionSource::MailReject], true)
+ ? $source
+ : ConversionSource::ProfileClaim;
+ }
+
private function ensureNotRateLimited(Request $request, int $companyId): void
{
$perDay = (int) config('newsroom.claim.links_per_day', 3);
diff --git a/app/Mail/PressReleasePublished.php b/app/Mail/PressReleasePublished.php
index 00e38c1d..fbf5fef8 100644
--- a/app/Mail/PressReleasePublished.php
+++ b/app/Mail/PressReleasePublished.php
@@ -33,6 +33,12 @@ class PressReleasePublished extends Mailable
*/
public string $recipientName;
+ /**
+ * „Diesen Newsroom verwalten" (Roadmap 1b.4, Entscheidung E4 vom
+ * 15.09.2026): Hinweis am Ende der Mail, kein Angebot. Null ohne Firma.
+ */
+ public ?string $manageNewsroomUrl = null;
+
public function __construct(
public readonly User $user,
public readonly PressRelease $pressRelease,
@@ -54,9 +60,30 @@ class PressReleasePublished extends Mailable
$pressRelease->publicPortalLinks(),
);
+ $this->manageNewsroomUrl = $this->manageNewsroomUrl($pressRelease);
+
$this->configureUnsubscribe($user, EmailNotificationType::PressReleasePublished);
}
+ /**
+ * Verwaltungsseite des Newsrooms auf dem Portal, auf dem die Meldung
+ * erschienen ist (bei beiden: presseecho). Nur für Firmen, die noch als
+ * Newsroom existieren — nicht 410-entfernt.
+ */
+ private function manageNewsroomUrl(PressRelease $pressRelease): ?string
+ {
+ $company = $pressRelease->company;
+
+ if ($company === null || ! $company->slug || $company->isGone() || $this->portalLinks === []) {
+ return null;
+ }
+
+ $host = rtrim((string) parse_url($this->portalLinks[0]['url'], PHP_URL_SCHEME).'://'.parse_url($this->portalLinks[0]['url'], PHP_URL_HOST), '/');
+ $edition = in_array($pressRelease->language, ['de', 'en'], true) ? $pressRelease->language : 'de';
+
+ return self::tagged(sprintf('%s/%s/newsroom/%s/verwalten', $host, $edition, $company->slug), $pressRelease->id);
+ }
+
public function envelope(): Envelope
{
return new Envelope(
diff --git a/app/Services/Company/NewsroomClaimService.php b/app/Services/Company/NewsroomClaimService.php
index c566141b..6adbf1a7 100644
--- a/app/Services/Company/NewsroomClaimService.php
+++ b/app/Services/Company/NewsroomClaimService.php
@@ -77,12 +77,12 @@ class NewsroomClaimService
* Verschickt die Anmeldelinks. Gibt zurück, an wie viele Adressen
* tatsächlich etwas ging — deaktivierte Konten bekommen nichts.
*/
- public function request(Company $company, ?string $ip): int
+ public function request(Company $company, ?string $ip, ConversionSource $source = ConversionSource::ProfileClaim): int
{
$sent = 0;
foreach ($this->recipients($company) as $email) {
- if ($this->contactAccess->requestMagicAccess($email, $ip, ConversionSource::ProfileClaim, $company->getKey())) {
+ if ($this->contactAccess->requestMagicAccess($email, $ip, $source, $company->getKey())) {
$sent++;
}
}
diff --git a/dev/2026-09-01/roadmap-2026.md b/dev/2026-09-01/roadmap-2026.md
index 159b8d17..02ad137e 100644
--- a/dev/2026-09-01/roadmap-2026.md
+++ b/dev/2026-09-01/roadmap-2026.md
@@ -121,7 +121,7 @@ Für selbst angelegte Firmendaten trägt der Nutzer die Verantwortung. Für die
| ~~1b.1~~ | Sichtbarkeitsregel: Newsroom öffentlich erst ab der ersten veröffentlichten Meldung, sonst nur Datensatz — **bestand; 15.09.2026 Rückbau im Cache vervollständigt** | S | keine |
| ~~1b.2~~ | Verwaltungszugang über Magic Link, unabhängig von der öffentlichen Sichtbarkeit — **gebaut 15.09.2026**: `/newsroom/{slug}/verwalten`, Link an die hinterlegte Firmen-E-Mail (maskiert), Herkunft `profile_claim`, Landung auf der Firmenseite; noch unverlinkt (→ 1b.4) | M | 1a.4 |
| 1b.3 | Kostenlose Profilpflege: Beschreibung, Logo, Bilder, Pressekontakte | M | 1b.2 |
-| 1b.4 | Hinweis "Diesen Newsroom verwalten" auf der Profilseite und in der Veröffentlichungsmail | S | 1b.2 |
+| ~~1b.4~~ | Hinweis "Diesen Newsroom verwalten" auf der Profilseite und in der Veröffentlichungsmail — **gebaut 15.09.2026** (E4: Hinweis unten in der Mail; Herkunft `mail_publish` läuft bis auf das Token durch) | S | 1b.2 |
| ~~1b.5~~ | Rückbau: fällt ein Newsroom durch Löschung oder Depublizierung auf null Meldungen, verschwindet die öffentliche Seite mit — **15.09.2026**: alle Admin-Wege (archivieren, Status ändern, löschen) leeren jetzt den Seiten-Cache | S | 1b.1 |
| 1b.6 | Nicht erreichbare Newsrooms aus der Bounce-Suppression im Admin markieren, nicht beanspruchbar, aus allen Mailstrecken ausgenommen | S | Bounce-Suppression live |
@@ -234,7 +234,7 @@ Braucht Betreuungszeit und ist teilweise von Dritten abhängig.
| ~~E1~~ | **Entschieden 11.09.2026: 3** (drei Live-Läufe, siehe `basiswerte-block0.md` und `umsetzung-protokoll.md` Teil 3). Konfiguriert in `config/newsroom.php`. | 1a.2 |
| ~~E2~~ | **Empfehlung 01.09.2026: Rohzahlen** — kein Bodensatz, Verteilung symmetrisch, Einordnung gegen den Schnitt wäre wegen der Wachstumskurve falsch (siehe `basiswerte-block0.md`). | 1c.1 |
| E3 | Anzahl kostenloser Prüfläufe und Preis der weiteren in Credits | 1d.4 |
-| E4 | Trägt die Veröffentlichungsmail den Magic-Link-Hinweis oder den Link auf die Infoseite; ein Element pro Mail | 1b.4, 1d.2 |
+| ~~E4~~ | **Entschieden 15.09.2026: der Magic-Link-Hinweis, am Ende der Mail, als Hinweis, nicht als Angebot.** Umgesetzt in 1b.4. Der Link auf die Infoseite (1d.2) bekommt damit keinen Platz in der Veröffentlichungsmail — ein Element pro Mail. | 1b.4, 1d.2 |
| E5 | Werden die bestehenden Ratgeberseiten konsolidiert oder ergänzt | 2.5 |
| E6 | Bleibt die Ablehnungsstrecke dauerhaft frei von kommerziellen Angeboten | 1d.1, 1d.6 |
diff --git a/dev/2026-09-01/umsetzung-protokoll.md b/dev/2026-09-01/umsetzung-protokoll.md
index 165c08bc..3506b629 100644
--- a/dev/2026-09-01/umsetzung-protokoll.md
+++ b/dev/2026-09-01/umsetzung-protokoll.md
@@ -955,3 +955,25 @@ Adresse, E-Mail, Telefon, Website, Land, Logo und Pressekontakte pflegen.
Was 1b.3 ergänzt: Beschreibungstext (durch die Rot-Ebene), Bilder (mit
KI-Deklaration und Audit-Log), und die Leitplanke, dass Links im Profiltext
systemseitig gesetzt werden.
+
+## 1b.4 — Hinweise (E4 entschieden 15.09.2026)
+
+**E4:** Die Veröffentlichungsmail trägt den Magic-Link-Hinweis — am Ende, als
+Hinweis, nicht als Angebot. Damit bekommt der Link auf die Infoseite (1d.2)
+keinen Platz in dieser Mail (ein Element pro Mail).
+
+- **Newsroom-Seite:** eine Zeile unter dem Firmenprofil („Sie vertreten
+ …? Profil, Logo und Pressekontakte lassen sich kostenlos pflegen. Diesen
+ Newsroom verwalten"), `rel="nofollow"`, 12 px, ink-3.
+- **Veröffentlichungsmail:** ein Absatz vor dem Hilfe-Hinweis, in derselben
+ grauen Kleinschrift wie der bestehende Login-Hinweis. Link auf
+ `/newsroom/{slug}/verwalten` des Portals, auf dem die Meldung erschien
+ (bei beiden: presseecho), getaggt mit `src=mail_publish&pr=`. Kein Hinweis
+ ohne Firma oder bei 410-entfernter Firma.
+- **Herkunft läuft durch:** Die Claim-Seite reicht `src` aus dem Mail-Link
+ per verstecktem Feld weiter; das Token trägt dann `mail_publish` statt
+ `profile_claim`. Stufe 1 (Klick) zeichnet die globale Middleware beim
+ Aufruf der Claim-Seite ohnehin auf. So unterscheidet die Leiter „aus der
+ Mail beansprucht" von „auf der Seite entdeckt".
+
+Tests: drei weitere in `NewsroomClaimTest` (Seite, Mail, Herkunft).
diff --git a/lang/de/web.php b/lang/de/web.php
index 05574812..4de4287c 100644
--- a/lang/de/web.php
+++ b/lang/de/web.php
@@ -284,6 +284,8 @@ return [
'total_hits_sub' => 'über alle Meldungen',
'latest' => 'Aktuellste Meldung',
'empty' => 'Aktuell keine weiteren Meldungen in diesem Newsroom.',
+ 'manage_hint' => 'Sie vertreten :company? Profil, Logo und Pressekontakte lassen sich kostenlos pflegen.',
+ 'manage_link' => 'Diesen Newsroom verwalten',
'own_title' => 'Eigenen Newsroom starten',
'own_text' => 'Bündeln Sie alle Ihre Pressemitteilungen unter einer eigenen Marken-URL – mit Logo, Branchen-Zuordnung und SEO-Vorteil.',
'own_cta' => 'Newsroom anlegen',
diff --git a/lang/en/web.php b/lang/en/web.php
index 87305edd..475e2e7c 100644
--- a/lang/en/web.php
+++ b/lang/en/web.php
@@ -279,6 +279,8 @@ return [
'total_hits_sub' => 'across all releases',
'latest' => 'Latest release',
'empty' => 'No further releases in this newsroom at the moment.',
+ 'manage_hint' => 'You represent :company? Profile, logo and press contacts can be maintained free of charge.',
+ 'manage_link' => 'Manage this newsroom',
'own_title' => 'Start your own newsroom',
'own_text' => 'Bundle all your press releases under your own branded URL – with logo, industry mapping and SEO benefit.',
'own_cta' => 'Create newsroom',
diff --git a/resources/views/emails/press-release/published.blade.php b/resources/views/emails/press-release/published.blade.php
index 3d37350d..4005cfd5 100644
--- a/resources/views/emails/press-release/published.blade.php
+++ b/resources/views/emails/press-release/published.blade.php
@@ -35,6 +35,16 @@
dort mit Ihrem Passwort oder per Magic-Link (Anmelde-Link per E-Mail) möglich.
+ {{-- 3) Verwaltungszugang zum Newsroom (Roadmap 1b.4, E4): ein Hinweis
+ am Ende, kein Angebot — der Zugang ist kostenlos. --}}
+ @if ($manageNewsroomUrl)
+
+ Hinweis: Der Newsroom von {{ $pressRelease->company->name }} lässt sich kostenlos pflegen —
+ Profil, Logo und Pressekontakte. Den Zugang erhält die hinterlegte Firmen-E-Mail:
+ Diesen Newsroom verwalten
+
+ @endif
+
diff --git a/resources/views/web/newsroom-claim.blade.php b/resources/views/web/newsroom-claim.blade.php
index d03ef3dc..1e31dccf 100644
--- a/resources/views/web/newsroom-claim.blade.php
+++ b/resources/views/web/newsroom-claim.blade.php
@@ -48,6 +48,8 @@