Spoke-Frontend-Audit: Fake-Inhalte raus, Newsletter-Rueckbau, echte Boersendaten, EN-Lokalisierung

Frontend-Umbau (Kevin, 06.07.2026), Ergebnis des businessportal24-Audits:

- Fake-Anzeigen und Mock-/Fallback-Daten entfernt (focus-ad, feed-ad,
  highlight-card, press-release-card, featured-releases, press-releases-grid);
  Sektionen rendern nur noch mit echten Daten oder gar nicht
- Newsletter-Strip und Newsletter-Box zurueckgebaut (Feature nachgelagert,
  vor Reaktivierung DOI-Flow noetig)
- Echte Boersendaten: MarketDataService (Yahoo-Finance-Quotes, config/market.php,
  Cache) + market:refresh alle 15 Min. im Scheduler; live-ticker blendet sich
  ohne Daten aus
- EN-Lokalisierung der Kern-Seiten via lang/de/web.php + lang/en/web.php;
  Carbon-Locale folgt app()->getLocale()
- Web-Tests auf den neuen Frontend-Stand nachgezogen (Suite: 993 gruen)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-06 13:36:23 +00:00
parent 3320aaaa85
commit bfad13af7d
67 changed files with 1735 additions and 1521 deletions

View file

@ -0,0 +1,36 @@
<?php
namespace App\Console\Commands;
use App\Services\Web\MarketDataService;
use Illuminate\Console\Command;
class RefreshMarketData extends Command
{
protected $signature = 'market:refresh';
protected $description = 'Kursdaten (DAX, EUR/USD, Brent) für den Web-Ticker abrufen und cachen';
public function handle(MarketDataService $marketData): int
{
if (! config('market.enabled')) {
$this->info('Marktdaten sind deaktiviert (MARKET_DATA_ENABLED=false).');
return self::SUCCESS;
}
$slides = $marketData->refresh();
if ($slides === []) {
$this->warn('Keine Kursdaten erhalten — Cache unverändert.');
return self::FAILURE;
}
foreach ($slides as $slide) {
$this->line(sprintf('%s: %s (%s)', $slide['label'], $slide['value'], $slide['delta']));
}
return self::SUCCESS;
}
}

View file

@ -9,12 +9,11 @@ use App\Models\PressRelease;
use App\Support\EditorialClock;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
/**
* Datenaufbereitung der öffentlichen Startseite einer Ausgabe: Lead-/Seiten-/
* meistgelesene Meldungen, aktive Newsrooms, Branchen-Index, „Heute im Fokus"
* (Spotlight), Ticker und Kennzahlen.
* (Spotlight) und Kennzahlen. Der Ad-Hoc-Ticker kommt zentral aus TickerData.
*/
class HomePageData
{
@ -207,26 +206,15 @@ class HomePageData
$spotlight = [
'industry' => $spotlightTranslation->name,
'stats' => [
['label' => 'Meldungen (7 Tage)', 'value' => number_format($windowCount, 0, ',', '.'), 'sub' => 'Stand '.$referenceNow->translatedFormat('j. M Y')],
['label' => 'Aktive Unternehmen', 'value' => number_format($activeCompanies, 0, ',', '.'), 'sub' => 'in dieser Branche'],
['label' => 'Im Archiv', 'value' => number_format($archiveCount, 0, ',', '.'), 'sub' => 'Meldungen gesamt'],
['label' => __('web.spotlight.stat_window'), 'value' => number_format($windowCount, 0, ',', '.'), 'sub' => __('web.spotlight.stat_window_sub', ['date' => $referenceNow->translatedFormat('j. M Y')])],
['label' => __('web.spotlight.stat_companies'), 'value' => number_format($activeCompanies, 0, ',', '.'), 'sub' => __('web.spotlight.stat_companies_sub')],
['label' => __('web.spotlight.stat_archive'), 'value' => number_format($archiveCount, 0, ',', '.'), 'sub' => __('web.spotlight.stat_archive_sub')],
],
'releases' => $spotlightReleases->all(),
];
}
}
// Ad-Hoc-Ticker aus den jüngsten echten Pressemitteilungen.
$tickerItems = $publishedQuery()
->orderByDesc('published_at')
->limit(8)
->get(['id', 'title', 'published_at'])
->map(static fn (PressRelease $release): array => [
'time' => $release->published_at?->format('H:i') ?? '',
'text' => Str::limit((string) $release->title, 70),
])
->all();
return [
'leadRelease' => $leadRelease,
'sideReleases' => $sideReleases,
@ -234,7 +222,6 @@ class HomePageData
'activeNewsrooms' => $activeNewsrooms,
'industryIndex' => $industryIndex,
'spotlight' => $spotlight,
'tickerItems' => $tickerItems,
'homeStats' => [
'publishedCount' => $publishedQuery()->count(),
'publishedToday' => $publishedQuery()->whereDate('published_at', $referenceNow->toDateString())->count(),

View file

@ -0,0 +1,109 @@
<?php
namespace App\Services\Web;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Kursdaten für den Ad-Hoc-Ticker der Web-Frontends.
*
* refresh() fragt die konfigurierten Symbole (config/market.php) über die
* Yahoo-Finance-Chart-API ab und legt sie in den Cache; die Views lesen nur
* aus dem Cache (slides()). Schlägt der Abruf für alle Symbole fehl, bleibt
* der bestehende Cache-Stand erhalten, damit ein API-Ausfall die Anzeige
* nicht sofort leert.
*/
class MarketDataService
{
/**
* @return list<array{label: string, value: string, delta: string, positive: bool}>
*/
public function slides(): array
{
if (! config('market.enabled')) {
return [];
}
/** @var list<array{label: string, value: string, delta: string, positive: bool}> $slides */
$slides = Cache::get((string) config('market.cache_key'), []);
return is_array($slides) ? $slides : [];
}
/**
* @return list<array{label: string, value: string, delta: string, positive: bool}>
*/
public function refresh(): array
{
$slides = [];
foreach ((array) config('market.symbols', []) as $config) {
$quote = $this->fetchQuote((string) $config['symbol']);
if ($quote === null) {
continue;
}
[$price, $previousClose] = $quote;
$deltaPercent = $previousClose > 0.0
? ($price - $previousClose) / $previousClose * 100
: 0.0;
$slides[] = [
'label' => (string) $config['label'],
'value' => number_format($price, (int) ($config['precision'] ?? 2), ',', '.').($config['suffix'] ?? ''),
'delta' => ($deltaPercent >= 0 ? '+' : '').number_format($deltaPercent, 1, ',', '.').'%',
'positive' => $deltaPercent >= 0,
];
}
if ($slides === []) {
Log::warning('market:refresh lieferte keine Kursdaten — bestehender Cache-Stand bleibt erhalten.');
return $this->slides();
}
Cache::put((string) config('market.cache_key'), $slides, (int) config('market.cache_ttl', 86400));
return $slides;
}
/**
* @return array{0: float, 1: float}|null [Kurs, Vortagesschluss]
*/
private function fetchQuote(string $symbol): ?array
{
try {
$response = Http::timeout(10)
->withHeaders(['User-Agent' => 'Mozilla/5.0 (compatible; PressekontoTicker/1.0)'])
->get(config('market.endpoint').rawurlencode($symbol), [
'range' => '1d',
'interval' => '1d',
]);
} catch (\Throwable $exception) {
Log::warning('Kursabruf fehlgeschlagen.', ['symbol' => $symbol, 'error' => $exception->getMessage()]);
return null;
}
if (! $response->successful()) {
Log::warning('Kursabruf fehlgeschlagen.', ['symbol' => $symbol, 'status' => $response->status()]);
return null;
}
$meta = $response->json('chart.result.0.meta');
$price = $meta['regularMarketPrice'] ?? null;
$previousClose = $meta['chartPreviousClose'] ?? $meta['previousClose'] ?? null;
if (! is_numeric($price) || ! is_numeric($previousClose)) {
return null;
}
return [(float) $price, (float) $previousClose];
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace App\Services\Web;
use App\Enums\Portal;
use App\Models\PressRelease;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
/**
* Ad-Hoc-Ticker-Meldungen der Web-Frontends: die jüngsten veröffentlichten
* Pressemitteilungen des Portals. Zentral hier statt je Seite, damit der
* Ticker auf allen Unterseiten identisch gefüllt ist; kurz gecacht, weil
* der Header auf jeder Seite rendert.
*/
class TickerData
{
private const CACHE_TTL_SECONDS = 300;
/**
* @return list<array{time: string, text: string}>
*/
public function items(Portal $portal, string $language): array
{
$cacheKey = sprintf('web.ticker.%s.%s', $portal->value, $language);
return Cache::remember($cacheKey, self::CACHE_TTL_SECONDS, function () use ($portal, $language): array {
return PressRelease::query()
->forPortalValues([$portal->value, Portal::Both->value])
->forLanguage($language)
->visibleInArchive()
->orderByDesc('published_at')
->limit(8)
->get(['id', 'title', 'published_at'])
->map(static fn (PressRelease $release): array => [
'time' => $release->published_at?->format('H:i') ?? '',
'text' => Str::limit((string) $release->title, 70),
])
->all();
});
}
}

View file

@ -23,13 +23,21 @@ class WebPortalContext
public readonly array $domainConfig,
) {}
/**
* Portal-Ableitung ohne Seiteneffekte (kein Anwenden der Domain-Config) —
* für Komponenten, die nur wissen müssen, auf welchem Portal sie rendern.
*/
public static function portalFromHost(): Portal
{
return str_contains(request()->getHost(), 'presseecho')
? Portal::Presseecho
: Portal::Businessportal24;
}
public static function fromRequest(): self
{
$host = request()->getHost();
[$domainKey, $portal] = str_contains($host, 'presseecho')
? ['presseecho', Portal::Presseecho]
: ['businessportal24', Portal::Businessportal24];
$portal = self::portalFromHost();
$domainKey = $portal === Portal::Presseecho ? 'presseecho' : 'businessportal24';
return new self(
domainKey: $domainKey,

View file

@ -57,11 +57,11 @@ return [
'report_email' => env('REPORT_EMAIL_PRESSEKONTO'),
'umami_website_id' => 'ca440b65-a5bd-42c1-8cdb-96f34734003c',
'tagline_short' => 'Publisher · Hub',
'tagline_long' => 'Der gemeinsame Publisher-Bereich für presseecho und businessportal24 – Pressemitteilungen schreiben, redaktionell prüfen lassen, auf beiden Reichweiten veröffentlichen.',
'tagline_long' => 'Der gemeinsame Publisher-Bereich für presseecho und businessportal24 – Pressemitteilungen schreiben, automatisiert prüfen lassen, auf beiden Reichweiten veröffentlichen.',
'footer_legal' => '© :year pressekonto · Alle Rechte vorbehalten',
'about_label' => 'Über pressekonto',
'meta_title' => 'pressekonto – Publisher-Hub für presseecho und businessportal24',
'meta_description' => 'Ein Konto, zwei Reichweiten: Pressemitteilungen redaktionell geprüft auf presseecho und businessportal24 gleichzeitig veröffentlichen.',
'meta_description' => 'Ein Konto, zwei Reichweiten: Pressemitteilungen qualitätsgeprüft auf presseecho und businessportal24 gleichzeitig veröffentlichen.',
],
],
@ -85,16 +85,16 @@ return [
'report_email' => env('REPORT_EMAIL_PRESSEECHO'),
'umami_website_id' => '3ea8a7b0-105d-4519-b200-138cadc0fbdc',
'tagline_short' => 'Pressemitteilungen · DACH',
'tagline_long' => 'Fachmeldungen aus Wirtschaft und Industrie. Redaktionell geprüft. Strukturiert distribuiert.',
'tagline_long' => 'Fachmeldungen aus Wirtschaft und Industrie. Qualitätsgeprüft. Strukturiert distribuiert.',
'footer_legal' => '© :year presseecho · Alle Rechte vorbehalten',
'about_label' => 'Über presseecho',
'meta_title' => 'Aktuelle Pressemitteilungen aus Wirtschaft & Industrie – presseecho',
'meta_description' => 'Redaktionell kuratierte Pressemitteilungen aus Wirtschaft und Industrie im DACH-Raum.',
'meta_description' => 'Qualitätsgeprüfte Pressemitteilungen aus Wirtschaft und Industrie im DACH-Raum.',
'newsletter_topics' => [
['name' => 'Tageszusammenfassung', 'meta' => 'Mo–Fr · 07:00 Uhr', 'active' => true],
['name' => 'Wochenrückblick', 'meta' => 'Sonntags · 18:00 Uhr', 'active' => false],
['name' => 'Energie & Klima', 'meta' => 'ab Content-Score 80 · max. 2× pro Woche', 'active' => true],
['name' => 'Industrie & Forschung', 'meta' => 'ausgewählt von der Redaktion · ca. 3–5 Mails/Monat', 'active' => false],
['name' => 'Industrie & Forschung', 'meta' => 'kuratierte Auswahl · ca. 3–5 Mails/Monat', 'active' => false],
],
],
],
@ -119,16 +119,16 @@ return [
'report_email' => env('REPORT_EMAIL_BUSINESSPORTAL'),
'umami_website_id' => 'f03a5fc3-ac44-488f-9118-84004861099f',
'tagline_short' => 'Pressemitteilungen · DACH',
'tagline_long' => 'Veröffentlichungs-Portal für redaktionell geprüfte Pressemitteilungen aus Deutschland, Österreich und der Schweiz.',
'tagline_long' => 'Veröffentlichungs-Portal für qualitätsgeprüfte Pressemitteilungen aus Deutschland, Österreich und der Schweiz.',
'footer_legal' => '© :year businessportal24 · Alle Rechte vorbehalten',
'about_label' => 'Über businessportal24',
'meta_title' => 'Aktuelle Pressemitteilungen aus der deutschen Wirtschaft – businessportal24',
'meta_description' => 'Pressemitteilungen aus Deutschland, Österreich und der Schweiz. Redaktionell geprüft. Strukturiert distribuiert.',
'meta_description' => 'Pressemitteilungen aus Deutschland, Österreich und der Schweiz. Qualitätsgeprüft. Strukturiert distribuiert.',
'newsletter_topics' => [
['name' => 'Tageszusammenfassung', 'meta' => 'Mo–Fr · 07:00 Uhr', 'active' => true],
['name' => 'Wochenrückblick', 'meta' => 'Sonntags · 18:00 Uhr', 'active' => false],
['name' => 'Energie & Klima', 'meta' => 'ab Content-Score 80 · max. 2× pro Woche', 'active' => true],
['name' => 'IPO & M&A', 'meta' => 'ausgewählt von der Redaktion · ca. 3–5 Mails/Monat', 'active' => false],
['name' => 'IPO & M&A', 'meta' => 'kuratierte Auswahl · ca. 3–5 Mails/Monat', 'active' => false],
],
],
],

40
config/market.php Normal file
View file

@ -0,0 +1,40 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Börsendaten im Ad-Hoc-Ticker
|--------------------------------------------------------------------------
|
| Kursdaten für den Ticker der Web-Frontends. Abgerufen per Scheduler
| (market:refresh) über die Yahoo-Finance-Chart-API und gecacht — die
| Views lesen ausschließlich aus dem Cache. Ohne Daten (API down, noch
| kein Abruf) wird der Markt-Slot im Ticker ausgeblendet.
|
*/
'enabled' => env('MARKET_DATA_ENABLED', true),
'cache_key' => 'web.market-data',
// Cache-Lebensdauer in Sekunden. Bewusst großzügig (24 h), damit ein
// vorübergehender API-Ausfall die Anzeige nicht sofort leert; der
// Scheduler aktualisiert deutlich häufiger.
'cache_ttl' => 86400,
'endpoint' => env('MARKET_DATA_ENDPOINT', 'https://query1.finance.yahoo.com/v8/finance/chart/'),
/*
| label — Anzeigename im Ticker
| symbol — Yahoo-Finance-Symbol
| precision — Nachkommastellen der Kursanzeige
| suffix — optionales Einheiten-Suffix (z. B. " $")
*/
'symbols' => [
['label' => 'DAX', 'symbol' => '^GDAXI', 'precision' => 0, 'suffix' => ''],
['label' => 'EUR/USD', 'symbol' => 'EURUSD=X', 'precision' => 2, 'suffix' => ''],
['label' => 'Brent Öl', 'symbol' => 'BZ=F', 'precision' => 2, 'suffix' => ' $'],
],
];

View file

@ -110,7 +110,7 @@ return [
'notes' => [
'slots' => [
'title' => 'Slots und Kontingente',
'text' => 'Ihr Monatskontingent (Slots) gilt für den jeweiligen Abrechnungsmonat; nicht genutzte Slots verfallen mit Monatsende. Slots sind kein Geldguthaben. Ein Slot wird nur durch eine tatsächliche Veröffentlichung verbraucht — abgelehnte Mitteilungen kosten keinen Slot; schlägt eine Veröffentlichung durch einen technischen Fehler bei uns fehl, wird der Slot automatisch zurückgebucht. Tageslimits schützen die redaktionelle Qualität der Portale; darüber hinaus eingereichte Mitteilungen können für den Folgetag eingeplant werden.',
'text' => 'Ihr Monatskontingent (Slots) gilt für den jeweiligen Abrechnungsmonat; nicht genutzte Slots verfallen mit Monatsende. Slots sind kein Geldguthaben. Ein Slot wird nur durch eine tatsächliche Veröffentlichung verbraucht — abgelehnte Mitteilungen kosten keinen Slot; schlägt eine Veröffentlichung durch einen technischen Fehler bei uns fehl, wird der Slot automatisch zurückgebucht. Tageslimits schützen die inhaltliche Qualität der Portale; darüber hinaus eingereichte Mitteilungen können für den Folgetag eingeplant werden.',
],
'credits' => [
'title' => 'Credits',

307
lang/de/web.php Normal file
View file

@ -0,0 +1,307 @@
<?php
/*
|--------------------------------------------------------------------------
| Web-Frontend (presseecho / businessportal24) — Deutsche Ausgabe
|--------------------------------------------------------------------------
|
| Alle UI-Texte der öffentlichen Web-Frontends. Die englische Ausgabe
| liegt in lang/en/web.php. Lange Inhaltsseiten (FAQ, Hilfe,
| Rechtstexte) sind separat; Rechtstexte bleiben bewusst deutsch.
|
*/
return [
'meta' => [
'home_title' => 'Aktuelle Pressemitteilungen aus Wirtschaft & Industrie – :brand',
'home_description' => 'Pressemitteilungen aus Deutschland, Österreich und der Schweiz. Qualitätsgeprüft. Strukturiert distribuiert.',
],
'shared' => [
'yesterday' => 'Gestern',
'read_time' => ':minutes Min. Lesezeit',
'minutes_short' => ':minutes Min.',
'recommendation' => 'Empfehlung',
],
'countries' => [
'de' => 'Deutschland',
'at' => 'Österreich',
'ch' => 'Schweiz',
],
'header' => [
'edition' => 'Ausgabe',
'pricing' => 'Preise',
'homepage' => 'Startseite',
'login' => 'Anmelden',
'publish' => 'Veröffentlichen',
'publish_aria' => 'Pressemitteilung veröffentlichen',
'search_label' => 'Pressemitteilungen, Unternehmen, Branchen, ISIN durchsuchen',
'search_placeholder' => 'Pressemitteilungen, Unternehmen, Branchen, ISIN…',
'search_open' => 'Suche öffnen',
'search_close' => 'Suche schließen',
'search_eyebrow' => 'Suche',
'search_title' => 'Pressemitteilungen durchsuchen',
'search_submit' => 'Suchen',
'advanced_search' => 'Erweiterte Suche',
'press' => 'Drücken Sie',
'to_close' => 'zum Schließen',
],
'nav' => [
'home' => 'Startseite',
'menu' => 'Menü',
'aria_label' => 'Hauptnavigation',
'open_menu' => 'Hauptmenü öffnen',
'section' => 'Rubrik',
],
'ticker' => [
'aria_label' => 'Ad-Hoc-Ticker',
'sr_intro' => 'Aktuelle Ad-Hoc-Meldungen:',
],
'hero' => [
'title' => 'Im Fokus',
'clock_format' => 'dddd, HH:mm [Uhr]',
'editorial_pick' => 'Top-Auswahl',
'top_story' => 'Top-Meldung',
'sidebar_title' => 'Was sonst zählt',
],
'feed' => [
'title' => 'Aktuelle Meldungen',
'filter_all' => 'Alle',
'filter_today' => 'Heute',
'filter_week' => 'Diese Woche',
'empty' => 'Aktuell liegen keine Meldungen vor. Sobald Pressemitteilungen veröffentlicht werden, erscheinen sie hier.',
'more' => 'Weitere :count Meldungen anzeigen',
],
'most_read' => [
'title' => 'Meistgelesen',
],
'newsrooms' => [
'title' => 'Aktive Newsrooms',
'today' => 'Heute',
'count_suffix' => 'Meldungen letzte 7 Tage',
'active_today' => 'heute aktiv',
'all' => 'Alle Newsrooms anzeigen',
],
'spotlight' => [
'title' => 'Heute im Fokus',
'stat_window' => 'Meldungen (7 Tage)',
'stat_window_sub' => 'Stand :date',
'stat_companies' => 'Aktive Unternehmen',
'stat_companies_sub' => 'in dieser Branche',
'stat_archive' => 'Im Archiv',
'stat_archive_sub' => 'Meldungen gesamt',
],
'industry_index' => [
'title' => 'Branchen-Index',
'window' => 'Letzte 7 Tage',
'releases' => 'Meldungen',
],
'publisher_cta' => [
'eyebrow' => 'Für Unternehmen',
'title' => 'Veröffentlichen Sie Ihre Pressemitteilung',
'subline' => 'Reichweite in DACH · Inhaltsprüfung · Strukturierte Distribution',
'submit' => 'Jetzt einreichen',
'pricing' => 'Tarife & Pakete ansehen',
],
'publish_band' => [
'eyebrow' => 'Für Unternehmen',
'title' => 'Ihre Pressemitteilung, qualitätsgeprüft 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',
'footnote' => 'Ohne Abo-Zwang · Inhaltsprüfung werktags innerhalb 24 h',
'step1_title' => 'Konto anlegen',
'step1_text' => 'In rund zwei Minuten — ein Konto für alle Portale, Anmeldung wahlweise passwortlos per Magic-Link.',
'step2_title' => 'Meldung einreichen',
'step2_text' => 'Text, Bild und Ansprechpartner hochladen — die automatisierte Inhaltsprüfung läuft direkt mit.',
'step3_title' => 'Veröffentlichen',
'step3_text' => 'Nach bestandener Inhaltsprüfung live — inklusive Newsroom, Kategorien-Zuordnung und Archiv.',
],
'pricing_teaser' => [
'title' => 'Tarife & Pakete',
'details' => 'Alle Details',
'popular' => 'Beliebt',
'month' => 'Monat',
'year' => 'Jahr',
'two_months_free' => '2 Monate gratis',
'quota' => 'Pressemitteilungen / Monat',
'daily_limit' => 'max. pro Tag',
'no_daily_limit' => 'kein Tageslimit',
'select' => 'Auswählen',
'footnote' => 'Alle Preise zzgl. MwSt. · Jederzeit kündbar · Auch ohne Abo als Einzelveröffentlichung möglich.',
'publish_direct' => 'Direkt veröffentlichen',
],
'quality' => [
'eyebrow' => 'Unser Qualitätsstandard',
'claim' => 'Alle Pressemitteilungen durchlaufen eine automatisierte Inhaltsprüfung.',
'detail' => 'Mindestqualität gewährleistet durch unseren Content-Score — eine automatische Bewertung von Quellenqualität, Verifizierungsstatus und inhaltlicher Relevanz.',
'guidelines' => 'Veröffentlichungsrichtlinien',
],
'footer' => [
'sections' => 'Rubriken',
'newsrooms' => 'Newsrooms',
'for_companies' => 'Für Unternehmen',
'publish' => 'Veröffentlichen',
'pricing' => 'Tarife & Pakete',
'company' => 'Unternehmen',
'contact' => 'Kontakt',
'help' => 'Hilfe-Center',
'cancellation' => 'Verträge hier kündigen',
'imprint' => 'Impressum',
'privacy' => 'Datenschutz',
'terms' => 'AGB',
'guidelines' => 'Richtlinien',
'report' => 'Inhalte melden',
],
'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.',
'eyebrow' => 'Archiv-Suche',
'intro' => 'Stichwort, Unternehmen oder Branche – durchsuchen Sie das vollständige, qualitätsgeprüfte Archiv.',
'placeholder' => 'Suchbegriff, Unternehmen oder Branche…',
'searching' => 'sucht…',
'reset' => 'Suche zurücksetzen',
'all_categories' => 'Alle Rubriken',
'results' => 'Ergebnis|Ergebnisse',
'for_term' => 'für „:term"',
'sort' => 'Sortierung',
'sort_newest' => 'Neueste zuerst',
'sort_oldest' => 'Älteste zuerst',
'no_results' => 'Keine Treffer',
'no_results_text' => 'Für Ihre Suche gibt es derzeit keine Pressemitteilungen. Versuchen Sie einen anderen Begriff oder entfernen Sie Filter.',
'pagination' => 'Seitennummerierung',
'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.',
'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.',
'eyebrow' => 'Unternehmens-Newsrooms',
'intro' => 'Alle aktiven Unternehmen mit ihren qualitätsgeprüften 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',
'search_label' => 'Newsroom suchen',
'search_placeholder' => 'Unternehmen suchen…',
'reset_filter' => 'Filter zurücksetzen',
'latest_release' => 'Letzte Meldung :date',
'days_30' => '30 Tage',
'none_found' => 'Keine Newsrooms gefunden',
'none_found_term' => 'Für „:term" gibt es derzeit keinen passenden Newsroom. Versuchen Sie einen anderen Suchbegriff.',
'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.',
],
'newsroom_page' => [
'meta_description' => 'Newsroom von :company: alle qualitätsgeprüften 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',
'since' => 'Newsroom seit :year',
'industries' => 'Branchen',
'last_30_days' => 'Letzte 30 Tage',
'published_releases' => 'veröffentlichte Meldungen',
'total_hits' => 'Aufrufe gesamt',
'total_hits_sub' => 'über alle Meldungen',
'latest' => 'Aktuellste Meldung',
'empty' => 'Aktuell keine weiteren Meldungen in diesem Newsroom.',
'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',
],
'category' => [
'fallback_name' => 'Branche',
'default_description' => 'Aktuelle Pressemitteilungen aus dem Bereich :category – qualitätsgeprüft und strukturiert distribuiert im DACH-Raum.',
'meta_title' => 'Pressemitteilungen :category – :brand',
'eyebrow' => 'Branche',
'back_aria' => 'Zurück zu allen Meldungen',
'all_releases' => 'Alle Meldungen',
'month_stat' => ':count Meldungen in den letzten 30 Tagen · :today am :date',
'sub_topics' => 'Sub-Themen',
'stat_today' => 'Meldungen aktueller Tag',
'vs_previous_week' => 'ggü. Vorwoche',
'last_7_days' => 'in den letzten 7 Tagen',
'more_top' => 'Weitere Top-Meldungen',
'no_more_top' => 'Aktuell keine weiteren Top-Meldungen.',
'no_more_releases' => 'Aktuell keine weiteren Meldungen in dieser Branche.',
'week_stat' => ':count Meldungen in den letzten 7 Tagen',
'search_archive' => 'Im Archiv suchen',
'empty_title' => 'Noch keine Meldungen in :category',
'empty_text' => 'Sobald Pressemitteilungen in dieser Branche veröffentlicht werden, erscheinen sie hier.',
'sub_industries' => 'Sub-Branchen',
'volume_30_days' => 'Volumen letzte 30 Tage',
'trend_7_days' => '7-Tage-Trend',
'related' => 'Verwandte Branchen',
'cta_eyebrow' => 'Für Unternehmen aus :category',
'cta_title' => 'Veröffentlichen Sie Ihre Pressemitteilung gezielt in :category.',
'cta_text' => 'Reichweite über aktive Newsrooms · Strukturierte Distribution im DACH-Raum · Qualitätsgeprüft.',
'cta_submit' => 'Pressemitteilung einreichen',
'cta_pricing' => 'Tarife & Branchen-Pakete ansehen',
],
'detail' => [
'breadcrumbs' => 'Brotkrumen',
'press_release' => 'Pressemitteilung',
'embargo_lifted' => 'Embargo aufgehoben',
'audience' => 'Für Wirtschaftsjournalisten',
'time_at' => ':time Uhr',
'words' => ':count Wörter',
'recommended' => 'Empfehlung',
'recommended_hint' => 'Diese Pressemitteilung erreicht einen hohen Content-Score — basierend auf Quellenqualität, Verifizierungsstatus und inhaltlicher Relevanz.',
'image_placeholder' => 'Bild · Pressefoto',
'image_placeholder_company' => 'Bild · Pressefoto :company',
'photo' => 'Foto',
'about_company' => 'Über das Unternehmen',
'press_contact' => 'Pressekontakt',
'verified_hint' => 'Verifizierter Publisher — Identität und Domain bestätigt',
'verified_by' => 'Identität und Domain durch :brand bestätigt',
'verified_publisher' => 'Verifizierter Publisher',
'published_by' => 'Veröffentlicht von',
'to_newsroom' => 'Zum Newsroom',
'share' => 'Teilen',
'email' => 'E-Mail',
'copy_link' => 'Link kopieren',
'copied' => 'Kopiert!',
'keywords' => 'Schlagwörter',
'further' => 'Weiterführend',
'print' => 'Drucken',
'report_release' => 'Pressemitteilung melden',
'privacy_rights' => 'Datenschutz & Persönlichkeitsrecht',
'legal_question' => 'Sie haben ein rechtliches Anliegen zu dieser Mitteilung?',
'report_content' => 'Inhalt melden',
'remove_data' => 'Eigene Daten entfernen',
'reports_free' => 'Meldungen sind kostenlos.',
'more_from' => 'Mehr von :company',
'more_from_sub' => 'Aktuelle Mitteilungen aus diesem Newsroom',
'all_from' => 'Alle Mitteilungen von :company',
'related' => 'Verwandte Meldungen',
'related_sub' => 'Reaktionen, ähnliche Themen, Branchenkontext',
'all_from_category' => 'Alle Meldungen aus :category',
],
];

302
lang/en/web.php Normal file
View file

@ -0,0 +1,302 @@
<?php
/*
|--------------------------------------------------------------------------
| Web frontend (presseecho / businessportal24) — English edition
|--------------------------------------------------------------------------
*/
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.',
],
'shared' => [
'yesterday' => 'Yesterday',
'read_time' => ':minutes min read',
'minutes_short' => ':minutes min',
'recommendation' => 'Recommended',
],
'countries' => [
'de' => 'Germany',
'at' => 'Austria',
'ch' => 'Switzerland',
],
'header' => [
'edition' => 'Edition',
'pricing' => 'Pricing',
'homepage' => 'Homepage',
'login' => 'Sign in',
'publish' => 'Publish',
'publish_aria' => 'Publish a press release',
'search_label' => 'Search press releases, companies, industries, ISIN',
'search_placeholder' => 'Press releases, companies, industries, ISIN…',
'search_open' => 'Open search',
'search_close' => 'Close search',
'search_eyebrow' => 'Search',
'search_title' => 'Search press releases',
'search_submit' => 'Search',
'advanced_search' => 'Advanced search',
'press' => 'Press',
'to_close' => 'to close',
],
'nav' => [
'home' => 'Home',
'menu' => 'Menu',
'aria_label' => 'Main navigation',
'open_menu' => 'Open main menu',
'section' => 'Section',
],
'ticker' => [
'aria_label' => 'Ad-hoc ticker',
'sr_intro' => 'Latest ad-hoc announcements:',
],
'hero' => [
'title' => 'In Focus',
'clock_format' => 'dddd, HH:mm',
'editorial_pick' => 'Top selection',
'top_story' => 'Top story',
'sidebar_title' => 'Also worth noting',
],
'feed' => [
'title' => 'Latest releases',
'filter_all' => 'All',
'filter_today' => 'Today',
'filter_week' => 'This week',
'empty' => 'There are no releases at the moment. As soon as press releases are published, they will appear here.',
'more' => 'Show :count more releases',
],
'most_read' => [
'title' => 'Most read',
],
'newsrooms' => [
'title' => 'Active newsrooms',
'today' => 'Today',
'count_suffix' => 'releases in the last 7 days',
'active_today' => 'active today',
'all' => 'View all newsrooms',
],
'spotlight' => [
'title' => 'In focus today',
'stat_window' => 'Releases (7 days)',
'stat_window_sub' => 'As of :date',
'stat_companies' => 'Active companies',
'stat_companies_sub' => 'in this industry',
'stat_archive' => 'In the archive',
'stat_archive_sub' => 'releases in total',
],
'industry_index' => [
'title' => 'Industry index',
'window' => 'Last 7 days',
'releases' => 'releases',
],
'publisher_cta' => [
'eyebrow' => 'For companies',
'title' => 'Publish your press release',
'subline' => 'Reach across the DACH region · Quality check · Structured distribution',
'submit' => 'Submit now',
'pricing' => 'View plans & packages',
],
'publish_band' => [
'eyebrow' => 'For companies',
'title' => 'Your press release, quality-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',
'footnote' => 'No subscription required · Quality check within 24 h on business days',
'step1_title' => 'Create an account',
'step1_text' => 'Takes about two minutes — one account for all portals, optionally passwordless via magic link.',
'step2_title' => 'Submit your release',
'step2_text' => 'Upload text, image and press contact — an automated quality check runs alongside.',
'step3_title' => 'Publish',
'step3_text' => 'Live after passing the quality check — including newsroom, category mapping and archive.',
],
'pricing_teaser' => [
'title' => 'Plans & packages',
'details' => 'All details',
'popular' => 'Popular',
'month' => 'month',
'year' => 'year',
'two_months_free' => '2 months free',
'quota' => 'press releases / month',
'daily_limit' => 'max. per day',
'no_daily_limit' => 'no daily limit',
'select' => 'Select',
'footnote' => 'All prices excl. VAT · Cancel anytime · Single releases available without a subscription.',
'publish_direct' => 'Publish directly',
],
'quality' => [
'eyebrow' => 'Our quality standard',
'claim' => 'Every press release goes through a quality check.',
'detail' => 'Minimum quality ensured by our content score — an automated assessment of source quality, verification status and content relevance.',
'guidelines' => 'Publishing guidelines',
],
'footer' => [
'sections' => 'Sections',
'newsrooms' => 'Newsrooms',
'for_companies' => 'For companies',
'publish' => 'Publish',
'pricing' => 'Plans & packages',
'company' => 'Company',
'contact' => 'Contact',
'help' => 'Help center',
'cancellation' => 'Cancel contracts here',
'imprint' => 'Imprint',
'privacy' => 'Privacy',
'terms' => 'Terms',
'guidelines' => 'Guidelines',
'report' => 'Report content',
],
'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.',
'eyebrow' => 'Archive search',
'intro' => 'Keyword, company or industry – search the complete, quality-checked archive.',
'placeholder' => 'Keyword, company or industry…',
'searching' => 'searching…',
'reset' => 'Reset search',
'all_categories' => 'All sections',
'results' => 'result|results',
'for_term' => 'for ":term"',
'sort' => 'Sort by',
'sort_newest' => 'Newest first',
'sort_oldest' => 'Oldest first',
'no_results' => 'No results',
'no_results_text' => 'There are currently no press releases matching your search. Try a different term or remove filters.',
'pagination' => 'Pagination',
'previous_page' => 'Previous page',
'next_page' => 'Next page',
'discover_title' => 'Search the archive',
'discover_text' => ':count reviewed press releases — searchable by keyword, company and industry. Enter a search term above or choose a section.',
'popular_categories' => 'Popular sections',
],
'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.',
'eyebrow' => 'Company newsrooms',
'intro' => 'All active companies with their quality-checked 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',
'search_label' => 'Search newsrooms',
'search_placeholder' => 'Search companies…',
'reset_filter' => 'Reset filter',
'latest_release' => 'Latest release :date',
'days_30' => '30 days',
'none_found' => 'No newsrooms found',
'none_found_term' => 'There is currently no newsroom matching ":term". Try a different search term.',
'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.',
],
'newsroom_page' => [
'meta_description' => ':company newsroom: all quality-checked 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',
'since' => 'Newsroom since :year',
'industries' => 'Industries',
'last_30_days' => 'Last 30 days',
'published_releases' => 'published releases',
'total_hits' => 'Total views',
'total_hits_sub' => 'across all releases',
'latest' => 'Latest release',
'empty' => 'No further releases in this newsroom at the moment.',
'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',
],
'category' => [
'fallback_name' => 'Industry',
'default_description' => 'Latest press releases from the :category sector – quality-checked and distributed across the DACH region.',
'meta_title' => ':category press releases – :brand',
'eyebrow' => 'Industry',
'back_aria' => 'Back to all releases',
'all_releases' => 'All releases',
'month_stat' => ':count releases in the last 30 days · :today on :date',
'sub_topics' => 'Subtopics',
'stat_today' => 'Releases today',
'vs_previous_week' => 'vs. previous week',
'last_7_days' => 'in the last 7 days',
'more_top' => 'More top stories',
'no_more_top' => 'No further top stories at the moment.',
'no_more_releases' => 'No further releases in this industry at the moment.',
'week_stat' => ':count releases in the last 7 days',
'search_archive' => 'Search the archive',
'empty_title' => 'No releases in :category yet',
'empty_text' => 'As soon as press releases are published in this industry, they will appear here.',
'sub_industries' => 'Sub-industries',
'volume_30_days' => 'volume last 30 days',
'trend_7_days' => '7-day trend',
'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_submit' => 'Submit press release',
'cta_pricing' => 'View plans & industry packages',
],
'detail' => [
'breadcrumbs' => 'Breadcrumbs',
'press_release' => 'Press release',
'embargo_lifted' => 'Embargo lifted',
'audience' => 'For business journalists',
'time_at' => ':time',
'words' => ':count words',
'recommended' => 'Recommended',
'recommended_hint' => 'This press release achieves a high content score — based on source quality, verification status and content relevance.',
'image_placeholder' => 'Image · Press photo',
'image_placeholder_company' => 'Image · Press photo :company',
'photo' => 'Photo',
'about_company' => 'About the company',
'press_contact' => 'Press contact',
'verified_hint' => 'Verified publisher — identity and domain confirmed',
'verified_by' => 'Identity and domain confirmed by :brand',
'verified_publisher' => 'Verified publisher',
'published_by' => 'Published by',
'to_newsroom' => 'Visit newsroom',
'share' => 'Share',
'email' => 'Email',
'copy_link' => 'Copy link',
'copied' => 'Copied!',
'keywords' => 'Keywords',
'further' => 'Further options',
'print' => 'Print',
'report_release' => 'Report press release',
'privacy_rights' => 'Privacy & personal rights',
'legal_question' => 'Do you have a legal concern about this release?',
'report_content' => 'Report content',
'remove_data' => 'Remove my data',
'reports_free' => 'Reports are free of charge.',
'more_from' => 'More from :company',
'more_from_sub' => 'Latest releases from this newsroom',
'all_from' => 'All releases from :company',
'related' => 'Related releases',
'related_sub' => 'Reactions, similar topics, industry context',
'all_from_category' => 'All releases in :category',
],
];

View file

@ -86,7 +86,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, redaktionell geprüft, auf beiden Portalen veröffentlichen.
— ein Konto, qualitätsgeprüft, auf beiden Portalen veröffentlichen.
</td>
</tr>
</table>

View file

@ -78,7 +78,7 @@
<li>{{ __('Aussagen entsprechen Ihrem Wissensstand und sind sachlich richtig.') }}</li>
</ul>
<p class="m-0 mt-2 text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __('Sie stellen die Plattform von Ansprüchen Dritter frei, die aus einer unberechtigten Nutzung von Inhalten resultieren. Die endgültige Veröffentlichung erfolgt nach redaktioneller Prüfung.') }}
{{ __('Sie stellen die Plattform von Ansprüchen Dritter frei, die aus einer unberechtigten Nutzung von Inhalten resultieren. Die endgültige Veröffentlichung erfolgt nach Abschluss der Inhaltsprüfung.') }}
</p>
</div>

View file

@ -6,23 +6,13 @@
$palette = ['#0098A6', '#21A038', '#0018A8', '#1E1E1E', '#E20074', '#003781', '#C84A1E', '#5A6E2F'];
$companies = collect($companies)->take(6)->values();
if ($companies->isEmpty()) {
$companies = collect([
['initial' => 'S', 'name' => 'Siemens AG', 'count' => 18, 'color' => '#0098A6'],
['initial' => 'B', 'name' => 'BASF SE', 'count' => 14, 'color' => '#21A038'],
['initial' => 'D', 'name' => 'Deutsche Bank', 'count' => 11, 'color' => '#0018A8'],
['initial' => 'V', 'name' => 'Volkswagen AG', 'count' => 16, 'color' => '#1E1E1E'],
['initial' => 'T', 'name' => 'Deutsche Telekom', 'count' => 9, 'color' => '#E20074'],
['initial' => 'A', 'name' => 'Allianz SE', 'count' => 8, 'color' => '#003781'],
]);
}
@endphp
@if ($companies->isNotEmpty())
<section>
<header class="flex items-baseline justify-between mb-3.5 min-h-[34px]">
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] leading-[1.2] text-ink">Aktive Newsrooms</h2>
<span class="eyebrow muted text-[10px]">Heute</span>
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] leading-[1.2] text-ink">{{ __('web.newsrooms.title') }}</h2>
<span class="eyebrow muted text-[10px]">{{ __('web.newsrooms.today') }}</span>
</header>
<hr class="rule-strong mb-1">
@ -46,17 +36,22 @@
<div class="min-w-0">
<div class="text-[13.5px] font-semibold leading-[1.2] text-ink truncate group-hover:text-brand transition-colors">{{ $name }}</div>
<div class="text-[11px] text-ink-3 mt-0.5">
<span class="font-mono">{{ $count }}</span> Meldungen letzte 7 Tage
<span class="font-mono">{{ $count }}</span> {{ __('web.newsrooms.count_suffix') }}
</div>
</div>
<span class="inline-flex items-center gap-1.5 text-[10.5px] font-semibold text-ink-2 px-2 py-0.5 bg-brand/[0.06] border border-brand/20 whitespace-nowrap rounded-[2px]">
<span class="w-1.5 h-1.5 rounded-full bg-brand pulse-dot"></span>
heute aktiv
</span>
@if ($company['today'] ?? false)
<span class="inline-flex items-center gap-1.5 text-[10.5px] font-semibold text-ink-2 px-2 py-0.5 bg-brand/[0.06] border border-brand/20 whitespace-nowrap rounded-[2px]">
<span class="w-1.5 h-1.5 rounded-full bg-brand pulse-dot"></span>
{{ __('web.newsrooms.active_today') }}
</span>
@else
<span></span>
@endif
</a>
@endforeach
<a href="{{ route('newsrooms') }}" class="block mt-3 text-[12px] text-brand font-semibold cursor-pointer hover:text-brand-deep transition-colors">
Alle Newsrooms anzeigen →
{{ __('web.newsrooms.all') }} →
</a>
</section>
@endif

View file

@ -1,52 +0,0 @@
@props(['ad'])
@php
$time = $ad['time'] ?? '';
$date = $ad['date'] ?? '';
$category = $ad['category'] ?? 'Anzeige';
$title = $ad['title'] ?? '';
$company = $ad['company'] ?? '';
$href = $ad['href'] ?? '#';
$catClass = 'self-start whitespace-nowrap text-[9.5px] font-bold uppercase leading-snug tracking-[0.14em] text-card-warm-cat max-md:mt-1 max-md:text-[9px] max-md:tracking-[0.1em]';
@endphp
<div class="-mx-4 my-2">
<div class="flex items-center gap-2.5 px-4">
<span class="h-px flex-1 bg-bg-card-warm-rule"></span>
<span class="text-[9px] font-bold uppercase tracking-[0.22em] text-ink-on-dark-muted">Anzeige</span>
<span class="h-px flex-1 bg-bg-card-warm-rule"></span>
</div>
<a href="{{ $href }}" rel="sponsored nofollow" @class([
'grid items-start bg-bg-card-warm py-4 pl-1.5 pr-4 sm:pl-2',
'cursor-pointer transition-colors hover:bg-bg-card-warm-hover',
'max-md:grid-cols-[auto_minmax(0,1fr)] max-md:gap-x-3',
'md:grid-cols-[3.75rem_6.25rem_minmax(0,1fr)] md:gap-x-4',
])>
<div class="flex min-w-0 shrink-0 flex-col gap-0.5 tabular-nums pt-0.5">
@if ($time !== '')
<span class="font-mono text-[11px] leading-none text-card-warm-cat sm:text-[12px]">{{ $time }}</span>
@endif
@if ($date !== '')
<span class="text-[9px] leading-tight text-ink-4 sm:text-[10px]">{{ $date }}</span>
@endif
<span class="{{ $catClass }} md:hidden">{{ $category }}</span>
</div>
<span class="{{ $catClass }} hidden md:inline">{{ $category }}</span>
<div class="min-w-0">
<div class="font-serif text-[15.5px] font-medium leading-[1.3] text-card-warm-title">
{{ $title }}
</div>
@if ($company)
<div class="mt-1 text-[11px] text-card-warm-cat">{{ $company }}</div>
@endif
</div>
</a>
<div class="flex items-center gap-2.5 px-4">
<span class="h-px flex-1 bg-bg-card-warm-rule"></span>
<span class="text-[9px] font-bold uppercase tracking-[0.22em] text-ink-on-dark-muted">Ende Anzeige</span>
<span class="h-px flex-1 bg-bg-card-warm-rule"></span>
</div>
</div>

View file

@ -1,49 +1,26 @@
@props([
'release' => null,
'mock' => null,
'recommended' => false,
])
@php
\Carbon\Carbon::setLocale('de');
$category = $release?->category?->translations?->firstWhere('locale', app()->getLocale())
?? $release?->category?->translations?->first();
$categoryName = $category?->name;
$company = $release?->company?->name;
$city = $release?->company?->city ?? null;
$title = $release->title;
$href = route('release.detail', ['slug' => $release->slug]);
if ($release) {
$category = $release?->category?->translations?->firstWhere('locale', 'de')
?? $release?->category?->translations?->first();
$categoryName = $category?->name ?? 'Wirtschaft';
$company = $release?->company?->name ?? 'Unternehmen';
$city = $release?->company?->city ?? null;
$title = $release->title;
$href = route('release.detail', ['slug' => $release->slug]);
$recommended = $recommended;
$published = $release->published_at;
if ($published) {
$timeLine = $published->format('H:i');
if ($published->isToday()) {
$dateLine = $published->translatedFormat('j. M');
} elseif ($published->isYesterday()) {
$dateLine = 'Gestern';
} else {
$dateLine = $published->translatedFormat('j. M');
}
$datetimeAttr = $published->toIso8601String();
} else {
$timeLine = '—';
$dateLine = '';
$datetimeAttr = null;
}
$published = $release->published_at;
if ($published) {
$timeLine = $published->format('H:i');
$dateLine = $published->isYesterday() ? __('web.shared.yesterday') : $published->translatedFormat('j. M');
$datetimeAttr = $published->toIso8601String();
} else {
$mock ??= [];
$categoryName = $mock['category'] ?? 'Wirtschaft';
$company = $mock['company'] ?? '';
$city = $mock['city'] ?? null;
$title = $mock['title'] ?? '';
$href = $mock['href'] ?? '#';
$timeLine = $mock['time'] ?? '';
$dateLine = $mock['date'] ?? '';
$timeLine = '—';
$dateLine = '';
$datetimeAttr = null;
$recommended = $recommended || ($mock['recommended'] ?? false);
}
$bpCatClass = 'bp-cat self-start whitespace-nowrap leading-snug max-md:mt-1 max-md:text-[9px] max-md:tracking-[0.1em]';
@ -80,7 +57,7 @@
<svg width="9" height="9" viewBox="0 0 12 12" class="flex-shrink-0" aria-hidden="true">
<path d="M6 1l1.5 3.2 3.5.4-2.6 2.4.7 3.5L6 8.8l-3.1 1.7.7-3.5L1 4.6l3.5-.4z" fill="currentColor" />
</svg>
Empfehlung
{{ __('web.shared.recommendation') }}
</span>
@endif
</div>

View file

@ -1,38 +1,22 @@
@props([
'release' => null,
'mock' => null,
])
@php
\Carbon\Carbon::setLocale('de');
if ($release) {
$category = $release?->category?->translations?->firstWhere('locale', 'de')
?? $release?->category?->translations?->first();
$categoryName = $category?->name ?? 'Wirtschaft';
$image = $release?->images?->first();
$imageUrl = $image?->variantUrl('card') ?? $image?->url();
$teaser = (string) \Illuminate\Support\Str::of(strip_tags((string) ($release?->text ?? '')))->squish()->limit(180);
$company = $release?->company?->name ?? 'Unternehmen';
$title = $release->title;
$href = route('release.detail', ['slug' => $release->slug]);
$time = $release?->published_at?->isToday()
? $release->published_at->format('H:i')
: ($release?->published_at?->isYesterday() ? 'Gestern' : $release?->published_at?->isoFormat('D. MMM'));
$city = $release?->company?->city ?? null;
$readTime = max(1, (int) round(str_word_count(strip_tags((string) ($release?->text ?? ''))) / 200));
} else {
$mock ??= [];
$categoryName = $mock['category'] ?? 'Bauen · Immobilien';
$imageUrl = $mock['image'] ?? null;
$teaser = $mock['teaser'] ?? 'Nach 18 Monaten der Konsolidierung deuten Frühindikatoren auf eine Erholung hin. ImmoConsult-Studie analysiert 240 Märkte in DACH.';
$company = $mock['company'] ?? 'ImmoConsult Deutschland';
$title = $mock['title'] ?? 'Immobilienmarkt 2025: Experten prognostizieren Trendwende bei Kaufpreisen';
$href = $mock['href'] ?? '#';
$time = $mock['time'] ?? '14:18';
$city = $mock['city'] ?? 'München';
$readTime = $mock['readTime'] ?? 5;
}
$category = $release?->category?->translations?->firstWhere('locale', app()->getLocale())
?? $release?->category?->translations?->first();
$categoryName = $category?->name;
$image = $release?->images?->first();
$imageUrl = $image?->variantUrl('card') ?? $image?->url();
$teaser = (string) \Illuminate\Support\Str::of(strip_tags((string) ($release?->text ?? '')))->squish()->limit(180);
$company = $release?->company?->name;
$title = $release->title;
$href = route('release.detail', ['slug' => $release->slug]);
$time = $release?->published_at?->isToday()
? $release->published_at->format('H:i')
: ($release?->published_at?->isYesterday() ? __('web.shared.yesterday') : $release?->published_at?->isoFormat('D. MMM'));
$city = $release?->company?->city ?? null;
$readTime = max(1, (int) round(str_word_count(strip_tags((string) ($release?->text ?? ''))) / 200));
@endphp
<article class="py-6 border-b border-bg-rule grid gap-6 grid-cols-1 md:grid-cols-[240px_1fr] group">
@ -59,7 +43,7 @@
</g>
</svg>
@endif
<span class="bp-tag absolute top-2.5 left-2.5">Top-Meldung</span>
<span class="bp-tag absolute top-2.5 left-2.5">{{ __('web.hero.top_story') }}</span>
</a>
<div>
@ -86,7 +70,7 @@
<span>{{ $city }}</span>
@endif
<span aria-hidden="true">·</span>
<span>{{ $readTime }} Min. Lesezeit</span>
<span>{{ __('web.shared.read_time', ['minutes' => $readTime]) }}</span>
</div>
</div>
</article>

View file

@ -1,50 +0,0 @@
@props(['ad' => null])
@php
$ad ??= [
'category' => 'Logistik & Transport',
'title' => 'DHL Express erweitert Same-Day-Netz auf 24 deutsche Großstädte',
'company' => 'Deutsche Post DHL Group',
'gradient' => 'linear-gradient(135deg,#3A2818,#A88466)',
'href' => '#',
];
$category = $ad['category'] ?? 'Anzeige';
$title = $ad['title'] ?? 'Werbeplatz';
$company = $ad['company'] ?? '';
$gradient = $ad['gradient'] ?? 'linear-gradient(135deg,#3A2818,#A88466)';
$href = $ad['href'] ?? '#';
@endphp
<div class="mt-4">
<div class="flex items-center gap-2.5 mb-2.5">
<span class="h-px bg-bg-rule-strong flex-1"></span>
<span class="text-[9px] font-bold tracking-[0.22em] text-ink-on-dark-muted uppercase">Anzeige</span>
<span class="h-px bg-bg-rule-strong flex-1"></span>
</div>
<a href="{{ $href }}" rel="sponsored nofollow" class="block px-3.5 py-4 bg-bg-card-warm border border-bg-card-warm-border cursor-pointer hover:border-brand/50 hover:bg-bg-card-warm-hover transition-colors">
<div class="grid items-start gap-3" style="grid-template-columns: 64px 1fr;">
<div class="w-16 h-16 flex-shrink-0 relative overflow-hidden opacity-85" style="background: {{ $gradient }};"></div>
<div>
<div class="text-[9.5px] font-bold tracking-[0.16em] text-card-warm-cat uppercase mb-1">
{{ $category }}
</div>
<h3 class="font-serif text-[15.5px] leading-[1.3] m-0 font-medium text-card-warm-title">
{{ $title }}
</h3>
@if ($company)
<div class="text-[11.5px] mt-1.5 text-card-warm-cat">
{{ $company }}
</div>
@endif
</div>
</div>
</a>
<div class="flex items-center gap-2.5 mt-2.5">
<span class="h-px bg-bg-rule-strong flex-1"></span>
<span class="text-[9px] font-bold tracking-[0.22em] text-ink-on-dark-muted uppercase">Ende Anzeige</span>
<span class="h-px bg-bg-rule-strong flex-1"></span>
</div>
</div>

View file

@ -1,36 +1,33 @@
@props([
'leadRelease' => null,
'sideReleases' => [],
'advertisement' => null,
])
@php
\Carbon\Carbon::setLocale('de');
$leadCategory = $leadRelease?->category?->translations?->firstWhere('locale', 'de')
?? $leadRelease?->category?->translations?->first();
$leadCategoryName = $leadCategory?->name ?? 'KI & Innovation';
$leadImage = $leadRelease?->images?->first();
$leadImageUrl = $leadImage?->variantUrl('card') ?? $leadImage?->url();
$leadTitle = $leadRelease?->title ?? 'Die Zukunft der KI im deutschen Mittelstand';
$leadTeaser = $leadRelease
? (string) \Illuminate\Support\Str::of(strip_tags((string) $leadRelease->text))->squish()->limit(200)
: 'Im exklusiven Interview spricht Dr. Maria Schmidt, Leiterin des KI-Instituts München, über Chancen und Herausforderungen der KI für mittelständische Unternehmen.';
$leadCompany = $leadRelease?->company?->name ?? 'KI-Institut München';
$leadHref = $leadRelease ? route('release.detail', ['slug' => $leadRelease->slug]) : '#';
$leadReadTime = max(1, (int) round(str_word_count(strip_tags((string) ($leadRelease?->text ?? ''))) / 200)) ?: 5;
$leadPublishedAt = $leadRelease?->published_at;
$sideReleases = collect($sideReleases)->take(4)->values();
if ($leadRelease) {
$leadCategory = $leadRelease->category?->translations?->firstWhere('locale', app()->getLocale())
?? $leadRelease->category?->translations?->first();
$leadCategoryName = $leadCategory?->name;
$leadImage = $leadRelease->images?->first();
$leadImageUrl = $leadImage?->variantUrl('card') ?? $leadImage?->url();
$leadTitle = $leadRelease->title;
$leadTeaser = (string) \Illuminate\Support\Str::of(strip_tags((string) $leadRelease->text))->squish()->limit(200);
$leadCompany = $leadRelease->company?->name;
$leadHref = route('release.detail', ['slug' => $leadRelease->slug]);
$leadReadTime = max(1, (int) round(str_word_count(strip_tags((string) $leadRelease->text)) / 200));
$leadPublishedAt = $leadRelease->published_at;
}
@endphp
@if ($leadRelease)
<section class="max-w-layout mx-auto px-8 pt-8">
<header class="flex items-baseline justify-between mb-4 flex-wrap gap-2">
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">
Im Fokus
<span class="text-[16px] text-ink-3 font-normal ml-2">· 🇩🇪 Deutschland</span>
{{ __('web.hero.title') }}
</h2>
<div class="eyebrow muted">{{ ucfirst(now()->isoFormat('dddd, HH:mm')) }} Uhr · Auswahl der Redaktion</div>
<div class="eyebrow muted">{{ ucfirst(now()->isoFormat(__('web.hero.clock_format'))) }} · {{ __('web.hero.editorial_pick') }}</div>
</header>
<hr class="rule-strong mb-6">
@ -46,29 +43,19 @@
loading="eager">
<div class="absolute inset-0 bg-black/10"></div>
@else
<svg width="100%" height="100%" viewBox="0 0 760 500" class="absolute inset-0 opacity-40" aria-hidden="true">
<defs>
<pattern id="bp-hero-dots" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
<circle cx="20" cy="20" r="1.5" fill="white" opacity="0.3" />
</pattern>
</defs>
<rect width="760" height="500" fill="url(#bp-hero-dots)" />
<circle cx="600" cy="180" r="120" fill="white" opacity="0.05" />
<circle cx="160" cy="380" r="80" fill="white" opacity="0.06" />
</svg>
<x-web.hero-fallback />
@endif
</div>
<div class="absolute top-6 left-6 flex gap-2">
<span class="bp-tag">TOP-MELDUNG</span>
<span class="bp-tag" style="background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.3);color:white;backdrop-filter:blur(4px);">
Audio · {{ $leadReadTime * 4 }} min
</span>
<span class="bp-tag">{{ __('web.hero.top_story') }}</span>
</div>
<div class="absolute left-0 right-0 bottom-0 px-9 pb-8 pt-[60px] text-white"
style="background:linear-gradient(180deg,transparent,rgba(0,0,0,0.92));">
<div class="eyebrow on-dark mb-3">{{ $leadCategoryName }}</div>
@if ($leadCategoryName)
<div class="eyebrow on-dark mb-3">{{ $leadCategoryName }}</div>
@endif
<h1 class="font-serif text-[38px] font-semibold m-0 leading-[1.1] tracking-[-0.7px] max-w-[680px]">
{{ $leadTitle }}
</h1>
@ -76,54 +63,31 @@
{{ $leadTeaser }}
</p>
<div class="mt-4 flex items-center gap-3.5 text-[12px] text-white/65 flex-wrap">
<span>{{ $leadCompany }}</span>
<span aria-hidden="true">·</span>
@if ($leadCompany)
<span>{{ $leadCompany }}</span>
<span aria-hidden="true">·</span>
@endif
@if ($leadPublishedAt)
<time datetime="{{ $leadPublishedAt->toIso8601String() }}" class="font-mono">
{{ $leadPublishedAt->format('d.m.Y · H:i') }}
</time>
<span aria-hidden="true">·</span>
@endif
<span>{{ $leadReadTime }} Min. Lesezeit</span>
<span>{{ __('web.shared.read_time', ['minutes' => $leadReadTime]) }}</span>
</div>
</div>
</a>
</div>
<div class="flex items-center justify-between pt-3.5 px-1 gap-3">
<div class="flex gap-2 items-center" aria-label="Top-Meldung Paginierung">
<span class="block h-[3px] w-9 bg-bg-rule-strong relative overflow-hidden">
<span class="absolute inset-y-0 left-0 bg-brand" style="width:35%;"></span>
</span>
<span class="block h-[3px] w-[18px] bg-bg-rule-strong"></span>
<span class="block h-[3px] w-[18px] bg-bg-rule-strong"></span>
</div>
<div class="text-[12px] text-ink-3">
<span class="text-ink font-semibold">1</span> von 3 Top-Meldungen
</div>
</div>
</article>
{{-- SIDEBAR: Was sonst zählt --}}
<aside>
<div class="eyebrow muted mb-3.5">Was sonst zählt</div>
<div class="eyebrow muted mb-3.5">{{ __('web.hero.sidebar_title') }}</div>
@if ($sideReleases->isNotEmpty())
@foreach ($sideReleases as $index => $release)
<x-web.focus-side-item :release="$release" :index="$index + 1" :first="$loop->first" />
@endforeach
@else
@foreach ([
['n' => '01', 'cat' => 'Energie & Umwelt', 'time' => '10:42', 'title' => 'Energiewende beschleunigt sich: Neue Rekorde bei Erneuerbaren', 'company' => 'GreenPower Deutschland', 'gradient' => 'linear-gradient(135deg,#3A5A3A,#7BA876)'],
['n' => '02', 'cat' => 'Finanzen', 'time' => '08:15', 'title' => 'FinTech-Startup PaymentFlow sichert sich 45 Mio. Euro', 'company' => 'PaymentFlow GmbH', 'gradient' => 'linear-gradient(135deg,#2A3548,#6B7C95)'],
['n' => '03', 'cat' => 'Industrie', 'time' => '07:00', 'title' => 'Deutsche Unternehmen investieren 15 Mrd. in Automatisierung', 'company' => 'TechVision Analytics', 'gradient' => 'linear-gradient(135deg,#3A2A22,#9B6B52)'],
['n' => '04', 'cat' => 'Mobilität', 'time' => 'Gestern', 'title' => 'VW eröffnet Batteriewerk in Salzgitter — 4.000 Arbeitsplätze', 'company' => 'Volkswagen AG', 'gradient' => 'linear-gradient(135deg,#1F2A3A,#5B7A99)'],
] as $idx => $mock)
<x-web.focus-side-item :mock="$mock" :first="$idx === 0" />
@endforeach
@endif
<x-web.focus-ad :ad="$advertisement" />
@foreach ($sideReleases as $index => $release)
<x-web.focus-side-item :release="$release" :index="$index + 1" :first="$loop->first" />
@endforeach
</aside>
</div>
</section>
@endif

View file

@ -1,35 +1,23 @@
@props([
'release' => null,
'mock' => null,
'index' => 1,
'first' => false,
])
@php
if ($release) {
$category = $release?->category?->translations?->firstWhere('locale', 'de')
?? $release?->category?->translations?->first();
$categoryName = $category?->name ?? 'Wirtschaft';
$time = $release?->published_at?->isToday()
? $release->published_at->format('H:i')
: ($release?->published_at?->isYesterday() ? 'Gestern' : $release?->published_at?->isoFormat('D. MMM'));
$title = $release->title;
$company = $release?->company?->name ?? 'Unternehmen';
$href = route('release.detail', ['slug' => $release->slug]);
$image = $release?->images?->first();
$imageUrl = $image?->variantUrl('card') ?? $image?->url();
$gradient = 'linear-gradient(135deg,#3A4A5A,#6B7C95)';
$number = str_pad((string) $index, 2, '0', STR_PAD_LEFT);
} else {
$categoryName = $mock['cat'];
$time = $mock['time'];
$title = $mock['title'];
$company = $mock['company'];
$href = '#';
$imageUrl = null;
$gradient = $mock['gradient'] ?? 'linear-gradient(135deg,#3A4A5A,#6B7C95)';
$number = $mock['n'];
}
$category = $release?->category?->translations?->firstWhere('locale', app()->getLocale())
?? $release?->category?->translations?->first();
$categoryName = $category?->name;
$time = $release?->published_at?->isToday()
? $release->published_at->format('H:i')
: ($release?->published_at?->isYesterday() ? __('web.shared.yesterday') : $release?->published_at?->isoFormat('D. MMM'));
$title = $release->title;
$company = $release?->company?->name;
$href = route('release.detail', ['slug' => $release->slug]);
$image = $release?->images?->first();
$imageUrl = $image?->variantUrl('card') ?? $image?->url();
$gradient = 'linear-gradient(135deg,#3A4A5A,#6B7C95)';
$number = str_pad((string) $index, 2, '0', STR_PAD_LEFT);
@endphp
<article class="px-3.5 py-4 border-b border-bg-rule cursor-pointer transition-colors hover:bg-bg-elev group {{ $first ? 'border-t border-bg-rule-strong' : '' }}">

View file

@ -0,0 +1,20 @@
{{--
Generierter Hintergrund für Top-Meldungen ohne Bild (Punktraster + Kreise),
identisch zur Startseiten-Hero. Liegt absolut im übergeordneten Container
(position: relative + bg-hero-grad erwartet).
--}}
@php
// Eindeutige Pattern-ID, falls die Komponente mehrfach pro Seite rendert.
$patternId = 'bp-hero-dots-'.\Illuminate\Support\Str::random(6);
@endphp
<svg width="100%" height="100%" viewBox="0 0 760 500" preserveAspectRatio="xMidYMid slice" class="absolute inset-0 opacity-40" aria-hidden="true">
<defs>
<pattern id="{{ $patternId }}" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
<circle cx="20" cy="20" r="1.5" fill="white" opacity="0.3" />
</pattern>
</defs>
<rect width="760" height="500" fill="url(#{{ $patternId }})" />
<circle cx="600" cy="180" r="120" fill="white" opacity="0.05" />
<circle cx="160" cy="380" r="80" fill="white" opacity="0.06" />
</svg>

View file

@ -1,88 +0,0 @@
@props(['highlight'])
@php
// Map industry to Heroicon
$industryIcons = [
'IT & Digitalisierung' => 'cpu-chip',
'KI & Innovation' => 'cpu-chip',
'Industrie & Technik' => 'cog-6-tooth',
'Finanzen & Versicherungen' => 'currency-dollar',
'Handel & E-Commerce' => 'shopping-cart',
'Bauen & Immobilien' => 'building-office',
'Mobilität & Logistik' => 'truck',
'Energie & Umwelt' => 'bolt',
'Medizin & Gesundheit' => 'heart',
'Personal & HR' => 'user-group',
'Marketing, PR & Medien' => 'megaphone',
'Recht & Steuern' => 'scale',
'Politik, Verbände & NGOs' => 'flag',
'Wissenschaft & Forschung' => 'beaker',
'Lifestyle' => 'sparkles',
'Tourismus & Kultur' => 'globe-alt',
];
$iconName = $industryIcons[$highlight['industry']] ?? 'building-office';
$iconPath = "/heroicons/optimized/24/outline/{$iconName}.svg";
@endphp
<a href="#" class="highlight-card-link">
<article class="highlight-card">
<div class="grid md:grid-cols-2">
<!-- Image -->
<div class="highlight-card-image">
<img src="{{ $highlight['image'] }}" alt="{{ $highlight['title'] }}"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105">
<div class="absolute top-4 left-4">
<span class="highlight-badge badge badge-{{ $highlight['badgeType'] }}">
{{ $highlight['badge'] }}
</span>
</div>
</div>
<!-- Content -->
<div class="highlight-card-content">
<div>
{{-- Meta Info --}}
<div class="highlight-meta">
<div class="flex items-center gap-2">
<div class="industry-icon-badge">
<img src="{{ $iconPath }}" alt="{{ $highlight['industry'] }}" class="h-4 w-4" />
</div>
<span
class="font-medium text-zinc-700 dark:text-zinc-300">{{ $highlight['industry'] }}</span>
</div>
<time datetime="{{ $highlight['date'] }}"
class="text-zinc-600 dark:text-zinc-400">{{ $highlight['date'] }}</time>
</div>
{{-- Title --}}
<h3 class="highlight-title">
{{ $highlight['title'] }}
</h3>
{{-- Teaser --}}
<p class="highlight-text">
{{ $highlight['text'] }}
</p>
</div>
{{-- Footer --}}
<div class="highlight-footer">
<div class="flex items-center gap-2">
<svg class="h-4 w-4 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4">
</path>
</svg>
<span
class="text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ $highlight['author'] }}</span>
</div>
<svg class="h-5 w-5 transition-transform group-hover:translate-x-1" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</div>
</div>
</div>
</article>
</a>

View file

@ -16,10 +16,11 @@
$industries = array_slice($industries, 0, 8);
@endphp
@if (! empty($industries))
<section class="max-w-layout mx-auto px-8 mt-16">
<header class="flex items-baseline justify-between mb-4 flex-wrap gap-3">
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">Branchen-Index</h2>
<span class="eyebrow muted">Letzte 7 Tage</span>
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">{{ __('web.industry_index.title') }}</h2>
<span class="eyebrow muted">{{ __('web.industry_index.window') }}</span>
</header>
<hr class="rule-strong">
@ -49,7 +50,7 @@
</div>
<div class="flex items-baseline gap-2 mb-2">
<span class="font-serif text-[22px] font-semibold tracking-[-0.3px] text-ink">{{ $industry['count'] }}</span>
<span class="text-[11px] text-ink-3">Meldungen</span>
<span class="text-[11px] text-ink-3">{{ __('web.industry_index.releases') }}</span>
</div>
<div class="h-[3px] bg-bg-rule">
<div class="h-full {{ $barClass }}" style="width: {{ $percent }}%;"></div>
@ -58,3 +59,4 @@
@endforeach
</div>
</section>
@endif

View file

@ -2,118 +2,73 @@
'industry' => null,
'stats' => null,
'releases' => [],
'study' => null,
])
@php
$industry ??= 'Energie & Klima';
$stats ??= [
['label' => 'Meldungen heute', 'value' => '47', 'sub' => '+18 ggü. Wochenschnitt'],
['label' => 'Investitions­volumen', 'value' => '€8,4 Mrd.', 'sub' => 'Q4 2025 angekündigt'],
['label' => 'Aktive Unternehmen', 'value' => '23', 'sub' => 'in dieser Branche heute'],
];
$releases = collect($releases);
if ($releases->isEmpty()) {
$releases = collect([
['time' => '14:12', 'date' => '12. Mai', 'category' => 'Energie', 'title' => 'RWE startet 1,8 GW Offshore-Windpark vor Helgoland — größtes Projekt in der Nordsee 2025', 'company' => 'RWE AG', 'city' => 'Essen', 'minutes' => 4, 'href' => '#'],
['time' => '12:28', 'date' => '12. Mai', 'category' => 'Energie', 'title' => 'EnBW unterzeichnet Wasserstoff-Lieferabkommen mit Air Liquide', 'company' => 'EnBW Energie', 'city' => 'Karlsruhe', 'minutes' => 3, 'href' => '#'],
['time' => '10:55', 'date' => '12. Mai', 'category' => 'Klima', 'title' => 'Stadtwerke München erreichen 100 % Ökostrom-Ziel für Privatkunden', 'company' => 'SWM', 'city' => 'München', 'minutes' => 2, 'href' => '#'],
]);
}
$study ??= [
'kicker' => 'STUDIE · ENERGIE',
'title' => 'Energiewende-Monitor 2025: Investitionen, Engpässe, Potenziale im DACH-Raum',
'source' => 'Fraunhofer-Institut für Solare Energiesysteme',
'meta' => 'PDF · 84 Seiten · Veröffentlicht 11. Nov 2025',
'href' => '#',
];
$stats = collect($stats ?? []);
@endphp
@if ($industry && $releases->isNotEmpty())
<section class="max-w-layout mx-auto px-8 mt-16">
<header class="flex items-baseline justify-between mb-4 flex-wrap gap-3">
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">
Heute im Fokus <span class="text-brand">· {{ $industry }}</span>
{{ __('web.spotlight.title') }} <span class="text-brand">· {{ $industry }}</span>
</h2>
<div class="eyebrow muted">Aktualisiert {{ now()->format('H:i') }} Uhr · {{ $stats[0]['value'] ?? '47' }} Meldungen</div>
</header>
<hr class="rule-strong">
<div class="grid grid-cols-1 md:grid-cols-3 border-b border-bg-rule">
@foreach ($stats as $idx => $stat)
<div class="px-6 py-5 {{ $idx < count($stats) - 1 ? 'md:border-r border-bg-rule' : '' }}">
<div class="eyebrow muted mb-2">{{ $stat['label'] }}</div>
<div class="font-serif text-[32px] font-semibold leading-none tracking-[-0.5px] text-ink">{{ $stat['value'] }}</div>
<div class="text-[11.5px] text-ink-3 mt-1.5">{{ $stat['sub'] }}</div>
</div>
@endforeach
</div>
<div class="grid gap-9 mt-6 grid-cols-1 lg:grid-cols-[1.7fr_1fr]">
<div>
@foreach ($releases as $release)
<a href="{{ $release['href'] }}" @class([
'grid items-start border-b border-bg-rule py-4',
'cursor-pointer transition-colors group hover:bg-bg-elev',
'pl-1.5 pr-2 sm:pl-2',
'max-md:grid-cols-[auto_minmax(0,1fr)] max-md:gap-x-3',
'md:grid-cols-[3.75rem_6.25rem_minmax(0,1fr)] md:gap-x-4',
])>
<div class="flex min-w-0 shrink-0 flex-col gap-0.5 tabular-nums pt-0.5">
<span class="font-mono text-[11px] leading-none text-ink-3 sm:text-[12px]">{{ $release['time'] }}</span>
@if (($release['date'] ?? '') !== '')
<span class="text-[9px] leading-tight text-ink-4 sm:text-[10px]">{{ $release['date'] }}</span>
@endif
<span class="bp-cat self-start whitespace-nowrap leading-snug max-md:mt-1 max-md:text-[9px] max-md:tracking-[0.1em] md:hidden">{{ $release['category'] }}</span>
</div>
<span class="bp-cat hidden self-start whitespace-nowrap leading-snug md:inline">{{ $release['category'] }}</span>
<div class="min-w-0">
<div class="font-serif text-[16px] font-medium leading-[1.3] tracking-[-0.1px] text-ink transition-colors group-hover:text-brand">
{{ $release['title'] }}
</div>
<div class="text-[11.5px] text-ink-3 mt-1 flex items-center gap-1.5 flex-wrap">
<svg width="10" height="10" viewBox="0 0 11 11" class="text-ok" aria-hidden="true">
<circle cx="5.5" cy="5.5" r="5" fill="currentColor" />
<path d="M3 5.5l1.8 1.8L8 4" stroke="white" stroke-width="1.4" fill="none" stroke-linecap="round" />
</svg>
<span>{{ $release['company'] }}</span>
@if ($release['city'] ?? null)
<span aria-hidden="true">·</span>
<span>{{ $release['city'] }}</span>
@endif
@if ($release['minutes'] ?? null)
<span aria-hidden="true">·</span>
<span>{{ $release['minutes'] }} Min.</span>
@endif
</div>
</div>
</a>
@if ($stats->isNotEmpty())
<div class="grid grid-cols-1 md:grid-cols-3 border-b border-bg-rule">
@foreach ($stats as $idx => $stat)
<div class="px-6 py-5 {{ $idx < count($stats) - 1 ? 'md:border-r border-bg-rule' : '' }}">
<div class="eyebrow muted mb-2">{{ $stat['label'] }}</div>
<div class="font-serif text-[32px] font-semibold leading-none tracking-[-0.5px] text-ink">{{ $stat['value'] }}</div>
<div class="text-[11.5px] text-ink-3 mt-1.5">{{ $stat['sub'] }}</div>
</div>
@endforeach
</div>
@endif
<aside>
<div class="bg-bg-card-warm p-6 border-t-[3px] border-brand">
<div class="eyebrow mb-2.5">{{ $study['kicker'] }}</div>
<h3 class="font-serif text-[20px] font-semibold m-0 mb-3 tracking-[-0.2px] leading-[1.25] text-ink">
{{ $study['title'] }}
</h3>
<div class="text-[12px] text-ink-3 mb-3.5">
{{ $study['source'] }}<br>
<span class="font-mono text-[11px]">{{ $study['meta'] }}</span>
<div class="mt-6">
@foreach ($releases as $release)
<a href="{{ $release['href'] }}" @class([
'grid items-start border-b border-bg-rule py-4',
'cursor-pointer transition-colors group hover:bg-bg-elev',
'pl-1.5 pr-2 sm:pl-2',
'max-md:grid-cols-[auto_minmax(0,1fr)] max-md:gap-x-3',
'md:grid-cols-[3.75rem_6.25rem_minmax(0,1fr)] md:gap-x-4',
])>
<div class="flex min-w-0 shrink-0 flex-col gap-0.5 tabular-nums pt-0.5">
<span class="font-mono text-[11px] leading-none text-ink-3 sm:text-[12px]">{{ $release['time'] }}</span>
@if (($release['date'] ?? '') !== '')
<span class="text-[9px] leading-tight text-ink-4 sm:text-[10px]">{{ $release['date'] }}</span>
@endif
<span class="bp-cat self-start whitespace-nowrap leading-snug max-md:mt-1 max-md:text-[9px] max-md:tracking-[0.1em] md:hidden">{{ $release['category'] }}</span>
</div>
<a href="{{ $study['href'] }}"
class="w-full inline-flex items-center justify-center gap-2 px-[18px] py-2.5 text-[13px] font-semibold bg-transparent text-ink border border-ink rounded-[2px] cursor-pointer hover:bg-ink hover:text-bg transition-colors">
↓ Studie herunterladen
</a>
<div class="mt-2.5 text-[10.5px] leading-[1.45] flex items-center gap-1.5 text-ink-3">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" class="flex-shrink-0 text-ink-4" aria-hidden="true">
<rect x="1.5" y="3" width="9" height="6.5" stroke="currentColor" stroke-width="1" />
<path d="M1.5 3l4.5 3.5L10.5 3" stroke="currentColor" stroke-width="1" fill="none" />
</svg>
<span>Kostenlos · Anmeldung per E-Mail erforderlich</span>
<span class="bp-cat hidden self-start whitespace-nowrap leading-snug md:inline">{{ $release['category'] }}</span>
<div class="min-w-0">
<div class="font-serif text-[16px] font-medium leading-[1.3] tracking-[-0.1px] text-ink transition-colors group-hover:text-brand">
{{ $release['title'] }}
</div>
<div class="text-[11.5px] text-ink-3 mt-1 flex items-center gap-1.5 flex-wrap">
<svg width="10" height="10" viewBox="0 0 11 11" class="text-ok" aria-hidden="true">
<circle cx="5.5" cy="5.5" r="5" fill="currentColor" />
<path d="M3 5.5l1.8 1.8L8 4" stroke="white" stroke-width="1.4" fill="none" stroke-linecap="round" />
</svg>
<span>{{ $release['company'] }}</span>
@if ($release['city'] ?? null)
<span aria-hidden="true">·</span>
<span>{{ $release['city'] }}</span>
@endif
@if ($release['minutes'] ?? null)
<span aria-hidden="true">·</span>
<span>{{ __('web.shared.minutes_short', ['minutes' => $release['minutes']]) }}</span>
@endif
</div>
</div>
</div>
</aside>
</a>
@endforeach
</div>
</section>
@endif

View file

@ -1,112 +1,93 @@
@props([
'items' => [],
'marketIndex' => null,
'asideSlides' => null,
])
@php
$items = collect($items)->all();
if (empty($items)) {
$items = [
['time' => '14:32', 'text' => 'Siemens Energy hebt Jahresprognose an'],
['time' => '14:18', 'text' => 'BASF: Restrukturierung in Ludwigshafen abgeschlossen'],
['time' => '13:55', 'text' => 'Deutsche Bahn meldet Rekord bei Fernverkehr'],
];
}
// Selbstversorgend, damit der Ticker auf jeder Unterseite gefüllt ist
// (nicht nur auf der Startseite): jüngste PMs des Portals, kurz gecacht.
$items = app(\App\Services\Web\TickerData::class)
->items(\App\Support\WebPortalContext::portalFromHost(), app()->getLocale());
$marketIndex ??= [
'label' => 'DAX',
'value' => '18.247',
'delta' => '+0,8%',
'positive' => true,
];
$asideSlides = collect($asideSlides ?? [
$marketIndex,
[
'label' => 'EUR/USD',
'value' => '1,08',
'delta' => '+0,2%',
'positive' => true,
],
[
'label' => 'Brent Öl',
'value' => '78,40 $',
'delta' => '-0,9%',
'positive' => false,
],
])->values()->all();
// Echte Kursdaten aus dem Cache (market:refresh); leer = Slot ausblenden.
$marketSlides = app(\App\Services\Web\MarketDataService::class)->slides();
@endphp
<div class="bg-topbar-grad text-ink-on-dark" aria-label="Ad-Hoc-Ticker">
@if (! empty($items) || ! empty($marketSlides))
<div class="bg-topbar-grad text-ink-on-dark" aria-label="{{ __('web.ticker.aria_label') }}">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-2.5 flex items-center gap-3 sm:gap-5 text-[12.5px]">
<div class="flex items-center gap-3 sm:gap-4 overflow-hidden min-w-0 flex-1">
<span class="flex items-center gap-1.5 text-accent-warm font-bold text-[10.5px] sm:text-[11px] tracking-[0.15em] flex-shrink-0">
<span class="w-[7px] h-[7px] rounded-full bg-accent-warm pulse-dot"></span>
AD-HOC
</span>
@if (! empty($items))
<div class="flex items-center gap-3 sm:gap-4 overflow-hidden min-w-0 flex-1">
<span class="flex items-center gap-1.5 text-accent-warm font-bold text-[10.5px] sm:text-[11px] tracking-[0.15em] flex-shrink-0">
<span class="w-[7px] h-[7px] rounded-full bg-accent-warm pulse-dot"></span>
AD-HOC
</span>
<span class="sr-only">
Ad-hoc-Meldungen:
@foreach ($items as $item)
{{ $item['time'] }} {{ $item['text'] }}.
@endforeach
</span>
<div class="overflow-hidden min-w-0 flex-1" aria-hidden="true">
<div class="ticker-marquee-track text-[12px] sm:text-[12.5px] whitespace-nowrap">
@foreach (array_merge($items, $items) as $item)
<span class="inline-flex shrink-0 gap-2 items-baseline">
<span class="font-mono text-[11px] text-ink-on-dark-muted">{{ $item['time'] }}</span>
<span class="text-ink-on-dark">{{ $item['text'] }}</span>
</span>
<span class="sr-only">
{{ __('web.ticker.sr_intro') }}
@foreach ($items as $item)
{{ $item['time'] }} {{ $item['text'] }}.
@endforeach
</span>
<div class="overflow-hidden min-w-0 flex-1" aria-hidden="true">
<div class="ticker-marquee-track text-[12px] sm:text-[12.5px] whitespace-nowrap">
@foreach (array_merge($items, $items) as $item)
<span class="inline-flex shrink-0 gap-2 items-baseline">
<span class="font-mono text-[11px] text-ink-on-dark-muted">{{ $item['time'] }}</span>
<span class="text-ink-on-dark">{{ $item['text'] }}</span>
</span>
@endforeach
</div>
</div>
</div>
</div>
@else
<span class="flex-1"></span>
@endif
<span class="hidden sm:inline-block w-px h-[18px] bg-ink-on-dark-rule flex-shrink-0"></span>
@if (! empty($marketSlides))
<span class="hidden sm:inline-block w-px h-[18px] bg-ink-on-dark-rule flex-shrink-0"></span>
{{-- Kein position:absolute: sonst null Breite im Fluss, bis Layout kollabiert --}}
<div class="shrink-0 flex justify-end items-center min-w-0 max-w-[min(92vw,280px)] sm:max-w-[min(42vw,280px)]"
x-data="{
slides: @js($asideSlides),
i: 0,
init() {
if (this.slides.length < 2) {
return;
}
window.setInterval(() => {
this.i = (this.i + 1) % this.slides.length;
}, 5200);
},
}"
aria-live="polite">
<div class="grid min-h-[22px] justify-items-end overflow-hidden [grid-template-columns:minmax(0,max-content)]">
<template x-for="(slide, idx) in slides" :key="idx">
<div x-show="i === idx"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 -translate-y-full"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-280"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-full"
class="col-start-1 row-start-1 flex items-center gap-2 whitespace-nowrap">
<span class="text-ink-on-dark-muted font-semibold text-[10.5px] tracking-wider" x-text="slide.label"></span>
<span class="text-ink-on-dark font-mono text-[12.5px]" x-text="slide.value"></span>
<span class="font-semibold text-[11.5px] font-mono"
:class="(slide.positive ?? true) ? 'text-gain' : 'text-loss'"
x-text="slide.delta"></span>
<svg width="26" height="10" viewBox="0 0 26 10"
class="hidden sm:inline-block shrink-0"
:class="(slide.positive ?? true) ? 'text-gain' : 'text-loss'"
aria-hidden="true">
<path d="M0,8 L4,7 L8,5 L12,6 L16,4 L20,3 L24,2"
fill="none" stroke="currentColor" stroke-width="1.2" />
</svg>
</div>
</template>
{{-- Kein position:absolute: sonst null Breite im Fluss, bis Layout kollabiert --}}
<div class="shrink-0 flex justify-end items-center min-w-0 max-w-[min(92vw,280px)] sm:max-w-[min(42vw,280px)]"
x-data="{
slides: @js($marketSlides),
i: 0,
init() {
if (this.slides.length < 2) {
return;
}
window.setInterval(() => {
this.i = (this.i + 1) % this.slides.length;
}, 5200);
},
}"
aria-live="polite">
<div class="grid min-h-[22px] justify-items-end overflow-hidden [grid-template-columns:minmax(0,max-content)]">
<template x-for="(slide, idx) in slides" :key="idx">
<div x-show="i === idx"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 -translate-y-full"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-280"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-full"
class="col-start-1 row-start-1 flex items-center gap-2 whitespace-nowrap">
<span class="text-ink-on-dark-muted font-semibold text-[10.5px] tracking-wider" x-text="slide.label"></span>
<span class="text-ink-on-dark font-mono text-[12.5px]" x-text="slide.value"></span>
<span class="font-semibold text-[11.5px] font-mono"
:class="(slide.positive ?? true) ? 'text-gain' : 'text-loss'"
x-text="slide.delta"></span>
{{-- Dekorativer Richtungspfeil (auf/ab je Vorzeichen, keine echte Kurve) --}}
<svg width="26" height="10" viewBox="0 0 26 10"
class="hidden sm:inline-block shrink-0"
:class="(slide.positive ?? true) ? 'text-gain' : 'text-loss'"
aria-hidden="true">
<path x-show="slide.positive ?? true" d="M0,8 L4,7 L8,5 L12,6 L16,4 L20,3 L24,2"
fill="none" stroke="currentColor" stroke-width="1.2" />
<path x-show="!(slide.positive ?? true)" d="M0,2 L4,3 L8,5 L12,4 L16,6 L20,7 L24,8"
fill="none" stroke="currentColor" stroke-width="1.2" />
</svg>
</div>
</template>
</div>
</div>
</div>
@endif
</div>
</div>
@endif

View file

@ -3,29 +3,19 @@
])
@php
$releases = collect($releases)->take(4)->values();
$fallback = collect([
['title' => 'FinTech-Startup PaymentFlow sichert sich 45 Mio. Euro', 'hits' => 12428],
['title' => 'Energiewende: Neue Rekorde bei Erneuerbaren', 'hits' => 9831],
['title' => 'Immobilienmarkt 2025: Trendwende bei Kaufpreisen', 'hits' => 7104],
['title' => 'Telemedizin-Boom: 3 Mio. Online-Sprechstunden', 'hits' => 5298],
$items = collect($releases)->take(4)->values()->map(fn ($release) => [
'title' => \Illuminate\Support\Str::limit($release->title, 70),
'hits' => (int) ($release->hits ?? 0),
'href' => route('release.detail', ['slug' => $release->slug]),
]);
$items = $releases->isNotEmpty()
? $releases->map(fn ($release) => [
'title' => \Illuminate\Support\Str::limit($release->title, 70),
'hits' => (int) ($release->hits ?? 0),
'href' => route('release.detail', ['slug' => $release->slug]),
])
: $fallback->map(fn ($mock) => array_merge($mock, ['href' => '#']));
$maxHits = max(1, $items->max('hits'));
@endphp
@if ($items->isNotEmpty())
<section>
<header class="flex items-baseline justify-between mb-4 min-h-[34px]">
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] leading-[1.2] text-ink">Meistgelesen</h2>
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] leading-[1.2] text-ink">{{ __('web.most_read.title') }}</h2>
</header>
<hr class="rule-strong mb-1">
@ -46,3 +36,4 @@
</a>
@endforeach
</section>
@endif

View file

@ -1,76 +0,0 @@
@props([
'topics' => null,
'subscribers' => 84000,
])
@php
$themeKey = config('app.theme');
$topics ??= config('domains.domains.'.$themeKey.'.brand.newsletter_topics', [
['name' => 'Tageszusammenfassung', 'meta' => 'Mo–Fr · 07:00 Uhr', 'active' => true],
['name' => 'Wochenrückblick', 'meta' => 'Sonntags · 18:00 Uhr', 'active' => false],
['name' => 'Energie & Klima', 'meta' => 'ab Content-Score 80 · max. 2× pro Woche', 'active' => true],
['name' => 'IPO & M&A', 'meta' => 'ausgewählt von der Redaktion · ca. 3–5 Mails/Monat', 'active' => false],
]);
@endphp
<section id="newsletter" class="max-w-layout mx-auto px-8 mt-16">
<div class="relative overflow-hidden bg-topbar-grad text-ink-on-dark p-12 px-14">
<svg width="280" height="280" viewBox="0 0 280 280" class="absolute -right-10 -top-10 opacity-[0.06]" aria-hidden="true">
<circle cx="140" cy="140" r="120" stroke="white" stroke-width="1" fill="none" />
<circle cx="140" cy="140" r="80" stroke="white" stroke-width="1" fill="none" />
<circle cx="140" cy="140" r="40" stroke="white" stroke-width="1" fill="none" />
</svg>
<div class="relative grid gap-12 grid-cols-1 lg:grid-cols-[1.1fr_1fr]">
<div>
<div class="eyebrow on-dark mb-3">Bleiben Sie informiert</div>
<h2 class="font-serif text-[30px] font-semibold m-0 leading-[1.18] tracking-[-0.4px] mb-3.5 max-w-[460px] text-white">
Pressemeldungen, kuratiert in Ihren Posteingang
</h2>
<p class="text-[14px] leading-[1.55] text-white/70 max-w-[440px] m-0 mb-6">
Die wichtigsten Meldungen aus DACH — täglich, wöchentlich oder branchenspezifisch. Keine Werbung, jederzeit kündbar.
</p>
<form action="#newsletter-subscribe" method="post" class="flex mb-3.5 max-w-[460px]">
@csrf
<label class="sr-only" for="newsletter-email">E-Mail-Adresse</label>
<input id="newsletter-email" type="email" name="email" required
placeholder="Ihre E-Mail-Adresse"
class="flex-1 px-4 py-3.5 text-[14px] bg-white/[0.08] border border-white/[0.18] text-white outline-none rounded-none placeholder-white/40 focus:border-brand focus:ring-0">
<button type="submit"
class="px-[22px] py-3.5 text-[13px] font-semibold bg-brand text-white whitespace-nowrap rounded-none cursor-pointer hover:bg-brand-deep transition-colors">
Kostenlos abonnieren →
</button>
</form>
<div class="text-[11px] text-white/50">
Über {{ number_format($subscribers, 0, ',', '.') }} Abonnenten · Datenschutz nach DSGVO
</div>
</div>
<div class="flex flex-col gap-2.5 self-stretch">
@foreach ($topics as $topic)
<label class="flex items-start gap-3.5 px-4 py-3.5 cursor-pointer transition-colors
{{ ($topic['active'] ?? false) ? 'bg-white/[0.06] border border-brand/40 hover:bg-white/[0.1]' : 'border border-white/10 hover:border-white/30 hover:bg-white/[0.04]' }}">
@if ($topic['active'] ?? false)
<span class="w-[18px] h-[18px] flex-shrink-0 mt-0.5 bg-brand border border-brand flex items-center justify-center">
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M2 6l3 3 5-6" stroke="white" stroke-width="2" />
</svg>
</span>
@else
<span class="w-[18px] h-[18px] flex-shrink-0 mt-0.5 border border-white/30"></span>
@endif
<div class="flex-1">
<div class="text-[13.5px] font-semibold text-white">{{ $topic['name'] }}</div>
<div class="text-[11.5px] text-white/55 mt-0.5">{{ $topic['meta'] }}</div>
</div>
<input type="checkbox" name="topics[]" value="{{ \Illuminate\Support\Str::slug($topic['name']) }}"
{{ ($topic['active'] ?? false) ? 'checked' : '' }} class="sr-only">
</label>
@endforeach
</div>
</div>
</div>
</section>

View file

@ -7,7 +7,7 @@
$start = max(1, $paginator->currentPage() - 2);
$end = min($paginator->lastPage(), $paginator->currentPage() + 2);
@endphp
<nav class="mt-8 flex items-center justify-center gap-1.5" aria-label="Seitennummerierung">
<nav class="mt-8 flex items-center justify-center gap-1.5" aria-label="{{ __('web.search.pagination') }}">
@if ($paginator->onFirstPage())
<span class="inline-flex items-center justify-center w-9 h-9 border border-bg-rule text-ink-4 opacity-40 cursor-not-allowed" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M15 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
@ -15,7 +15,7 @@
@else
<a href="{{ $paginator->previousPageUrl() }}" rel="prev"
class="inline-flex items-center justify-center w-9 h-9 border border-bg-rule-strong text-ink hover:bg-bg-elev transition-colors"
aria-label="Vorherige Seite">
aria-label="{{ __('web.search.previous_page') }}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M15 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</a>
@endif
@ -32,7 +32,7 @@
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}" rel="next"
class="inline-flex items-center justify-center w-9 h-9 border border-bg-rule-strong text-ink hover:bg-bg-elev transition-colors"
aria-label="Nächste Seite">
aria-label="{{ __('web.search.next_page') }}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M9 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</a>
@else

View file

@ -1,159 +0,0 @@
@props([
'title',
'teaser',
'company',
'industry',
'region',
'date',
'contentType' => 'FACHMELDUNG', // ANALYSE, INTERVIEW, FACHMELDUNG
'slug',
'imageUrl' => null,
])
@php
// Map industry to Heroicon
$industryIcons = [
'IT & Digitalisierung' => 'cpu-chip',
'Industrie & Technik' => 'cog-6-tooth',
'Finanzen & Versicherungen' => 'currency-dollar',
'Handel & E-Commerce' => 'shopping-cart',
'Bauen & Immobilien' => 'building-office',
'Mobilität & Logistik' => 'truck',
'Energie & Umwelt' => 'bolt',
'Medizin & Gesundheit' => 'heart',
'Personal & HR' => 'user-group',
'Marketing, PR & Medien' => 'megaphone',
'Recht & Steuern' => 'scale',
'Politik, Verbände & NGOs' => 'flag',
'Wissenschaft & Forschung' => 'beaker',
'Lifestyle' => 'sparkles',
'Tourismus & Kultur' => 'globe-alt',
];
$iconName = $industryIcons[$industry] ?? 'building-office';
$iconPath = "/heroicons/optimized/24/outline/{$iconName}.svg";
// Content Type Badge Classes
$contentTypeClasses = match ($contentType) {
'ANALYSE' => 'bg-[var(--color-primary)] text-white',
'INTERVIEW' => 'bg-[var(--color-secondary)] text-white',
default => 'bg-white/80 dark:bg-zinc-900/90 border border-[var(--color-primary)]/30',
};
$badgeExtraClasses = $imageUrl ? 'shadow-md backdrop-blur-sm' : 'shadow-sm';
@endphp
<a href="/release/{{ $slug }}" class="group block">
<article
class="bg-gradient-to-br from-[var(--color-white)] to-[var(--color-primary)]/3 dark:from-[var(--color-zinc-800)] dark:to-[var(--color-primary)]/3 rounded-xl border border-[var(--color-primary)]/20 dark:border-[var(--color-primary)]/20 overflow-hidden transition-all duration-300 hover:shadow-xl hover:border-[var(--color-primary)]/30 hover:translate-y-[-2px] shadow-sm h-full flex flex-col">
@if ($imageUrl)
{{-- Card mit Bild --}}
<div
class="relative h-46 bg-gradient-to-br from-zinc-100 to-zinc-50 dark:from-zinc-800 dark:to-zinc-900 overflow-hidden">
<img src="{{ $imageUrl }}" alt="{{ $title }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
loading="lazy" />
{{-- Content Type Label --}}
<div class="absolute top-4 left-4">
<span
class="inline-flex bg-primary/10 items-center gap-2 text-[11px] font-semibold px-3 py-1.5 {{ $contentTypeClasses }} {{ $badgeExtraClasses }} rounded-xl shadow-sm">
<div class="industry-icon-badge">
<img src="{{ $iconPath }}" alt="{{ $industry }}" class="h-4 w-4" />
</div>
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $industry }}</span>
</span>
</div>
</div>
{{-- Content --}}
<div class="p-7 flex flex-col flex-grow">
{{-- Meta Info --}}
<div class="flex justify-end left gap-2 mb-4 text-xs text-zinc-500 dark:text-zinc-400">
<time datetime="{{ $date }}"
class="text-zinc-600 dark:text-zinc-400">{{ $date }}</time>
</div>
{{-- Title --}}
<h3
class="text-xl font-bold text-zinc-900 dark:text-zinc-100 line-clamp-2 mb-4 group-hover:text-[var(--color-primary)] transition-colors leading-tight">
{{ $title }}
</h3>
{{-- Teaser --}}
<p class="text-[15px] leading-relaxed text-zinc-600 dark:text-zinc-400 line-clamp-3 mb-5 flex-grow">
{{ $teaser }}
</p>
{{-- Company Footer --}}
<div
class="flex items-center justify-between pt-5 border-t border-[var(--color-primary)]/10 group-hover:border-[var(--color-primary)]/20 transition-all duration-300">
<div class="flex items-center gap-2">
<svg class="h-4 w-4 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4">
</path>
</svg>
<span class="text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ $company }}</span>
</div>
<svg class="h-5 w-5 transition-transform group-hover:translate-x-2" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</div>
</div>
@else
{{-- Card ohne Bild - Textbasiert mit farbigem Hintergrund --}}
<div class="relative p-8 flex flex-col h-full">
{{-- Content Type Label --}}
<div class="mb-0 mt-0">
<span
class="inline-flex items-center gap-2 text-[11px] font-bold px-3 py-2 {{ $contentTypeClasses }} {{ $badgeExtraClasses }} rounded-xl">
<div class="industry-icon-badge">
<img src="{{ $iconPath }}" alt="{{ $industry }}" class="h-4 w-4" />
</div>
<span class="font-medium text-zinc-700 dark:text-zinc-300">{{ $industry }}</span>
</span>
</div>
{{-- Meta Info --}}
<div class="flex justify-end left gap-2 mb-10 text-xs text-zinc-500 dark:text-zinc-400">
<time datetime="{{ $date }}"
class="text-zinc-600 dark:text-zinc-400">{{ $date }}</time>
</div>
{{-- Title - Prominenter ohne Bild --}}
<h3
class="text-2xl font-bold text-zinc-900 dark:text-zinc-100 mb-5 group-hover:text-[var(--color-primary)] transition-colors leading-tight">
{{ $title }}
</h3>
{{-- Teaser as Quote/Highlight --}}
<blockquote
class="text-base leading-relaxed text-zinc-700 dark:text-zinc-300 line-clamp-5 mb-6 flex-grow italic border-l-4 border-[var(--color-primary)] pl-4">
{{ $teaser }}
</blockquote>
{{-- Company Footer --}}
<div
class="flex items-center justify-between pt-5 border-t border-[var(--color-primary)]/10 group-hover:border-[var(--color-primary)]/20 transition-all duration-300">
<div class="flex items-center gap-2">
<svg class="h-4 w-4 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4">
</path>
</svg>
<span class="text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ $company }}</span>
</div>
<svg class="h-5 w-5 transition-transform group-hover:translate-x-2" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</div>
</div>
@endif
</article>
</a>

View file

@ -18,9 +18,9 @@
@if ($plans->isNotEmpty())
<section class="max-w-layout mx-auto px-8 mt-16">
<header class="flex items-baseline justify-between mb-4 flex-wrap gap-3">
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">Tarife &amp; Pakete</h2>
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">{{ __('web.pricing_teaser.title') }}</h2>
<a href="{{ $pricingHref }}" class="eyebrow muted hover:text-ink transition-colors inline-flex items-center gap-1.5 cursor-pointer">
Alle Details
{{ __('web.pricing_teaser.details') }}
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M2 6h8M6 2l4 4-4 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" />
</svg>
@ -37,7 +37,7 @@
<div class="relative flex flex-col p-6 bg-bg {{ $isPopular ? 'ring-1 ring-inset ring-brand' : '' }}">
@if ($isPopular)
<span class="absolute top-0 right-0 inline-flex items-center px-2.5 py-1 text-[9.5px] font-bold uppercase tracking-[0.14em] text-white bg-brand">
Beliebt
{{ __('web.pricing_teaser.popular') }}
</span>
@endif
@ -46,14 +46,14 @@
<div class="flex items-baseline gap-1.5 mb-1">
<span class="font-serif text-[34px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmtPrice((int) $plan->monthly_price_cents) }}&nbsp;€</span>
@if ($hasMonthly)
<span class="text-[12px] text-ink-3">/&nbsp;Monat</span>
<span class="text-[12px] text-ink-3">/&nbsp;{{ __('web.pricing_teaser.month') }}</span>
@endif
</div>
@if ($hasMonthly && (int) $plan->yearly_price_cents > 0)
<div class="text-[11.5px] text-ink-3 mb-4">
{{ $fmtPrice((int) $plan->yearly_price_cents) }}&nbsp;€ / Jahr
<span class="text-gain font-medium">· 2 Monate gratis</span>
{{ $fmtPrice((int) $plan->yearly_price_cents) }}&nbsp;€ / {{ __('web.pricing_teaser.year') }}
<span class="text-gain font-medium">· {{ __('web.pricing_teaser.two_months_free') }}</span>
</div>
@else
<div class="mb-4"></div>
@ -62,17 +62,17 @@
<ul class="space-y-2 mb-6 flex-1 list-none p-0 m-0 text-[12.5px] text-ink-2">
<li class="flex items-baseline gap-2">
<span class="font-mono text-[12px] font-semibold text-ink">{{ $plan->press_release_quota }}</span>
<span>Pressemitteilungen / Monat</span>
<span>{{ __('web.pricing_teaser.quota') }}</span>
</li>
@if ($plan->daily_limit !== null)
<li class="flex items-baseline gap-2">
<span class="font-mono text-[12px] font-semibold text-ink">{{ $plan->daily_limit }}</span>
<span>max. pro Tag</span>
<span>{{ __('web.pricing_teaser.daily_limit') }}</span>
</li>
@else
<li class="flex items-baseline gap-2 text-ink-3">
<span class="font-mono text-[12px]">∞</span>
<span>kein Tageslimit</span>
<span>{{ __('web.pricing_teaser.no_daily_limit') }}</span>
</li>
@endif
</ul>
@ -80,7 +80,7 @@
<a href="{{ $pricingHref }}"
class="inline-flex items-center justify-center gap-1.5 w-full px-4 py-2.5 text-[12.5px] font-semibold rounded-[2px] transition-colors cursor-pointer
{{ $isPopular ? 'bg-brand text-white hover:bg-brand-deep' : 'border border-bg-rule-strong text-ink hover:bg-bg-elev' }}">
Auswählen
{{ __('web.pricing_teaser.select') }}
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M2 6h8M6 2l4 4-4 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" />
</svg>
@ -91,10 +91,10 @@
<div class="flex items-center justify-between gap-4 flex-wrap mt-5">
<p class="text-[12px] text-ink-3 m-0">
Alle Preise zzgl. MwSt. · Jederzeit kündbar · Auch ohne Abo als Einzelveröffentlichung möglich.
{{ __('web.pricing_teaser.footnote') }}
</p>
<a href="{{ $submitHref }}" class="inline-flex items-center gap-1.5 text-[12.5px] font-semibold text-brand hover:text-brand-deep transition-colors cursor-pointer whitespace-nowrap">
Direkt veröffentlichen
{{ __('web.pricing_teaser.publish_direct') }}
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M2 6h8M6 2l4 4-4 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" />
</svg>

View file

@ -0,0 +1,55 @@
{{--
Abschluss-CTA der Startseite (ehemaliger Newsletter-Platz vor dem Footer):
zahlt auf Anmelden / Einreichen / Veröffentlichen ein.
--}}
<section id="veroeffentlichen-cta" class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-16">
<div class="relative overflow-hidden bg-topbar-grad text-ink-on-dark p-8 sm:p-12 lg:px-14">
<svg width="280" height="280" viewBox="0 0 280 280" class="absolute -right-10 -top-10 opacity-[0.06]" aria-hidden="true">
<circle cx="140" cy="140" r="120" stroke="white" stroke-width="1" fill="none" />
<circle cx="140" cy="140" r="80" stroke="white" stroke-width="1" fill="none" />
<circle cx="140" cy="140" r="40" stroke="white" stroke-width="1" fill="none" />
</svg>
<div class="relative grid gap-10 lg:gap-12 grid-cols-1 lg:grid-cols-[1.1fr_1fr]">
<div>
<div class="eyebrow on-dark mb-3">{{ __('web.publish_band.eyebrow') }}</div>
<h2 class="font-serif text-[26px] sm:text-[30px] font-semibold m-0 leading-[1.18] tracking-[-0.4px] mb-3.5 max-w-[460px] text-white">
{{ __('web.publish_band.title') }}
</h2>
<p class="text-[14px] leading-[1.55] text-white/70 max-w-[440px] m-0 mb-7">
{{ __('web.publish_band.subline') }}
</p>
<div class="flex flex-col sm:flex-row gap-3 max-w-[460px]">
<a href="{{ route('veroeffentlichen') }}"
class="inline-flex items-center justify-center px-[22px] py-3.5 text-[13.5px] font-semibold bg-brand text-white whitespace-nowrap cursor-pointer hover:bg-brand-deep transition-colors">
{{ __('web.publish_band.cta_submit') }} →
</a>
<a href="{{ route('preise') }}"
class="inline-flex items-center justify-center px-[22px] py-3.5 text-[13.5px] font-semibold text-white border border-white/30 whitespace-nowrap cursor-pointer hover:bg-white/10 transition-colors">
{{ __('web.publish_band.cta_pricing') }}
</a>
</div>
<div class="text-[11px] text-white/50 mt-4">
{{ __('web.publish_band.footnote') }}
</div>
</div>
<ol class="flex flex-col gap-2.5 self-center m-0 p-0 list-none">
@foreach ([
['n' => '01', 't' => __('web.publish_band.step1_title'), 'd' => __('web.publish_band.step1_text')],
['n' => '02', 't' => __('web.publish_band.step2_title'), 'd' => __('web.publish_band.step2_text')],
['n' => '03', 't' => __('web.publish_band.step3_title'), 'd' => __('web.publish_band.step3_text')],
] as $step)
<li class="flex items-start gap-3.5 px-4 py-3.5 border border-white/10 bg-white/[0.04]">
<span class="font-mono text-[13px] text-brand-soft font-semibold flex-shrink-0 mt-0.5">{{ $step['n'] }}</span>
<div class="flex-1">
<div class="text-[13.5px] font-semibold text-white">{{ $step['t'] }}</div>
<div class="text-[11.5px] text-white/55 mt-0.5 leading-[1.5]">{{ $step['d'] }}</div>
</div>
</li>
@endforeach
</ol>
</div>
</div>
</section>

View file

@ -9,22 +9,22 @@
@endphp
<section class="bg-topbar-grad text-ink-on-dark p-6">
<div class="eyebrow on-dark mb-2">Für Unternehmen</div>
<div class="eyebrow on-dark mb-2">{{ __('web.publisher_cta.eyebrow') }}</div>
<h3 class="font-serif text-[20px] font-semibold m-0 mb-2 tracking-[-0.2px] leading-[1.2] text-white">
Veröffentlichen Sie Ihre Pressemitteilung
{{ __('web.publisher_cta.title') }}
</h3>
<p class="text-[12.5px] leading-[1.5] m-0 mb-4 text-white/85">
Reichweite in DACH · Redaktionelle Prüfung · Strukturierte Distribution
{{ __('web.publisher_cta.subline') }}
</p>
<a href="{{ $submitHref }}"
class="w-full inline-flex items-center justify-center gap-2 px-[18px] py-2.5 text-[13px] font-semibold bg-brand text-white rounded-[2px] cursor-pointer hover:bg-brand-deep transition-colors">
Jetzt einreichen →
{{ __('web.publisher_cta.submit') }} →
</a>
<a href="{{ $pricingHref }}"
class="flex items-center justify-center gap-1.5 w-full mt-3 py-2 text-[12px] font-medium text-white/80 cursor-pointer hover:text-white underline underline-offset-[3px] decoration-white/30 hover:decoration-white/70 transition-colors">
Tarife &amp; Pakete ansehen
{{ __('web.publisher_cta.pricing') }}
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M2 6h8M6 2l4 4-4 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" />
</svg>

View file

@ -10,16 +10,16 @@
</svg>
</div>
<div>
<div class="eyebrow mb-1">Redaktioneller Qualitätsstandard</div>
<div class="eyebrow mb-1">{{ __('web.quality.eyebrow') }}</div>
<div class="text-[13.5px] text-ink leading-[1.5] font-medium">
Alle Pressemitteilungen werden redaktionell geprüft.
{{ __('web.quality.claim') }}
<span class="text-ink-2 font-normal">
Mindestqualität gewährleistet durch unseren Content-Score — eine Bewertung von Quellenqualität, Verifizierungsstatus und redaktioneller Relevanz.
{{ __('web.quality.detail') }}
</span>
</div>
</div>
<a href="{{ route('faq') }}" class="text-[12px] font-semibold text-brand whitespace-nowrap underline underline-offset-[3px] decoration-brand/40 cursor-pointer hover:text-brand-deep hover:decoration-brand transition-colors">
Redaktionsrichtlinien →
<a href="{{ route('richtlinien') }}" class="text-[12px] font-semibold text-brand whitespace-nowrap underline underline-offset-[3px] decoration-brand/40 cursor-pointer hover:text-brand-deep hover:decoration-brand transition-colors">
{{ __('web.quality.guidelines') }} →
</a>
</div>
</section>

View file

@ -8,7 +8,7 @@
'name' => 'businessportal',
'accent' => '24',
'tagline_short' => 'Pressemitteilungen · DACH',
'tagline_long' => 'Veröffentlichungs-Portal für redaktionell geprüfte Pressemitteilungen aus Deutschland, Österreich und der Schweiz.',
'tagline_long' => 'Veröffentlichungs-Portal für qualitätsgeprüfte Pressemitteilungen aus Deutschland, Österreich und der Schweiz.',
'footer_legal' => '© :year businessportal24 · Alle Rechte vorbehalten',
'about_label' => 'Über businessportal24',
]);
@ -26,7 +26,7 @@
<footer class="mt-16 bg-topbar-grad text-ink-on-dark">
<div class="max-w-layout mx-auto px-8 py-12 grid gap-10 grid-cols-1 md:grid-cols-2 lg:grid-cols-[1.4fr_1fr_1fr_1fr]">
<div>
<a href="{{ route('web.home') }}" class="block cursor-pointer group" aria-label="{{ $brandName }}{{ $brandAccent }} Startseite">
<a href="{{ route('web.home') }}" class="block cursor-pointer group" aria-label="{{ $brandName }}{{ $brandAccent }} {{ __('web.header.homepage') }}">
<div class="text-[24px] font-semibold leading-none tracking-[-0.5px]">
<x-web.brand-mark :brand="$themeKey" variant="on-dark" />
</div>
@ -40,33 +40,33 @@
</div>
<div>
<div class="eyebrow mb-3.5">Rubriken</div>
<div class="eyebrow mb-3.5">{{ __('web.footer.sections') }}</div>
<ul class="space-y-2 text-[13px] text-white/75 list-none p-0 m-0">
@foreach (\App\Support\CategoryNavigation::items(5) as $footerCategory)
<li><a href="{{ $footerCategory['href'] }}" class="cursor-pointer hover:text-white transition-colors">{{ $footerCategory['label'] }}</a></li>
@endforeach
<li><a href="{{ route('newsrooms') }}" class="cursor-pointer hover:text-white transition-colors">Newsrooms</a></li>
<li><a href="{{ route('newsrooms') }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.newsrooms') }}</a></li>
</ul>
</div>
<div>
<div class="eyebrow mb-3.5">Für Unternehmen</div>
<div class="eyebrow mb-3.5">{{ __('web.footer.for_companies') }}</div>
<ul class="space-y-2 text-[13px] text-white/75 list-none p-0 m-0">
<li><a href="{{ route('veroeffentlichen') }}" class="cursor-pointer hover:text-white transition-colors">Veröffentlichen</a></li>
<li><a href="{{ route('preise') }}" class="cursor-pointer hover:text-white transition-colors">Tarife &amp; Pakete</a></li>
<li><a href="{{ route('api') }}" class="cursor-pointer hover:text-white transition-colors">API &amp; RSS</a></li>
<li><a href="{{ route('veroeffentlichen') }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.publish') }}</a></li>
<li><a href="{{ route('preise') }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.pricing') }}</a></li>
<li><a href="{{ route('api') }}" class="cursor-pointer hover:text-white transition-colors">API</a></li>
</ul>
</div>
<div>
<div class="eyebrow mb-3.5">Unternehmen</div>
<div class="eyebrow mb-3.5">{{ __('web.footer.company') }}</div>
<ul class="space-y-2 text-[13px] text-white/75 list-none p-0 m-0">
<li><a href="mailto:{{ $contactEmail }}" class="cursor-pointer hover:text-white transition-colors">Kontakt</a></li>
<li><a href="mailto:{{ $contactEmail }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.contact') }}</a></li>
<li><a href="{{ route('faq') }}" class="cursor-pointer hover:text-white transition-colors">FAQ</a></li>
<li><a href="{{ route('hilfe') }}" class="cursor-pointer hover:text-white transition-colors">Hilfe-Center</a></li>
<li><a href="{{ route('kuendigung') }}" class="cursor-pointer hover:text-white transition-colors">Verträge hier kündigen</a></li>
<li><a href="{{ route('impressum') }}" class="cursor-pointer hover:text-white transition-colors">Impressum</a></li>
<li><a href="{{ route('datenschutz') }}" class="cursor-pointer hover:text-white transition-colors">Datenschutz</a></li>
<li><a href="{{ route('hilfe') }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.help') }}</a></li>
<li><a href="{{ route('kuendigung') }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.cancellation') }}</a></li>
<li><a href="{{ route('impressum') }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.imprint') }}</a></li>
<li><a href="{{ route('datenschutz') }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.privacy') }}</a></li>
</ul>
</div>
</div>
@ -75,10 +75,9 @@
<div class="max-w-layout mx-auto px-8 py-5 flex items-center justify-between gap-4 text-[11.5px] text-white/50 flex-wrap">
<span>{{ $legal }}</span>
<span class="flex items-center gap-4 flex-wrap">
<a href="{{ route('agb') }}" class="cursor-pointer hover:text-white transition-colors">AGB</a>
<a href="{{ route('richtlinien') }}" class="cursor-pointer hover:text-white transition-colors">Richtlinien</a>
<a href="{{ route('melden') }}" class="cursor-pointer hover:text-white transition-colors">Inhalte melden</a>
<a href="{{ route('datenschutz') }}" class="cursor-pointer hover:text-white transition-colors">Datenschutz</a>
<a href="{{ route('agb') }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.terms') }}</a>
<a href="{{ route('richtlinien') }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.guidelines') }}</a>
<a href="{{ route('melden') }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.report') }}</a>
</span>
</div>
</div>

View file

@ -1,8 +1,5 @@
@props([
'navigation' => null,
'ticker' => [],
'marketIndex' => null,
'asideSlides' => null,
'brand' => null,
])
@ -32,7 +29,7 @@
// Navigation = echte Rubriken der aktiven Ausgabe. Es gibt keine separate
// Übersichtsseite; jeder Eintrag führt direkt auf die Kategorieseite.
$navigation ??= collect([
['label' => 'Startseite', 'href' => route('web.home'), 'active' => request()->routeIs('web.home')],
['label' => __('web.nav.home'), 'href' => route('web.home'), 'active' => request()->routeIs('web.home')],
])->concat(
\App\Support\CategoryNavigation::items()->map(fn (array $category): array => [
'label' => $category['label'],
@ -41,7 +38,7 @@
])
)->all();
$activeLabel = collect($navigation)->firstWhere('active', true)['label'] ?? 'Menü';
$activeLabel = collect($navigation)->firstWhere('active', true)['label'] ?? __('web.nav.menu');
@endphp
<header>
@ -54,7 +51,7 @@
</span>
<span class="hidden md:flex items-center px-3.5 text-[9.5px] uppercase font-semibold tracking-[0.16em] text-ink-3 whitespace-nowrap">
Ausgabe
{{ __('web.header.edition') }}
</span>
@foreach ($editions as $edition)
@ -62,7 +59,7 @@
class="flex items-center gap-1 sm:gap-1.5 px-1.5 sm:px-2.5 py-2 text-[11px] sm:text-[11.5px] whitespace-nowrap -mb-px border-b-2 transition-colors
@if ($edition['code'] === $activeEdition) font-semibold text-ink-on-dark bg-white/[0.06] border-brand
@else font-medium text-ink-on-dark-2 border-transparent hover:text-ink-on-dark hover:bg-white/[0.04] @endif"
aria-label="Ausgabe {{ $edition['label'] }}"
aria-label="{{ __('web.header.edition') }} {{ $edition['label'] }}"
@if ($edition['code'] === $activeEdition) aria-current="true" @endif>
<span class="text-[11px] sm:text-[12px] leading-none">{{ $edition['flag'] }}</span>
<span>{{ $edition['label'] }}</span>
@ -71,7 +68,7 @@
<span class="flex-1"></span>
{{-- Sprache: mobil verkürzt; ab lg mit Newsletter/RSS/API --}}
{{-- Sprache: mobil verkürzt; ab lg mit FAQ/API --}}
<span class="flex items-center gap-3 sm:gap-4 py-2 whitespace-nowrap">
<span class="inline-flex items-center gap-1.5">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" class="opacity-60" aria-hidden="true">
@ -84,7 +81,7 @@
</strong>
</span>
<span class="hidden lg:inline-block w-px h-[14px] bg-ink-on-dark-rule"></span>
<a href="{{ route('preise') }}" class="cursor-pointer font-medium text-ink-on-dark hover:text-brand transition-colors">Preise</a>
<a href="{{ route('preise') }}" class="cursor-pointer font-medium text-ink-on-dark hover:text-brand transition-colors">{{ __('web.header.pricing') }}</a>
<span class="hidden lg:inline-block w-px h-[14px] bg-ink-on-dark-rule"></span>
<a href="{{ route('faq') }}" class="hidden lg:inline cursor-pointer hover:text-ink-on-dark transition-colors">FAQ</a>
<a href="{{ route('api') }}" class="hidden lg:inline cursor-pointer hover:text-ink-on-dark transition-colors">API</a>
@ -95,9 +92,11 @@
{{-- Header (Logo + Suche + Buttons + Burger) --}}
<div id="header" class="header-normal bg-bg border-b border-bg-rule"
x-data="{ searchOpen: false }"
@keydown.escape.window="searchOpen = false">
@keydown.escape.window="searchOpen = false"
@keydown.window.prevent.cmd.k="$refs.desktopSearch ? $refs.desktopSearch.focus() : searchOpen = true"
@keydown.window.prevent.ctrl.k="$refs.desktopSearch ? $refs.desktopSearch.focus() : searchOpen = true">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-3 lg:py-[18px] flex items-center gap-3 sm:gap-4 lg:gap-6">
<a href="{{ route('web.home') }}" class="block cursor-pointer group flex-shrink-0" aria-label="{{ $brandName }}{{ $brandAccent }} Startseite">
<a href="{{ route('web.home') }}" class="block cursor-pointer group flex-shrink-0" aria-label="{{ $brandName }}{{ $brandAccent }} {{ __('web.header.homepage') }}">
<div class="text-[22px] sm:text-[24px] lg:text-[28px] font-semibold leading-none tracking-[-0.5px]">
<x-web.brand-mark :brand="$themeKey" />
</div>
@ -108,14 +107,14 @@
{{-- Suchfeld nur ab md sichtbar --}}
<form action="{{ route('suche') }}" method="get" class="hidden md:block flex-1 max-w-[460px]">
<label class="sr-only" for="site-search">Pressemitteilungen, Unternehmen, Branchen, ISIN durchsuchen</label>
<label class="sr-only" for="site-search">{{ __('web.header.search_label') }}</label>
<div class="flex items-center gap-2.5 px-3.5 py-2.5 border border-bg-rule bg-bg-elev text-[13px] text-ink-3 rounded-[2px] focus-within:border-brand transition-colors">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="7" cy="7" r="5" stroke="currentColor" stroke-width="1.5" />
<path d="M11 11l3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
<input id="site-search" type="search" name="q"
placeholder="Pressemitteilungen, Unternehmen, Branchen, ISIN…"
<input id="site-search" type="search" name="q" x-ref="desktopSearch"
placeholder="{{ __('web.header.search_placeholder') }}"
class="flex-1 bg-transparent border-0 p-0 text-[13px] text-ink placeholder:text-ink-3 focus:outline-none focus:ring-0 min-w-0">
<span class="hidden xl:inline-block text-[10.5px] text-ink-4 border border-bg-rule px-1.5 py-0.5 rounded-[2px] font-mono">⌘K</span>
</div>
@ -124,7 +123,7 @@
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M2 3h8M3.5 6h5M5 9h2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
</svg>
Erweiterte Suche
{{ __('web.header.advanced_search') }}
</a>
</div>
</form>
@ -135,7 +134,7 @@
<button type="button"
@click="searchOpen = true"
class="md:hidden inline-flex items-center justify-center w-10 h-10 text-ink rounded-[2px] cursor-pointer hover:bg-bg-elev transition-colors"
aria-label="Suche öffnen">
aria-label="{{ __('web.header.search_open') }}">
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="7" cy="7" r="5" stroke="currentColor" stroke-width="1.5" />
<path d="M11 11l3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
@ -145,23 +144,23 @@
{{-- Anmelden: führt in den Hub (Portal-Domain). --}}
<a href="{{ rtrim(config('domains.domain_portal_url', config('app.url')), '/') }}/login"
class="inline-flex items-center gap-2 px-2 sm:px-3 lg:px-4 py-2 lg:py-2.5 text-[13px] font-semibold text-ink rounded-[2px] cursor-pointer hover:text-brand transition-colors"
aria-label="Anmelden">
aria-label="{{ __('web.header.login') }}">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" class="sm:hidden" aria-hidden="true">
<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.8" />
<path d="M4 21c0-4 4-7 8-7s8 3 8 7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</svg>
<span class="hidden sm:inline">Anmelden</span>
<span class="hidden sm:inline">{{ __('web.header.login') }}</span>
</a>
{{-- Veröffentlichen --}}
<a href="{{ route('veroeffentlichen') }}"
class="inline-flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 lg:px-[18px] py-2 lg:py-2.5 text-[12.5px] sm:text-[13px] font-semibold text-white bg-brand cursor-pointer hover:bg-brand-deep rounded-[2px] transition-colors whitespace-nowrap"
aria-label="Pressemitteilung veröffentlichen">
aria-label="{{ __('web.header.publish_aria') }}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" class="sm:hidden" aria-hidden="true">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" />
</svg>
<span class="sm:hidden">PM</span>
<span class="hidden sm:inline">Veröffentlichen</span>
<span class="hidden sm:inline">{{ __('web.header.publish') }}</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" class="hidden sm:inline" aria-hidden="true">
<path d="M3 6h6M6 3l3 3-3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
@ -180,13 +179,13 @@
<div class="max-w-layout mx-auto px-4 sm:px-6 py-6">
<div class="flex items-start justify-between mb-4">
<div>
<div class="eyebrow mb-1">Suche</div>
<h2 class="font-serif text-[22px] font-semibold text-ink">Pressemitteilungen durchsuchen</h2>
<div class="eyebrow mb-1">{{ __('web.header.search_eyebrow') }}</div>
<h2 class="font-serif text-[22px] font-semibold text-ink">{{ __('web.header.search_title') }}</h2>
</div>
<button type="button"
@click="searchOpen = false"
class="inline-flex items-center justify-center w-10 h-10 text-ink rounded-[2px] cursor-pointer hover:bg-bg-elev transition-colors"
aria-label="Suche schließen">
aria-label="{{ __('web.header.search_close') }}">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M6 6l12 12M6 18L18 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</svg>
@ -194,20 +193,20 @@
</div>
<form action="{{ route('suche') }}" method="get">
<label class="sr-only" for="site-search-mobile">Pressemitteilungen durchsuchen</label>
<label class="sr-only" for="site-search-mobile">{{ __('web.header.search_title') }}</label>
<div class="flex items-center gap-2.5 px-4 py-3.5 border border-bg-rule-strong bg-bg-elev text-[15px] text-ink-3 rounded-[2px]">
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="7" cy="7" r="5" stroke="currentColor" stroke-width="1.5" />
<path d="M11 11l3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
<input id="site-search-mobile" type="search" name="q"
placeholder="Pressemitteilungen, Unternehmen, Branchen, ISIN…"
placeholder="{{ __('web.header.search_placeholder') }}"
x-ref="searchInput"
x-init="$watch('searchOpen', value => value && setTimeout(() => $refs.searchInput.focus(), 50))"
class="flex-1 bg-transparent border-0 p-0 text-[15px] text-ink placeholder:text-ink-3 focus:outline-none focus:ring-0 min-w-0">
<button type="submit"
class="px-3 py-1.5 text-[12px] font-semibold bg-brand text-white rounded-[2px] cursor-pointer hover:bg-brand-deep transition-colors">
Suchen
{{ __('web.header.search_submit') }}
</button>
</div>
<div class="mt-3 flex justify-between text-[11.5px] text-ink-3">
@ -215,12 +214,12 @@
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M2 3h8M3.5 6h5M5 9h2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
</svg>
Erweiterte Suche
{{ __('web.header.advanced_search') }}
</a>
<span class="hidden sm:inline-flex items-center gap-1.5">
Drücken Sie
{{ __('web.header.press') }}
<kbd class="font-mono text-[10.5px] text-ink-4 border border-bg-rule px-1.5 py-0.5 rounded-[2px]">Esc</kbd>
zum Schließen
{{ __('web.header.to_close') }}
</span>
</div>
</form>
@ -234,7 +233,7 @@
x-data="{ menuOpen: false }"
@keydown.escape.window="menuOpen = false"
@click.outside="menuOpen = false"
aria-label="Hauptnavigation">
aria-label="{{ __('web.nav.aria_label') }}">
{{-- Desktop: horizontale Tabs --}}
<div class="hidden lg:flex max-w-layout mx-auto px-8 items-stretch">
@ -255,7 +254,7 @@
class="flex items-center justify-between w-full py-3 cursor-pointer group"
:aria-expanded="menuOpen.toString()"
aria-controls="mobile-nav-menu"
aria-label="Hauptmenü öffnen">
aria-label="{{ __('web.nav.open_menu') }}">
<span class="flex items-center gap-3">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" class="text-ink"
:class="{ 'hidden': menuOpen }" aria-hidden="true">
@ -265,7 +264,7 @@
:class="{ 'hidden': !menuOpen, '!block': menuOpen }" aria-hidden="true">
<path d="M6 6l12 12M6 18L18 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
<span class="text-[10px] font-bold tracking-[0.18em] uppercase text-ink-3 group-hover:text-ink transition-colors">Rubrik</span>
<span class="text-[10px] font-bold tracking-[0.18em] uppercase text-ink-3 group-hover:text-ink transition-colors">{{ __('web.nav.section') }}</span>
<span class="font-serif text-[15px] font-semibold text-ink leading-none">{{ $activeLabel }}</span>
</span>
<svg width="14" height="14" viewBox="0 0 12 12" fill="none" class="text-ink-3 transition-transform"
@ -304,5 +303,6 @@
</div>
</nav>
<x-web.live-ticker :items="$ticker" :market-index="$marketIndex" :aside-slides="$asideSlides" />
{{-- Befüllt sich selbst (TickerData) — auf allen Seiten identisch. --}}
<x-web.live-ticker />
</header>

View file

@ -280,7 +280,7 @@ new #[Layout('components.layouts.app'), Title('Tarife & Pakete')] class extends
</div>
<flux:textarea wire:model="benefits" :label="__('Vorteile (eine Zeile = ein Vorteil)')" rows="5"
:placeholder="__('Redaktionelle Prüfung inklusive&#10;Höheres Monatskontingent&#10;Günstigere Extra-Pressemitteilungen')"
:placeholder="__('Inhaltsprüfung inklusive&#10;Höheres Monatskontingent&#10;Günstigere Extra-Pressemitteilungen')"
:description="__('Wird auf der Buchungs-Seite und der öffentlichen Preise-Seite als Vorteilsliste angezeigt.')" />
<flux:switch wire:model="isActive" :label="__('Tarif aktiv (buchbar und auf der Tarif-Seite sichtbar)')" />

View file

@ -392,7 +392,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
@if ($selectedCompany->website)
<p class="m-0 text-[12px] text-[color:var(--color-ink-3)] mt-3">
<span
class="font-semibold text-[color:var(--color-ink-2)]">{{ __('Web') }}:</span>
class="font-semibold text-[color:var(--color-ink-2)]">Web:</span>
{{ $selectedCompany->website }}
</p>
@endif

View file

@ -655,7 +655,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
@if ($selectedCompany->website)
<p class="m-0 text-[12px] text-[color:var(--color-ink-3)] mt-3">
<span
class="font-semibold text-[color:var(--color-ink-2)]">{{ __('Web') }}:</span>
class="font-semibold text-[color:var(--color-ink-2)]">Web:</span>
{{ $selectedCompany->website }}
</p>
@endif

View file

@ -404,7 +404,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
</x-portal.stat-card>
<x-portal.stat-card variant="warn" :label="__('In Prüfung')" :value="number_format($stats['review'])">
<x-slot:meta>{{ __('queue') }}</x-slot:meta>
<x-slot:trend>{{ __('redaktionelle Prüfung') }}</x-slot:trend>
<x-slot:trend>{{ __('Inhaltsprüfung') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Entwürfe')" :value="number_format($stats['draft'])">
<x-slot:meta>{{ __('privat') }}</x-slot:meta>

View file

@ -253,7 +253,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
<flux:icon.clock class="size-[18px]" />
</div>
<div class="flex-1 min-w-[220px] text-[13px] text-[color:var(--color-ink-2)]">
<p class="m-0">{{ __('Diese PM wartet auf eine redaktionelle Entscheidung.') }}</p>
<p class="m-0">{{ __('Diese PM wartet auf eine Prüf-Entscheidung.') }}</p>
@if ($latestClassification && $latestClassification->reason)
<p class="m-0 mt-1 text-[12px] text-[color:var(--color-ink-3)]">
<strong class="text-[color:var(--color-ink-2)]">{{ __('KI-Hinweis') }}:</strong>

View file

@ -294,7 +294,7 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
<x-portal.stat-card variant="warn" :label="__('In Prüfung')" :value="$stats['review']">
<x-slot:meta>{{ __('Ø 4 h') }}</x-slot:meta>
<x-slot:trend>{{ __('redaktionelle Prüfung') }}</x-slot:trend>
<x-slot:trend>{{ __('Inhaltsprüfung') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Entwürfe')" :value="$stats['draft']">
@ -365,7 +365,7 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
{{ __('Noch keine Pressemitteilungen') }}
</div>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[460px] text-[color:var(--color-ink-3)]">
{{ __('Starten Sie mit einer ersten Mitteilung für die aktive Firma oder Ihr Kundenkonto. Veröffentlichung erfolgt nach redaktioneller Prüfung auf beiden Portalen.') }}
{{ __('Starten Sie mit einer ersten Mitteilung für die aktive Firma oder Ihr Kundenkonto. Veröffentlichung erfolgt nach bestandener Inhaltsprüfung auf beiden Portalen.') }}
</p>
<div class="mt-6 flex items-center gap-2.5">
<flux:button variant="primary" icon="plus" href="{{ route('me.press-releases.create') }}" wire:navigate>

View file

@ -131,7 +131,7 @@ new #[Layout('components.layouts.app'), Title('Neue Firma anlegen')] class exten
{{ __('Neue Firma anlegen') }}
</h1>
<p class="text-[12.5px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-3)]">
{{ __('Lege Stammdaten und Portal-Zuordnung an. Die Firma steht sofort zur Verfügung — die redaktionelle Prüfung erfolgt erst bei der ersten Pressemitteilung.') }}
{{ __('Lege Stammdaten und Portal-Zuordnung an. Die Firma steht sofort zur Verfügung — die Inhaltsprüfung erfolgt erst bei der ersten Pressemitteilung.') }}
</p>
</div>

View file

@ -430,7 +430,7 @@ new #[Layout('components.layouts.app'), Title('Meine Firmen')] class extends Com
</div>
<p class="mt-3 text-[12.5px] leading-[1.55] max-w-[640px] m-0 text-[color:var(--color-ink-3)]">
{{ __('Eine Firma ist der Container für Pressemitteilungen: Stammdaten, Boilerplate, Pressekontakte. Anlage ohne separate Freigabe — die redaktionelle Prüfung erfolgt erst bei der Pressemitteilung.') }}
{{ __('Eine Firma ist der Container für Pressemitteilungen: Stammdaten, Boilerplate, Pressekontakte. Anlage ohne separate Freigabe — die Inhaltsprüfung erfolgt erst bei der Pressemitteilung.') }}
</p>
</div>

View file

@ -334,7 +334,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
Flux::toast(
heading: __('Eingereicht'),
text: __('Die Redaktion prüft die Pressemitteilung typischerweise innerhalb von 24 h.'),
text: __('Die Inhaltsprüfung ist typischerweise innerhalb von 24 h abgeschlossen.'),
variant: 'success',
);
} else {
@ -734,7 +734,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
<p class="m-0">{!! nl2br(e($selectedCompany->boilerplate)) !!}</p>
@if ($selectedCompany->website)
<p class="m-0 text-[12px] text-[color:var(--color-ink-3)] mt-3">
<span class="font-semibold text-[color:var(--color-ink-2)]">{{ __('Web') }}:</span>
<span class="font-semibold text-[color:var(--color-ink-2)]">Web:</span>
{{ $selectedCompany->website }}
</p>
@endif
@ -789,7 +789,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
</flux:button>
</flux:modal.trigger>
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-2 leading-[1.45] m-0">
{{ __('Warnungen blockieren nicht. Pflichtfelder blockieren. Die Redaktion prüft typ. innerhalb von 24h.') }}
{{ __('Warnungen blockieren nicht. Pflichtfelder blockieren. Die Inhaltsprüfung dauert typ. bis zu 24h.') }}
</p>
<hr class="border-0 border-t border-[color:var(--color-bg-rule)] my-3" />
@ -979,7 +979,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
{{ __('Sofort nach Freigabe') }}
</span>
<span class="text-[11px] text-[color:var(--color-ink-3)] block mt-0.5 leading-tight">
{{ __('geht live, sobald die Redaktion grünes Licht gibt') }}
{{ __('geht live, sobald die Inhaltsprüfung grünes Licht gibt') }}
</span>
</span>
</label>

View file

@ -324,7 +324,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
Flux::toast(
heading: __('Eingereicht'),
text: __('Die Redaktion prüft die Pressemitteilung typischerweise innerhalb von 24 h.'),
text: __('Die Inhaltsprüfung ist typischerweise innerhalb von 24 h abgeschlossen.'),
variant: 'success',
);
} else {
@ -656,7 +656,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
<p class="m-0">{!! nl2br(e($selectedCompany->boilerplate)) !!}</p>
@if ($selectedCompany->website)
<p class="m-0 text-[12px] text-[color:var(--color-ink-3)] mt-3">
<span class="font-semibold text-[color:var(--color-ink-2)]">{{ __('Web') }}:</span>
<span class="font-semibold text-[color:var(--color-ink-2)]">Web:</span>
{{ $selectedCompany->website }}
</p>
@endif
@ -715,7 +715,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
</flux:button>
</flux:modal.trigger>
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-2 leading-[1.45] m-0">
{{ __('Warnungen blockieren nicht. Pflichtfelder blockieren. Die Redaktion prüft typ. innerhalb von 24h.') }}
{{ __('Warnungen blockieren nicht. Pflichtfelder blockieren. Die Inhaltsprüfung dauert typ. bis zu 24h.') }}
</p>
<hr class="border-0 border-t border-[color:var(--color-bg-rule)] my-3" />
@ -941,7 +941,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
{{ __('Sofort nach Freigabe') }}
</span>
<span class="text-[11px] text-[color:var(--color-ink-3)] block mt-0.5 leading-tight">
{{ __('geht live, sobald die Redaktion grünes Licht gibt') }}
{{ __('geht live, sobald die Inhaltsprüfung grünes Licht gibt') }}
</span>
</span>
</label>

View file

@ -459,7 +459,7 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
Schnell-Submits, der die Bestätigungen umgehen würde. --}}
<flux:modal.trigger :name="'confirm-submit-'.$pr->id">
<button type="button" class="inline-action"
title="{{ __('Zur redaktionellen Prüfung senden') }}">
title="{{ __('Zur Inhaltsprüfung senden') }}">
{{ __('Zur Prüfung →') }}
</button>
</flux:modal.trigger>
@ -579,7 +579,7 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
{{-- Empty: gar keine PMs überhaupt --}}
<x-portal.empty-state
:title="__('Noch keine Pressemitteilungen')"
:subtitle="__('Starten Sie mit Ihrer ersten Mitteilung. Nach redaktioneller Prüfung erscheint sie i. d. R. binnen 4 Stunden werktags auf beiden Portalen.')"
:subtitle="__('Starten Sie mit Ihrer ersten Mitteilung. Nach bestandener Inhaltsprüfung erscheint sie i. d. R. binnen 4 Stunden werktags auf beiden Portalen.')"
>
<x-slot:icon>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
@ -653,7 +653,7 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
<ul
class="text-[11.5px] text-[color:var(--color-ink-2)] leading-[1.7] mt-2 list-none p-0 m-0 space-y-0.5">
<li>{{ __('Vorschau') }}</li>
<li class="text-[color:var(--color-ink-3)] italic">{{ __('Warten auf Redaktion') }}</li>
<li class="text-[color:var(--color-ink-3)] italic">{{ __('Warten auf Prüfung') }}</li>
</ul>
</div>
<div>

View file

@ -77,7 +77,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
Flux::toast(
heading: __('Eingereicht'),
text: __('Die Redaktion prüft die Pressemitteilung typischerweise innerhalb von 24 h.'),
text: __('Die Inhaltsprüfung ist typischerweise innerhalb von 24 h abgeschlossen.'),
variant: 'success',
);
}

View file

@ -1,101 +0,0 @@
<?php
use Livewire\Volt\Component;
new class extends Component {
public function with(): array
{
// Mock-Daten für Featured Releases
return [
'releases' => [
[
'slug' => 'ki-revolution-deutsche-unternehmen',
'title' => 'KI-Revolution: Deutsche Unternehmen investieren Milliarden in Automatisierung',
'teaser' => 'Eine neue Studie zeigt: Unternehmen im DACH-Raum planen für 2025 Investitionen in Höhe von über 15 Milliarden Euro in KI-gestützte Automatisierungslösungen.',
'company' => 'TechVision Analytics',
'industry' => 'IT & Software',
'region' => 'Deutschland',
'date' => 'Heute, 14:30',
'hasImage' => true,
'hasPdf' => true,
'imageUrl' => 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&h=600&fit=crop',
'companyLogo' => 'logo',
],
[
'slug' => 'energiewende-beschleunigt',
'title' => 'Energiewende beschleunigt sich: Neue Rekorde bei erneuerbaren Energien',
'teaser' => 'Im ersten Quartal 2025 erreicht der Anteil erneuerbarer Energien am Strommix einen historischen Höchststand von 58%. Experten sprechen von einem Wendepunkt.',
'company' => 'GreenPower Deutschland',
'industry' => 'Energie',
'region' => 'Deutschland',
'date' => 'Heute, 11:15',
'hasImage' => true,
'imageUrl' => 'https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800&h=600&fit=crop',
],
[
'slug' => 'fintech-startup-series-b',
'title' => 'FinTech-Startup sichert sich 45 Millionen Euro in Series-B-Runde',
'teaser' => 'Das Berliner FinTech-Startup PaymentFlow konnte in einer Series-B-Finanzierungsrunde 45 Millionen Euro einsammeln.',
'company' => 'PaymentFlow GmbH',
'industry' => 'Finanzen',
'region' => 'Berlin',
'date' => 'Gestern, 16:45',
'hasPdf' => true,
'imageUrl' => 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=600&fit=crop',
],
]
];
}
}; ?>
<div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Large Featured Card - Takes 2 columns -->
<div class="lg:col-span-2">
<x-web.press-release-card
:title="$releases[0]['title']"
:teaser="$releases[0]['teaser']"
:company="$releases[0]['company']"
:industry="$releases[0]['industry']"
:region="$releases[0]['region']"
:date="$releases[0]['date']"
:hasImage="$releases[0]['hasImage']"
:hasPdf="$releases[0]['hasPdf']"
:slug="$releases[0]['slug']"
:imageUrl="$releases[0]['imageUrl']"
:companyLogo="$releases[0]['companyLogo']"
/>
</div>
<!-- Two Smaller Featured Cards - Stack in right column -->
<div class="flex flex-col gap-6">
<x-web.press-release-card
:title="$releases[1]['title']"
:teaser="$releases[1]['teaser']"
:company="$releases[1]['company']"
:industry="$releases[1]['industry']"
:region="$releases[1]['region']"
:date="$releases[1]['date']"
:hasImage="$releases[1]['hasImage']"
:hasPdf="$releases[1]['hasPdf'] ?? false"
:slug="$releases[1]['slug']"
:imageUrl="$releases[1]['imageUrl']"
:companyLogo="null"
/>
<x-web.press-release-card
:title="$releases[2]['title']"
:teaser="$releases[2]['teaser']"
:company="$releases[2]['company']"
:industry="$releases[2]['industry']"
:region="$releases[2]['region']"
:date="$releases[2]['date']"
:hasImage="$releases[2]['hasImage'] ?? false"
:hasPdf="$releases[2]['hasPdf']"
:slug="$releases[2]['slug']"
:imageUrl="$releases[2]['imageUrl']"
:companyLogo="null"
/>
</div>
</div>
</div>

View file

@ -94,20 +94,20 @@ new class extends Component
<div wire:loading.class="opacity-60">
<header class="flex items-baseline justify-between mb-4 min-h-[34px] flex-wrap gap-3">
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] leading-[1.2] text-ink">
Aktuelle Meldungen
{{ __('web.feed.title') }}
</h2>
<div class="flex gap-[18px] text-[12.5px] text-ink-3">
<button type="button" wire:click="setTimeframe('all')"
class="cursor-pointer transition-colors @if ($timeframe === 'all') text-ink border-b-[1.5px] border-brand pb-0.5 @else hover:text-ink @endif">
Alle
{{ __('web.feed.filter_all') }}
</button>
<button type="button" wire:click="setTimeframe('today')"
class="cursor-pointer transition-colors @if ($timeframe === 'today') text-ink border-b-[1.5px] border-brand pb-0.5 @else hover:text-ink @endif">
Heute
{{ __('web.feed.filter_today') }}
</button>
<button type="button" wire:click="setTimeframe('week')"
class="cursor-pointer transition-colors @if ($timeframe === 'week') text-ink border-b-[1.5px] border-brand pb-0.5 @else hover:text-ink @endif">
Diese Woche
{{ __('web.feed.filter_week') }}
</button>
</div>
</header>
@ -117,55 +117,23 @@ new class extends Component
$items = $releases->items();
$top = $items[0] ?? null;
$rest = array_slice($items, 1);
$mockFeedItems = [
['time' => '13:42', 'date' => '12. Mai', 'category' => 'Tourismus', 'title' => 'Nachhaltiger Tourismus auf Erfolgskurs: Buchungen steigen um 45 %', 'company' => 'GreenTravel Consulting', 'city' => 'Berlin', 'recommended' => true],
['time' => '12:55', 'date' => '12. Mai', 'category' => 'Bildung', 'title' => 'Digitalisierung im Bildungssektor: Schulen erhalten 2 Mrd. Euro Förderung', 'company' => 'EduTech Initiative', 'city' => 'Frankfurt'],
['time' => '11:20', 'date' => '12. Mai', 'category' => 'Medien', 'title' => 'Medienbranche im Umbruch: Streaming-Dienste überholen klassisches TV', 'company' => 'MediaWatch Analytics', 'city' => 'Hamburg'],
['time' => '10:48', 'date' => '12. Mai', 'category' => 'Handel', 'title' => 'Einzelhandel setzt auf KI: Personalisierte Shopping-Erlebnisse werden Standard', 'company' => 'RetailTech Innovations', 'city' => 'Köln', 'recommended' => true],
['time' => '09:33', 'date' => '12. Mai', 'category' => 'Gesundheit', 'title' => 'Telemedizin-Boom: 3 Millionen Online-Sprechstunden im letzten Quartal', 'company' => 'HealthConnect Digital', 'city' => 'Stuttgart'],
['time' => '08:15', 'date' => '12. Mai', 'category' => 'Mobilität', 'title' => 'E-Mobilität: Ladeinfrastruktur wächst um 38 % gegenüber Vorjahr', 'company' => 'eMobility Verband', 'city' => 'Düsseldorf'],
];
$inFeedAd = [
'time' => '12:14',
'date' => now()->translatedFormat('j. M'),
'category' => 'Cloud · Software',
'title' => 'Microsoft Azure: Neue EU-Region Frankfurt mit DSGVO-zertifizierter KI-Infrastruktur',
'company' => 'Microsoft Deutschland GmbH',
];
@endphp
@if ($top)
<x-web.feed-top-item :release="$top" />
@else
<x-web.feed-top-item />
<p class="py-8 text-[13.5px] text-ink-3 m-0">{{ __('web.feed.empty') }}</p>
@endif
@if (! empty($rest))
@foreach ($rest as $i => $release)
<x-web.feed-item :release="$release" :recommended="in_array($i, [0, 3], true)" />
@endforeach
@else
@foreach ($mockFeedItems as $mock)
<x-web.feed-item :mock="$mock" />
@endforeach
@endif
<x-web.feed-ad :ad="$inFeedAd" />
@foreach ($rest as $release)
<x-web.feed-item :release="$release" :recommended="($release->content_score ?? 0) >= 80" />
@endforeach
@if ($releases->hasMorePages())
<div class="flex justify-center mt-7">
<a href="{{ $releases->nextPageUrl() }}"
class="inline-flex items-center gap-2 px-[18px] py-2.5 text-[13px] font-semibold text-ink bg-transparent border border-bg-rule-strong rounded-[2px] cursor-pointer hover:bg-ink hover:text-bg transition-colors">
Weitere {{ number_format(max(0, $totalCount - $releases->currentPage() * $releases->perPage()), 0, ',', '.') }} Meldungen anzeigen →
</a>
</div>
@elseif (! $top)
<div class="flex justify-center mt-7">
<a href="{{ route('web.home') }}"
class="inline-flex items-center gap-2 px-[18px] py-2.5 text-[13px] font-semibold text-ink bg-transparent border border-bg-rule-strong rounded-[2px] cursor-pointer hover:bg-ink hover:text-bg transition-colors">
Zur Startseite →
{{ __('web.feed.more', ['count' => number_format(max(0, $totalCount - $releases->currentPage() * $releases->perPage()), 0, ',', '.')]) }} →
</a>
</div>
@endif

View file

@ -1,187 +0,0 @@
<?php
use Livewire\Volt\Component;
new class extends Component {
public function with(): array
{
// Mock-Daten für Press Releases Grid
return [
'releases' => [
[
'slug' => 'ki-revolution-deutsche-unternehmen',
'title' => 'KI-Revolution: Deutsche Unternehmen investieren Milliarden in Automatisierung',
'teaser' => 'Eine neue Studie zeigt: Unternehmen im DACH-Raum planen für 2025 Investitionen in Höhe von über 15 Milliarden Euro in KI-gestützte Automatisierungslösungen.',
'company' => 'TechVision Analytics',
'industry' => 'IT & Digitalisierung',
'region' => 'Deutschland',
'date' => '17. Okt 2024',
'contentType' => 'ANALYSE',
'hasImage' => true,
'hasPdf' => true,
'imageUrl' => 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&h=600&fit=crop',
'companyLogo' => 'logo',
],
[
'slug' => 'energiewende-beschleunigt',
'title' => 'Energiewende beschleunigt sich: Neue Rekorde bei erneuerbaren Energien',
'teaser' => 'Im ersten Quartal 2025 erreicht der Anteil erneuerbarer Energien am Strommix einen historischen Höchststand von 58%.',
'company' => 'GreenPower Deutschland',
'industry' => 'Energie & Umwelt',
'region' => 'Deutschland',
'date' => '16. Okt 2024',
'contentType' => 'FACHMELDUNG',
'hasImage' => true,
'imageUrl' => 'https://images.unsplash.com/photo-1466611653911-95081537e5b7?w=800&h=600&fit=crop',
],
[
'slug' => 'fintech-startup-series-b',
'title' => 'FinTech-Startup sichert sich 45 Millionen Euro in Series-B-Runde',
'teaser' => 'Das Berliner FinTech-Startup PaymentFlow konnte in einer Series-B-Finanzierungsrunde 45 Millionen Euro einsammeln.',
'company' => 'PaymentFlow GmbH',
'industry' => 'Finanzen & Versicherungen',
'region' => 'Berlin',
'date' => '15. Okt 2024',
'contentType' => 'FACHMELDUNG',
'hasPdf' => true,
'imageUrl' => null,
],
[
'slug' => 'gesundheitsbranche-digital',
'title' => 'Gesundheitsbranche setzt verstärkt auf digitale Lösungen',
'teaser' => 'Telemedizin und KI-gestützte Diagnostik werden zum Standard: 78% der Krankenhäuser in Deutschland planen Investitionen.',
'company' => 'MediTech Solutions',
'industry' => 'Medizin & Gesundheit',
'region' => 'München',
'date' => '14. Okt 2024',
'contentType' => 'INTERVIEW',
'hasImage' => true,
'imageUrl' => 'https://images.unsplash.com/photo-1576091160399-112ba8d25d1d?w=800&h=600&fit=crop',
],
[
'slug' => 'automobilindustrie-transformation',
'title' => 'Automobilindustrie: Transformation zur E-Mobilität nimmt Fahrt auf',
'teaser' => 'Führende Automobilhersteller kündigen massive Investitionen in E-Mobilität an. Bis 2030 sollen 80% der Neufahrzeuge elektrisch sein.',
'company' => 'Auto Industry Report',
'industry' => 'Mobilität & Logistik',
'region' => 'Stuttgart',
'date' => '13. Okt 2024',
'contentType' => 'ANALYSE',
'hasImage' => true,
'hasPdf' => true,
'imageUrl' => 'https://images.unsplash.com/photo-1593941707882-a5bba14938c7?w=800&h=600&fit=crop',
],
[
'slug' => 'cybersecurity-massnahmen',
'title' => 'Cybersecurity: Unternehmen verstärken Schutzmaßnahmen gegen Hackerangriffe',
'teaser' => 'Nach einer Serie von Cyberattacken erhöhen deutsche Unternehmen ihre Investitionen in IT-Sicherheit um durchschnittlich 35%.',
'company' => 'CyberSafe Europe',
'industry' => 'IT & Digitalisierung',
'region' => 'Frankfurt',
'date' => '12. Okt 2024',
'contentType' => 'FACHMELDUNG',
'hasImage' => true,
'imageUrl' => 'https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=800&h=600&fit=crop',
],
// Neue Artikel ohne Bild für verschiedene Branchen
[
'slug' => 'immobilienmarkt-trendwende',
'title' => 'Immobilienmarkt 2025: Experten prognostizieren Trendwende bei Kaufpreisen',
'teaser' => 'Nach Jahren steigender Preise zeigen aktuelle Analysen eine Stabilisierung des Immobilienmarktes. Besonders Metropolregionen verzeichnen erstmals rückläufige Tendenzen.',
'company' => 'ImmoConsult Deutschland',
'industry' => 'Bauen & Immobilien',
'region' => 'Hamburg',
'date' => '11. Okt 2024',
'contentType' => 'ANALYSE',
'hasImage' => false,
'hasPdf' => true,
'imageUrl' => null,
],
[
'slug' => 'tourismus-nachhaltigkeit',
'title' => 'Nachhaltiger Tourismus auf Erfolgskurs: Buchungen steigen um 45%',
'teaser' => 'Öko-Hotels und klimaneutrales Reisen liegen im Trend. Die Reisebranche verzeichnet einen deutlichen Wandel im Buchungsverhalten deutscher Urlauber.',
'company' => 'GreenTravel Consulting',
'industry' => 'Tourismus & Kultur',
'region' => 'Österreich',
'date' => '10. Okt 2024',
'contentType' => 'FACHMELDUNG',
'hasImage' => false,
'imageUrl' => null,
],
[
'slug' => 'bildungssektor-digitalisierung',
'title' => 'Digitalisierung im Bildungssektor: Schulen erhalten 2 Milliarden Euro Förderung',
'teaser' => 'Bundesweites Digitalpakt-Programm wird ausgeweitet. Schwerpunkt liegt auf KI-gestützten Lernplattformen und moderner IT-Infrastruktur für Schulen.',
'company' => 'EduTech Initiative',
'industry' => 'Wissenschaft & Forschung',
'region' => 'Berlin',
'date' => '09. Okt 2024',
'contentType' => 'FACHMELDUNG',
'hasImage' => false,
'hasPdf' => true,
'imageUrl' => null,
],
[
'slug' => 'medienbranche-streaming',
'title' => 'Medienbranche im Umbruch: Streaming-Dienste überholen klassisches TV',
'teaser' => 'Erstmals nutzen mehr Deutsche regelmäßig Streaming-Plattformen als lineares Fernsehen. Die Werbebudgets verschieben sich entsprechend.',
'company' => 'MediaWatch Analytics',
'industry' => 'Marketing, PR & Medien',
'region' => 'Köln',
'date' => '08. Okt 2024',
'contentType' => 'ANALYSE',
'hasImage' => false,
'imageUrl' => null,
],
[
'slug' => 'einzelhandel-ki-revolution',
'title' => 'Einzelhandel setzt auf KI: Personalisierte Shopping-Erlebnisse werden Standard',
'teaser' => 'Künstliche Intelligenz revolutioniert den Handel. Von virtuellen Anproben bis zu KI-gestützter Beratung – der Einzelhandel wird digital.',
'company' => 'RetailTech Innovations',
'industry' => 'Handel & E-Commerce',
'region' => 'Düsseldorf',
'date' => '07. Okt 2024',
'contentType' => 'INTERVIEW',
'hasImage' => false,
'hasPdf' => true,
'imageUrl' => null,
],
[
'slug' => 'gesundheit-telemedizin-boom',
'title' => 'Telemedizin-Boom: 3 Millionen Online-Sprechstunden im letzten Quartal',
'teaser' => 'Die Akzeptanz von Videosprechstunden erreicht neue Höchststände. Besonders ländliche Regionen profitieren von der digitalen Gesundheitsversorgung.',
'company' => 'HealthConnect Digital',
'industry' => 'Medizin & Gesundheit',
'region' => 'Schweiz',
'date' => '06. Okt 2024',
'contentType' => 'FACHMELDUNG',
'hasImage' => false,
'imageUrl' => null,
],
],
];
}
}; ?>
<div>
@php
$theme = config('app.theme', 'businessportal24');
$isPresseecho = $theme === 'presseecho';
@endphp
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach ($releases as $release)
@if ($isPresseecho)
{{-- Presseecho: Hochwertige Magazine-Card --}}
<x-web.press-release-card :title="$release['title']" :teaser="$release['teaser']" :company="$release['company']" :industry="$release['industry']"
:region="$release['region']" :date="$release['date']" :contentType="$release['contentType'] ?? 'FACHMELDUNG'" :slug="$release['slug']" :imageUrl="$release['imageUrl'] ?? null" />
@else
{{-- Businessportal24: Standard-Card --}}
<x-web.press-release-card :title="$release['title']" :teaser="$release['teaser']" :company="$release['company']" :industry="$release['industry']"
:region="$release['region']" :date="$release['date']" :hasImage="$release['hasImage'] ?? false" :hasPdf="$release['hasPdf'] ?? false" :slug="$release['slug']"
:imageUrl="$release['imageUrl'] ?? null" :companyLogo="$release['companyLogo'] ?? null" />
@endif
@endforeach
</div>
</div>

View file

@ -165,7 +165,7 @@ new class extends Component
<div wire:loading.class="opacity-60" class="transition-opacity">
{{-- Suchfeld --}}
<form wire:submit.prevent class="mb-6">
<label class="sr-only" for="search-input">Pressemitteilungen durchsuchen</label>
<label class="sr-only" for="search-input">{{ __('web.header.search_title') }}</label>
<div class="flex items-center gap-3 px-4 py-3.5 border border-bg-rule-strong bg-bg-elev focus-within:border-brand transition-colors">
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" class="flex-shrink-0 text-ink-3" aria-hidden="true">
<circle cx="7" cy="7" r="5" stroke="currentColor" stroke-width="1.5" />
@ -173,14 +173,14 @@ new class extends Component
</svg>
<input id="search-input" type="search"
wire:model.live.debounce.400ms="q"
placeholder="Suchbegriff, Unternehmen oder Branche…"
placeholder="{{ __('web.search.placeholder') }}"
autocomplete="off"
class="flex-1 bg-transparent border-0 p-0 text-[15px] text-ink placeholder:text-ink-3 focus:outline-none focus:ring-0 min-w-0">
<span wire:loading wire:target="q" class="font-mono text-[11px] text-ink-3 whitespace-nowrap">sucht…</span>
<span wire:loading wire:target="q" class="font-mono text-[11px] text-ink-3 whitespace-nowrap">{{ __('web.search.searching') }}</span>
@if ($q !== '' || $category !== '')
<button type="button" wire:click="clearFilters"
class="flex-shrink-0 inline-flex items-center justify-center w-7 h-7 text-ink-3 hover:text-ink transition-colors"
aria-label="Suche zurücksetzen">
aria-label="{{ __('web.search.reset') }}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M6 6l12 12M6 18L18 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</svg>
@ -197,7 +197,7 @@ new class extends Component
'border-brand bg-brand text-white' => $category === '',
'border-bg-rule-strong text-ink hover:bg-bg-elev' => $category !== '',
])>
Alle Rubriken
{{ __('web.search.all_categories') }}
</button>
@foreach ($categories as $cat)
<button type="button" wire:click="selectCategory('{{ $cat['slug'] }}')" @class([
@ -216,18 +216,18 @@ new class extends Component
<div class="flex items-baseline justify-between gap-3 flex-wrap mb-1">
<h2 class="font-serif text-[22px] font-semibold m-0 tracking-[-0.3px] text-ink">
{{ number_format($results->total(), 0, ',', '.') }}
{{ $results->total() === 1 ? 'Ergebnis' : 'Ergebnisse' }}
{{ trans_choice('web.search.results', $results->total()) }}
@if ($term !== '')
<span class="text-ink-3 font-normal">für „{{ $term }}"</span>
<span class="text-ink-3 font-normal">{{ __('web.search.for_term', ['term' => $term]) }}</span>
@endif
</h2>
<label class="flex items-center gap-2 text-[12.5px] text-ink-3">
<span class="whitespace-nowrap">Sortierung</span>
<span class="whitespace-nowrap">{{ __('web.search.sort') }}</span>
<select wire:model.live="sort"
class="border border-bg-rule-strong bg-bg-elev text-ink text-[12.5px] py-1.5 pl-2.5 pr-7 focus:outline-none focus:border-brand">
<option value="newest">Neueste zuerst</option>
<option value="oldest">Älteste zuerst</option>
<option value="most-read">Meistgelesen</option>
<option value="newest">{{ __('web.search.sort_newest') }}</option>
<option value="oldest">{{ __('web.search.sort_oldest') }}</option>
<option value="most-read">{{ __('web.most_read.title') }}</option>
</select>
</label>
</div>
@ -237,22 +237,22 @@ new class extends Component
<x-web.feed-item :release="$release" />
@empty
<div class="border border-bg-rule bg-bg-elev px-6 py-12 text-center mt-4">
<h3 class="font-serif text-[20px] font-semibold text-ink m-0 mb-2">Keine Treffer</h3>
<h3 class="font-serif text-[20px] font-semibold text-ink m-0 mb-2">{{ __('web.search.no_results') }}</h3>
<p class="text-[13px] text-ink-3 m-0 mb-5 max-w-md mx-auto">
Für Ihre Suche gibt es derzeit keine Pressemitteilungen. Versuchen Sie einen anderen Begriff oder entfernen Sie Filter.
{{ __('web.search.no_results_text') }}
</p>
<button type="button" wire:click="clearFilters"
class="inline-flex items-center gap-2 px-4 py-2.5 text-[13px] font-semibold border border-bg-rule-strong bg-white text-ink hover:bg-bg-elev transition-colors">
Suche zurücksetzen
{{ __('web.search.reset') }}
</button>
</div>
@endforelse
@if ($results->hasPages())
<nav class="mt-7 flex items-center justify-center gap-1.5" aria-label="Seitennummerierung">
<nav class="mt-7 flex items-center justify-center gap-1.5" aria-label="{{ __('web.search.pagination') }}">
<button type="button" wire:click="previousPage" @disabled($results->onFirstPage())
class="inline-flex items-center justify-center w-9 h-9 border border-bg-rule-strong text-ink disabled:opacity-40 disabled:cursor-not-allowed hover:bg-bg-elev transition-colors"
aria-label="Vorherige Seite">
aria-label="{{ __('web.search.previous_page') }}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M15 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
@foreach ($results->getUrlRange(max(1, $results->currentPage() - 2), min($results->lastPage(), $results->currentPage() + 2)) as $page => $url)
@ -264,7 +264,7 @@ new class extends Component
@endforeach
<button type="button" wire:click="nextPage" @disabled(! $results->hasMorePages())
class="inline-flex items-center justify-center w-9 h-9 border border-bg-rule-strong text-ink disabled:opacity-40 disabled:cursor-not-allowed hover:bg-bg-elev transition-colors"
aria-label="Nächste Seite">
aria-label="{{ __('web.search.next_page') }}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M9 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</nav>
@ -278,15 +278,14 @@ new class extends Component
<path d="M11 11l3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
</div>
<h2 class="font-serif text-[22px] font-semibold text-ink m-0 mb-2">Durchsuchen Sie das Archiv</h2>
<h2 class="font-serif text-[22px] font-semibold text-ink m-0 mb-2">{{ __('web.search.discover_title') }}</h2>
<p class="text-[13.5px] text-ink-3 m-0 max-w-lg mx-auto">
{{ number_format($archiveTotal, 0, ',', '.') }} geprüfte Pressemitteilungen — durchsuchbar nach Stichwort,
Unternehmen und Branche. Geben Sie oben einen Suchbegriff ein oder wählen Sie eine Rubrik.
{{ __('web.search.discover_text', ['count' => number_format($archiveTotal, 0, ',', '.')]) }}
</p>
@if ($categories->isNotEmpty())
<div class="mt-7">
<div class="eyebrow muted text-[10px] mb-3">Beliebte Rubriken</div>
<div class="eyebrow muted text-[10px] mb-3">{{ __('web.search.popular_categories') }}</div>
<div class="flex flex-wrap gap-2 justify-center">
@foreach ($categories->take(8) as $cat)
<button type="button" wire:click="selectCategory('{{ $cat['slug'] }}')"

View file

@ -4,8 +4,13 @@
$brand = config('domains.domains.businessportal24.brand', []);
@endphp
@section('title', $brand['meta_title'] ?? 'Aktuelle Pressemitteilungen aus der deutschen Wirtschaft – businessportal24')
@section('meta_description', $brand['meta_description'] ?? 'Pressemitteilungen aus Deutschland, Österreich und der Schweiz. Redaktionell geprüft. Strukturiert distribuiert.')
{{-- DE-Meta kommt brand-spezifisch aus config/domains.php, EN aus den Übersetzungsdateien. --}}
@section('title', app()->getLocale() === 'de'
? ($brand['meta_title'] ?? 'Aktuelle Pressemitteilungen aus der deutschen Wirtschaft – businessportal24')
: __('web.meta.home_title', ['brand' => ($brand['name'] ?? 'businessportal').($brand['accent'] ?? '')]))
@section('meta_description', app()->getLocale() === 'de'
? ($brand['meta_description'] ?? 'Pressemitteilungen aus Deutschland, Österreich und der Schweiz. Qualitätsgeprüft. Strukturiert distribuiert.')
: __('web.meta.home_description'))
@section('hreflang')
<link rel="alternate" hreflang="de" href="{{ url('/de') }}">
@ -14,7 +19,7 @@
@endsection
@section('content')
<x-web.site-header :ticker="$tickerItems ?? []" />
<x-web.site-header />
<main class="bg-bg text-ink">
<x-web.focus-hero
@ -42,7 +47,9 @@
<x-web.pricing-teaser />
<x-web.newsletter-strip />
{{-- Newsletter-Sektion bewusst entfernt (Double-Opt-in nachgelagert,
siehe dev/release/entwicklungsplan-release.md) — stattdessen Veröffentlichen-CTA. --}}
<x-web.publish-band />
<x-web.quality-summary :stats="$homeStats ?? []" />
</main>

View file

@ -36,7 +36,7 @@
[
'title' => 'Pressemitteilungen & Inhalte',
'items' => [
['q' => 'Welche Dateiformate werden unterstützt?', 'a' => 'Bilder: JPG, PNG, WebP (max. 16 MB). Anhänge: PDF, Word, Excel, PowerPoint, ZIP und TXT (max. 25 MB je Datei).'],
['q' => 'Welche Dateiformate werden unterstützt?', 'a' => 'Bilder: JPG, PNG, WebP (max. 16 MB im Editor; über die API max. 8 MB). Anhänge: PDF, Word, Excel, PowerPoint, ZIP und TXT (max. 25 MB je Datei).'],
['q' => 'Kann ich eine Pressemitteilung nach Veröffentlichung korrigieren?', 'a' => 'Ja — wir korrigieren statt zu löschen: Die URL Ihrer Mitteilung bleibt erhalten, Verweise und Verlinkungen bestehen weiter. Wenden Sie sich mit Korrekturwünschen an unseren Support.'],
['q' => 'Wie optimiere ich meine Pressemitteilung für SEO?', 'a' => 'Nutzen Sie aussagekräftige Headlines, integrieren Sie relevante Keywords natürlich, hinterlegen Sie Bildtitel und strukturieren Sie den Text mit Zwischenüberschriften.'],
['q' => 'Was passiert, wenn eine Mitteilung abgelehnt wird?', 'a' => 'Sie erhalten die Ablehnung mit Begründung und können die Mitteilung überarbeiten und erneut einreichen. Ein abgelehnter Beitrag verbraucht kein Kontingent.'],

View file

@ -3,16 +3,14 @@
@php
use Illuminate\Support\Str;
\Carbon\Carbon::setLocale('de');
$catTranslation = $category->translations->firstWhere('locale', 'de') ?? $category->translations->first();
$catName = $catTranslation?->name ?? 'Branche';
$catTranslation = $category->translations->firstWhere('locale', app()->getLocale()) ?? $category->translations->first();
$catName = $catTranslation?->name ?? __('web.category.fallback_name');
$catSlug = $catTranslation?->slug;
$catDescription = filled($catTranslation?->description)
? $catTranslation->description
: 'Aktuelle Pressemitteilungen aus dem Bereich '.$catName.' – redaktionell geprüft und strukturiert distribuiert im DACH-Raum.';
: __('web.category.default_description', ['category' => $catName]);
$parentTranslation = $category->parent?->translations->firstWhere('locale', 'de') ?? $category->parent?->translations->first();
$parentTranslation = $category->parent?->translations->firstWhere('locale', app()->getLocale()) ?? $category->parent?->translations->first();
$parentName = $parentTranslation?->name;
$parentHref = $parentTranslation?->slug ? route('kategorie', ['slug' => $parentTranslation->slug]) : null;
@ -25,7 +23,7 @@
$leadImage = $leadRelease?->images->first();
$leadImageUrl = $leadImage?->variantUrl('detail') ?? $leadImage?->variantUrl('card') ?? $leadImage?->url();
$leadCat = $leadRelease?->category?->translations->firstWhere('locale', 'de') ?? $leadRelease?->category?->translations->first();
$leadCat = $leadRelease?->category?->translations->firstWhere('locale', app()->getLocale()) ?? $leadRelease?->category?->translations->first();
$leadCompany = $leadRelease?->company?->name;
$leadLead = $leadRelease
? (filled($leadRelease->subtitle)
@ -36,7 +34,7 @@
$leadReadTime = max(1, (int) round($leadWords / 200));
@endphp
@section('title', 'Pressemitteilungen ' . $catName . ' – ' . config('app.name'))
@section('title', __('web.category.meta_title', ['category' => $catName, 'brand' => config('app.name')]))
@section('meta_description', Str::limit(strip_tags($catDescription), 155))
@section('content')
@ -46,7 +44,7 @@
{{-- Breadcrumb --}}
<div class="border-b border-bg-rule">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-2.5 flex items-center gap-2 text-[11.5px] text-ink-3">
<a href="{{ route('web.home') }}" class="hover:text-ink transition-colors">Startseite</a>
<a href="{{ route('web.home') }}" class="hover:text-ink transition-colors">{{ __('web.nav.home') }}</a>
@if ($parentName)
<span class="text-ink-4" aria-hidden="true">/</span>
<a href="{{ $parentHref }}" class="hover:text-ink transition-colors">{{ $parentName }}</a>
@ -59,10 +57,10 @@
{{-- Masthead --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-8 lg:pt-9">
<div class="flex items-center justify-between gap-4 mb-4">
<div class="eyebrow">Branche</div>
<a href="{{ route('web.home') }}" class="inline-flex items-center gap-1.5 text-[12px] font-semibold text-ink-3 hover:text-ink transition-colors whitespace-nowrap" aria-label="Zurück zu allen Meldungen">
<div class="eyebrow">{{ __('web.category.eyebrow') }}</div>
<a href="{{ route('web.home') }}" class="inline-flex items-center gap-1.5 text-[12px] font-semibold text-ink-3 hover:text-ink transition-colors whitespace-nowrap" aria-label="{{ __('web.category.back_aria') }}">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M15 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
Alle Meldungen
{{ __('web.category.all_releases') }}
</a>
</div>
@ -72,12 +70,12 @@
<p class="font-serif text-[16px] leading-[1.55] m-0 mt-4 max-w-[680px] text-ink-2">{{ $catDescription }}</p>
<div class="mt-4 text-[11.5px] text-ink-3 flex items-center gap-2.5 flex-wrap">
<span class="w-1.5 h-1.5 rounded-full bg-brand pulse-dot"></span>
{{ $fmt($categoryStats['month'] ?? 0) }} Meldungen in den letzten 30 Tagen · {{ $fmt($categoryStats['today'] ?? 0) }} am {{ $referenceNow->translatedFormat('j. M Y') }}
{{ __('web.category.month_stat', ['count' => $fmt($categoryStats['month'] ?? 0), 'today' => $fmt($categoryStats['today'] ?? 0), 'date' => $referenceNow->translatedFormat('j. M Y')]) }}
</div>
@if ($subCategories->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">Sub-Themen</span>
<span class="eyebrow muted text-[10px] mr-1.5">{{ __('web.category.sub_topics') }}</span>
@foreach ($subCategories as $sub)
<a href="{{ $sub['href'] }}" class="inline-flex items-baseline gap-[7px] px-3 py-1.5 text-[12px] font-medium border border-bg-rule-strong bg-bg text-ink hover:bg-white hover:border-brand transition-colors">
{{ $sub['name'] }}
@ -93,26 +91,26 @@
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-6">
<div class="grid grid-cols-2 lg:grid-cols-4 border-y border-bg-rule">
<div class="px-6 py-5 border-r border-bg-rule">
<div class="eyebrow muted text-[10px] mb-2.5">Meldungen aktueller Tag</div>
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.category.stat_today') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($categoryStats['today'] ?? 0) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">Stand {{ $referenceNow->translatedFormat('j. M Y') }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.spotlight.stat_window_sub', ['date' => $referenceNow->translatedFormat('j. M Y')]) }}</div>
</div>
<div class="px-6 py-5 lg:border-r border-bg-rule">
<div class="eyebrow muted text-[10px] mb-2.5">Letzte 7 Tage</div>
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.industry_index.window') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($categoryStats['week'] ?? 0) }}</div>
<div class="text-[11.5px] mt-2 {{ $weekDelta >= 0 ? 'text-gain' : 'text-loss' }} font-semibold">
{{ $weekDelta >= 0 ? '+' : '' }}{{ $fmt(abs($weekDelta)) }}@if ($weekDeltaPct !== null) ({{ $weekDelta >= 0 ? '+' : '-' }}{{ abs($weekDeltaPct) }} %)@endif <span class="text-ink-3 font-normal">ggü. Vorwoche</span>
{{ $weekDelta >= 0 ? '+' : '' }}{{ $fmt(abs($weekDelta)) }}@if ($weekDeltaPct !== null) ({{ $weekDelta >= 0 ? '+' : '-' }}{{ abs($weekDeltaPct) }} %)@endif <span class="text-ink-3 font-normal">{{ __('web.category.vs_previous_week') }}</span>
</div>
</div>
<div class="px-6 py-5 border-r border-bg-rule border-t lg:border-t-0">
<div class="eyebrow muted text-[10px] mb-2.5">Aktive Unternehmen</div>
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.spotlight.stat_companies') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($categoryStats['activeCompanies'] ?? 0) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">in den letzten 7 Tagen</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.category.last_7_days') }}</div>
</div>
<div class="px-6 py-5 border-t lg:border-t-0">
<div class="eyebrow muted text-[10px] mb-2.5">Im Archiv</div>
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.spotlight.stat_archive') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($categoryStats['total'] ?? 0) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">Meldungen gesamt</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.spotlight.stat_archive_sub') }}</div>
</div>
</div>
</section>
@ -123,12 +121,15 @@
<div class="grid gap-9 lg:grid-cols-[1.7fr_1fr]">
<article>
<a href="{{ route('release.detail', ['slug' => $leadRelease->slug]) }}" class="block group">
<div class="relative overflow-hidden bg-hero-grad hatch-dark" style="aspect-ratio: 16 / 10;">
{{-- Kein hatch-dark: dessen background-image würde den bg-hero-grad-Verlauf überschreiben. --}}
<div class="relative overflow-hidden bg-hero-grad" style="aspect-ratio: 16 / 10;">
@if ($leadImageUrl)
<img src="{{ $leadImageUrl }}" alt="{{ $leadRelease->title }}" class="absolute inset-0 w-full h-full object-cover" loading="lazy">
@else
<x-web.hero-fallback />
@endif
<div class="absolute top-5 left-5 flex gap-2">
<span class="bp-tag">Top-Meldung</span>
<span class="bp-tag">{{ __('web.hero.top_story') }}</span>
</div>
<div class="absolute left-0 right-0 bottom-0 px-6 sm:px-9 pb-8 pt-[70px] text-white" style="background:linear-gradient(180deg,transparent,rgba(0,0,0,0.92));">
@if ($leadCat)
@ -152,7 +153,7 @@
<span class="font-mono">{{ $leadRelease->published_at->translatedFormat('d.m.Y · H:i') }}</span>
<span aria-hidden="true">·</span>
@endif
<span>{{ $leadReadTime }} Min. Lesezeit</span>
<span>{{ __('web.shared.read_time', ['minutes' => $leadReadTime]) }}</span>
</div>
</div>
</div>
@ -160,10 +161,10 @@
</article>
<aside>
<div class="eyebrow muted mb-3.5">Weitere Top-Meldungen</div>
<div class="eyebrow muted mb-3.5">{{ __('web.category.more_top') }}</div>
@forelse ($topReleases as $top)
@php
$topCat = $top->category?->translations->firstWhere('locale', 'de') ?? $top->category?->translations->first();
$topCat = $top->category?->translations->firstWhere('locale', app()->getLocale()) ?? $top->category?->translations->first();
@endphp
<article class="py-[18px] {{ $loop->first ? 'border-t border-bg-rule-strong' : '' }} border-b border-bg-rule">
<a href="{{ route('release.detail', ['slug' => $top->slug]) }}" class="block group">
@ -181,7 +182,7 @@
</a>
</article>
@empty
<p class="text-[12.5px] text-ink-3 py-4 border-t border-bg-rule">Aktuell keine weiteren Top-Meldungen.</p>
<p class="text-[12.5px] text-ink-3 py-4 border-t border-bg-rule">{{ __('web.category.no_more_top') }}</p>
@endforelse
<div class="mt-[22px]">
@ -196,7 +197,7 @@
<div>
<header class="flex items-baseline justify-between mb-4 min-h-[34px] flex-wrap gap-3">
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">
Aktuelle Meldungen <span class="text-[14px] text-ink-3 font-normal ml-1.5">· {{ $catName }}</span>
{{ __('web.feed.title') }} <span class="text-[14px] text-ink-3 font-normal ml-1.5">· {{ $catName }}</span>
</h2>
</header>
<hr class="rule-strong">
@ -204,12 +205,12 @@
@forelse ($feedReleases as $release)
<x-web.feed-item :release="$release" :recommended="$release->content_score !== null && $release->content_score >= 80" />
@empty
<p class="text-[13px] text-ink-3 py-6">Aktuell keine weiteren Meldungen in dieser Branche.</p>
<p class="text-[13px] text-ink-3 py-6">{{ __('web.category.no_more_releases') }}</p>
@endforelse
<div class="flex justify-between items-center mt-7 flex-wrap gap-3">
<span class="text-[11.5px] text-ink-3">{{ $fmt($categoryStats['week'] ?? 0) }} Meldungen in den letzten 7 Tagen</span>
<a href="{{ route('suche') }}" class="px-3.5 py-2.5 text-[13px] font-semibold border border-bg-rule-strong bg-white text-ink hover:bg-bg-elev transition-colors">Im Archiv suchen →</a>
<span class="text-[11.5px] text-ink-3">{{ __('web.category.week_stat', ['count' => $fmt($categoryStats['week'] ?? 0)]) }}</span>
<a href="{{ route('suche') }}" class="px-3.5 py-2.5 text-[13px] font-semibold border border-bg-rule-strong bg-white text-ink hover:bg-bg-elev transition-colors">{{ __('web.category.search_archive') }} →</a>
</div>
</div>
@ -221,8 +222,8 @@
@else
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-12 mb-4">
<div class="border border-bg-rule bg-bg-elev px-6 py-10 text-center">
<h2 class="font-serif text-[22px] font-semibold text-ink m-0 mb-2">Noch keine Meldungen in {{ $catName }}</h2>
<p class="text-[13px] text-ink-3 m-0">Sobald Pressemitteilungen in dieser Branche veröffentlicht werden, erscheinen sie hier.</p>
<h2 class="font-serif text-[22px] font-semibold text-ink m-0 mb-2">{{ __('web.category.empty_title', ['category' => $catName]) }}</h2>
<p class="text-[13px] text-ink-3 m-0">{{ __('web.category.empty_text') }}</p>
</div>
</section>
@endif
@ -230,8 +231,8 @@
@if ($subCategories->isNotEmpty())
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-16">
<header class="flex items-baseline justify-between mb-4 flex-wrap gap-3">
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">Sub-Branchen <span class="text-[14px] text-ink-3 font-normal ml-1.5">· Volumen letzte 30 Tage</span></h2>
<span class="eyebrow muted">7-Tage-Trend</span>
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">{{ __('web.category.sub_industries') }} <span class="text-[14px] text-ink-3 font-normal ml-1.5">· {{ __('web.category.volume_30_days') }}</span></h2>
<span class="eyebrow muted">{{ __('web.category.trend_7_days') }}</span>
</header>
<hr class="rule-strong">
@php $subMax = max(1, (int) $subCategories->max('total')); @endphp
@ -244,7 +245,7 @@
</div>
<div class="flex items-baseline gap-2 mb-2.5">
<span class="font-serif text-[24px] font-semibold tracking-[-0.3px] text-ink">{{ $fmt($sub['total']) }}</span>
<span class="text-[11px] text-ink-3">Meldungen</span>
<span class="text-[11px] text-ink-3">{{ __('web.industry_index.releases') }}</span>
</div>
<div class="h-[3px] bg-bg-rule"><div class="h-full {{ $sub['delta'] >= 0 ? 'bg-gain' : 'bg-brand' }}" style="width: {{ max(8, (int) round($sub['total'] / $subMax * 100)) }}%;"></div></div>
</a>
@ -255,7 +256,7 @@
@if ($relatedCategories->isNotEmpty())
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-16">
<header class="mb-4"><h2 class="font-serif text-[22px] font-semibold m-0 tracking-[-0.2px] text-ink">Verwandte Branchen</h2></header>
<header class="mb-4"><h2 class="font-serif text-[22px] font-semibold m-0 tracking-[-0.2px] text-ink">{{ __('web.category.related') }}</h2></header>
<hr class="rule mb-5">
<div class="flex flex-wrap gap-2.5">
@foreach ($relatedCategories as $related)
@ -273,13 +274,13 @@
<div class="bg-topbar-grad text-ink-on-dark p-6 sm:p-9">
<div class="grid gap-9 lg:grid-cols-[1.4fr_1fr] lg:items-center">
<div>
<div class="eyebrow on-dark mb-2.5">Für Unternehmen aus {{ $catName }}</div>
<h3 class="font-serif text-[24px] sm:text-[28px] font-semibold m-0 mb-2.5 leading-[1.15] text-white tracking-[-0.3px]">Veröffentlichen Sie Ihre Pressemitteilung gezielt in {{ $catName }}.</h3>
<p class="text-[14px] leading-[1.55] m-0 max-w-[540px] text-white/80">Reichweite über aktive Newsrooms · Distribution an Fach- und Wirtschaftsredaktionen · Redaktionell geprüft.</p>
<div class="eyebrow on-dark mb-2.5">{{ __('web.category.cta_eyebrow', ['category' => $catName]) }}</div>
<h3 class="font-serif text-[24px] sm:text-[28px] font-semibold m-0 mb-2.5 leading-[1.15] text-white tracking-[-0.3px]">{{ __('web.category.cta_title', ['category' => $catName]) }}</h3>
<p class="text-[14px] leading-[1.55] m-0 max-w-[540px] text-white/80">{{ __('web.category.cta_text') }}</p>
</div>
<div class="flex flex-col gap-2.5">
<a href="{{ route('veroeffentlichen') }}" class="w-full inline-flex items-center justify-center px-[18px] py-3.5 text-[14px] font-semibold bg-brand text-white hover:bg-brand-deep transition-colors">Pressemitteilung einreichen →</a>
<a href="{{ route('preise') }}" class="w-full inline-flex items-center justify-center px-[18px] py-3 text-[14px] font-semibold text-white border border-white/30 hover:bg-white/10 transition-colors">Tarife &amp; Branchen-Pakete ansehen</a>
<a href="{{ route('veroeffentlichen') }}" class="w-full inline-flex items-center justify-center px-[18px] py-3.5 text-[14px] font-semibold bg-brand text-white hover:bg-brand-deep transition-colors">{{ __('web.category.cta_submit') }} →</a>
<a href="{{ route('preise') }}" class="w-full inline-flex items-center justify-center px-[18px] py-3 text-[14px] font-semibold text-white border border-white/30 hover:bg-white/10 transition-colors">{{ __('web.category.cta_pricing') }}</a>
</div>
</div>
</div>

View file

@ -8,8 +8,10 @@
@php
$siteName = $domainName ?? config('app.name', 'Laravel');
$pageTitle = trim($__env->yieldContent('title')) ?: $siteName;
$pageDescription = trim($__env->yieldContent('meta_description')) ?: $siteName;
// Inline-@section('title', …)-Werte kommen von Blade bereits escaped an —
// einmal dekodieren, damit {{ }} unten nicht doppelt escapt („&amp;amp;").
$pageTitle = html_entity_decode(trim($__env->yieldContent('title')), ENT_QUOTES, 'UTF-8') ?: $siteName;
$pageDescription = html_entity_decode(trim($__env->yieldContent('meta_description')), ENT_QUOTES, 'UTF-8') ?: $siteName;
// Self-Canonical als Default aus der Portal-URL + Request-Pfad (ohne Query):
// pro Portal/Domain getrennt und unabhängig von URL::forceRootUrl – verhindert Cross-Portal-Duplicate.
$portalBase = rtrim($domainUrl ?? config('app.url'), '/');

View file

@ -35,7 +35,7 @@
@endphp
@section('title', $company->name.' – Newsroom · '.$brandName.$brandAccent)
@section('meta_description', Str::limit($boilerplate ?? ('Newsroom von '.$company->name.': alle redaktionell geprüften Pressemitteilungen gebündelt an einem Ort.'), 155))
@section('meta_description', Str::limit($boilerplate ?? __('web.newsroom_page.meta_description', ['company' => $company->name]), 155))
@section('og_type', 'profile')
@section('hreflang')
@ -51,7 +51,7 @@
{{-- Breadcrumb --}}
<div class="border-b border-bg-rule">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-2.5 flex items-center gap-2 text-[11.5px] text-ink-3">
<a href="{{ route('web.home') }}" class="hover:text-ink transition-colors">Startseite</a>
<a href="{{ route('web.home') }}" class="hover:text-ink transition-colors">{{ __('web.nav.home') }}</a>
<span class="text-ink-4" aria-hidden="true">/</span>
<a href="{{ route('newsrooms') }}" class="hover:text-ink transition-colors">Newsrooms</a>
<span class="text-ink-4" aria-hidden="true">/</span>
@ -63,9 +63,9 @@
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-8 lg:pt-9">
<div class="flex items-center justify-between gap-4 mb-4">
<div class="eyebrow">Newsroom</div>
<a href="{{ route('newsrooms') }}" class="inline-flex items-center gap-1.5 text-[12px] font-semibold text-ink-3 hover:text-ink transition-colors whitespace-nowrap" aria-label="Zurück zu allen Newsrooms">
<a href="{{ route('newsrooms') }}" class="inline-flex items-center gap-1.5 text-[12px] font-semibold text-ink-3 hover:text-ink transition-colors whitespace-nowrap" aria-label="{{ __('web.newsroom_page.back_aria') }}">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M15 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
Alle Newsrooms
{{ __('web.newsroom_page.all_newsrooms') }}
</a>
</div>
@ -91,11 +91,11 @@
@endif
<span class="inline-flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-brand pulse-dot"></span>
{{ $fmt($newsroomStats['recent'] ?? 0) }} Meldungen in den letzten 30 Tagen
{{ __('web.newsroom_page.recent_stat', ['count' => $fmt($newsroomStats['recent'] ?? 0)]) }}
</span>
@if (! empty($newsroomStats['sinceYear']))
<span class="text-ink-4" aria-hidden="true">·</span>
<span>Newsroom seit {{ $newsroomStats['sinceYear'] }}</span>
<span>{{ __('web.newsroom_page.since', ['year' => $newsroomStats['sinceYear']]) }}</span>
@endif
</div>
</div>
@ -107,7 +107,7 @@
@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">Branchen</span>
<span class="eyebrow muted text-[10px] mr-1.5">{{ __('web.newsroom_page.industries') }}</span>
@foreach ($categories as $cat)
<a href="{{ $cat['href'] }}" class="inline-flex items-baseline gap-[7px] px-3 py-1.5 text-[12px] font-medium border border-bg-rule-strong bg-bg text-ink hover:bg-white hover:border-brand transition-colors">
{{ $cat['name'] }}
@ -123,24 +123,24 @@
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-6">
<div class="grid grid-cols-2 lg:grid-cols-4 border-y border-bg-rule">
<div class="px-6 py-5 border-r border-bg-rule">
<div class="eyebrow muted text-[10px] mb-2.5">Meldungen aktueller Tag</div>
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.category.stat_today') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($newsroomStats['today'] ?? 0) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">Stand {{ $referenceNow->translatedFormat('j. M Y') }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.spotlight.stat_window_sub', ['date' => $referenceNow->translatedFormat('j. M Y')]) }}</div>
</div>
<div class="px-6 py-5 lg:border-r border-bg-rule">
<div class="eyebrow muted text-[10px] mb-2.5">Letzte 30 Tage</div>
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.newsroom_page.last_30_days') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($newsroomStats['recent'] ?? 0) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">veröffentlichte Meldungen</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.newsroom_page.published_releases') }}</div>
</div>
<div class="px-6 py-5 border-r border-bg-rule border-t lg:border-t-0">
<div class="eyebrow muted text-[10px] mb-2.5">Im Archiv</div>
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.spotlight.stat_archive') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($newsroomStats['total'] ?? 0) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">Meldungen gesamt</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.spotlight.stat_archive_sub') }}</div>
</div>
<div class="px-6 py-5 border-t lg:border-t-0">
<div class="eyebrow muted text-[10px] mb-2.5">Aufrufe gesamt</div>
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.newsroom_page.total_hits') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($newsroomStats['totalHits'] ?? 0) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">über alle Meldungen</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.newsroom_page.total_hits_sub') }}</div>
</div>
</div>
</section>
@ -150,12 +150,15 @@
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-10">
<article>
<a href="{{ route('release.detail', ['slug' => $leadRelease->slug]) }}" class="block group">
<div class="relative overflow-hidden bg-hero-grad hatch-dark" style="aspect-ratio: 16 / 9;">
{{-- Kein hatch-dark: dessen background-image würde den bg-hero-grad-Verlauf überschreiben. --}}
<div class="relative overflow-hidden bg-hero-grad" style="aspect-ratio: 16 / 9;">
@if ($leadImageUrl)
<img src="{{ $leadImageUrl }}" alt="{{ $leadRelease->title }}" class="absolute inset-0 w-full h-full object-cover" loading="lazy">
@else
<x-web.hero-fallback />
@endif
<div class="absolute top-5 left-5 flex gap-2">
<span class="bp-tag">Aktuellste Meldung</span>
<span class="bp-tag">{{ __('web.newsroom_page.latest') }}</span>
</div>
<div class="absolute left-0 right-0 bottom-0 px-6 sm:px-9 pb-8 pt-[70px] text-white" style="background:linear-gradient(180deg,transparent,rgba(0,0,0,0.92));">
@if ($leadCat)
@ -172,7 +175,7 @@
<span class="font-mono">{{ $leadRelease->published_at->translatedFormat('d.m.Y · H:i') }}</span>
<span aria-hidden="true">·</span>
@endif
<span>{{ $leadReadTime }} Min. Lesezeit</span>
<span>{{ __('web.shared.read_time', ['minutes' => $leadReadTime]) }}</span>
</div>
</div>
</div>
@ -186,7 +189,7 @@
<div>
<header class="flex items-baseline justify-between mb-4 min-h-[34px] flex-wrap gap-3">
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">
Alle Meldungen <span class="text-[14px] text-ink-3 font-normal ml-1.5">· {{ $fmt($newsroomStats['total'] ?? 0) }}</span>
{{ __('web.category.all_releases') }} <span class="text-[14px] text-ink-3 font-normal ml-1.5">· {{ $fmt($newsroomStats['total'] ?? 0) }}</span>
</h2>
</header>
<hr class="rule-strong">
@ -194,7 +197,7 @@
@forelse ($releases as $release)
<x-web.feed-item :release="$release" />
@empty
<p class="text-[13px] text-ink-3 py-6">Aktuell keine weiteren Meldungen in diesem Newsroom.</p>
<p class="text-[13px] text-ink-3 py-6">{{ __('web.newsroom_page.empty') }}</p>
@endforelse
<x-web.pagination :paginator="$releases" />
@ -205,16 +208,16 @@
{{-- Eigenen Newsroom starten --}}
<section class="border border-bg-rule-strong bg-bg-elev p-6">
<div class="eyebrow mb-2">Für Unternehmen</div>
<div class="eyebrow mb-2">{{ __('web.publisher_cta.eyebrow') }}</div>
<h3 class="font-serif text-[20px] font-semibold m-0 mb-2 tracking-[-0.2px] leading-[1.2] text-ink">
Eigenen Newsroom starten
{{ __('web.newsroom_page.own_title') }}
</h3>
<p class="text-[12.5px] leading-[1.5] m-0 mb-4 text-ink-2">
Bündeln Sie alle Ihre Pressemitteilungen unter einer eigenen Marken-URL – mit Logo, Branchen-Zuordnung und SEO-Vorteil.
{{ __('web.newsroom_page.own_text') }}
</p>
<a href="{{ route('veroeffentlichen') }}"
class="w-full inline-flex items-center justify-center gap-2 px-[18px] py-2.5 text-[13px] font-semibold bg-white border border-bg-rule-strong text-ink rounded-[2px] cursor-pointer hover:border-brand hover:text-brand transition-colors">
Newsroom anlegen →
{{ __('web.newsroom_page.own_cta') }} →
</a>
</section>

View file

@ -13,8 +13,8 @@
$fmt = fn ($value) => number_format((int) $value, 0, ',', '.');
@endphp
@section('title', 'Newsrooms – '.$brandName.$brandAccent)
@section('meta_description', 'Alle aktiven Unternehmens-Newsrooms im Überblick – redaktionell geprüfte Pressemitteilungen je Unternehmen, gebündelt und dauerhaft auffindbar im DACH-Raum.')
@section('title', __('web.newsrooms_page.meta_title', ['brand' => $brandName.$brandAccent]))
@section('meta_description', __('web.newsrooms_page.meta_description'))
@section('hreflang')
<link rel="alternate" hreflang="de" href="{{ url('/de/newsrooms') }}">
@ -38,31 +38,31 @@
{{-- Masthead --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-8 lg:pt-10">
<div class="max-w-[680px]">
<div class="eyebrow mb-3">Unternehmens-Newsrooms</div>
<div class="eyebrow mb-3">{{ __('web.newsrooms_page.eyebrow') }}</div>
<h1 class="font-serif text-[36px] sm:text-[46px] lg:text-[50px] font-semibold m-0 mb-4 leading-[1.05] tracking-[-1px] text-ink text-balance">
Newsrooms
</h1>
<p class="font-serif text-[16px] leading-[1.55] m-0 text-ink-2">
Alle aktiven Unternehmen mit ihren redaktionell geprüften Pressemitteilungen – gebündelt an einem Ort.
{{ __('web.newsrooms_page.intro') }}
</p>
</div>
{{-- Stat strip --}}
<div class="grid grid-cols-2 lg:grid-cols-3 border-y border-bg-rule mt-8">
<div class="px-6 py-5 border-r border-bg-rule">
<div class="eyebrow muted text-[10px] mb-2.5">Aktive Newsrooms</div>
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.newsrooms.title') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($totalNewsrooms) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">mit veröffentlichten Meldungen</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.newsrooms_page.stat_total_sub') }}</div>
</div>
<div class="px-6 py-5 lg:border-r border-bg-rule">
<div class="eyebrow muted text-[10px] mb-2.5">Aktiv (30 Tage)</div>
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.newsrooms_page.stat_active') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($activeNewsroomsCount) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">Stand {{ $referenceNow->translatedFormat('j. M Y') }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.spotlight.stat_window_sub', ['date' => $referenceNow->translatedFormat('j. M Y')]) }}</div>
</div>
<div class="px-6 py-5 border-t lg:border-t-0">
<div class="eyebrow muted text-[10px] mb-2.5">Meldungen im Archiv</div>
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.newsrooms_page.stat_archive') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($archiveTotal) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">redaktionell geprüft</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.newsrooms_page.stat_archive_sub') }}</div>
</div>
</div>
</section>
@ -70,18 +70,18 @@
{{-- Suche + Verzeichnis --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-16">
<form action="{{ route('newsrooms') }}" method="get" class="mb-7 max-w-[520px]">
<label class="sr-only" for="newsroom-search">Newsroom suchen</label>
<label class="sr-only" for="newsroom-search">{{ __('web.newsrooms_page.search_label') }}</label>
<div class="flex items-center gap-3 px-4 py-3 border border-bg-rule-strong bg-bg-elev focus-within:border-brand transition-colors">
<svg width="18" height="18" viewBox="0 0 16 16" fill="none" class="flex-shrink-0 text-ink-3" aria-hidden="true">
<circle cx="7" cy="7" r="5" stroke="currentColor" stroke-width="1.5" />
<path d="M11 11l3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
<input id="newsroom-search" type="search" name="q" value="{{ $term }}"
placeholder="Unternehmen suchen…"
placeholder="{{ __('web.newsrooms_page.search_placeholder') }}"
autocomplete="off"
class="flex-1 bg-transparent border-0 p-0 text-[14px] text-ink placeholder:text-ink-3 focus:outline-none focus:ring-0 min-w-0">
<button type="submit" class="flex-shrink-0 px-3.5 py-1.5 text-[12.5px] font-semibold bg-brand text-white rounded-[2px] hover:bg-brand-deep transition-colors">
Suchen
{{ __('web.header.search_submit') }}
</button>
</div>
</form>
@ -91,11 +91,11 @@
{{ $fmt($newsrooms->total()) }}
{{ $newsrooms->total() === 1 ? 'Newsroom' : 'Newsrooms' }}
@if ($term !== '')
<span class="text-ink-3 font-normal">für „{{ $term }}"</span>
<span class="text-ink-3 font-normal">{{ __('web.search.for_term', ['term' => $term]) }}</span>
@endif
</h2>
@if ($term !== '')
<a href="{{ route('newsrooms') }}" class="text-[12.5px] text-brand font-semibold hover:text-brand-deep transition-colors">Filter zurücksetzen</a>
<a href="{{ route('newsrooms') }}" class="text-[12.5px] text-brand font-semibold hover:text-brand-deep transition-colors">{{ __('web.newsrooms_page.reset_filter') }}</a>
@endif
</header>
<hr class="rule-strong mb-6">
@ -123,18 +123,18 @@
<div class="min-w-0 flex-1">
<h3 class="font-serif text-[16px] font-semibold leading-[1.25] text-ink group-hover:text-brand transition-colors truncate">{{ $company->name }}</h3>
@if ($latest)
<div class="text-[11px] text-ink-3 mt-1">Letzte Meldung {{ $latest->translatedFormat('j. M Y') }}</div>
<div class="text-[11px] text-ink-3 mt-1">{{ __('web.newsrooms_page.latest_release', ['date' => $latest->translatedFormat('j. M Y')]) }}</div>
@endif
</div>
</div>
<div class="mt-auto flex items-center justify-between pt-3 border-t border-bg-rule">
<span class="text-[12px] text-ink-2">
<span class="font-mono font-semibold text-ink">{{ $fmt($company->releases_count ?? 0) }}</span> Meldungen
<span class="font-mono font-semibold text-ink">{{ $fmt($company->releases_count ?? 0) }}</span> {{ __('web.industry_index.releases') }}
</span>
@if ($isRecentlyActive)
<span class="inline-flex items-center gap-1.5 text-[10.5px] font-semibold text-ink-2 px-2 py-0.5 bg-brand/[0.06] border border-brand/20 rounded-[2px]">
<span class="w-1.5 h-1.5 rounded-full bg-brand"></span>
{{ $fmt($company->recent_count) }} / 30 Tage
{{ $fmt($company->recent_count) }} / {{ __('web.newsrooms_page.days_30') }}
</span>
@endif
</div>
@ -145,17 +145,17 @@
<x-web.pagination :paginator="$newsrooms" />
@else
<div class="border border-bg-rule bg-bg-elev px-6 py-12 text-center">
<h3 class="font-serif text-[20px] font-semibold text-ink m-0 mb-2">Keine Newsrooms gefunden</h3>
<h3 class="font-serif text-[20px] font-semibold text-ink m-0 mb-2">{{ __('web.newsrooms_page.none_found') }}</h3>
<p class="text-[13px] text-ink-3 m-0 mb-5 max-w-md mx-auto">
@if ($term !== '')
Für „{{ $term }}" gibt es derzeit keinen passenden Newsroom. Versuchen Sie einen anderen Suchbegriff.
{{ __('web.newsrooms_page.none_found_term', ['term' => $term]) }}
@else
Aktuell sind keine aktiven Newsrooms verfügbar.
{{ __('web.newsrooms_page.none_available') }}
@endif
</p>
@if ($term !== '')
<a href="{{ route('newsrooms') }}" class="inline-flex items-center gap-2 px-4 py-2.5 text-[13px] font-semibold border border-bg-rule-strong bg-white text-ink hover:bg-bg-elev transition-colors">
Alle Newsrooms anzeigen
{{ __('web.newsrooms.all') }}
</a>
@endif
</div>
@ -167,13 +167,13 @@
<div class="bg-topbar-grad text-ink-on-dark p-6 sm:p-9">
<div class="grid gap-9 lg:grid-cols-[1.4fr_1fr] lg:items-center">
<div>
<div class="eyebrow on-dark mb-2.5">Eigener Newsroom</div>
<h3 class="font-serif text-[24px] sm:text-[28px] font-semibold m-0 mb-2.5 leading-[1.15] text-white tracking-[-0.3px]">Bündeln Sie Ihre Pressemitteilungen im eigenen Newsroom.</h3>
<p class="text-[14px] leading-[1.55] m-0 max-w-[540px] text-white/80">Eigene Marken-URL · alle Meldungen zentral · SEO-Vorteil für Ihr Unternehmen · redaktionell geprüft.</p>
<div class="eyebrow on-dark mb-2.5">{{ __('web.newsrooms_page.cta_eyebrow') }}</div>
<h3 class="font-serif text-[24px] sm:text-[28px] font-semibold m-0 mb-2.5 leading-[1.15] text-white tracking-[-0.3px]">{{ __('web.newsrooms_page.cta_title') }}</h3>
<p class="text-[14px] leading-[1.55] m-0 max-w-[540px] text-white/80">{{ __('web.newsrooms_page.cta_text') }}</p>
</div>
<div class="flex flex-col gap-2.5">
<a href="{{ route('veroeffentlichen') }}" class="w-full inline-flex items-center justify-center px-[18px] py-3.5 text-[14px] font-semibold bg-brand text-white hover:bg-brand-deep transition-colors">Pressemitteilung einreichen →</a>
<a href="{{ route('preise') }}" class="w-full inline-flex items-center justify-center px-[18px] py-3 text-[14px] font-semibold text-white border border-white/30 hover:bg-white/10 transition-colors">Tarife &amp; Pakete ansehen</a>
<a href="{{ route('veroeffentlichen') }}" class="w-full inline-flex items-center justify-center px-[18px] py-3.5 text-[14px] font-semibold bg-brand text-white hover:bg-brand-deep transition-colors">{{ __('web.category.cta_submit') }} →</a>
<a href="{{ route('preise') }}" class="w-full inline-flex items-center justify-center px-[18px] py-3 text-[14px] font-semibold text-white border border-white/30 hover:bg-white/10 transition-colors">{{ __('web.publisher_cta.pricing') }}</a>
</div>
</div>
</div>

View file

@ -285,8 +285,8 @@
</div>
</section>
{{-- Abschluss mit vorhandener Editorial-Komponente. --}}
<x-web.newsletter-strip />
{{-- Newsletter-Sektion bewusst entfernt: kommt nachgelagert mit echtem
Double-Opt-in (siehe dev/release/entwicklungsplan-release.md). --}}
</main>
<x-web.site-footer />

View file

@ -4,8 +4,13 @@
$brand = config('domains.domains.presseecho.brand', []);
@endphp
@section('title', $brand['meta_title'] ?? 'Aktuelle Pressemitteilungen aus Wirtschaft & Industrie – presseecho')
@section('meta_description', $brand['meta_description'] ?? 'Redaktionell kuratierte Pressemitteilungen aus Wirtschaft und Industrie im DACH-Raum.')
{{-- DE-Meta kommt brand-spezifisch aus config/domains.php, EN aus den Übersetzungsdateien. --}}
@section('title', app()->getLocale() === 'de'
? ($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.')
: __('web.meta.home_description'))
@section('hreflang')
<link rel="alternate" hreflang="de" href="{{ url('/de') }}">
@ -14,7 +19,7 @@
@endsection
@section('content')
<x-web.site-header :ticker="$tickerItems ?? []" />
<x-web.site-header />
<main class="bg-bg text-ink">
<x-web.focus-hero
@ -42,7 +47,9 @@
<x-web.pricing-teaser />
<x-web.newsletter-strip />
{{-- Newsletter-Sektion bewusst entfernt (Double-Opt-in nachgelagert,
siehe dev/release/entwicklungsplan-release.md) — stattdessen Veröffentlichen-CTA. --}}
<x-web.publish-band />
<x-web.quality-summary :stats="$homeStats ?? []" />
</main>

View file

@ -39,7 +39,7 @@
<p class="text-[16px] lg:text-[17px] leading-[1.55] text-ink-2 mt-7 m-0 max-w-[560px]">
<x-web.brand-mark brand="pressekonto" :serif="false" /> ist der gemeinsame Publisher-Bereich für
unsere beiden Pressefachportale. Pressemitteilungen schreiben, redaktionell prüfen lassen, auf
unsere beiden Pressefachportale. Pressemitteilungen schreiben, automatisiert prüfen lassen, auf
beiden Reichweiten veröffentlichen — und Mitteilungen, Newsrooms und Abrechnung an einem Ort
verwalten.
</p>
@ -234,7 +234,7 @@
Pressemitteilungen auf beiden Portalen
</h3>
<p class="text-[13.5px] leading-[1.6] text-ink-2 mt-3 m-0">
Eine Mitteilung schreiben, redaktionell prüfen lassen, auf <x-web.brand-mark brand="presseecho"
Eine Mitteilung schreiben, Inhaltsprüfung durchlaufen lassen, auf <x-web.brand-mark brand="presseecho"
:serif="false" /> und <x-web.brand-mark brand="businessportal24" :serif="false" />
gleichzeitig veröffentlichen. Vorschau und Qualitäts-Checkliste inklusive.
</p>
@ -291,7 +291,7 @@
Aufrufe, Status und Abrechnung zentral
</h3>
<p class="text-[13.5px] leading-[1.6] text-ink-2 mt-3 m-0">
Aufrufe je Mitteilung über beide Portale. Prüf-Status und redaktionelles Feedback direkt an der
Aufrufe je Mitteilung über beide Portale. Prüf-Status und Feedback direkt an der
Mitteilung, Buchungen und Rechnungen zentral im Konto.
</p>
<ul class="mt-5 space-y-1.5 text-[12.5px] text-ink-3 list-none p-0">
@ -326,8 +326,9 @@
</div>
<div class="text-[16.5px] font-semibold text-ink tracking-[-0.2px] mb-2">Registrieren</div>
<p class="text-[12.5px] text-ink-2 leading-[1.55] m-0">
Konto in unter zwei Minuten anlegen — mit E-Mail und Passwort oder Google-Konto. Nach
Bestätigung Ihrer E-Mail-Adresse ist das Konto für beide Portale freigeschaltet.
Konto in unter zwei Minuten anlegen — mit E-Mail und Passwort, Anmeldung wahlweise
auch passwortlos per Magic-Link. Nach Bestätigung Ihrer E-Mail-Adresse ist das Konto
für beide Portale freigeschaltet.
</p>
</li>
<li class="relative px-7 py-7 border-r border-b lg:border-b-0 border-bg-rule">
@ -539,7 +540,7 @@
</h2>
<p class="text-[14.5px] text-ink-2 leading-[1.55] m-0 mt-4 max-w-[760px]">
<x-web.brand-mark brand="pressekonto" :serif="false" /> ist nicht „irgendein Tool" — es ist die
zentrale Plattform für unsere beiden redaktionell geführten Pressefachportale. Jedes Portal hat einen
zentrale Plattform für unsere beiden Pressefachportale. Jedes Portal hat einen
eigenen Charakter, eigene Leserschaft und eigene Themen-Schwerpunkte.
</p>
</header>
@ -564,7 +565,7 @@
<div class="px-7 py-7">
<p class="text-[13.5px] leading-[1.6] text-ink-2 m-0">
Themen-Sammlung und Langzeit-Archiv für Pressemitteilungen aus DACH. Editorial-Charakter,
Themen-Tiefe, redaktionell erschlossen — der Ort, an dem eine Mitteilung über Jahre auffindbar
Themen-Tiefe, nach Branchen erschlossen — der Ort, an dem eine Mitteilung über Jahre auffindbar
bleibt.
</p>
<hr class="rule my-5" />
@ -577,7 +578,7 @@
<div>
<div class="font-mono text-[18px] font-semibold text-ink leading-none tabular-nums">
{{ $fmtCount($hubStats['categories']) }} Branchen</div>
<div class="text-[11px] text-ink-3 mt-1">redaktionell erschlossen</div>
<div class="text-[11px] text-ink-3 mt-1">nach Branchen erschlossen</div>
</div>
</div>
<a href="{{ config('domains.domain_presseecho_url') }}"
@ -797,7 +798,7 @@
</svg>
</summary>
<div class="pb-5 text-[13.5px] text-ink-2 leading-[1.65] max-w-[640px]">
Nach der redaktionellen Freigabe geht Ihre Mitteilung sofort live — werktags in der Regel
Nach bestandener Inhaltsprüfung geht Ihre Mitteilung sofort live — werktags in der Regel
noch am selben Tag. Alternativ legen Sie einen Wunschtermin fest: Dann erscheint die
Mitteilung automatisch zum geplanten Zeitpunkt, geprüft und freigegeben ist sie bereits davor.
</div>

View file

@ -3,17 +3,15 @@
@php
use Illuminate\Support\Str;
\Carbon\Carbon::setLocale('de');
$company = $release->company;
$categoryTranslation = $release->category?->translations?->firstWhere('locale', 'de')
$categoryTranslation = $release->category?->translations?->firstWhere('locale', app()->getLocale())
?? $release->category?->translations?->first();
$categoryName = $categoryTranslation?->name;
$categorySlug = $categoryTranslation?->slug;
$categoryHref = $categorySlug ? route('kategorie', ['slug' => $categorySlug]) : route('web.home');
$parentTranslation = $release->category?->parent?->translations?->firstWhere('locale', 'de')
$parentTranslation = $release->category?->parent?->translations?->firstWhere('locale', app()->getLocale())
?? $release->category?->parent?->translations?->first();
$parentName = $parentTranslation?->name;
$parentSlug = $parentTranslation?->slug;
@ -38,9 +36,9 @@
$companyName = $company?->name;
$companyInitial = $companyName ? mb_strtoupper(mb_substr($companyName, 0, 1)) : '·';
$companyHref = $company?->slug ? route('newsrooms').'#'.$company->slug : route('newsrooms');
$companyHref = $company?->slug ? route('newsroom', ['slug' => $company->slug]) : route('newsrooms');
$countryNames = ['DE' => 'Deutschland', 'AT' => 'Österreich', 'CH' => 'Schweiz'];
$countryNames = ['DE' => __('web.countries.de'), 'AT' => __('web.countries.at'), 'CH' => __('web.countries.ch')];
$companyCountry = $company?->country_code ? ($countryNames[strtoupper($company->country_code)] ?? null) : null;
$boilerplate = filled($release->boilerplate_override) ? $release->boilerplate_override : $company?->boilerplate;
@ -84,8 +82,8 @@
<article>
{{-- Breadcrumbs --}}
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-7">
<nav class="flex flex-wrap gap-2 text-[11.5px] text-ink-3 mb-2 font-mono tracking-wide" aria-label="Brotkrumen">
<a href="{{ route('web.home') }}" class="cursor-pointer hover:text-ink transition-colors">Startseite</a>
<nav class="flex flex-wrap gap-2 text-[11.5px] text-ink-3 mb-2 font-mono tracking-wide" aria-label="{{ __('web.detail.breadcrumbs') }}">
<a href="{{ route('web.home') }}" class="cursor-pointer hover:text-ink transition-colors">{{ __('web.nav.home') }}</a>
<span aria-hidden="true">/</span>
@if ($parentName)
<a href="{{ $parentHref }}" class="cursor-pointer hover:text-ink transition-colors">{{ $parentName }}</a>
@ -95,7 +93,7 @@
<a href="{{ $categoryHref }}" class="cursor-pointer text-brand hover:text-brand-deep transition-colors">{{ $categoryName }}</a>
<span aria-hidden="true">/</span>
@endif
<span class="text-ink-2">Pressemitteilung</span>
<span class="text-ink-2">{{ __('web.detail.press_release') }}</span>
</nav>
</div>
@ -105,7 +103,7 @@
@if ($embargoAt)
<div class="flex items-center gap-2.5 mb-3.5 flex-wrap">
<span class="text-[9.5px] font-bold tracking-[0.18em] uppercase text-brand px-2.5 py-[3px] border border-brand font-mono">
● Embargo aufgehoben
● {{ __('web.detail.embargo_lifted') }}
</span>
<span class="text-[11px] text-ink-3 font-mono">{{ $embargoAt->translatedFormat('j. F Y, H:i') }}</span>
</div>
@ -116,7 +114,7 @@
<a href="{{ $categoryHref }}" class="bp-cat cursor-pointer hover:text-brand-deep transition-colors">{{ $categoryName }}</a>
<span class="w-1 h-1 rounded-full bg-ink-4" aria-hidden="true"></span>
@endif
<span class="text-[11px] text-ink-3 italic font-serif">Für Wirtschaftsjournalisten</span>
<span class="text-[11px] text-ink-3 italic font-serif">{{ __('web.detail.audience') }}</span>
</div>
<h1 class="font-serif text-[32px] sm:text-[40px] lg:text-[48px] font-semibold m-0 mb-[22px] tracking-[-0.8px] leading-[1.1] max-w-[980px]" style="text-wrap:balance;">
@ -139,10 +137,10 @@
@endif
@if ($publishedAt)
<time datetime="{{ $publishedAt->toIso8601String() }}" class="font-mono text-[11.5px] text-ink-3">
{{ $publishedAt->translatedFormat('j. F Y') }} · {{ $publishedAt->format('H:i') }} Uhr
{{ $publishedAt->translatedFormat('j. F Y') }} · {{ __('web.detail.time_at', ['time' => $publishedAt->format('H:i')]) }}
</time>
@endif
<span class="font-mono text-[11.5px] text-ink-3">{{ $readTime }} Min. Lesezeit · {{ number_format($wordCount, 0, ',', '.') }} Wörter</span>
<span class="font-mono text-[11.5px] text-ink-3">{{ __('web.shared.read_time', ['minutes' => $readTime]) }} · {{ __('web.detail.words', ['count' => number_format($wordCount, 0, ',', '.')]) }}</span>
@if ($companyCountry)
<span class="inline-flex items-center gap-1.5 font-mono text-[11.5px] text-ink-3">
<svg width="11" height="11" viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M7 1a5 5 0 015 5c0 4-5 7-5 7s-5-3-5-7a5 5 0 015-5z" stroke="currentColor" stroke-width="1.3" fill="none" /><circle cx="7" cy="6" r="1.5" fill="currentColor" /></svg>
@ -152,12 +150,12 @@
@if ($isRecommended)
<span class="inline-flex items-center gap-1.5 px-2 py-[3px] text-[9.5px] font-bold tracking-[0.14em] uppercase text-brand bg-brand/[0.04] border border-brand/30 font-mono cursor-help"
title="Diese Pressemitteilung wurde von der Redaktion empfohlen — basierend auf Quellenqualität, Verifizierungsstatus und redaktioneller Relevanz.">
title="{{ __('web.detail.recommended_hint') }}">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 2l9 4v6c0 5-3.5 9-9 10-5.5-1-9-5-9-10V6z" stroke="currentColor" stroke-width="2" fill="none" />
<path d="M8 12l3 3 5-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
Redaktionsempfehlung
{{ __('web.detail.recommended') }}
</span>
@endif
</div>
@ -172,13 +170,13 @@
class="absolute inset-0 h-full w-full object-cover" loading="eager">
@else
<span class="relative text-[10px] font-mono tracking-wider uppercase text-white/50">
{{ $companyName ? 'Bild · Pressefoto '.$companyName : 'Bild · Pressefoto' }} (16:9)
{{ $companyName ? __('web.detail.image_placeholder_company', ['company' => $companyName]) : __('web.detail.image_placeholder') }} (16:9)
</span>
@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">Foto: {{ $imageCredit }}</span>@endif
{{ $imageCaption }}@if ($imageCaption && $imageCredit) · @endif@if ($imageCredit)<span class="not-italic">{{ __('web.detail.photo') }}: {{ $imageCredit }}</span>@endif
</div>
@endif
</section>
@ -193,45 +191,20 @@
@if (filled($boilerplate))
<div class="mt-9 pt-5 border-t border-bg-rule-strong max-w-[680px]">
<div class="eyebrow muted text-[9.5px] mb-2.5">Über das Unternehmen</div>
<div class="eyebrow muted text-[9.5px] mb-2.5">{{ __('web.detail.about_company') }}</div>
<p class="text-[13px] leading-[1.6] text-ink-3 m-0 max-w-[620px]">{{ $boilerplate }}</p>
</div>
@endif
{{-- Inline Ad --}}
<div class="my-8 max-w-[680px]">
<div class="flex items-center gap-2.5 mb-2.5">
<span class="h-px bg-bg-rule-strong flex-1"></span>
<span class="text-[9px] font-bold tracking-[0.22em] text-ink-on-dark-muted uppercase font-mono">Anzeige</span>
<span class="h-px bg-bg-rule-strong flex-1"></span>
</div>
<a href="#" rel="sponsored nofollow" class="grid px-[22px] py-5 bg-bg-card-warm items-center gap-[22px] cursor-pointer hover:bg-bg-card-warm-hover transition-colors" style="grid-template-columns:120px 1fr;">
<div class="aspect-square bg-white hatch-light flex items-center justify-center text-[9px] text-ink-on-dark-muted font-mono">LOGO</div>
<div>
<div class="text-[9.5px] font-bold tracking-[0.16em] uppercase mb-1.5 text-card-warm-cat">Partner · Branchen-Whitepaper</div>
<h3 class="font-serif text-[18px] leading-[1.3] m-0 mb-2 font-semibold tracking-[-0.2px] text-card-warm-title">
CO₂-Reporting nach CSRD: Was Industriebetriebe jetzt automatisieren müssen
</h3>
<div class="text-[12px] leading-[1.5] mb-2.5 text-card-warm-cat">Whitepaper · 32 Seiten · Praxisleitfaden für Mittelstand und Konzerne</div>
<span class="text-[12px] font-semibold underline text-card-warm-title">Studie kostenfrei anfordern →</span>
</div>
</a>
<div class="flex items-center gap-2.5 mt-2.5">
<span class="h-px bg-bg-rule-strong flex-1"></span>
<span class="text-[9px] font-bold tracking-[0.22em] text-ink-on-dark-muted uppercase font-mono">Ende Anzeige</span>
<span class="h-px bg-bg-rule-strong flex-1"></span>
</div>
</div>
{{-- Pressekontakt --}}
@if ($contact)
<div class="mt-8 px-5 sm:px-7 py-6 bg-white border border-bg-rule-strong max-w-[680px]">
<div class="eyebrow mb-3 text-[10.5px]">Pressekontakt</div>
<div class="eyebrow mb-3 text-[10.5px]">{{ __('web.detail.press_contact') }}</div>
<div class="flex gap-1.5 items-center mb-1 flex-wrap">
<strong class="font-serif text-[19px] font-semibold tracking-[-0.2px]">{{ $contactName ?: $companyName }}</strong>
@if ($publisherVerified)
<span title="Verifizierter Publisher — Identität und Domain bestätigt" class="inline-flex items-center text-verify-border flex-shrink-0">
<span title="{{ __('web.detail.verified_hint') }}" class="inline-flex items-center text-verify-border flex-shrink-0">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 1l2.5 3 3.8-.4-.5 3.8L21 9l-2.6 2.5L19 15l-3.8.4L12 19l-2.5-3.6L5.7 15 5.6 11.4 3 9l2.5-1.6L4.7 4 8.5 4z" fill="currentColor" />
<path d="M7.5 12l3 3 6-6" stroke="white" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round" />
@ -277,16 +250,16 @@
{{-- Inhalt melden (T13): sichtbar an jeder Mitteilung, nicht nur in der Sidebar --}}
<div class="mt-8 pt-4 border-t border-bg-rule flex flex-wrap items-center gap-x-4 gap-y-1.5 text-[11.5px] text-ink-3">
<span>Sie haben ein rechtliches Anliegen zu dieser Mitteilung?</span>
<span>{{ __('web.detail.legal_question') }}</span>
<a href="{{ route('legal-request.create', ['slug' => $release->slug, 'type' => 'report']) }}" class="inline-flex items-center gap-1.5 text-ink-2 underline underline-offset-2 hover:text-ink transition-colors">
<svg width="12" height="12" viewBox="0 0 14 14" class="flex-shrink-0" aria-hidden="true"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.3" fill="none" /><path d="M7 4v3.5M7 9.5v.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" /></svg>
Inhalt melden
{{ __('web.detail.report_content') }}
</a>
<a href="{{ route('legal-request.create', ['slug' => $release->slug, 'type' => 'dsgvo']) }}" class="inline-flex items-center gap-1.5 text-ink-2 underline underline-offset-2 hover:text-ink transition-colors">
<svg width="12" height="12" viewBox="0 0 14 14" class="flex-shrink-0" aria-hidden="true"><path d="M7 1l5 2v4c0 3-2.2 5.2-5 6-2.8-.8-5-3-5-6V3z" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linejoin="round" /></svg>
Eigene Daten entfernen
{{ __('web.detail.remove_data') }}
</a>
<span class="text-ink-4">Meldungen sind kostenlos.</span>
<span class="text-ink-4">{{ __('web.detail.reports_free') }}</span>
</div>
</div>
@ -295,7 +268,7 @@
{{-- Publisher --}}
@if ($companyName)
<div class="p-4 bg-white border border-bg-rule-strong">
<div class="eyebrow muted text-[9.5px] mb-3">Veröffentlicht von</div>
<div class="eyebrow muted text-[9.5px] mb-3">{{ __('web.detail.published_by') }}</div>
<div class="flex gap-3 items-start mb-3">
<span class="w-11 h-11 bg-black text-white inline-flex items-center justify-center text-[20px] font-bold font-serif flex-shrink-0">{{ $companyInitial }}</span>
<div class="min-w-0">
@ -308,18 +281,17 @@
@if ($publisherVerified)
<div class="flex items-center gap-2 px-2.5 py-2 mb-3 bg-verify-bg border border-verify-border text-verify-ink cursor-help"
title="Identität und Domain durch {{ $brandLabel }} bestätigt">
title="{{ __('web.detail.verified_by', ['brand' => $brandLabel]) }}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" class="text-verify-border flex-shrink-0" aria-hidden="true">
<path d="M12 1l2.5 3 3.8-.4-.5 3.8L21 9l-2.6 2.5L19 15l-3.8.4L12 19l-2.5-3.6L5.7 15 5.6 11.4 3 9l2.5-1.6L4.7 4 8.5 4z" fill="currentColor" />
<path d="M7.5 12l3 3 6-6" stroke="white" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="text-[11.5px] font-semibold">Verifizierter Publisher</span>
<span class="text-[11.5px] font-semibold">{{ __('web.detail.verified_publisher') }}</span>
</div>
@endif
<div class="pt-3 border-t border-bg-rule flex flex-col gap-2">
<button type="button" class="px-2.5 py-2 text-[11.5px] font-semibold bg-ink text-white hover:bg-black transition-colors cursor-pointer">+ Newsroom folgen</button>
<a href="{{ $companyHref }}" class="text-[11.5px] text-ink-2 text-center py-1.5 underline underline-offset-2 cursor-pointer hover:text-ink transition-colors">Alle Mitteilungen →</a>
<a href="{{ $companyHref }}" class="px-2.5 py-2 text-center text-[11.5px] font-semibold bg-ink text-white hover:bg-black transition-colors cursor-pointer">{{ __('web.detail.to_newsroom') }} →</a>
</div>
</div>
@endif
@ -331,7 +303,7 @@
copied: false,
copy() { navigator.clipboard?.writeText(this.url).then(() => { this.copied = true; setTimeout(() => this.copied = false, 2000); }); }
}">
<div class="eyebrow muted text-[9.5px] mb-2.5">Teilen</div>
<div class="eyebrow muted text-[9.5px] mb-2.5">{{ __('web.detail.share') }}</div>
<div class="grid grid-cols-2 gap-1.5">
<a :href="`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`" target="_blank" rel="noopener" class="inline-flex items-center gap-[7px] px-2.5 py-2 text-[11.5px] font-medium bg-white border border-bg-rule-strong text-ink hover:bg-bg-elev transition-colors cursor-pointer">
<svg width="13" height="13" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6h2.5v8H3V6zm1.25-3a1.25 1.25 0 110 2.5 1.25 1.25 0 010-2.5zM7 6h2.4v1.1c.4-.7 1.3-1.3 2.6-1.3 2.7 0 3 1.7 3 3.9V14h-2.5v-3.7c0-.9 0-2-1.3-2s-1.5 1-1.5 2V14H7V6z" fill="currentColor" /></svg>
@ -343,11 +315,11 @@
</a>
<a :href="`mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(url)}`" class="inline-flex items-center gap-[7px] px-2.5 py-2 text-[11.5px] font-medium bg-white border border-bg-rule-strong text-ink hover:bg-bg-elev transition-colors cursor-pointer">
<svg width="13" height="13" viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="3" width="12" height="10" stroke="currentColor" stroke-width="1.3" fill="none" /><path d="M2 4l6 4 6-4" stroke="currentColor" stroke-width="1.3" fill="none" /></svg>
E-Mail
{{ __('web.detail.email') }}
</a>
<button type="button" @click="copy()" class="inline-flex items-center gap-[7px] px-2.5 py-2 text-[11.5px] font-medium bg-white border border-bg-rule-strong text-ink hover:bg-bg-elev transition-colors cursor-pointer">
<svg width="13" height="13" viewBox="0 0 16 16" aria-hidden="true"><path d="M6 9l4-4M5 4h2a3 3 0 010 6h-1M11 12H9a3 3 0 010-6h1" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round" /></svg>
<span x-text="copied ? 'Kopiert!' : 'Link kopieren'">Link kopieren</span>
<span x-text="copied ? @js(__('web.detail.copied')) : @js(__('web.detail.copy_link'))">{{ __('web.detail.copy_link') }}</span>
</button>
</div>
</div>
@ -355,7 +327,7 @@
{{-- Schlagwörter --}}
@if ($keywords->isNotEmpty())
<div>
<div class="eyebrow muted text-[9.5px] mb-2.5">Schlagwörter</div>
<div class="eyebrow muted text-[9.5px] mb-2.5">{{ __('web.detail.keywords') }}</div>
<div class="flex flex-wrap gap-[5px]">
@foreach ($keywords as $keyword)
<a href="{{ route('suche', ['q' => $keyword]) }}" class="px-2.5 py-1 text-[11px] font-medium bg-white border border-bg-rule-strong text-ink-2 hover:text-ink transition-colors cursor-pointer">{{ $keyword }}</a>
@ -364,40 +336,26 @@
</div>
@endif
{{-- Newsletter --}}
<div class="p-4 bg-ink text-white">
<div class="eyebrow on-dark text-[9.5px] mb-2">Newsletter</div>
<h4 class="font-serif text-[17px] font-semibold m-0 mb-2 leading-[1.25]">
{{ $categoryName ?: 'Wirtschaft' }} · ab Score 80
</h4>
<p class="text-[11.5px] text-white/70 leading-[1.5] m-0 mb-3">
Höchstens 2 Mails pro Woche. Nur Meldungen ab Content-Score 80.
</p>
<a href="#newsletter" class="block text-center w-full py-2.5 text-[12px] font-semibold bg-brand hover:bg-brand-deep text-white transition-colors cursor-pointer">Abonnieren →</a>
</div>
{{-- Newsletter-Box bewusst entfernt: kommt nachgelagert mit echtem
Double-Opt-in (siehe dev/release/entwicklungsplan-release.md). --}}
{{-- Weiterführend --}}
<div>
<div class="eyebrow muted text-[9.5px] mb-2.5">Weiterführend</div>
<div class="eyebrow muted text-[9.5px] mb-2.5">{{ __('web.detail.further') }}</div>
<div class="flex flex-col border border-bg-rule-strong bg-white">
<a href="#" class="flex items-center gap-2.5 px-3 py-2.5 text-[12px] text-ink-2 hover:bg-bg-elev transition-colors cursor-pointer">
<svg width="13" height="13" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><path d="M2 1h7l3 3v9H2z" stroke="currentColor" stroke-width="1.3" fill="none" /><path d="M9 1v3h3" stroke="currentColor" stroke-width="1.3" fill="none" /></svg>
<span class="flex-1">Permalink &amp; Zitiervorschlag</span>
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" class="text-ink-4" aria-hidden="true"><path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" /></svg>
</a>
<a href="#" class="flex items-center gap-2.5 px-3 py-2.5 text-[12px] text-ink-2 border-t border-bg-rule hover:bg-bg-elev transition-colors cursor-pointer">
<button type="button" onclick="window.print()" class="flex items-center gap-2.5 px-3 py-2.5 text-[12px] text-ink-2 hover:bg-bg-elev transition-colors cursor-pointer text-left">
<svg width="13" height="13" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><path d="M2 4h10v8H2zM2 4V2h10v2" stroke="currentColor" stroke-width="1.3" fill="none" /></svg>
<span class="flex-1">Drucken &amp; PDF</span>
<span class="flex-1">{{ __('web.detail.print') }}</span>
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" class="text-ink-4" aria-hidden="true"><path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" /></svg>
</a>
</button>
<a href="{{ route('legal-request.create', ['slug' => $release->slug, 'type' => 'report']) }}" class="flex items-center gap-2.5 px-3 py-2.5 text-[12px] text-ink-2 border-t border-bg-rule hover:bg-bg-elev transition-colors cursor-pointer">
<svg width="13" height="13" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.3" fill="none" /><path d="M7 4v3.5M7 9.5v.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" /></svg>
<span class="flex-1">Pressemitteilung melden</span>
<span class="flex-1">{{ __('web.detail.report_release') }}</span>
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" class="text-ink-4" aria-hidden="true"><path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" /></svg>
</a>
<a href="{{ route('legal-request.create', ['slug' => $release->slug, 'type' => 'dsgvo']) }}" class="flex items-center gap-2.5 px-3 py-2.5 text-[12px] text-ink-2 border-t border-bg-rule hover:bg-bg-elev transition-colors cursor-pointer">
<svg width="13" height="13" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><path d="M7 1l5 2v4c0 3-2.2 5.2-5 6-2.8-.8-5-3-5-6V3z" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linejoin="round" /></svg>
<span class="flex-1">Datenschutz &amp; Persönlichkeitsrecht</span>
<span class="flex-1">{{ __('web.detail.privacy_rights') }}</span>
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" class="text-ink-4" aria-hidden="true"><path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" /></svg>
</a>
</div>
@ -411,26 +369,26 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
@if ($companyReleases->isNotEmpty())
<div>
<h3 class="font-serif text-[20px] font-semibold m-0 mb-1 tracking-[-0.2px]">Mehr von {{ $companyName }}</h3>
<div class="text-[11.5px] text-ink-3 mb-3.5">Aktuelle Mitteilungen aus diesem Newsroom</div>
<h3 class="font-serif text-[20px] font-semibold m-0 mb-1 tracking-[-0.2px]">{{ __('web.detail.more_from', ['company' => $companyName]) }}</h3>
<div class="text-[11.5px] text-ink-3 mb-3.5">{{ __('web.detail.more_from_sub') }}</div>
<hr class="rule-strong mb-1" />
@foreach ($companyReleases as $companyRelease)
<x-web.feed-item :release="$companyRelease" />
@endforeach
<a href="{{ $companyHref }}" class="block mt-3.5 text-[12.5px] font-semibold text-brand hover:text-brand-deep transition-colors cursor-pointer">Alle Mitteilungen von {{ $companyName }} →</a>
<a href="{{ $companyHref }}" class="block mt-3.5 text-[12.5px] font-semibold text-brand hover:text-brand-deep transition-colors cursor-pointer">{{ __('web.detail.all_from', ['company' => $companyName]) }} →</a>
</div>
@endif
@if ($relatedReleases->isNotEmpty())
<div>
<h3 class="font-serif text-[20px] font-semibold m-0 mb-1 tracking-[-0.2px]">Verwandte Meldungen</h3>
<div class="text-[11.5px] text-ink-3 mb-3.5">Reaktionen, ähnliche Themen, Branchenkontext</div>
<h3 class="font-serif text-[20px] font-semibold m-0 mb-1 tracking-[-0.2px]">{{ __('web.detail.related') }}</h3>
<div class="text-[11.5px] text-ink-3 mb-3.5">{{ __('web.detail.related_sub') }}</div>
<hr class="rule-strong mb-1" />
@foreach ($relatedReleases as $relatedRelease)
<x-web.feed-item :release="$relatedRelease" />
@endforeach
@if ($categoryName)
<a href="{{ $categoryHref }}" class="block mt-3.5 text-[12.5px] font-semibold text-brand hover:text-brand-deep transition-colors cursor-pointer">Alle Meldungen aus {{ $categoryName }} →</a>
<a href="{{ $categoryHref }}" class="block mt-3.5 text-[12.5px] font-semibold text-brand hover:text-brand-deep transition-colors cursor-pointer">{{ __('web.detail.all_from_category', ['category' => $categoryName]) }} →</a>
@endif
</div>
@endif

View file

@ -6,8 +6,8 @@
$brandAccent = $brandConfig['accent'] ?? '24';
@endphp
@section('title', 'Pressemitteilungen durchsuchen – '.$brandName.$brandAccent)
@section('meta_description', 'Durchsuchen Sie das geprüfte Pressemitteilungs-Archiv nach Stichwort, Unternehmen und Branche – redaktionell geprüft und dauerhaft auffindbar im DACH-Raum.')
@section('title', __('web.search.meta_title', ['brand' => $brandName.$brandAccent]))
@section('meta_description', __('web.search.meta_description'))
@section('hreflang')
<link rel="alternate" hreflang="de" href="{{ url('/de/suche') }}">
@ -24,19 +24,19 @@
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-2.5 flex items-center gap-2 text-[11.5px] text-ink-3">
<a href="{{ route('web.home') }}" class="hover:text-ink transition-colors">{{ $brandName }}{{ $brandAccent }}</a>
<span class="text-ink-4" aria-hidden="true">›</span>
<span class="text-ink font-semibold">Suche</span>
<span class="text-ink font-semibold">{{ __('web.header.search_eyebrow') }}</span>
</div>
</div>
{{-- Masthead --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-8 lg:pt-10">
<div class="max-w-[680px]">
<div class="eyebrow mb-3">Archiv-Suche</div>
<div class="eyebrow mb-3">{{ __('web.search.eyebrow') }}</div>
<h1 class="font-serif text-[36px] sm:text-[46px] lg:text-[50px] font-semibold m-0 mb-4 leading-[1.05] tracking-[-1px] text-ink text-balance">
Pressemitteilungen durchsuchen
{{ __('web.header.search_title') }}
</h1>
<p class="font-serif text-[16px] leading-[1.55] m-0 text-ink-2">
Stichwort, Unternehmen oder Branche – durchsuchen Sie das vollständige, redaktionell geprüfte Archiv.
{{ __('web.search.intro') }}
</p>
</div>
<hr class="rule-strong mt-8">

View file

@ -27,9 +27,10 @@
@section('title', 'Pressemitteilung veröffentlichen – '.$brandName.$brandAccent)
@section('meta_description', 'Pressemitteilungen geprüft veröffentlichen und dauerhaft auffindbar im Archiv. Faire Preise, keine Vertragsbindung – Einreichung über den zentralen Publisher-Bereich.')
{{-- Kein hreflang="en": Der Seiteninhalt liegt bisher nur auf Deutsch vor.
EN-Übersetzung der Langform-Seiten ist nachgelagert (siehe Release-Plan). --}}
@section('hreflang')
<link rel="alternate" hreflang="de" href="{{ url('/de/veroeffentlichen') }}">
<link rel="alternate" hreflang="en" href="{{ url('/en/veroeffentlichen') }}">
<link rel="alternate" hreflang="x-default" href="{{ url('/de/veroeffentlichen') }}">
@endsection
@ -48,7 +49,7 @@
<div class="hidden md:flex items-center gap-[18px]">
<span class="inline-flex items-center gap-1.5">
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true"><path d="M2 5.5l2.2 2.2L9 3" stroke="currentColor" class="text-ok" stroke-width="1.6" fill="none" stroke-linecap="round"/></svg>
<span>Redaktionell geprüft seit {{ $sinceYear }}</span>
<span>Qualitätsgeprüft seit {{ $sinceYear }}</span>
</span>
<span class="text-ink-4">·</span>
<a href="{{ route('hilfe') }}" class="text-ink-2 hover:text-ink transition-colors">Hilfe &amp; Support</a>
@ -229,8 +230,8 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-px bg-bg-rule border-y border-bg-rule-strong">
@foreach ([
['n' => '01', 'meta' => '≈ 2 Minuten', 't' => 'Konto anlegen', 'd' => 'Mit E-Mail und Passwort oder Google-Konto. Konto und Abrechnung liegen im zentralen Publisher-Bereich — ein Konto für beide Portale.'],
['n' => '02', 'meta' => 'werktags · innerhalb 24 h', 't' => 'Mitteilung einreichen', 'd' => 'Text, Bild, Ansprechpartner. Eine automatische Qualitätsprüfung läuft parallel zu einer kurzen redaktionellen Sichtung.'],
['n' => '01', 'meta' => '≈ 2 Minuten', 't' => 'Konto anlegen', 'd' => 'Mit E-Mail-Adresse und Passwort — Anmeldung wahlweise auch passwortlos per Magic-Link. Konto und Abrechnung liegen im zentralen Publisher-Bereich — ein Konto für beide Portale.'],
['n' => '02', 'meta' => 'werktags · innerhalb 24 h', 't' => 'Mitteilung einreichen', 'd' => 'Text, Bild, Ansprechpartner. Die automatisierte Inhaltsprüfung läuft direkt beim Einreichen mit.'],
['n' => '03', 'meta' => 'permanent · indexiert', 't' => 'Veröffentlichung', 'd' => 'Nach Freigabe online und im Archiv. Bei Bedarf jederzeit korrigierbar — die URL bleibt stabil, Verweise bestehen weiter.'],
] as $step)
<div class="bg-bg-elev px-7 pt-8 pb-9">
@ -291,7 +292,7 @@
Nicht geeignet sind reine Werbeanzeigen, SEO-Linkbuilding-Texte oder Inhalte ohne nachvollziehbaren Pressewert.
</h3>
<p class="mt-[18px] mb-0 text-[13.5px] leading-[1.6] text-ink-2">
Die redaktionelle Sichtung sichert den Charakter der Plattform. Mitteilungen, die diese Schwelle nicht
Die Inhaltsprüfung sichert den Charakter der Plattform. Mitteilungen, die diese Schwelle nicht
erreichen, werden mit einer kurzen Begründung zurückgegeben — meist mit einem konkreten Korrekturvorschlag.
</p>
<hr class="rule my-5" />
@ -382,7 +383,7 @@
<div class="grid items-start gap-10 lg:gap-16 grid-cols-1 lg:grid-cols-[1fr_1.6fr]">
<div class="lg:sticky lg:top-6 pt-2">
<p class="font-serif m-0 text-[16px] leading-[1.55] text-ink-2">Sie finden hier keine Antwort?</p>
<a href="mailto:{{ config('domains.domains.'.config('app.theme').'.brand.contact_email') ?? 'info@businessportal24.com' }}" class="mt-3.5 inline-flex items-center gap-1.5 text-[13.5px] font-medium text-brand border-b border-brand pb-0.5">Redaktion direkt anschreiben</a>
<a href="mailto:{{ config('domains.domains.'.config('app.theme').'.brand.contact_email') ?? 'info@businessportal24.com' }}" class="mt-3.5 inline-flex items-center gap-1.5 text-[13.5px] font-medium text-brand border-b border-brand pb-0.5">Direkt Kontakt aufnehmen</a>
<div class="mt-3.5 text-[12px] text-ink-3 leading-[1.55]">Antwort werktags per E-Mail</div>
</div>
@ -419,7 +420,7 @@
<div class="eyebrow mb-3.5 text-[10.5px] text-brand-soft">Mitteilung einreichen · Schritt eins</div>
<h2 class="font-serif m-0 text-[34px] lg:text-[48px] font-semibold leading-[1.08] text-ink-on-dark max-w-[700px] tracking-[-0.9px] text-balance">Bereit, Ihre Mitteilung einzureichen?</h2>
<p class="font-serif mt-5 mb-0 text-[16px] lg:text-[17px] leading-[1.55] text-ink-on-dark-2 max-w-[580px]">
Konto in zwei Minuten anlegen, Text und Bild einreichen, nach redaktioneller Sichtung veröffentlicht.
Konto in zwei Minuten anlegen, Text und Bild einreichen, nach bestandener Inhaltsprüfung veröffentlicht.
Einmaliges Konto — beide Portale (businessportal24 &amp; presseecho.de) nutzbar.
</p>
</div>

View file

@ -4,6 +4,7 @@ use App\Console\Commands\GenerateManualInvoices;
use App\Console\Commands\PublishScheduledPressReleases;
use App\Console\Commands\PurgeExpiredPressReleaseDrafts;
use App\Console\Commands\PurgeMagicLinks;
use App\Console\Commands\RefreshMarketData;
use App\Console\Commands\ResetMonthlyPressReleaseQuota;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
@ -63,3 +64,14 @@ Schedule::command(ResetMonthlyPressReleaseQuota::class)
->monthlyOn(1, '00:05')
->withoutOverlapping()
->runInBackground();
// ========================================
// Börsendaten für den Web-Ticker
// ========================================
// Kurse (DAX, EUR/USD, Brent) alle 15 Minuten aktualisieren; die Views lesen
// nur aus dem Cache — ein API-Ausfall leert die Anzeige nicht sofort.
Schedule::command(RefreshMarketData::class)
->everyFifteenMinutes()
->withoutOverlapping()
->runInBackground();

View file

@ -7,8 +7,32 @@ use App\Models\Company;
use App\Models\PressRelease;
use Tests\TestCase;
test('businessportal24 homepage renders the editorial shell', function () {
test('businessportal24 homepage renders the editorial shell with real data', function () {
/** @var TestCase $this */
$category = Category::factory()->create([
'portal' => Portal::Businessportal24,
]);
$category->translations()->create([
'locale' => 'de',
'name' => 'Energie & Klima',
'slug' => 'energie-klima',
]);
$company = Company::factory()->businessportal24()->create([
'name' => 'Muster Energie GmbH',
]);
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create([
'title' => 'Shell-Test Meldung',
'slug' => 'shell-test-meldung',
'published_at' => now()->subHour(),
]);
$this->get('https://businessportal24.test/de')
->assertSuccessful()
->assertSee('businessportal', false)
@ -16,19 +40,69 @@ test('businessportal24 homepage renders the editorial shell', function () {
->assertSeeText('Aktuelle Meldungen')
->assertSeeText('Im Fokus')
->assertSeeText('Aktive Newsrooms')
->assertSeeText('Bleiben Sie informiert')
->assertSeeText('Veröffentlichen Sie Ihre Pressemitteilung')
->assertSeeText('Redaktioneller Qualitätsstandard')
->assertSeeText('Ihre Pressemitteilung, qualitätsgeprüft veröffentlicht')
->assertSeeText('Konto anlegen')
->assertSeeText('Unser Qualitätsstandard')
->assertDontSeeText('redaktionell geprüft')
->assertSeeText('AD-HOC')
->assertSeeText('Branchen-Index')
->assertSeeText('Heute im Fokus')
->assertSee('businessportal', false)
->assertSee('>24<', false)
->assertDontSeeText('führende Plattform')
->assertDontSeeText('maximale Reichweite')
->assertDontSeeText('Exklusiv-Interview');
});
test('the ad-hoc ticker also renders on subpages', function () {
/** @var TestCase $this */
$category = Category::factory()->create([
'portal' => Portal::Businessportal24,
]);
$category->translations()->create([
'locale' => 'de',
'name' => 'Energie & Klima',
'slug' => 'energie-klima',
]);
$company = Company::factory()->businessportal24()->create([
'name' => 'Muster Energie GmbH',
]);
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create([
'title' => 'Ticker-Meldung auf Unterseite',
'slug' => 'ticker-meldung-auf-unterseite',
'published_at' => now()->subHour(),
]);
$this->get('https://businessportal24.test/de/newsrooms')
->assertSuccessful()
->assertSeeText('AD-HOC')
->assertSeeText('Ticker-Meldung auf Unterseite');
$this->get('https://businessportal24.test/de/preise')
->assertSuccessful()
->assertSeeText('AD-HOC')
->assertSeeText('Ticker-Meldung auf Unterseite');
});
test('businessportal24 homepage hides data sections and shows the empty feed state without content', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de')
->assertSuccessful()
->assertSeeText('Aktuelle Meldungen')
->assertSeeText('Aktuell liegen keine Meldungen vor')
->assertDontSeeText('Im Fokus')
->assertDontSeeText('Aktive Newsrooms')
->assertDontSeeText('Branchen-Index')
->assertDontSeeText('Heute im Fokus');
});
test('businessportal24 homepage feed only shows published businessportal content', function () {
/** @var TestCase $this */
$category = Category::factory()->create([

View file

@ -23,6 +23,22 @@ test('both editions render the homepage', function () {
$this->get('https://businessportal24.test/en')->assertSuccessful();
});
test('the english edition renders a localized ui', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/en')
->assertSuccessful()
->assertSeeText('Latest releases')
->assertSeeText('Publish your press release')
->assertSeeText('Our quality standard')
->assertDontSeeText('Aktuelle Meldungen')
->assertDontSeeText('Unser Qualitätsstandard');
$this->get('https://businessportal24.test/de')
->assertSuccessful()
->assertSeeText('Aktuelle Meldungen')
->assertDontSeeText('Latest releases');
});
test('an unsupported edition prefix returns 404', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/fr')->assertNotFound();

View file

@ -0,0 +1,98 @@
<?php
use App\Services\Web\MarketDataService;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
beforeEach(function () {
Cache::forget((string) config('market.cache_key'));
config()->set('market.enabled', true);
config()->set('market.symbols', [
['label' => 'DAX', 'symbol' => '^GDAXI', 'precision' => 0, 'suffix' => ''],
['label' => 'EUR/USD', 'symbol' => 'EURUSD=X', 'precision' => 2, 'suffix' => ''],
]);
});
function fakeYahooChart(float $price, float $previousClose): array
{
return [
'chart' => [
'result' => [
['meta' => ['regularMarketPrice' => $price, 'chartPreviousClose' => $previousClose]],
],
],
];
}
it('holt Kurse, berechnet Deltas und cacht die Slides', function () {
Http::fake([
'*GDAXI*' => Http::response(fakeYahooChart(24150.0, 24000.0)),
'*EURUSD*' => Http::response(fakeYahooChart(1.0815, 1.0902)),
]);
$slides = app(MarketDataService::class)->refresh();
expect($slides)->toHaveCount(2)
->and($slides[0]['label'])->toBe('DAX')
->and($slides[0]['value'])->toBe('24.150')
->and($slides[0]['delta'])->toBe('+0,6%')
->and($slides[0]['positive'])->toBeTrue()
->and($slides[1]['delta'])->toBe('-0,8%')
->and($slides[1]['positive'])->toBeFalse();
Http::fake();
expect(app(MarketDataService::class)->slides())->toBe($slides);
});
it('behält den Cache-Stand, wenn der Abruf komplett fehlschlägt', function () {
Cache::put((string) config('market.cache_key'), [
['label' => 'DAX', 'value' => '24.150', 'delta' => '+0,6%', 'positive' => true],
], 3600);
Http::fake(['*' => Http::response(null, 500)]);
$slides = app(MarketDataService::class)->refresh();
expect($slides)->toHaveCount(1)
->and($slides[0]['value'])->toBe('24.150');
});
it('liefert keine Slides, wenn Marktdaten deaktiviert sind', function () {
config()->set('market.enabled', false);
Cache::put((string) config('market.cache_key'), [
['label' => 'DAX', 'value' => '24.150', 'delta' => '+0,6%', 'positive' => true],
], 3600);
expect(app(MarketDataService::class)->slides())->toBe([]);
});
it('überspringt Symbole mit unbrauchbarer API-Antwort', function () {
Http::fake([
'*GDAXI*' => Http::response(['chart' => ['result' => [['meta' => []]]]]),
'*EURUSD*' => Http::response(fakeYahooChart(1.0815, 1.0902)),
]);
$slides = app(MarketDataService::class)->refresh();
expect($slides)->toHaveCount(1)
->and($slides[0]['label'])->toBe('EUR/USD');
});
it('aktualisiert Kurse über den Artisan-Command', function () {
Http::fake(['*' => Http::response(fakeYahooChart(24150.0, 24000.0))]);
$this->artisan('market:refresh')
->expectsOutputToContain('DAX')
->assertSuccessful();
expect(app(MarketDataService::class)->slides())->not->toBeEmpty();
});
it('meldet einen Fehlschlag des Commands ohne Kursdaten', function () {
Http::fake(['*' => Http::response(null, 500)]);
$this->artisan('market:refresh')->assertFailed();
});

View file

@ -7,8 +7,32 @@ use App\Models\Company;
use App\Models\PressRelease;
use Tests\TestCase;
test('presseecho homepage renders the editorial shell', function () {
test('presseecho homepage renders the editorial shell with real data', function () {
/** @var TestCase $this */
$category = Category::factory()->create([
'portal' => Portal::Presseecho,
]);
$category->translations()->create([
'locale' => 'de',
'name' => 'Energie & Klima',
'slug' => 'energie-klima',
]);
$company = Company::factory()->presseecho()->create([
'name' => 'Muster Energie GmbH',
]);
PressRelease::factory()
->published()
->forPortal(Portal::Presseecho)
->for($category)
->for($company)
->create([
'title' => 'Shell-Test Meldung',
'slug' => 'shell-test-meldung',
'published_at' => now()->subHour(),
]);
$this->get('https://presseecho.test/de')
->assertSuccessful()
->assertSee('presseecho', false)
@ -16,14 +40,28 @@ test('presseecho homepage renders the editorial shell', function () {
->assertSeeText('Aktuelle Meldungen')
->assertSeeText('Im Fokus')
->assertSeeText('Aktive Newsrooms')
->assertSeeText('Bleiben Sie informiert')
->assertSeeText('Veröffentlichen Sie Ihre Pressemitteilung')
->assertSeeText('Redaktioneller Qualitätsstandard')
->assertSeeText('Ihre Pressemitteilung, qualitätsgeprüft veröffentlicht')
->assertSeeText('Konto anlegen')
->assertSeeText('Unser Qualitätsstandard')
->assertDontSeeText('redaktionell geprüft')
->assertSeeText('AD-HOC')
->assertSeeText('Branchen-Index')
->assertSeeText('Heute im Fokus');
});
test('presseecho homepage hides data sections and shows the empty feed state without content', function () {
/** @var TestCase $this */
$this->get('https://presseecho.test/de')
->assertSuccessful()
->assertSeeText('Aktuelle Meldungen')
->assertSeeText('Aktuell liegen keine Meldungen vor')
->assertDontSeeText('Im Fokus')
->assertDontSeeText('Aktive Newsrooms')
->assertDontSeeText('Branchen-Index')
->assertDontSeeText('Heute im Fokus');
});
test('presseecho homepage feed only shows published presseecho content', function () {
/** @var TestCase $this */
$category = Category::factory()->create([