newsroom: Hinweis „Diesen Newsroom verwalten“ auf der Seite und in der Mail (Roadmap 1b.4, E4)
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) <noreply@anthropic.com>
This commit is contained in:
parent
0c1c072487
commit
7f714fb664
11 changed files with 141 additions and 5 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -35,6 +35,16 @@
|
|||
dort mit Ihrem Passwort oder per Magic-Link (Anmelde-Link per E-Mail) möglich.
|
||||
</p>
|
||||
|
||||
{{-- 3) Verwaltungszugang zum Newsroom (Roadmap 1b.4, E4): ein Hinweis
|
||||
am Ende, kein Angebot — der Zugang ist kostenlos. --}}
|
||||
@if ($manageNewsroomUrl)
|
||||
<p style="font-size: 12.5px; color: #6b7280;">
|
||||
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:<br>
|
||||
<a href="{{ $manageNewsroomUrl }}">Diesen Newsroom verwalten</a>
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<div class="note">
|
||||
<p>Bei Fragen erreichen Sie uns über den <a href="{{ config('domains.domain_portal_url') }}/de/hilfe">Hilfe-Bereich auf pressekonto</a>.</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@
|
|||
|
||||
<form method="POST" action="{{ route('newsroom.claim.store', ['slug' => $company->slug]) }}" class="space-y-5">
|
||||
@csrf
|
||||
{{-- Herkunft aus dem Mail-Link (?src=) bis auf das Token durchreichen. --}}
|
||||
<input type="hidden" name="src" value="{{ request()->query('src') }}" />
|
||||
|
||||
{{-- Honeypot --}}
|
||||
<div class="hidden" aria-hidden="true">
|
||||
|
|
|
|||
|
|
@ -121,6 +121,13 @@
|
|||
<p class="font-serif text-[15.5px] leading-[1.6] m-0 mt-5 pt-5 border-t border-bg-rule text-ink-2">{{ Str::limit($boilerplate, 360) }}</p>
|
||||
@endif
|
||||
|
||||
{{-- Verwaltungszugang (Roadmap 1b.4): ein Hinweis, kein Angebot.
|
||||
Der Zugang ist kostenlos und führt zur Firmen-E-Mail. --}}
|
||||
<p class="text-[12px] text-ink-3 m-0 mt-5 pt-4 border-t border-bg-rule">
|
||||
{{ __('web.newsroom_page.manage_hint', ['company' => $company->name]) }}
|
||||
<a href="{{ route('newsroom.claim', ['slug' => $company->slug]) }}" rel="nofollow" class="font-medium underline underline-offset-2 hover:text-ink transition-colors">{{ __('web.newsroom_page.manage_link') }}</a>
|
||||
</p>
|
||||
|
||||
@if ($categories->isNotEmpty())
|
||||
<div class="flex flex-wrap items-center gap-2 mt-5 pt-5 border-t border-bg-rule">
|
||||
<span class="eyebrow muted text-[10px] mr-1.5">{{ __('web.newsroom_page.industries') }}</span>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ use App\Enums\ConversionEventType;
|
|||
use App\Enums\ConversionSource;
|
||||
use App\Enums\Portal;
|
||||
use App\Mail\MagicLoginLink;
|
||||
use App\Mail\PressReleasePublished;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanyEmailGroup;
|
||||
use App\Models\Contact;
|
||||
use App\Models\ConversionEvent;
|
||||
use App\Models\MagicLink;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\User;
|
||||
use App\Services\Company\CompanyMergeService;
|
||||
use App\Services\Company\CompanyRemovalService;
|
||||
|
|
@ -152,3 +154,50 @@ test('the honeypot answers neutrally without sending', function () {
|
|||
|
||||
Mail::assertNothingQueued();
|
||||
});
|
||||
|
||||
// ------------------------------------------------- 1b.4 Hinweise
|
||||
|
||||
test('the newsroom page links to the claim page as a hint', function () {
|
||||
/** @var TestCase $this */
|
||||
$company = claimCompany();
|
||||
PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create([
|
||||
'company_id' => $company->id, 'published_at' => now()->subDay(), 'language' => 'de',
|
||||
]);
|
||||
|
||||
$this->get('https://businessportal24.test/de/newsroom/muster-gmbh')
|
||||
->assertOk()
|
||||
->assertSeeText('Diesen Newsroom verwalten')
|
||||
->assertSee('/de/newsroom/muster-gmbh/verwalten', false);
|
||||
});
|
||||
|
||||
test('the publish mail carries the manage hint at the bottom, tagged with its origin', function () {
|
||||
/** @var TestCase $this */
|
||||
$company = claimCompany();
|
||||
$release = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create([
|
||||
'company_id' => $company->id, 'language' => 'de',
|
||||
]);
|
||||
|
||||
$mail = new PressReleasePublished($release->user, $release);
|
||||
|
||||
// E4 (15.09.2026): Hinweis in der Mail, unten, kein Angebot.
|
||||
expect($mail->manageNewsroomUrl)->toStartWith('https://businessportal24.test/de/newsroom/muster-gmbh/verwalten?')
|
||||
->and($mail->manageNewsroomUrl)->toContain('src='.ConversionSource::MailPublish->value)
|
||||
->and($mail->render())->toContain('Diesen Newsroom verwalten');
|
||||
|
||||
// Ohne Firma kein Hinweis — und nichts bricht.
|
||||
$orphan = PressRelease::factory()->published()->create(['company_id' => null]);
|
||||
expect((new PressReleasePublished($orphan->user, $orphan))->manageNewsroomUrl)->toBeNull();
|
||||
});
|
||||
|
||||
test('a claim coming from the publish mail keeps the mail origin on the token', function () {
|
||||
/** @var TestCase $this */
|
||||
$company = claimCompany();
|
||||
|
||||
$this->post(claimUrl(), ['src' => ConversionSource::MailPublish->value])->assertSessionHas('newsroom-claim-status');
|
||||
|
||||
$user = User::query()->where('email', 'info@muster.de')->firstOrFail();
|
||||
$link = MagicLink::query()->where('user_id', $user->id)->latest('id')->firstOrFail();
|
||||
|
||||
expect($link->source)->toBe(ConversionSource::MailPublish->value)
|
||||
->and($link->payload['company_id'])->toBe($company->id);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue