web: Boost als „Gebuchte Platzierung“ mit Erklärung gekennzeichnet
Label „Gebuchte Platzierung“ (en „Booked placement“) statt „Top-Platzierung“ auf Start- und Branchenseite. Die Erklärung „Das Unternehmen hat diesen Platz gebucht …“ erscheint am Desktop beim Überfahren und auf Mobil per Tipp, ohne den umgebenden Link zu öffnen; Screenreader lesen sie mit. Kopfzeile über dem Aufmacher „Aktuell“ statt „Top-Auswahl“ (automatische Auswahl, keine Redaktion). Preisseite nennt die Kennzeichnung (Rechtspaket U10). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8124d20946
commit
2150d4b7ae
12 changed files with 72 additions and 27 deletions
|
|
@ -126,7 +126,7 @@ return [
|
|||
],
|
||||
'boost' => [
|
||||
'title' => 'Boost — was Sie bekommen und was nicht',
|
||||
'text' => 'Geschuldet ist die Platzierung während der gebuchten Laufzeit — beim Startseiten-Boost auf Startseite und Kategorieseite, beim Kategorie-Boost auf der Kategorieseite der jeweiligen Rubrik. Die Zahl gleichzeitiger Boosts je Seite ist begrenzt; sind alle Plätze belegt, ist eine neue Buchung erst möglich, sobald ein Platz frei wird — verkauft wird also nur, was auch sichtbar ist. Eine bestimmte Reichweite oder Zugriffszahl versprechen wir nicht — das wäre unseriös. Buchbar nur für Mitteilungen mit Prüfergebnis Grün. Ist das Portal während eines aktiven Boosts aus von uns zu vertretenden Gründen länger als 60 Minuten am Stück nicht erreichbar, verlängert sich die Boost-Laufzeit entsprechend.',
|
||||
'text' => 'Geschuldet ist die Platzierung während der gebuchten Laufzeit — beim Startseiten-Boost auf Startseite und Kategorieseite, beim Kategorie-Boost auf der Kategorieseite der jeweiligen Rubrik. Geboostete Meldungen sind als gebuchte Platzierung gekennzeichnet. Die Zahl gleichzeitiger Boosts je Seite ist begrenzt; sind alle Plätze belegt, ist eine neue Buchung erst möglich, sobald ein Platz frei wird — verkauft wird also nur, was auch sichtbar ist. Eine bestimmte Reichweite oder Zugriffszahl versprechen wir nicht — das wäre unseriös. Buchbar nur für Mitteilungen mit Prüfergebnis Grün. Ist das Portal während eines aktiven Boosts aus von uns zu vertretenden Gründen länger als 60 Minuten am Stück nicht erreichbar, verlängert sich die Boost-Laufzeit entsprechend.',
|
||||
],
|
||||
'partners' => [
|
||||
'title' => 'Partnerportale',
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ return [
|
|||
'read_time' => ':minutes Min. Lesezeit',
|
||||
'minutes_short' => ':minutes Min.',
|
||||
'recommendation' => 'Empfehlung',
|
||||
'boosted_placement' => 'Top-Platzierung',
|
||||
'boosted_placement' => 'Gebuchte Platzierung',
|
||||
'boosted_placement_hint' => 'Das Unternehmen hat diesen Platz gebucht. Der Inhalt ist eine Pressemitteilung des Unternehmens.',
|
||||
],
|
||||
|
||||
'countries' => [
|
||||
|
|
@ -76,7 +77,7 @@ return [
|
|||
'hero' => [
|
||||
'title' => 'Im Fokus',
|
||||
'clock_format' => 'dddd, HH:mm [Uhr]',
|
||||
'editorial_pick' => 'Top-Auswahl',
|
||||
'latest' => 'Aktuell',
|
||||
'top_story' => 'Top-Meldung',
|
||||
'sidebar_title' => 'Was sonst zählt',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ return [
|
|||
],
|
||||
'boost' => [
|
||||
'title' => 'Boost — what you get and what you do not',
|
||||
'text' => 'What is owed is the placement during the booked period — on the homepage and category page for a homepage boost, on the category page of the respective industry for a category boost. The number of simultaneous boosts per page is limited; once all slots are taken, a new booking is only possible when a slot becomes free — we only sell what is actually visible. We do not promise a specific reach or number of hits — that would be dishonest. Bookable only for releases with a green review result. If the portal is unavailable for more than 60 consecutive minutes during an active boost for reasons within our responsibility, the boost period is extended accordingly.',
|
||||
'text' => 'What is owed is the placement during the booked period — on the homepage and category page for a homepage boost, on the category page of the respective industry for a category boost. Boosted releases are labelled as a booked placement. The number of simultaneous boosts per page is limited; once all slots are taken, a new booking is only possible when a slot becomes free — we only sell what is actually visible. We do not promise a specific reach or number of hits — that would be dishonest. Bookable only for releases with a green review result. If the portal is unavailable for more than 60 consecutive minutes during an active boost for reasons within our responsibility, the boost period is extended accordingly.',
|
||||
],
|
||||
'partners' => [
|
||||
'title' => 'Partner portals',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ return [
|
|||
'read_time' => ':minutes min read',
|
||||
'minutes_short' => ':minutes min',
|
||||
'recommendation' => 'Recommended',
|
||||
'boosted_placement' => 'Featured',
|
||||
'boosted_placement' => 'Booked placement',
|
||||
'boosted_placement_hint' => 'The company booked this spot. The content is a press release issued by the company.',
|
||||
],
|
||||
|
||||
'countries' => [
|
||||
|
|
@ -71,7 +72,7 @@ return [
|
|||
'hero' => [
|
||||
'title' => 'In Focus',
|
||||
'clock_format' => 'dddd, HH:mm',
|
||||
'editorial_pick' => 'Top selection',
|
||||
'latest' => 'Latest',
|
||||
'top_story' => 'Top story',
|
||||
'sidebar_title' => 'Also worth noting',
|
||||
],
|
||||
|
|
|
|||
27
resources/views/components/web/boosted-label.blade.php
Normal file
27
resources/views/components/web/boosted-label.blade.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
@props([
|
||||
// tag = Schild auf dem Aufmacher-Bild, inline = kleiner Rahmen hinter der Überschrift in Listen
|
||||
'variant' => 'inline',
|
||||
])
|
||||
|
||||
{{-- Kennzeichnung gebuchter Platzierungen (Rechtspaket 10/2026, U10): das
|
||||
Label allein sagt, dass der Platz gebucht wurde; die Erklärung erscheint
|
||||
am Desktop beim Überfahren und auf Mobil per Tipp, ohne dass der Tipp den
|
||||
umgebenden Link öffnet. Screenreader lesen die Erklärung als Teil des
|
||||
Linktexts mit. --}}
|
||||
<span x-data="{ hover: false, pinned: false }"
|
||||
@mouseenter="hover = true" @mouseleave="hover = false"
|
||||
@click.prevent.stop="pinned = ! pinned" @click.outside="pinned = false"
|
||||
class="relative {{ $variant === 'tag' ? 'inline-flex' : 'inline' }} cursor-help" data-boosted-label>
|
||||
@if ($variant === 'tag')
|
||||
<span class="bp-tag">{{ __('web.shared.boosted_placement') }}</span>
|
||||
@else
|
||||
<span class="inline-flex items-center whitespace-nowrap align-middle border border-brand/30 bg-brand/[0.04] px-1.5 py-px font-mono text-[9.5px] font-bold uppercase tracking-[0.1em] text-brand">{{ __('web.shared.boosted_placement') }}</span>
|
||||
@endif
|
||||
<span class="sr-only">: {{ __('web.shared.boosted_placement_hint') }}</span>
|
||||
<span x-show="hover || pinned" style="display: none" aria-hidden="true" data-boosted-label-hint
|
||||
@class([
|
||||
'z-30 bg-bg-dark px-3 py-2 font-sans text-[12px] font-normal normal-case leading-[1.45] tracking-normal text-white shadow-lg',
|
||||
'absolute left-0 top-full mt-1.5 w-64 max-w-[calc(100vw-3rem)]' => $variant === 'tag',
|
||||
'mt-1.5 block sm:absolute sm:right-0 sm:top-full sm:w-64' => $variant !== 'tag',
|
||||
])>{{ __('web.shared.boosted_placement_hint') }}</span>
|
||||
</span>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">
|
||||
{{ __('web.hero.title') }}
|
||||
</h2>
|
||||
<div class="eyebrow muted">{{ ucfirst(now()->isoFormat(__('web.hero.clock_format'))) }} · {{ __('web.hero.editorial_pick') }}</div>
|
||||
<div class="eyebrow muted">{{ ucfirst(now()->isoFormat(__('web.hero.clock_format'))) }} · {{ __('web.hero.latest') }}</div>
|
||||
</header>
|
||||
<hr class="rule-strong mb-6">
|
||||
|
||||
|
|
@ -52,10 +52,10 @@
|
|||
<x-web.hero-fallback />
|
||||
@endif
|
||||
|
||||
<div class="absolute top-4 left-4 sm:top-6 sm:left-6 z-10 flex gap-2">
|
||||
<div class="absolute top-4 left-4 sm:top-6 sm:left-6 z-20 flex gap-2">
|
||||
<span class="bp-tag">{{ __('web.hero.top_story') }}</span>
|
||||
@if (in_array($leadRelease->id, $boostedIds, true))
|
||||
<span class="bp-tag">{{ __('web.shared.boosted_placement') }}</span>
|
||||
<x-web.boosted-label variant="tag" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -47,9 +47,7 @@
|
|||
<h3 class="font-serif text-[15.5px] leading-[1.3] m-0 font-medium tracking-[-0.1px] text-ink group-hover:text-brand transition-colors">
|
||||
{{ $title }}
|
||||
@if ($boosted)
|
||||
<span class="inline-flex items-center whitespace-nowrap align-middle border border-brand/30 bg-brand/[0.04] px-1.5 py-px font-mono text-[9.5px] font-bold uppercase tracking-[0.1em] text-brand">
|
||||
{{ __('web.shared.boosted_placement') }}
|
||||
</span>
|
||||
<x-web.boosted-label />
|
||||
@endif
|
||||
</h3>
|
||||
<div class="text-[11.5px] text-ink-3 mt-1.5 flex items-center gap-1.5">
|
||||
|
|
|
|||
|
|
@ -183,10 +183,10 @@
|
|||
@else
|
||||
<x-web.hero-fallback />
|
||||
@endif
|
||||
<div class="absolute top-4 left-4 sm:top-5 sm:left-5 z-10 flex gap-2">
|
||||
<div class="absolute top-4 left-4 sm:top-5 sm:left-5 z-20 flex gap-2">
|
||||
<span class="bp-tag">{{ __('web.hero.top_story') }}</span>
|
||||
@if (in_array($leadRelease->id, $boostedIds ?? [], true))
|
||||
<span class="bp-tag">{{ __('web.shared.boosted_placement') }}</span>
|
||||
<x-web.boosted-label variant="tag" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="relative z-10 px-4 sm:px-9 pb-6 sm:pb-8 pt-14 sm:pt-[70px] text-white" style="background:linear-gradient(180deg,transparent,rgba(0,0,0,0.92));">
|
||||
|
|
@ -233,9 +233,7 @@
|
|||
<h3 class="font-serif text-[17px] leading-[1.28] m-0 font-semibold tracking-[-0.2px] text-ink group-hover:text-brand transition-colors">
|
||||
{{ $top->title }}
|
||||
@if (in_array($top->id, $boostedIds ?? [], true))
|
||||
<span class="inline-flex items-center whitespace-nowrap align-middle border border-brand/30 bg-brand/[0.04] px-1.5 py-px font-mono text-[9.5px] font-bold uppercase tracking-[0.1em] text-brand">
|
||||
{{ __('web.shared.boosted_placement') }}
|
||||
</span>
|
||||
<x-web.boosted-label />
|
||||
@endif
|
||||
</h3>
|
||||
@if ($top->company?->name)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use Tests\TestCase;
|
|||
/**
|
||||
* Boost-Platzierung (Decision-Update §2.2): geboostete PMs besetzen die
|
||||
* Top-Positionen auf Start- und Branchenseite vor der Chronologie und werden
|
||||
* als „Top-Platzierung" markiert.
|
||||
* als „Gebuchte Platzierung" markiert.
|
||||
*/
|
||||
function boostPlacementFixture(): array
|
||||
{
|
||||
|
|
@ -69,10 +69,10 @@ test('a boosted release takes the homepage lead ahead of newer releases and is l
|
|||
// Die verdrängte neueste Meldung rückt in die Seitenleiste nach.
|
||||
->and($data['sideReleases']->modelKeys())->toContain($newest->id);
|
||||
|
||||
// Badge „Top-Platzierung" hängt am Hero-Lead (nach dem Ticker-Vorkommen).
|
||||
// Badge „Gebuchte Platzierung" hängt am Hero-Lead (nach dem Ticker-Vorkommen).
|
||||
$this->get('https://presseecho.test/de')
|
||||
->assertSuccessful()
|
||||
->assertSeeTextInOrder(['Top-Platzierung', 'Geboostete aeltere Meldung']);
|
||||
->assertSeeTextInOrder(['Gebuchte Platzierung', 'Geboostete aeltere Meldung']);
|
||||
});
|
||||
|
||||
test('an expired boost does not influence the homepage placement', function () {
|
||||
|
|
@ -87,7 +87,7 @@ test('an expired boost does not influence the homepage placement', function () {
|
|||
|
||||
$this->get('https://presseecho.test/de')
|
||||
->assertSuccessful()
|
||||
->assertDontSeeText('Top-Platzierung');
|
||||
->assertDontSeeText('Gebuchte Platzierung');
|
||||
});
|
||||
|
||||
test('booking a boost invalidates the cached start and category pages immediately', function () {
|
||||
|
|
@ -101,7 +101,7 @@ test('booking a boost invalidates the cached start and category pages immediatel
|
|||
expect(app(HomePageData::class)->build(Portal::Presseecho)['leadRelease']->id)->toBe($newest->id);
|
||||
$this->get('https://presseecho.test/de/category/energie-klima')
|
||||
->assertSuccessful()
|
||||
->assertDontSeeText('Top-Platzierung');
|
||||
->assertDontSeeText('Gebuchte Platzierung');
|
||||
|
||||
app(BoostService::class)->boost($owner, $boosted, 7);
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ test('booking a boost invalidates the cached start and category pages immediatel
|
|||
expect(app(HomePageData::class)->build(Portal::Presseecho)['leadRelease']->id)->toBe($boosted->id);
|
||||
$this->get('https://presseecho.test/de/category/energie-klima')
|
||||
->assertSuccessful()
|
||||
->assertSeeText('Top-Platzierung');
|
||||
->assertSeeText('Gebuchte Platzierung');
|
||||
});
|
||||
|
||||
test('a boosted release leads its category page and is labelled', function () {
|
||||
|
|
@ -123,7 +123,7 @@ test('a boosted release leads its category page and is labelled', function () {
|
|||
$this->get('https://presseecho.test/de/category/energie-klima')
|
||||
->assertSuccessful()
|
||||
->assertSeeTextInOrder([
|
||||
'Top-Platzierung',
|
||||
'Gebuchte Platzierung',
|
||||
'Geboostete aeltere Meldung',
|
||||
'Neueste unbezahlte Meldung',
|
||||
]);
|
||||
|
|
@ -138,7 +138,7 @@ test('a category boost leads its category page but never the homepage', function
|
|||
// Branchenseite: Kategorie-Boost besetzt den Lead-Slot.
|
||||
$this->get('https://presseecho.test/de/category/energie-klima')
|
||||
->assertSuccessful()
|
||||
->assertSeeTextInOrder(['Top-Platzierung', 'Geboostete aeltere Meldung']);
|
||||
->assertSeeTextInOrder(['Gebuchte Platzierung', 'Geboostete aeltere Meldung']);
|
||||
|
||||
// Startseite: bleibt bei der Chronologie — der Kategorie-Boost kauft
|
||||
// sie nicht mit (Entscheidung 13.07.).
|
||||
|
|
@ -189,7 +189,7 @@ test('booking a category boost invalidates the category page but keeps the homep
|
|||
expect(app(HomePageData::class)->build(Portal::Presseecho)['leadRelease']->id)->toBe($newest->id);
|
||||
$this->get('https://presseecho.test/de/category/energie-klima')
|
||||
->assertSuccessful()
|
||||
->assertDontSeeText('Top-Platzierung');
|
||||
->assertDontSeeText('Gebuchte Platzierung');
|
||||
|
||||
app(BoostService::class)->boost($owner, $boosted, 7, BoostPlacement::Category);
|
||||
|
||||
|
|
@ -198,5 +198,24 @@ test('booking a category boost invalidates the category page but keeps the homep
|
|||
expect(Cache::has(webPageCacheKey('web.home.presseecho.de')))->toBeTrue();
|
||||
$this->get('https://presseecho.test/de/category/energie-klima')
|
||||
->assertSuccessful()
|
||||
->assertSeeText('Top-Platzierung');
|
||||
->assertSeeText('Gebuchte Platzierung');
|
||||
});
|
||||
|
||||
test('the booked placement label explains itself and the hero no longer claims an editorial pick', function () {
|
||||
/** @var TestCase $this */
|
||||
['boosted' => $boosted] = boostPlacementFixture();
|
||||
|
||||
Boost::factory()->for($boosted)->create();
|
||||
|
||||
// Erklärung am Label (Tooltip/Tipp) und als Screenreader-Text; die
|
||||
// Kopfzeile des Aufmachers nennt keine Auswahl mehr, sondern „Aktuell".
|
||||
$this->get('https://presseecho.test/de')
|
||||
->assertSuccessful()
|
||||
->assertSee('data-boosted-label', false)
|
||||
->assertSeeText('Das Unternehmen hat diesen Platz gebucht. Der Inhalt ist eine Pressemitteilung des Unternehmens.')
|
||||
->assertSeeText('Aktuell')
|
||||
->assertDontSeeText('Top-Auswahl');
|
||||
|
||||
expect(__('web.shared.boosted_placement', [], 'en'))->toBe('Booked placement')
|
||||
->and(__('web.hero.latest', [], 'en'))->toBe('Latest');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ test('the pricing page renders the editorial shell with real plans, single pm an
|
|||
->assertSeeText('Add-ons & Einzelkosten')
|
||||
->assertSeeText('Extra-Pressemitteilung')
|
||||
->assertSeeText('Boost-Platzierung')
|
||||
->assertSeeText('Geboostete Meldungen sind als gebuchte Platzierung gekennzeichnet.')
|
||||
->assertSeeText('Veröffentlichungsnachweis (PDF)')
|
||||
->assertSeeText('Credit-Pakete')
|
||||
->assertSeeText('7 Tage') // boost 7d aus config
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue