Frontend: Editorial-Relaunch der öffentlichen Strecke + Ausgaben-Routing

Öffentliche Seiten auf gemeinsames Editorial-Design (x-web.site-header/-footer,
Design-Tokens) und Ausgaben-Präfix /{edition}/ (de|en) umgestellt.

- Routing: neue Middleware SetEdition (Locale + URL::defaults), /{edition}-Gruppe
  in routes/web.php, Root-Redirect auf /de, 301 für Legacy-.html-URLs,
  Baseline-Default in AppServiceProvider.
- Neue URL-Schemata: /{edition}/press-release/{slug}, /{edition}/category/{slug}.
- Ausgabe = Sprache: DE/EN-Umschalter (Region/CH/AT entfernt); EditorialClock
  und Livewire-Komponenten sprachdynamisch.
- Detail-, Kategorie- und Veröffentlichen-Seite mit echten Daten neu aufgebaut.
- Suche aktiviert: Volt-Komponente livewire/web/search (Titel/Text/Keywords +
  Firma + Rubrik, Filter, Sortierung, Pagination, URL-Parameter q/category/sort).
- Rubriken-Navigation statt Übersichtsseite: Helper CategoryNavigation;
  web/kategorien.blade.php + Route entfernt (Legacy-301).
- Tests: Edition-Routing, Kategorie-Seite/-Navigation, Detail, Veröffentlichen,
  Suche, EditorialClock. Doku in "Echte öffentliche Unterseiten.md" aktualisiert.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin Adametz 2026-06-16 16:39:28 +00:00
parent a0547208d3
commit 253141c6dc
64 changed files with 4457 additions and 2971 deletions

View file

@ -0,0 +1,47 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpFoundation\Response;
/**
* Setzt die aktive Ausgabe (Sprache) für den Public-Frontend-Request.
*
* Das Frontend wird unter einem Sprach-Präfix ausgeliefert: `/de/...` und
* `/en/...`. Die Ausgabe entspricht 1:1 der Inhalts-Sprache.
*
* Aufgaben:
* 1. Ausgabe aus dem ersten URL-Segment lesen (de|en), sonst Fallback `de`.
* 2. `URL::defaults(['edition' => …])` setzen, damit benannte Routen
* (`route('kategorie', …)`) das Präfix automatisch erhalten auch auf
* Seiten ohne Edition-Präfix (Hub/Auth) bleibt so ein gültiger Default.
* 3. App-Locale nur für echte Edition-URLs übernehmen, damit Admin-/Hub-Seiten
* ihre eigene Locale behalten.
*/
class SetEdition
{
/**
* @var array<int, string>
*/
public const EDITIONS = ['de', 'en'];
public const DEFAULT_EDITION = 'de';
public function handle(Request $request, Closure $next): Response
{
$segment = $request->segment(1);
$isEdition = in_array($segment, self::EDITIONS, true);
$edition = $isEdition ? $segment : self::DEFAULT_EDITION;
URL::defaults(['edition' => $edition]);
if ($isEdition) {
app()->setLocale($edition);
}
return $next($request);
}
}

View file

@ -6,6 +6,7 @@ use App\Contracts\NewsletterSyncClient;
use App\Helpers\ThemeHelper;
use App\Http\Middleware\EnsureUserIsAdmin;
use App\Http\Middleware\LogSlowAdminRequests;
use App\Http\Middleware\SetEdition;
use App\Listeners\ActivateUserAfterVerification;
use App\Listeners\SyncCompanyMembershipsOnLogin;
use App\Models\AdminPreset;
@ -59,6 +60,13 @@ class AppServiceProvider extends ServiceProvider
URL::forceScheme('https');
}
// Baseline für das Ausgabe-Präfix (de|en). Die SetEdition-Middleware
// überschreibt dies pro Request mit der tatsächlichen Ausgabe; der
// Default hier stellt sicher, dass route('…')-Aufrufe das {edition}-
// Segment auch außerhalb der HTTP-Middleware erhalten (Livewire-Tests,
// Konsole, Mails).
URL::defaults(['edition' => SetEdition::DEFAULT_EDITION]);
// Registrierung → Verifizierungsmail; bestätigter Klick → Aktivierung.
Event::listen(Registered::class, SendEmailVerificationNotification::class);
Event::listen(Verified::class, ActivateUserAfterVerification::class);

View file

@ -0,0 +1,67 @@
<?php
namespace App\Support;
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Models\Category;
use App\Scopes\PortalScope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
/**
* Liefert die Rubriken-Navigation der aktiven Ausgabe (Portal × Sprache).
*
* Es gibt bewusst keine separate Rubriken-Übersichtsseite: Die Navigation
* führt direkt auf die jeweilige Kategorieseite (`route('kategorie', …)`),
* auf der die Pressemitteilungen dieser Rubrik erscheinen.
*/
class CategoryNavigation
{
/**
* Top-Level-Rubriken der aktiven Ausgabe als Navigationsitems.
*
* @return Collection<int, array{label:string, slug:string, href:string}>
*/
public static function items(int $limit = 9): Collection
{
$portal = Portal::tryFrom((string) config('app.theme')) ?? Portal::Businessportal24;
$language = app()->getLocale();
$portalValues = [$portal->value, Portal::Both->value];
return Category::query()
->with(['translations' => fn ($query) => $query->where('locale', $language)])
->withCount([
'pressReleases as published_count' => function (Builder $query) use ($portalValues, $language): void {
$query
->withoutGlobalScope(PortalScope::class)
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', $language)
->whereNotNull('published_at');
},
])
->whereIn('portal', $portalValues)
->where('is_active', true)
->whereNull('parent_id')
->orderByDesc('published_count')
->orderBy('id')
->limit($limit)
->get()
->map(function (Category $category): ?array {
$translation = $category->translations->first();
if (! $translation || ! $translation->slug) {
return null;
}
return [
'label' => $translation->name,
'slug' => $translation->slug,
'href' => route('kategorie', ['slug' => $translation->slug]),
];
})
->filter()
->values();
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Support;
use App\Enums\PressReleaseStatus;
use App\Models\PressRelease;
use App\Scopes\PortalScope;
use Illuminate\Support\Carbon;
/**
* Liefert den „Redaktions-Stichtag" für aktualitätsbezogene Startseiten-Abfragen.
*
* Das migrierte Archiv ist historisch: Der jüngste veröffentlichte Datensatz
* kann Wochen/Monate in der Vergangenheit liegen. Würde man „heute / letzte 7
* Tage" hart gegen das echte now() rechnen, liefern alle Aktualitäts-Module
* 0 Treffer und fallen auf Mock-Daten zurück.
*
* Stattdessen wird der Stichtag an den jüngsten verfügbaren Veröffentlichungs-
* zeitpunkt gekoppelt (gedeckelt auf now()): Liegt das jüngste published_at in
* der Vergangenheit, dient es als Referenz-„Jetzt"; sobald frische
* Pressemitteilungen mit aktuellem Datum einlaufen, greift automatisch wieder
* das echte now().
*/
class EditorialClock
{
/**
* @param array<int, string> $portalValues
*/
public static function reference(array $portalValues, string $language = 'de'): Carbon
{
$latest = PressRelease::query()
->withoutGlobalScope(PortalScope::class)
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', $language)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->max('published_at');
if (! $latest) {
return now();
}
$latest = Carbon::parse($latest);
return $latest->lt(now()) ? $latest : now();
}
}

View file

@ -4,6 +4,7 @@ use App\Http\Middleware\BasicAuthMiddleware;
use App\Http\Middleware\LogApiUsage;
use App\Http\Middleware\RejectLegacyApiKeys;
use App\Http\Middleware\SetCurrentPortal;
use App\Http\Middleware\SetEdition;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
@ -23,6 +24,9 @@ return Application::configure(basePath: dirname(__DIR__))
// Portal-Kontext nach dem Theme-Provider setzen (liest config('app.theme'))
$middleware->append(SetCurrentPortal::class);
// Ausgabe/Sprache (de|en) aus dem URL-Präfix ableiten und URL-Defaults setzen.
$middleware->append(SetEdition::class);
// Wohin eingeloggte User von Gast-Routen (/login, /register) gelenkt
// werden: rollen- und verifizierungsbewusst statt fix auf /dashboard,
// sonst landet ein Customer dort im 403 und sitzt fest.

View file

@ -506,7 +506,17 @@ Fünf Test-Szenarien rund um die Hub-Landing:
4. **`shows the brand-context banner when arriving from presseecho`** `?from=presseecho` triggert den Banner inkl. Link „Zurück zu presseecho.de".
5. **`shows the brand-context banner when arriving from businessportal24`** `?from=businessportal24` triggert den Banner für BP24.
**Stand:** 11 Web-Tests grün (BP24 3 + Presseecho 3 + Hub 5 = 79 Assertions). Gesamt-Suite: 220/221 (der eine Fail `ApiDocumentationTest` ist vorbestehend wegen fehlender `docs/api/v1.yml` und nicht UI-bezogen).
### Datei: `tests/Feature/Web/ReleaseDetailTest.php`
Fünf Szenarien rund um die Detailseite:
1. **`businessportal24 release detail renders the editorial shell with real data`** Titel, Lead/Subtitle, Newsroom, Rubrik, Pressekontakt, Boilerplate, Schlagwörter und sanitisierter Fließtext.
2. **`release detail counts a hit`** `hits` wird beim Aufruf um 1 erhöht.
3. **`release detail returns 404 for drafts and unknown slugs`** Entwürfe und unbekannte Slugs liefern 404.
4. **`release detail does not expose releases from another portal`** Portal-Trennung: presseecho-Release ist auf BP24 nicht abrufbar, auf presseecho schon.
5. **`release detail shows more from newsroom and related releases`** „Mehr von [Newsroom]" + „Verwandte Meldungen".
**Stand:** Web-Tests grün (BP24 3 + Presseecho 3 + Hub 5 + Detailseite 5 + Canonical/Meta). Gesamt-Suite: 612 grün, 1 skipped.
---
@ -567,6 +577,7 @@ vendor/bin/pint --dirty --format agent
| 17 | 13.05.2026 | `PressekontoHubHomeTest` (5 Szenarien inkl. Brand-Context-Banner-Conditional). Vite-Config + ThemeHelper + `web-master`-Fonts (Inter Tight + JetBrains Mono ohne Serif) für `pressekonto` ergänzt. | ✅ |
| 18 | 13.05.2026 | **Brand-Mark-Konvention etabliert** (Feintuning Marken-Schreibweise): keine TLD am Marken­schriftzug, Akzent farblich vom Basis-Wort abgesetzt. Single Source of Truth `<x-web.brand-mark>` (Marken-Tabelle inkl. Standard- und On-Dark-Akzentfarben, Serif/Sans-Switch). `config/domains.php` umgestellt (`presseecho`: `name=presse`/`accent=echo`; `pressekonto`: `name=presse`/`accent=konto`; Footer-Legal & Meta-Texte ohne TLD). Hub-Komponenten und Hub-View durchgehend auf Brand-Mark migriert (Top-Utility-Bar, Site-Header, Brand-Context-Banner, Site-Footer, Hero-Headline, Architektur-Diagramm, Tarif-Subline, Plattform-Familie, FAQ). Hub-Theme bekommt Source Serif 4 als `--font-serif` (für Marken-Mentions) Bunny-Font-Loader erweitert. **+1 neuer Test `uses the brand-mark splitting without TLDs`**; alle 12 Web-Tests grün. | ✅ |
| 19 | 12.05.2026 | **Aktuell offen:** Detailseite, Branchenseite, Veröffentlichen-Landing für BP24 + Presseecho. Hub-Folgeseiten (Konto-Erstellen-Flow als Landing, Tarif-Detail, Doku-Hub) ebenfalls offen. | 🟡 |
| 20 | 16.06.2026 | **Detailseite Pressemitteilung live** (1:1 zu `tailwind_v3/businessportal24_detailseite-tailwind.html`): `web/release-detail.blade.php` komplett auf Editorial-Design + Theme-Tokens umgebaut, nutzt `<x-web.site-header>`/`<x-web.site-footer>`, theme-fähig für BP24 **und** Presseecho. `release.detail`-Route lädt echte `PressRelease`-Daten (Lead/Subtitle, sanitisierter Fließtext via `renderedText()`, Hero-Bild mit Hatch-Fallback, Pressekontakt aus `contacts`, Schlagwörter aus `keywords`, Boilerplate, Hit-Zähler), 404 bei Entwurf/Fremd-Portal/Unbekannt. Related-Spalten „Mehr von [Newsroom]" + „Verwandte Meldungen" über `<x-web.feed-item>`. Lese-Fortschrittsbalken + Teilen (LinkedIn/X/Mail/Copy) via Alpine. Neue Status-/Prosa-Tokens (`warn-*`, `verify-*`, `.hatch-*`, `.pm-body`) zentral in `shared-styles.css`. **+5 Tests `ReleaseDetailTest`** (BP24-Shell, Hit-Zähler, 404-Fälle, Portal-Trennung, Related); `CanonicalMetaTest` auf echten Datensatz umgestellt. Gesamt-Suite 612 grün. | ✅ |
---
@ -575,7 +586,7 @@ vendor/bin/pint --dirty --format agent
| Reihenfolge | Aufgabe | Vorlage | Status |
| --- | --- | --- | --- |
| 1 | **Mobile-Feinschliff Startseite** gegen `dev/frontend/Mobile _ Startseite.png` durchgehen, alle Sektionen testen (besonders 7-Spalten-Events und 4×2 Branchen-Index Stack-Behavior) | `Mobile _ Startseite.html` | 🟡 noch nicht final geprüft |
| 2 | **Detailseite Pressemitteilung** umsetzen | `tailwind_v3/Detailseite Tailwind.html` + `Detailseite _ Pressemitteilung _aktiv.png` | 🔴 offen |
| 2 | **Detailseite Pressemitteilung** umsetzen | `tailwind_v3/businessportal24_detailseite-tailwind.html` + `Detailseite _ Pressemitteilung _aktiv.png` | ✅ **erledigt (16.06.2026)** `web/release-detail.blade.php` editorial, echte Daten, theme-fähig BP24+Presseecho, `ReleaseDetailTest` |
| 3 | **Branchenseite Energie & Klima** umsetzen (Template für alle Kategorien) | `tailwind_v3/Branchenseite Tailwind.html` + `Branchenseite _ Energie _ Klima _aktiv.png` | 🔴 offen |
| 4 | **Veröffentlichen-Landing** umsetzen (Variante A) | `tailwind_v3/Veröffentlichen Tailwind.html` + `Ver_ffentlichen _ Variante A _aktiv_.png` | 🔴 offen |
| 5 | **Echte Datenquellen** für aktuell statische Komponenten anbinden: Live-Ticker (Ad-Hoc-Meldungen), Events-Week, Newsletter-Topics, Industry-Spotlight-Studie | | 🔴 offen |

View file

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1,012 KiB

After

Width:  |  Height:  |  Size: 1,012 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 492 KiB

After

Width:  |  Height:  |  Size: 492 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before After
Before After

File diff suppressed because it is too large Load diff

View file

@ -187,7 +187,7 @@
<div class="mt-1.5 pt-3.5 border-t border-bg-rule flex justify-between items-baseline text-[11px] text-ink-3">
<div>
<div class="eyebrow muted text-[9px] mb-1">Redaktions-Ansprech</div>
<span class="text-[12px] text-ink-2"><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="0a6f646f786d636f4a687a383e246e6f">[email&#160;protected]</a></span>
<span class="text-[12px] text-ink-2"><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d6b3b8b3a4b1bfb396b4a6e4e2f8b2b3">[email&#160;protected]</a></span>
</div>
<span class="inline-flex items-center gap-1 text-[10.5px] text-ink-3 font-mono cursor-help" style="border-bottom:1px dotted #9A958D;" title="NACE C-35 · D · E">
<svg width="10" height="10" viewBox="0 0 12 12" fill="none"><circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1"/><path d="M6 5.5v3M6 3.5v.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>
@ -350,7 +350,7 @@
</div>
<div class="text-[12px] text-ink-2 leading-[1.55] mb-3">
<div><span class="font-mono text-ink-3">Tel.</span> +49 201 5179-5111</div>
<div><span class="font-mono text-ink-3">Mail</span> <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="cebebcabbdbdab8ebcb9abe0ada1a3">[email&#160;protected]</a></div>
<div><span class="font-mono text-ink-3">Mail</span> <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="bdcdcfd8ceced8fdcfcad893ded2d0">[email&#160;protected]</a></div>
</div>
<div class="grid grid-cols-2 gap-2">
<button class="px-2.5 py-2 text-[12px] font-semibold border border-ink text-ink bg-white">Pressemappe</button>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View file

@ -415,7 +415,7 @@
</span>
<span class="flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0"><path d="M1 3h12v8H1zM1 3l6 4 6-4" stroke="currentColor" stroke-width="1.2" fill="none" /></svg>
<a href="/cdn-cgi/l/email-protection#bfcfcddaccccdaffddd2c8d491ddcad1db91dbda" class="text-ink underline underline-offset-2"><span class="__cf_email__" data-cfemail="e6969483959583a6848b918dc884938882c88283">[email&#160;protected]</span></a>
<a href="/cdn-cgi/l/email-protection#1e6e6c7b6d6d7b5e7c736975307c6b707a307a7b" class="text-ink underline underline-offset-2"><span class="__cf_email__" data-cfemail="79090b1c0a0a1c391b140e12571b0c171d571d1c">[email&#160;protected]</span></a>
</span>
<span class="flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2" fill="none" /><path d="M1.5 7h11M7 1.5c2 1.5 2 9.5 0 11M7 1.5c-2 1.5-2 9.5 0 11" stroke="currentColor" stroke-width="1" fill="none" /></svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

View file

@ -165,7 +165,7 @@
<span class="absolute left-0 top-0 bottom-0 w-[3px] bg-brand"></span>
<div>
<div class="eyebrow mb-2.5 text-[10.5px]" style="color:#FF8B6F;">Einreichen im Publisher-Bereich</div>
<h3 class="font-serif m-0 text-[23px] font-semibold leading-[1.25] tracking-[-0.3px] text-ink-on-dark">Die Einreichung läuft über pressekonto.de.</h3>
<h3 class="font-serif m-0 text-[23px] font-semibold leading-[1.25] tracking-[-0.3px] text-ink-on-dark">Die Einreichung läuft über presseportale.com.</h3>
<p class="mt-2.5 mb-0 text-[13.5px] leading-[1.55] text-ink-on-dark-2 max-w-[540px]">Dort verwalten Sie Mitteilungen, Credits und Newsroom — einmaliges Konto, beide Portale nutzbar (businessportal24 &amp; presseecho.de).</p>
</div>
<div class="flex flex-col items-end gap-2.5">
@ -342,7 +342,7 @@
<p class="mt-4.5 mb-0 text-[12px] text-ink-3" style="margin-top:18px;">
Die Veröffentlichung erfolgt über den zentralen Publisher-Bereich auf
<a href="#" class="text-brand font-medium border-b border-brand">pressekonto.de</a>.
<a href="#" class="text-brand font-medium border-b border-brand">presseportale.com</a>.
Cross-Publishing nach presseecho.de ist optional verfügbar.
</p>
</div>
@ -517,7 +517,7 @@
<div class="sticky 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="#" 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>
<div class="mt-3.5 text-[12px] text-ink-3 leading-[1.55]"><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="55273031343e213c3a3b153720263c3b302626253a2721343967617b363a38">[email&#160;protected]</a><br />MoFr · 09:0017:00 Uhr (MEZ)</div>
<div class="mt-3.5 text-[12px] text-ink-3 leading-[1.55]"><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="c7b5a2a3a6acb3aea8a987a5b2b4aea9a2b4b4b7a8b5b3a6abf5f3e9a4a8aa">[email&#160;protected]</a><br />MoFr · 09:0017:00 Uhr (MEZ)</div>
</div>
<div style="border-top:1px solid #1C1A17;">
@ -583,7 +583,7 @@
Zum Publisher-Bereich
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" class="ml-0.5"><path d="M4 8L8.5 3.5M8.5 3.5H5M8.5 3.5V7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="text-[12px] text-ink-on-dark-2">Einreichung läuft über pressekonto.de · Login per Magic-Link</span>
<span class="text-[12px] text-ink-on-dark-2">Einreichung läuft über presseportale.com · Login per Magic-Link</span>
<a href="#" class="mt-1.5 text-[13px] text-ink-on-dark border-b pb-0.5" style="border-color:rgba(255,255,255,0.35);">Oder zuerst Beispiele ansehen →</a>
</div>
</div>
@ -597,7 +597,7 @@
<div class="grid pb-7" style="grid-template-columns:1.5fr 1fr 1fr 1fr;gap:56px;border-bottom:1px solid rgba(255,255,255,0.08);">
<div>
<div class="font-serif text-[22px] font-semibold" style="letter-spacing:-0.4px;">businessportal<span class="text-brand">24</span></div>
<p class="mt-3 text-[12.5px] text-ink-on-dark-2 leading-[1.6] max-w-[380px]">businessportal24 ist ein Service der Pressekonto-Gruppe. Plattform für Pressemitteilungen mittelständischer Unternehmen, Selbstständiger und PR-Agenturen im deutschsprachigen Raum.</p>
<p class="mt-3 text-[12.5px] text-ink-on-dark-2 leading-[1.6] max-w-[380px]">businessportal24 ist ein Service der Presseportale-Gruppe. Plattform für Pressemitteilungen mittelständischer Unternehmen, Selbstständiger und PR-Agenturen im deutschsprachigen Raum.</p>
</div>
<div>
<div class="eyebrow mb-3.5 text-[10px]" style="color:rgba(255,255,255,0.5);">Einreichen</div>
@ -628,7 +628,7 @@
</div>
</div>
<div class="mt-5.5 flex justify-between items-center flex-wrap gap-4 text-[12px] text-ink-on-dark-2" style="margin-top:22px;">
<div>© 2026 Pressekonto-Gruppe · Alle Rechte vorbehalten</div>
<div>© 2026 Presseportale-Gruppe · Alle Rechte vorbehalten</div>
<div class="flex items-center gap-4">
<span class="text-ink-on-dark-2">Für fachlich-spezifische Themen:</span>
<a href="#" class="text-ink-on-dark border-b pb-px" style="border-color:rgba(255,255,255,0.3);">presseecho.de →</a>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

View file

@ -2,15 +2,43 @@
> der oeffentlichen Strecke. Welcher Punkt bereits umgesetzt ist, ist
> jeweils mit einer kurzen IST-Notiz markiert.
> **Update 16.06.2026 — Editorial-Relaunch & Ausgaben-Routing.**
> Die oeffentliche Strecke wurde auf ein gemeinsames Editorial-Design
> (Komponenten `x-web.site-header` / `x-web.site-footer`, Design-Tokens in
> `resources/css/web/shared-styles.css`) umgestellt und auf ein
> Ausgaben-Praefix `/{edition}/…` (Sprache `de` | `en`) gehoben.
>
> - **Routing**: Neue Middleware `app/Http/Middleware/SetEdition.php` liest
> die Ausgabe aus dem ersten URL-Segment, setzt Locale + `URL::defaults`.
> `routes/web.php` gruppiert alle oeffentlichen Routen unter `/{edition}`.
> `/` leitet auf die Default-Ausgabe (`/de`) um; Legacy-`.html`-URLs werden
> per 301 auf die neuen Pfade gemappt. Baseline-Default in
> `AppServiceProvider` fuer Route-Generierung ausserhalb von HTTP-Requests.
> - **Neue URL-Schemata**: Detailseite `/{edition}/press-release/{slug}`
> (Route `release.detail`), Kategorie `/{edition}/category/{slug}`
> (Route `kategorie`).
> - **Ausgabe = Sprache**: DE/EN-Umschalter im Header (Region-/CH-/AT-Auswahl
> entfernt). `EditorialClock` (`app/Support/EditorialClock.php`) und die
> Livewire-Komponenten sind sprachdynamisch.
> - **Rubriken-Navigation** statt Uebersichtsseite: Helper
> `app/Support/CategoryNavigation.php` liefert die Top-Rubriken der Ausgabe;
> Header/Footer verlinken direkt auf die Kategorieseiten. Die alte
> `web/kategorien.blade.php` + Route wurden entfernt (Legacy-301).
> - **Tests**: `tests/Feature/Web/` deckt Edition-Routing, Kategorie-Seite,
> Kategorie-Navigation, Detailseite, Veroeffentlichen, Suche und den
> EditorialClock ab.
Das sind die Seiten, die eigene URLs brauchen, weil sie verlinkbar sein müssen, SEO-Wert haben oder direkt von extern angesteuert werden.
#### Inhalts-Seiten (Lese-Erfahrung)
**1. Pressemitteilungs-Detailseite** `/p/[slug]` oder `/pressemitteilung/[id]` Die wichtigste Seite überhaupt. Jede einzelne PM bekommt eine eigene Seite. Hier landen 90% des Traffics aus Google, Newsletter und Social Shares.
_IST 21.05.2026_: umgesetzt als `resources/views/web/release-detail.blade.php` (Route `release.detail`, URL `/release/{slug}`). Das URL-Schema weicht vom Plan ab, ist aber konsistent über alle Themen.
_IST 16.06.2026_: im Editorial-Design neu aufgebaut, echte Daten (Firma, Kontakte, Bilder, verwandte Meldungen). Neue URL `/{edition}/press-release/{slug}`; Legacy-URLs per 301. Test: `tests/Feature/Web/ReleaseDetailTest.php`.
**2. Branchen-Übersichten** `/branche/[slug]` Zum Beispiel `/branche/energie-klima`, `/branche/finanzen`. Aggregierte Sicht auf alle PMs einer Branche, mit Sub-Filtern. Das sind deine SEO-Goldgruben (jede Branche eine ranking-fähige Landing Page).
_IST 21.05.2026_: umgesetzt (`web/kategorie.blade.php`, `web/kategorien.blade.php`).
_IST 16.06.2026_: `web/kategorie.blade.php` im Editorial-Design mit echten Daten (Top-Meldung, Feed, Sub-/verwandte Branchen, Stats, Newsrooms). Neue URL `/{edition}/category/{slug}`. Es gibt **keine** Gesamt-Uebersicht mehr — die Header-/Footer-Navigation fuehrt direkt auf die Rubrik (`CategoryNavigation`); `web/kategorien.blade.php` + Route entfernt (Legacy-301). Tests: `CategoryPageTest`, `CategoryNavigationTest`.
**3. Regionen-Übersichten** `/region/[slug]` `/region/deutschland`, `/region/bayern`, `/region/oesterreich`. Analog zu Branchen, regional gefiltert.
_IST 21.05.2026_: noch nicht umgesetzt.
@ -20,6 +48,7 @@ _IST 21.05.2026_: Layout vorhanden (`web/newsrooms.blade.php`), Daten-Anbindung
**5. Such-Ergebnisseite** `/suche?q=...` Volltextsuche mit Filtern (Erweiterte Suche schreibt in URL-Parameter, dadurch teilbar/bookmarkbar).
_IST 21.05.2026_: Layout vorhanden (`web/suche.blade.php`), Volltextsuche noch nicht aktiv.
_IST 16.06.2026_: aktive Suche umgesetzt. `web/suche.blade.php` im Editorial-Design + Volt-Komponente `livewire/web/search.blade.php`: Suche ueber Titel/Untertitel/Text/Keywords sowie Firmenname und Rubrik-Name, Rubriken-Filter, Sortierung (Neueste/Aelteste/Meistgelesen), Pagination. URL-Parameter `q`, `category`, `sort` (teilbar/bookmarkbar); portal-/sprachsensitiv. Test: `tests/Feature/Web/SearchPageTest.php`.
**6. Tag-/Themen-Seite** `/thema/[slug]` _(optional, später)_ Nicht im ersten Release zwingend, aber sehr SEO-wirksam für aktuelle Themen ("Künstliche Intelligenz", "Lieferkettengesetz", "Energiekrise"). Würde ich datengetrieben aus den meistverwendeten Tags generieren lassen.
_IST 21.05.2026_: nicht umgesetzt (bewusst spaeter).
@ -28,6 +57,7 @@ _IST 21.05.2026_: nicht umgesetzt (bewusst spaeter).
**7. Pressemitteilung einreichen / Veröffentlichen** `/veroeffentlichen` Die Conversion-Landingpage für neue Publisher. Erklärt Mehrwert, zeigt Tarife, Editor-Vorschau. Dahinter der eigentliche Editor (im User-Bereich).
_IST 21.05.2026_: Landing-Seite vorhanden (`web/veroeffentlichen.blade.php`). Editor-Strecke im User-Bereich ist umgesetzt (siehe Phase 7).
_IST 16.06.2026_: Landing-Seite im Editorial-Design neu aufgebaut, echte Kennzahlen (Archiv-Gesamt, heute veroeffentlicht/in Pruefung, aktive Newsrooms, Beispiel-Mitteilung). Einreichung verweist auf den zentralen Publisher-Bereich. Test: `tests/Feature/Web/VeroeffentlichenPageTest.php`.
**8. Tarife & Preise** `/preise` _(oder als Modal aus mehreren Stellen aufrufbar)_ Da Tarife auch im Modal aus dem CTA aufgerufen werden, ist die Frage: brauchen wir die Seite? Antwort ja, weil SEO ("Pressemitteilung veröffentlichen Preise" ist eine wichtige Suche) und weil sie verlinkbar sein muss aus AGB, Footer, Mediadaten.
_IST 21.05.2026_: Layout vorhanden (`web/preise.blade.php`), echte Tarife noch nicht hinterlegt (Tarif-Modul siehe `Presseportal Konzept für Relaunch.md` Abschnitt 8).

View file

@ -418,11 +418,14 @@ Im Header die Rolle als Badge zeigen, damit der User immer weiß, was er darf, o
## Offene Designentscheidungen
**1. Firmenwechsel-Bestätigung** Wenn ein User im Firmen-Detail arbeitet und über den Switcher die Firma wechselt sofort wechseln oder Warnung „ungespeicherte Änderungen"? Ich würde Standard-Browser-Verhalten beibehalten (`beforeunload` bei dirty forms), kein eigener Dialog.
#Ja Ohne Dialog.
**2. Firma deaktivieren vs. löschen** Im Datenmodell ist Aktiv/Inaktiv vorhanden. Echtes Löschen ist heikel wegen verknüpfter PMs, Rechnungen, Kontakte. Ich würde dem User **nur** Deaktivieren anbieten echtes Löschen läuft über Support-Anfrage. Senkt deine Risiken bei DSGVO-Konflikten.
#Ja Firmen nur dekativieren
**3. Owner-Übertragung** „Firma übertragen" ist ein sensibler Vorgang. Ich würde einen eigenen Wizard mit E-Mail-Bestätigung beim neuen Owner verlangen (ähnlich GitHub-Repo-Transfer). Macht den Punkt komplexer, aber sauber.
#ja genau so soll es gemacht werden, wird allerdings oft nach den Relaunch verschoben.
**4. Pressekontakt-Zuordnung beim PM-Erstellen** Beim Anlegen einer neuen PM: Sollen alle Pressekontakte der Firma automatisch zugeordnet werden, oder muss der User explizit auswählen? Ich tendiere zu „alle vorausgewählt, abwählbar" gibt dem User eine Voreinstellung, die in 90 % der Fälle stimmt.
Ja Würde ich so machen die meisten haben eh nur einen oder keine Pressekontakt.
---

View file

@ -20,6 +20,24 @@
@source "../../views/livewire/auth";
@source "../../views/components/layouts/auth";
/**
* Editorial-Status-Tokens (Detailseite/Branchenseite).
* Semantisch identisch über alle Web-Themes hinweg: Bernstein-Warnung
* (Korrekturhinweise) und grüne Verifizierung (verifizierte Publisher).
* Liegen hier zentral, damit beide Marken (BP24 + presseecho) dieselben
* Status-Utilities (bg-warn-bg, text-verify-ink ) generieren.
*/
@theme {
--color-warn-bg: #fff8e1;
--color-warn-border: #e8c77a;
--color-warn-ink: #7a5a0f;
--color-warn-ink-deep: #3d2f0f;
--color-verify-bg: #e8f4ec;
--color-verify-border: #1b8e3a;
--color-verify-ink: #0f5e26;
}
/* Tailwind Base Layer für gemeinsame Elemente */
@layer base {
*,
@ -81,6 +99,80 @@
/* Gemeinsame Component Styles */
@layer components {
/**
* Artikel-Fließtext der Detailseite (sanitisiertes PM-HTML).
* Bildet die Editorial-Typografie aus dem tailwind_v3-Mockup nach und
* nutzt ausschließlich Theme-Tokens, damit BP24 und presseecho identisch
* rendern (nur die Brand-Akzente unterscheiden sich).
*/
.pm-body > p {
font-family: var(--font-serif);
font-size: 17px;
line-height: 1.65;
margin: 0 0 18px;
color: var(--color-ink);
text-wrap: pretty;
}
.pm-body > h2 {
font-family: var(--font-serif);
font-size: 22px;
font-weight: 600;
letter-spacing: -0.3px;
line-height: 1.25;
margin: 32px 0 14px;
color: var(--color-ink);
}
.pm-body > h3 {
font-family: var(--font-serif);
font-size: 18px;
font-weight: 600;
line-height: 1.3;
margin: 26px 0 10px;
color: var(--color-ink);
}
.pm-body > ul,
.pm-body > ol {
font-family: var(--font-serif);
font-size: 17px;
line-height: 1.65;
color: var(--color-ink);
margin: 0 0 18px;
padding-left: 1.4em;
}
.pm-body > ul {
list-style: disc;
}
.pm-body > ol {
list-style: decimal;
}
.pm-body li {
margin-bottom: 6px;
}
.pm-body blockquote {
margin: 28px 0;
padding-left: 24px;
border-left: 3px solid var(--color-brand);
}
.pm-body blockquote p {
font-family: var(--font-serif);
font-size: 21px;
font-style: italic;
font-weight: 500;
line-height: 1.4;
letter-spacing: -0.2px;
margin: 0 0 12px;
color: var(--color-ink);
text-wrap: balance;
}
.pm-body a {
color: var(--color-brand);
text-decoration: underline;
text-underline-offset: 2px;
}
.pm-body strong {
font-weight: 600;
}
/* Button Base Styles */
.btn {
@apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium transition-all duration-200;
@ -196,6 +288,22 @@
text-wrap: balance;
}
/* Schraffur-Platzhalter für fehlende Pressefotos/Logos (Detailseite) */
.hatch-dark {
background-image: repeating-linear-gradient(
135deg,
rgba(255, 255, 255, 0.04) 0 10px,
transparent 10px 20px
);
}
.hatch-light {
background-image: repeating-linear-gradient(
135deg,
rgba(0, 0, 0, 0.04) 0 6px,
transparent 6px 12px
);
}
/* Animations */
@keyframes fade-in {
from {

View file

@ -8,7 +8,7 @@
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-[#b6332a]">Branchen</p>
<h2 class="mt-2 text-lg font-semibold text-zinc-950 dark:text-white">Zugänge nach Themen</h2>
</div>
<a href="{{ route('kategorien') }}" class="text-sm font-semibold text-[#cf3628] hover:text-[#a92c25]">Alle</a>
<a href="{{ route('web.home') }}" class="text-sm font-semibold text-[#cf3628] hover:text-[#a92c25]">Alle</a>
</div>
@if ($categories->isNotEmpty())

View file

@ -34,7 +34,7 @@
</svg>
iCal abonnieren
</a>
<a href="{{ route('kategorien') }}#termine" class="text-brand font-semibold cursor-pointer hover:text-brand-deep transition-colors">Alle Termine </a>
<a href="{{ route('web.home') }}" class="text-brand font-semibold cursor-pointer hover:text-brand-deep transition-colors">Alle Termine </a>
</div>
</header>
<hr class="rule-strong">

View file

@ -3,16 +3,14 @@
])
@php
$industries = $industries ?? [
['name' => 'Technologie', 'count' => 142, 'delta' => 12, 'href' => route('kategorien')],
['name' => 'Finanzen', 'count' => 98, 'delta' => 5, 'href' => route('kategorien')],
['name' => 'Industrie', 'count' => 87, 'delta' => -2, 'href' => route('kategorien')],
['name' => 'Energie', 'count' => 64, 'delta' => 18, 'href' => route('kategorien')],
['name' => 'Gesundheit', 'count' => 51, 'delta' => 3, 'href' => route('kategorien')],
['name' => 'Mobilität', 'count' => 44, 'delta' => 9, 'href' => route('kategorien')],
['name' => 'Handel', 'count' => 38, 'delta' => -1, 'href' => route('kategorien')],
['name' => 'Immobilien', 'count' => 32, 'delta' => 4, 'href' => route('kategorien')],
];
$industries = $industries ?? \App\Support\CategoryNavigation::items(8)
->map(fn (array $category): array => [
'name' => $category['label'],
'count' => 0,
'delta' => 0,
'href' => $category['href'],
])
->all();
$maxCount = max(1, collect($industries)->max('count'));
$industries = array_slice($industries, 0, 8);
@ -35,7 +33,7 @@
$isLastInRow = ($idx + 1) % 4 === 0;
$barClass = $delta < 0 ? 'bg-brand' : 'bg-gain';
@endphp
<a href="{{ $industry['href'] ?? route('kategorien') }}"
<a href="{{ $industry['href'] ?? route('web.home') }}"
class="block px-5 py-[18px] {{ $isLastInRow ? '' : 'md:border-r border-bg-rule' }} {{ $isTopRow ? '' : 'border-t border-bg-rule' }} cursor-pointer hover:bg-bg-elev transition-colors group">
<div class="flex justify-between items-baseline mb-2 gap-2">
<span class="text-[14px] font-semibold text-ink group-hover:text-brand transition-colors">{{ $industry['name'] }}</span>

View file

@ -36,7 +36,7 @@
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">
Heute im Fokus <span class="text-brand">· {{ $industry }}</span>
</h2>
<div class="eyebrow muted">Aktualisiert {{ now()->format('H:i') }} Uhr · {{ $stats[0]['value'] ?? '47' }} Meldungen heute</div>
<div class="eyebrow muted">Aktualisiert {{ now()->format('H:i') }} Uhr · {{ $stats[0]['value'] ?? '47' }} Meldungen</div>
</header>
<hr class="rule-strong">

View file

@ -24,7 +24,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('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 }} Startseite">
<div class="text-[24px] font-semibold leading-none tracking-[-0.5px]">
<x-web.brand-mark :brand="$themeKey" variant="on-dark" />
</div>
@ -38,13 +38,12 @@
</div>
<div>
<div class="eyebrow mb-3.5">Pressemitteilungen</div>
<div class="eyebrow mb-3.5">Rubriken</div>
<ul class="space-y-2 text-[13px] text-white/75 list-none p-0 m-0">
<li><a href="{{ route('kategorien') }}" class="cursor-pointer hover:text-white transition-colors">Alle Branchen</a></li>
<li><a href="#" class="cursor-pointer hover:text-white transition-colors">Ad-Hoc-Meldungen</a></li>
<li><a href="{{ route('kategorien') }}#termine" class="cursor-pointer hover:text-white transition-colors">Termine &amp; Events</a></li>
@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('kategorien') }}" class="cursor-pointer hover:text-white transition-colors">Branchen-Index</a></li>
</ul>
</div>

View file

@ -1,7 +1,4 @@
@props([
'activeRegion' => 'DE',
'language' => 'Deutsch',
'languageShort' => 'DE',
'navigation' => null,
'ticker' => [],
'marketIndex' => null,
@ -22,29 +19,27 @@
@endphp
@php
\Carbon\Carbon::setLocale('de');
$activeEdition = app()->getLocale();
\Carbon\Carbon::setLocale($activeEdition);
$today = now();
$regions = [
['code' => 'DE', 'flag' => '🇩🇪'],
['code' => 'AT', 'flag' => '🇦🇹'],
['code' => 'CH', 'flag' => '🇨🇭'],
['code' => 'EN', 'flag' => '🌐'],
// Ausgaben = Sprachen (DE/EN). Wechsel führt zur Startseite der jeweiligen Ausgabe.
$editions = [
['code' => 'de', 'label' => 'DE', 'flag' => '🇩🇪'],
['code' => 'en', 'label' => 'EN', 'flag' => '🌐'],
];
$navigation ??= [
['label' => 'Startseite', 'href' => route('home'), 'active' => true],
['label' => 'Wirtschaft', 'href' => route('kategorien')],
['label' => 'Technologie', 'href' => route('kategorien')],
['label' => 'Finanzen', 'href' => route('kategorien')],
['label' => 'Industrie', 'href' => route('kategorien')],
['label' => 'Energie', 'href' => route('kategorien')],
['label' => 'Gesundheit', 'href' => route('kategorien')],
['label' => 'Handel', 'href' => route('kategorien')],
['label' => 'Immobilien', 'href' => route('kategorien')],
['label' => 'Mobilität', 'href' => route('kategorien')],
['label' => 'Alle Rubriken', 'href' => route('kategorien')],
];
// 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')],
])->concat(
\App\Support\CategoryNavigation::items()->map(fn (array $category): array => [
'label' => $category['label'],
'href' => $category['href'],
'active' => request()->routeIs('kategorie') && request()->route('slug') === $category['slug'],
])
)->all();
$activeLabel = collect($navigation)->firstWhere('active', true)['label'] ?? 'Menü';
@endphp
@ -62,16 +57,16 @@
Ausgabe
</span>
@foreach ($regions as $region)
<button type="button"
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 cursor-pointer transition-colors
@if ($region['code'] === $activeRegion) font-semibold text-ink-on-dark bg-white/[0.06] border-brand
@foreach ($editions as $edition)
<a href="{{ route('web.home', ['edition' => $edition['code']]) }}"
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="Region {{ $region['code'] }}"
aria-pressed="{{ $region['code'] === $activeRegion ? 'true' : 'false' }}">
<span class="text-[11px] sm:text-[12px] leading-none">{{ $region['flag'] }}</span>
<span>{{ $region['code'] }}</span>
</button>
aria-label="Ausgabe {{ $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>
</a>
@endforeach
<span class="flex-1"></span>
@ -84,8 +79,8 @@
<path d="M1 6h10M6 1c2 1.5 2 8.5 0 10M6 1c-2 1.5-2 8.5 0 10" stroke="currentColor" stroke-width="1" fill="none" />
</svg>
<strong class="text-ink-on-dark font-semibold">
<span class="hidden lg:inline">{{ $language }}</span>
<span class="lg:hidden">{{ $languageShort }}</span>
<span class="hidden lg:inline">{{ $activeEdition === 'en' ? 'English' : 'Deutsch' }}</span>
<span class="lg:hidden">{{ strtoupper($activeEdition) }}</span>
</strong>
</span>
<span class="hidden lg:inline-block w-px h-[14px] bg-ink-on-dark-rule"></span>
@ -101,7 +96,7 @@
x-data="{ searchOpen: false }"
@keydown.escape.window="searchOpen = false">
<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('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 }} Startseite">
<div class="text-[22px] sm:text-[24px] lg:text-[28px] font-semibold leading-none tracking-[-0.5px]">
<x-web.brand-mark :brand="$themeKey" />
</div>

View file

@ -3,13 +3,16 @@
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Models\PressRelease;
use App\Support\EditorialClock;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Carbon;
use Livewire\Attributes\Locked;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new class extends Component {
new class extends Component
{
use WithPagination;
public string $timeframe = 'all';
@ -17,6 +20,9 @@ new class extends Component {
#[Locked]
public ?string $portal = null;
#[Locked]
public string $language = 'de';
protected array $queryString = [
'timeframe' => ['except' => 'all'],
];
@ -37,13 +43,20 @@ new class extends Component {
private function pressReleases(): LengthAwarePaginator
{
$reference = $this->referenceNow();
return $this->baseQuery()
->when($this->timeframe === 'today', fn (Builder $query) => $query->whereDate('published_at', today()))
->when($this->timeframe === 'week', fn (Builder $query) => $query->where('published_at', '>=', now()->subDays(7)))
->when($this->timeframe === 'today', fn (Builder $query) => $query->whereDate('published_at', $reference->toDateString()))
->when($this->timeframe === 'week', fn (Builder $query) => $query->where('published_at', '>=', $reference->copy()->subDays(7)))
->orderByDesc('published_at')
->paginate(8);
}
private function referenceNow(): Carbon
{
return EditorialClock::reference($this->portalValues(), $this->language);
}
private function totalCount(): int
{
return $this->baseQuery()->count();
@ -54,7 +67,7 @@ new class extends Component {
return PressRelease::query()
->with([
'company',
'category.translations' => fn ($query) => $query->where('locale', 'de'),
'category.translations' => fn ($query) => $query->where('locale', $this->language),
'images' => fn ($query) => $query
->orderByDesc('is_preview')
->orderBy('sort_order')
@ -62,7 +75,7 @@ new class extends Component {
])
->whereIn('portal', $this->portalValues())
->where('status', PressReleaseStatus::Published)
->where('language', 'de')
->where('language', $this->language)
->whereNotNull('published_at')
->where('published_at', '<=', now());
}
@ -150,9 +163,9 @@ new class extends Component {
</div>
@elseif (! $top)
<div class="flex justify-center mt-7">
<a href="{{ route('kategorien') }}"
<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">
Alle Rubriken entdecken
Zur Startseite
</a>
</div>
@endif

View file

@ -0,0 +1,302 @@
<?php
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Models\Category;
use App\Models\PressRelease;
use App\Scopes\PortalScope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Url;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new class extends Component
{
use WithPagination;
#[Url(as: 'q', except: '')]
public string $q = '';
#[Url(except: '')]
public string $category = '';
#[Url(except: 'newest')]
public string $sort = 'newest';
#[Locked]
public ?string $portal = null;
#[Locked]
public string $language = 'de';
public function updated(string $property): void
{
if (in_array($property, ['q', 'category', 'sort'], true)) {
$this->resetPage();
}
}
public function clearFilters(): void
{
$this->reset(['q', 'category', 'sort']);
$this->resetPage();
}
public function selectCategory(string $slug): void
{
$this->category = $this->category === $slug ? '' : $slug;
$this->resetPage();
}
public function with(): array
{
// Ausgabe/Sprache auch auf Livewire-Update-Requests konsistent halten
// (dort fehlt das /de- bzw. /en-Präfix in der URL).
app()->setLocale($this->language);
\Illuminate\Support\Facades\URL::defaults(['edition' => $this->language]);
$term = trim($this->q);
$hasQuery = mb_strlen($term) >= 2 || $this->category !== '';
return [
'hasQuery' => $hasQuery,
'term' => $term,
'results' => $hasQuery ? $this->results($term) : null,
'archiveTotal' => $this->archiveTotal(),
'categories' => \App\Support\CategoryNavigation::items(12),
];
}
private function results(string $term): LengthAwarePaginator
{
$query = $this->baseQuery();
if (mb_strlen($term) >= 2) {
$query->where(function (Builder $inner) use ($term): void {
$like = '%'.$term.'%';
$inner
->whereLike('title', $like)
->orWhereLike('subtitle', $like)
->orWhereLike('text', $like)
->orWhereLike('keywords', $like)
->orWhereHas('company', fn (Builder $company) => $company->whereLike('name', $like))
->orWhereHas('category.translations', fn (Builder $translation) => $translation
->where('locale', $this->language)
->whereLike('name', $like));
});
}
if ($this->category !== '') {
$categoryIds = $this->categoryIds($this->category);
$query->whereIn('category_id', $categoryIds);
}
return $this->applySort($query)
->paginate(10)
->withQueryString();
}
/**
* @return array<int, int>
*/
private function categoryIds(string $slug): array
{
$category = Category::query()
->whereIn('portal', $this->portalValues())
->whereHas('translations', fn (Builder $query) => $query
->where('locale', $this->language)
->where('slug', $slug))
->first();
if (! $category) {
return [-1];
}
return Category::query()
->where(fn (Builder $query) => $query
->whereKey($category->getKey())
->orWhere('parent_id', $category->getKey()))
->pluck('id')
->all();
}
private function applySort(Builder $query): Builder
{
return match ($this->sort) {
'oldest' => $query->orderBy('published_at'),
'most-read' => $query->orderByDesc('hits')->orderByDesc('published_at'),
default => $query->orderByDesc('published_at'),
};
}
private function baseQuery(): Builder
{
return PressRelease::query()
->withoutGlobalScope(PortalScope::class)
->with([
'company',
'category.translations' => fn ($query) => $query->where('locale', $this->language),
])
->whereIn('portal', $this->portalValues())
->where('status', PressReleaseStatus::Published)
->where('language', $this->language)
->whereNotNull('published_at')
->where('published_at', '<=', now());
}
private function archiveTotal(): int
{
return $this->baseQuery()->count();
}
/**
* @return array<int, string>
*/
private function portalValues(): array
{
$primary = $this->portal ?? Portal::Businessportal24->value;
return [$primary, Portal::Both->value];
}
}; ?>
<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>
<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" />
<path d="M11 11l3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
<input id="search-input" type="search"
wire:model.live.debounce.400ms="q"
placeholder="Suchbegriff, Unternehmen oder Branche…"
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>
@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">
<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>
</button>
@endif
</div>
</form>
{{-- Rubriken-Filter --}}
@if ($categories->isNotEmpty())
<div class="flex flex-wrap gap-2 mb-5">
<button type="button" wire:click="$set('category', '')" @class([
'px-3.5 py-2 text-[12.5px] font-medium border transition-colors',
'border-brand bg-brand text-white' => $category === '',
'border-bg-rule-strong text-ink hover:bg-bg-elev' => $category !== '',
])>
Alle Rubriken
</button>
@foreach ($categories as $cat)
<button type="button" wire:click="selectCategory('{{ $cat['slug'] }}')" @class([
'px-3.5 py-2 text-[12.5px] font-medium border transition-colors',
'border-brand bg-brand text-white' => $category === $cat['slug'],
'border-bg-rule-strong text-ink hover:bg-bg-elev' => $category !== $cat['slug'],
])>
{{ $cat['label'] }}
</button>
@endforeach
</div>
@endif
@if ($hasQuery && $results)
{{-- Ergebnis-Kopf --}}
<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' }}
@if ($term !== '')
<span class="text-ink-3 font-normal">für {{ $term }}"</span>
@endif
</h2>
<label class="flex items-center gap-2 text-[12.5px] text-ink-3">
<span class="whitespace-nowrap">Sortierung</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>
</select>
</label>
</div>
<hr class="rule-strong mb-1">
@forelse ($results as $release)
<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>
<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.
</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
</button>
</div>
@endforelse
@if ($results->hasPages())
<nav class="mt-7 flex items-center justify-center gap-1.5" aria-label="Seitennummerierung">
<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">
<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)
<button type="button" wire:click="gotoPage({{ $page }})" @class([
'inline-flex items-center justify-center min-w-9 h-9 px-2 border text-[13px] font-mono transition-colors',
'border-brand bg-brand text-white' => $page === $results->currentPage(),
'border-bg-rule-strong text-ink hover:bg-bg-elev' => $page !== $results->currentPage(),
])>{{ $page }}</button>
@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">
<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>
@endif
@else
{{-- Discovery / Leerzustand --}}
<div class="border border-bg-rule bg-bg-elev px-6 py-12 text-center">
<div class="w-14 h-14 mx-auto mb-5 inline-flex items-center justify-center border border-bg-rule-strong bg-bg text-ink-3">
<svg width="24" height="24" 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>
</div>
<h2 class="font-serif text-[22px] font-semibold text-ink m-0 mb-2">Durchsuchen Sie das Archiv</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.
</p>
@if ($categories->isNotEmpty())
<div class="mt-7">
<div class="eyebrow muted text-[10px] mb-3">Beliebte Rubriken</div>
<div class="flex flex-wrap gap-2 justify-center">
@foreach ($categories->take(8) as $cat)
<button type="button" wire:click="selectCategory('{{ $cat['slug'] }}')"
class="px-3.5 py-2 text-[12.5px] font-medium border border-bg-rule-strong text-ink hover:bg-white transition-colors">
{{ $cat['label'] }}
</button>
@endforeach
</div>
</div>
@endif
</div>
@endif
</div>

View file

@ -3,8 +3,14 @@
@section('title', 'Aktuelle Pressemitteilungen aus der deutschen Wirtschaft businessportal24')
@section('meta_description', 'Pressemitteilungen aus Deutschland, Österreich und der Schweiz. Redaktionell geprüft. Strukturiert distribuiert.')
@section('hreflang')
<link rel="alternate" hreflang="de" href="{{ url('/de') }}">
<link rel="alternate" hreflang="en" href="{{ url('/en') }}">
<link rel="alternate" hreflang="x-default" href="{{ url('/de') }}">
@endsection
@section('content')
<x-web.site-header />
<x-web.site-header :ticker="$tickerItems ?? []" />
<main class="bg-bg text-ink">
<x-web.focus-hero
@ -13,7 +19,7 @@
<section class="max-w-layout mx-auto px-8 mt-16">
<div class="grid gap-9 grid-cols-1 lg:grid-cols-[1.7fr_1fr]">
<livewire:web.press-release-feed :portal="\App\Enums\Portal::Businessportal24->value" />
<livewire:web.press-release-feed :portal="\App\Enums\Portal::Businessportal24->value" :language="app()->getLocale()" />
<aside class="flex flex-col gap-9">
<x-web.most-read :releases="$mostReadReleases ?? []" />
@ -23,9 +29,10 @@
</div>
</section>
<x-web.industry-spotlight />
<x-web.events-week />
<x-web.industry-spotlight
:industry="$spotlight['industry'] ?? null"
:stats="$spotlight['stats'] ?? null"
:releases="$spotlight['releases'] ?? []" />
<x-web.industry-index :industries="($industryIndex ?? collect())->isNotEmpty() ? $industryIndex->toArray() : null" />

View file

@ -1,400 +1,301 @@
@php
use App\Services\CategoryService;
// Kategorie aus Route-Parameter laden
$slug = request()->segment(2) ?? 'wirtschaft';
$category = CategoryService::getCategoryBySlug($slug);
// Wenn Kategorie nicht gefunden, 404
if (!$category) {
abort(404, 'Kategorie nicht gefunden');
}
// Alle Kategorien für Sidebar
$allCategories = CategoryService::getCategories();
// Icon- und Farb-Informationen
$iconPath = CategoryService::getIconPath($category['icon']);
$gradient = CategoryService::getGradientForColor($category['color']);
$iconColor = CategoryService::getClassForColor($category['color']);
@endphp
@extends('web.layouts.web-master')
@section('title', $category['name'] . ' - Pressemitteilungen - Business Portal 24')
@php
use Illuminate\Support\Str;
\Carbon\Carbon::setLocale('de');
$catTranslation = $category->translations->firstWhere('locale', 'de') ?? $category->translations->first();
$catName = $catTranslation?->name ?? 'Branche';
$catSlug = $catTranslation?->slug;
$catDescription = filled($catTranslation?->description)
? $catTranslation->description
: 'Aktuelle Pressemitteilungen aus dem Bereich '.$catName.' redaktionell geprüft und strukturiert distribuiert im DACH-Raum.';
$parentTranslation = $category->parent?->translations->firstWhere('locale', 'de') ?? $category->parent?->translations->first();
$parentName = $parentTranslation?->name;
$parentHref = $parentTranslation?->slug ? route('kategorie', ['slug' => $parentTranslation->slug]) : null;
$fmt = fn ($value) => number_format((int) $value, 0, ',', '.');
$weekDelta = (int) ($categoryStats['week'] ?? 0) - (int) ($categoryStats['previousWeek'] ?? 0);
$weekDeltaPct = ($categoryStats['previousWeek'] ?? 0) > 0
? (int) round($weekDelta / $categoryStats['previousWeek'] * 100)
: null;
$leadImage = $leadRelease?->images->first();
$leadImageUrl = $leadImage?->variantUrl('detail') ?? $leadImage?->variantUrl('card') ?? $leadImage?->url();
$leadCat = $leadRelease?->category?->translations->firstWhere('locale', 'de') ?? $leadRelease?->category?->translations->first();
$leadCompany = $leadRelease?->company?->name;
$leadLead = $leadRelease
? (filled($leadRelease->subtitle)
? $leadRelease->subtitle
: (string) Str::of(strip_tags((string) $leadRelease->text))->squish()->limit(220))
: null;
$leadWords = $leadRelease ? str_word_count(strip_tags((string) $leadRelease->text)) : 0;
$leadReadTime = max(1, (int) round($leadWords / 200));
@endphp
@section('title', 'Pressemitteilungen ' . $catName . ' ' . config('app.name'))
@section('meta_description', Str::limit(strip_tags($catDescription), 155))
@section('content')
<x-web.site-header />
<main class="min-h-screen flex flex-col bg-white dark:bg-zinc-950 transition-colors duration-200">
<!-- Burger Menu Component -->
<livewire:web.burger-menu />
<!-- Header Component -->
<livewire:web.header />
<!-- Breadcrumbs -->
<div class="container mx-auto px-4 py-4">
<x-web.breadcrumb :items="[['label' => 'Kategorien', 'url' => '/kategorien'], ['label' => $category['name']]]" />
<main class="bg-bg text-ink">
{{-- 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>
@if ($parentName)
<span class="text-ink-4" aria-hidden="true">/</span>
<a href="{{ $parentHref }}" class="hover:text-ink transition-colors">{{ $parentName }}</a>
@endif
<span class="text-ink-4" aria-hidden="true">/</span>
<span class="text-ink font-semibold">{{ $catName }}</span>
</div>
</div>
<!-- Hero Banner -->
<x-web.hero-banner :theme="$theme">
<x-slot name="title">
<div class="flex items-center justify-center gap-4 mb-4">
<div class="w-16 h-16 bg-white/10 backdrop-blur-sm rounded-xl flex items-center justify-center">
<img src="{{ $iconPath }}" alt="{{ $category['name'] }}" class="h-8 w-8 brightness-0 invert">
{{-- Masthead --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-8 lg:pt-9">
<div class="grid gap-8 lg:gap-12 lg:grid-cols-[1.5fr_1fr] lg:items-end mb-[18px]">
<div>
<div class="eyebrow mb-3">Branche</div>
<h1 class="font-serif text-[40px] sm:text-[52px] lg:text-[56px] font-semibold m-0 mb-4 leading-[1.02] tracking-[-1.2px] text-ink">{{ $catName }}</h1>
<p class="font-serif text-[16px] leading-[1.55] m-0 mb-[18px] max-w-[620px] text-ink-2">{{ $catDescription }}</p>
<div class="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') }}
</div>
</div>
Pressemitteilungen {{ $category['name'] }}
</x-slot>
<x-slot name="subtitle">
{{ $category['description'] }}
</x-slot>
</x-web.hero-banner>
<!-- SEO Text - Collapsible -->
{{-- <section class="section-light-bg border-b border-zinc-200 dark:border-zinc-800 transition-colors duration-200"
x-data="{ expanded: false }">
<div class="container mx-auto px-4 py-6">
<div class="card p-6">
<div :class="{ 'line-clamp-3': !expanded }" class="prose prose-sm max-w-none">
<p class="text-zinc-900 dark:text-zinc-100">
Die Kategorie <strong>{{ $category['name'] }}</strong> auf Business Portal vereint die
wichtigsten
Nachrichten und Pressemitteilungen aus diesem Bereich. {{ $category['description'] }}
</p>
<template x-if="expanded">
<div>
<p class="text-zinc-900 dark:text-zinc-100 mt-4">
Hier finden Sie aktuelle Meldungen, Produktankündigungen und Unternehmensnachrichten
aus Deutschland, Österreich und der Schweiz. Unternehmen nutzen unsere Plattform,
um ihre wichtigsten Neuigkeiten zu veröffentlichen.
</p>
<p class="text-zinc-900 dark:text-zinc-100 mt-4">
Als zentrale Anlaufstelle für Pressemitteilungen im Bereich {{ $category['name'] }}
bietet Business Portal Journalisten, Analysten und Entscheidern einen kompakten
Überblick über die neuesten Entwicklungen.
</p>
<p class="text-zinc-900 dark:text-zinc-100 mt-4">
Nutzen Sie die Filterfunktionen, um gezielt nach Themen, Regionen oder Zeiträumen zu
suchen. Abonnieren Sie den RSS-Feed oder Newsletter, um keine wichtige Meldung
mehr zu verpassen.
</p>
</div>
</template>
<aside class="flex flex-col gap-2.5">
<a href="{{ route('veroeffentlichen') }}" class="w-full px-4 py-3 inline-flex items-center justify-center gap-2 text-[13px] font-semibold text-white bg-brand hover:bg-brand-deep transition-colors">
Veröffentlichen in {{ $catName }}
</a>
<div class="grid grid-cols-2 gap-2">
<a href="{{ route('newsrooms') }}" class="inline-flex items-center justify-center gap-1.5 px-3 py-2.5 text-[12px] font-semibold border border-bg-rule-strong bg-white hover:bg-bg-elev transition-colors">Newsrooms</a>
<a href="{{ route('web.home') }}" class="inline-flex items-center justify-center gap-1.5 px-3 py-2.5 text-[12px] font-semibold border border-bg-rule-strong bg-white hover:bg-bg-elev transition-colors">Alle Meldungen</a>
</div>
<button @click="expanded = !expanded"
class="mt-4 inline-flex items-center text-sm font-medium text-[var(--color-primary)] hover:text-[var(--color-secondary)] transition-colors">
<svg class="h-4 w-4 mr-2 transition-transform" :class="{ 'rotate-180': expanded }" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7">
</path>
</svg>
<span x-text="expanded ? 'Weniger anzeigen' : 'Mehr lesen'"></span>
</button>
<div class="mt-1.5 pt-3.5 border-t border-bg-rule flex justify-between items-baseline text-[11px] text-ink-3">
<div>
<div class="eyebrow muted text-[9px] mb-1">Aktive Unternehmen</div>
<span class="font-mono text-[12px] text-ink-2">{{ $fmt($categoryStats['totalCompanies'] ?? 0) }} Newsrooms</span>
</div>
<span class="font-mono text-[11px] text-ink-3">{{ $fmt($categoryStats['total'] ?? 0) }} Meldungen gesamt</span>
</div>
</aside>
</div>
<hr class="rule-strong">
</section>
@if ($subCategories->isNotEmpty())
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-6 mb-2 flex flex-wrap items-center gap-2">
<span class="eyebrow muted text-[10px] mr-1.5">Sub-Themen</span>
@foreach ($subCategories as $sub)
<a href="{{ $sub['href'] }}" class="inline-flex items-baseline gap-[7px] px-3.5 py-2 text-[12.5px] font-medium border border-bg-rule-strong text-ink hover:bg-bg-elev transition-colors">
{{ $sub['name'] }}
<span class="font-mono text-[10.5px] text-ink-3 font-medium">{{ $fmt($sub['total']) }}</span>
</a>
@endforeach
</section>
@endif
{{-- Stat strip --}}
<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="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>
<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="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>
</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="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>
<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="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>
</div>
</section>
--}}
<!-- Main Content with Sidebar -->
<section class="py-8 section-light-bg dark:bg-zinc-950 transition-colors duration-200">
<div class="container mx-auto px-4">
<div class="flex flex-col lg:flex-row gap-8">
<!-- Main Content -->
<div class="flex-1">
<div class="flex items-center justify-between mb-6">
<h2 class="text-sm font-medium text-zinc-600 dark:text-zinc-400 flex items-center gap-2">
<span class="w-2 h-2 bg-[var(--color-primary)] rounded-full"></span>
{{ $category['count'] }} Pressemitteilungen in {{ $category['name'] }}
</h2>
@if ($leadRelease)
{{-- Top-Meldung + Begleiter --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-10">
<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;">
@if ($leadImageUrl)
<img src="{{ $leadImageUrl }}" alt="{{ $leadRelease->title }}" class="absolute inset-0 w-full h-full object-cover" loading="lazy">
@endif
<div class="absolute top-5 left-5 flex gap-2">
<span class="bp-tag">Top-Meldung</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)
<div class="eyebrow mb-3" style="color:#FF8B6F;">{{ $leadCat->name }}</div>
@endif
<h2 class="font-serif text-[26px] sm:text-[32px] font-semibold m-0 leading-[1.12] tracking-[-0.6px] max-w-[680px] group-hover:text-white">
{{ $leadRelease->title }}
</h2>
@if ($leadLead)
<p class="font-serif text-[14.5px] leading-[1.5] mt-3.5 max-w-[600px] text-white/85">{{ $leadLead }}</p>
@endif
<div class="mt-[18px] flex items-center gap-3.5 text-[12px] text-white/70 flex-wrap">
@if ($leadCompany)
<span class="inline-flex items-center gap-1.5">
<svg width="11" height="11" viewBox="0 0 11 11" style="color:#5BC07A;" 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>
{{ $leadCompany }}
</span>
<span aria-hidden="true">·</span>
@endif
@if ($leadRelease->published_at)
<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>
</div>
</div>
</div>
</a>
</article>
<aside>
<div class="eyebrow muted mb-3.5">Weitere Top-Meldungen</div>
@forelse ($topReleases as $top)
@php
$topCat = $top->category?->translations->firstWhere('locale', 'de') ?? $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">
<div class="flex justify-between items-baseline mb-2 gap-3">
<span class="bp-cat">{{ $topCat?->name ?? $catName }}</span>
<span class="font-mono text-[11px] text-ink-3 whitespace-nowrap">{{ $top->published_at?->translatedFormat('j. M') }}</span>
</div>
<h3 class="font-serif text-[17px] leading-[1.28] m-0 font-semibold tracking-[-0.2px] text-ink group-hover:text-brand transition-colors">{{ $top->title }}</h3>
@if ($top->company?->name)
<div class="mt-2.5 text-[11.5px] text-ink-3 flex items-center gap-2">
<svg width="11" height="11" 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>{{ $top->company->name }}</span>
</div>
@endif
</a>
</article>
@empty
<p class="text-[12.5px] text-ink-3 py-4 border-t border-bg-rule">Aktuell keine weiteren Top-Meldungen.</p>
@endforelse
<div class="mt-[22px]">
<x-web.publisher-cta />
</div>
<!-- Press Release Cards Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
<!-- Card 1 -->
<article class="card card-hover overflow-hidden group cursor-pointer">
<div class="p-5">
<div class="flex items-center gap-2 mb-3">
<span class="badge badge-primary">
IT & Software
</span>
<span class="text-xs text-zinc-600 dark:text-zinc-400">Heute, 14:30</span>
</div>
<h3
class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-2 line-clamp-2 group-hover:text-[var(--color-primary)] transition-colors">
KI-Revolution: Deutsche Unternehmen investieren Milliarden in Automatisierung
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-3">
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.
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span>Berlin</span>
</div>
<span
class="inline-flex items-center text-sm text-[var(--color-primary)] font-medium">
Lesen
</span>
</div>
</div>
</article>
<!-- Card 2 -->
<article class="card card-hover overflow-hidden group cursor-pointer">
<div class="p-5">
<div class="flex items-center justify-between gap-2 mb-3">
<span class="text-xs text-zinc-700 dark:text-zinc-400">Heute, 10:00</span>
<span class="badge badge-primary">
{{ $category['name'] }}
</span>
</div>
<h3
class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-2 line-clamp-2 group-hover:text-[var(--color-primary)] transition-colors">
Cloud-Migration: 78% der deutschen Unternehmen setzen auf Hybrid-Cloud-Strategien
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-3">
Neue Umfrage zeigt: Hybrid-Cloud wird zum Standard. Unternehmen kombinieren Public
und Private Cloud für maximale Flexibilität.
</p>
<div
class="flex items-center justify-between text-[var(--color-primary)]/60 pt-5 border-t border-[var(--color-primary)]/10 group-hover:border-[var(--color-primary)] 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">Unternehmen</span>
</div>
<svg class="h-5 w-5 transition-transform group-hover:translate-x-2 group-hover:text-[var(--color-primary)]"
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>
</article>
<!-- Card 3 -->
<article class="card card-hover overflow-hidden group cursor-pointer">
<div class="p-5">
<div class="flex items-center gap-2 mb-3">
<span class="badge badge-primary">
{{ $category['name'] }}
</span>
<span class="text-xs text-zinc-600 dark:text-zinc-400">Gestern, 15:20</span>
</div>
<h3
class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-2 line-clamp-2 group-hover:text-[var(--color-primary)] transition-colors">
Cybersecurity: Unternehmen verstärken Schutzmaßnahmen gegen Hackerangriffe
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-3">
Nach einer Serie von Cyberattacken erhöhen deutsche Unternehmen ihre Investitionen
in
IT-Sicherheit um durchschnittlich 35%.
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span>München</span>
</div>
<span
class="inline-flex items-center text-sm text-[var(--color-primary)] font-medium">
Lesen
</span>
</div>
</div>
</article>
<!-- Card 4 -->
<article class="card card-hover overflow-hidden group cursor-pointer">
<div class="p-5">
<div class="flex items-center gap-2 mb-3">
<span class="badge badge-primary">
{{ $category['name'] }}
</span>
<span class="text-xs text-zinc-600 dark:text-zinc-400">Gestern, 11:45</span>
</div>
<h3
class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-2 line-clamp-2 group-hover:text-[var(--color-primary)] transition-colors">
Low-Code-Plattformen revolutionieren die Softwareentwicklung
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-3">
Citizen Developer werden zur treibenden Kraft: Low-Code-Lösungen ermöglichen auch
Nicht-Programmierern professionelle Anwendungen.
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span>Berlin</span>
</div>
<span
class="inline-flex items-center text-sm text-[var(--color-primary)] font-medium">
Lesen
</span>
</div>
</div>
</article>
<!-- Card 5 -->
<article class="card card-hover overflow-hidden group cursor-pointer">
<div class="p-5">
<div class="flex items-center gap-2 mb-3">
<span class="badge badge-primary">
{{ $category['name'] }}
</span>
<span class="text-xs text-zinc-600 dark:text-zinc-400">2 Tage</span>
</div>
<h3
class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-2 line-clamp-2 group-hover:text-[var(--color-primary)] transition-colors">
5G-Rollout beschleunigt: Netzabdeckung erreicht 85% in Deutschland
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-3">
Mobilfunkanbieter berichten von schnellerem Ausbau als erwartet. Industrielle
IoT-Anwendungen profitieren enorm.
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400">
<svg class="h-4 w-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span>Deutschland</span>
</div>
<span
class="inline-flex items-center text-sm text-[var(--color-primary)] font-medium">
Lesen
</span>
</div>
</div>
</article>
<!-- Card 6 -->
<article class="card card-hover overflow-hidden group cursor-pointer">
<div class="p-5">
<div class="flex items-center gap-2 mb-3">
<span class="badge badge-primary">
{{ $category['name'] }}
</span>
<span class="text-xs text-zinc-600 dark:text-zinc-400">3 Tage</span>
</div>
<h3
class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-2 line-clamp-2 group-hover:text-[var(--color-primary)] transition-colors">
Quantum Computing: Deutscher Forschungsverbund meldet Durchbruch
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-3">
Wissenschaftler entwickeln stabilere Qubits. Kommerzielle Anwendungen in der
Materialforschung rücken näher.
</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-xs text-zinc-600 dark:text-zinc-400">
<svg class="h-4 w-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span>Stuttgart</span>
</div>
<span
class="inline-flex items-center text-sm text-[var(--color-primary)] font-medium">
Lesen
</span>
</div>
</div>
</article>
</div>
<!-- Pagination -->
<x-web.pagination :currentPage="1" :totalPages="10" />
</div>
<!-- Sidebar - Hidden on mobile -->
<aside class="lg:w-80 space-y-6">
<!-- Categories -->
<x-web.sidebar-categories :categories="$allCategories" :activeSlug="$category['slug']" />
<!-- Top Companies -->
<x-web.sidebar-companies />
<!-- Newsletter Subscribe -->
<x-web.sidebar-newsletter :title="$category['name'] . '-Newsletter'" :description="'Erhalten Sie die neuesten Pressemitteilungen aus ' .
$category['name'] .
' direkt in Ihr Postfach'" />
<!-- RSS Feed -->
<x-web.sidebar-rss :description="'Bleiben Sie über neue Meldungen in ' . $category['name'] . ' auf dem Laufenden'" />
</aside>
</div>
</section>
</section>
<!-- Footer Component -->
<livewire:web.footer />
{{-- Aktuelle Meldungen + Sidebar --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-16 grid gap-9 lg:grid-cols-[1.7fr_1fr]">
<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>
</h2>
</header>
<hr class="rule-strong">
@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>
@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>
</div>
</div>
<aside class="flex flex-col gap-9">
<x-web.most-read :releases="$mostReadReleases ?? []" />
<x-web.active-newsrooms :companies="$topNewsrooms ?? []" />
</aside>
</section>
@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>
</div>
</section>
@endif
@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>
</header>
<hr class="rule-strong">
@php $subMax = max(1, (int) $subCategories->max('total')); @endphp
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 border-b border-bg-rule">
@foreach ($subCategories as $sub)
<a href="{{ $sub['href'] }}" class="block px-[22px] py-5 border-t border-bg-rule lg:[&:nth-child(-n+4)]:border-t-0 {{ $loop->iteration % 4 !== 0 ? 'lg:border-r' : '' }} hover:bg-bg-elev transition-colors group">
<div class="flex justify-between items-baseline mb-2">
<span class="text-[14px] font-semibold text-ink group-hover:text-brand transition-colors">{{ $sub['name'] }}</span>
<span class="font-mono text-[12px] font-semibold {{ $sub['delta'] >= 0 ? 'text-gain' : 'text-loss' }}">{{ $sub['delta'] >= 0 ? '+' : '' }}{{ $fmt(abs($sub['delta'])) }}</span>
</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>
</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>
@endforeach
</div>
</section>
@endif
@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>
<hr class="rule mb-5">
<div class="flex flex-wrap gap-2.5">
@foreach ($relatedCategories as $related)
<a href="{{ $related['href'] }}" class="inline-flex items-baseline gap-2 px-3.5 py-2.5 text-[13px] font-semibold bg-bg-elev border border-bg-rule-strong text-ink hover:bg-white transition-colors">
{{ $related['name'] }}
<span class="font-mono text-[11px] text-ink-3 font-medium">{{ $fmt($related['count']) }}</span>
</a>
@endforeach
</div>
</section>
@endif
{{-- Publisher CTA (breit) --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-16 mb-4">
<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>
<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>
</div>
</div>
</div>
</section>
</main>
<x-web.site-footer />
@endsection
@push('styles')
<style>
/* Domain-spezifische Color-Variablen für Inline-Styles */
:root {
--color-primary: {{ $domainConfig['color_scheme']['primary'] ?? '#cf3628' }};
--color-secondary: {{ $domainConfig['color_scheme']['secondary'] ?? '#f0834a' }};
}
/* Alpine.js Cloak */
[x-cloak] {
display: none !important;
}
/* Line clamp utilities */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
@endpush

View file

@ -1,170 +0,0 @@
@extends('web.layouts.web-master')
@section('title', 'Kategorien - Business Portal 24')
@section('content')
<main class="min-h-screen flex flex-col bg-white dark:bg-zinc-950 transition-colors duration-200">
<livewire:web.burger-menu />
<livewire:web.header />
<!-- Hero Banner -->
<x-web.hero-banner :theme="$theme">
<x-slot name="title">
Kategorien
</x-slot>
<x-slot name="subtitle">
Entdecken Sie Pressemitteilungen nach Themengebieten
</x-slot>
</x-web.hero-banner>
<!-- Categories Grid Section -->
<section class="section-light-bg py-16 transition-colors duration-200">
<div class="container mx-auto px-4">
<div class="max-w-6xl mx-auto">
@php
use App\Services\CategoryService;
$categories = CategoryService::getCategories();
@endphp
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach ($categories as $category)
@php
$iconPath = CategoryService::getIconPath($category['icon']);
$gradient = CategoryService::getGradientForColor($category['color']);
$iconColor = CategoryService::getClassForColor($category['color']);
@endphp
<a href="/kategorie/{{ $category['slug'] }}"
class="group card card-hover cursor-pointer rounded-xl p-6 transition-all">
<div class="flex items-start gap-4">
<div
class="w-14 h-14 bg-gradient-to-br {{ $gradient }} rounded-lg flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform">
<div class="industry-icon-badge">
<img src="{{ $iconPath }}" alt="{{ $category['name'] }}"
class="h-7 w-7 {{ $iconColor }}" />
</div>
</div>
<div class="flex-1">
<h3
class="font-semibold text-lg text-zinc-900 dark:text-zinc-100 mb-2 group-hover:text-[var(--color-primary)] transition-colors">
{{ $category['name'] }}
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-3">
{{ $category['description'] }}
</p>
<span class="text-xs text-zinc-500 dark:text-zinc-500">{{ $category['count'] }}
Mitteilungen</span>
</div>
</div>
</a>
@endforeach
</div>
</div>
</div>
</section>
<!-- Stats Section -->
<section class="py-16 bg-zinc-50/50 dark:bg-zinc-900/50 transition-colors duration-200">
<div class="container mx-auto px-4">
<div class="max-w-4xl mx-auto">
<div class="grid grid-cols-2 md:grid-cols-4 gap-8 text-center">
<div>
<div
class="text-3xl font-bold bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] bg-clip-text text-transparent mb-2">
{{ App\Services\CategoryService::count() }}
</div>
<div class="text-sm text-zinc-600 dark:text-zinc-400">Kategorien</div>
</div>
<div>
<div
class="text-3xl font-bold bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] bg-clip-text text-transparent mb-2">
10.000+
</div>
<div class="text-sm text-zinc-600 dark:text-zinc-400">Pressemitteilungen</div>
</div>
<div>
<div
class="text-3xl font-bold bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] bg-clip-text text-transparent mb-2">
850+
</div>
<div class="text-sm text-zinc-600 dark:text-zinc-400">Neue pro Monat</div>
</div>
<div>
<div
class="text-3xl font-bold bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] bg-clip-text text-transparent mb-2">
2,5 Mio.
</div>
<div class="text-sm text-zinc-600 dark:text-zinc-400">Monatliche Leser</div>
</div>
</div>
</div>
</div>
</section>
<livewire:web.footer />
</main>
@endsection
@push('styles')
<style>
:root {
--color-primary: {{ $domainConfig['color_scheme']['primary'] ?? '#cf3628' }};
--color-secondary: {{ $domainConfig['color_scheme']['secondary'] ?? '#f0834a' }};
}
[x-cloak] {
display: none !important;
}
/* Heroicon Farbfilter für Kategorien */
.text-blue-500 img {
filter: invert(47%) sepia(96%) saturate(1632%) hue-rotate(201deg) brightness(99%) contrast(101%);
}
.text-purple-500 img {
filter: invert(42%) sepia(95%) saturate(2297%) hue-rotate(251deg) brightness(99%) contrast(101%);
}
.text-green-500 img {
filter: invert(64%) sepia(86%) saturate(475%) hue-rotate(77deg) brightness(95%) contrast(95%);
}
.text-yellow-600 img {
filter: invert(71%) sepia(90%) saturate(1207%) hue-rotate(1deg) brightness(96%) contrast(91%);
}
.text-red-500 img {
filter: invert(37%) sepia(93%) saturate(3026%) hue-rotate(346deg) brightness(99%) contrast(94%);
}
.text-indigo-500 img {
filter: invert(40%) sepia(100%) saturate(1736%) hue-rotate(231deg) brightness(102%) contrast(101%);
}
.text-orange-500 img {
filter: invert(60%) sepia(99%) saturate(1515%) hue-rotate(358deg) brightness(100%) contrast(101%);
}
.text-cyan-500 img {
filter: invert(71%) sepia(69%) saturate(2356%) hue-rotate(143deg) brightness(91%) contrast(101%);
}
.text-pink-500 img {
filter: invert(52%) sepia(68%) saturate(2952%) hue-rotate(309deg) brightness(104%) contrast(101%);
}
.text-teal-500 img {
filter: invert(55%) sepia(82%) saturate(432%) hue-rotate(124deg) brightness(92%) contrast(92%);
}
.text-lime-500 img {
filter: invert(82%) sepia(35%) saturate(1283%) hue-rotate(28deg) brightness(99%) contrast(95%);
}
.text-violet-500 img {
filter: invert(39%) sepia(86%) saturate(3028%) hue-rotate(254deg) brightness(98%) contrast(101%);
}
</style>
@endpush

View file

@ -25,6 +25,11 @@
{{-- Kanonische URL --}}
<link rel="canonical" href="{{ $canonicalUrl }}">
{{-- Sprach-Alternates (de/en) pro Seite optional gesetzt --}}
@hasSection('hreflang')
@yield('hreflang')
@endif
{{-- OpenGraph / Social --}}
<meta property="og:type" content="{{ $ogType }}">
<meta property="og:site_name" content="{{ $siteName }}">

View file

@ -7,8 +7,14 @@
@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.')
@section('hreflang')
<link rel="alternate" hreflang="de" href="{{ url('/de') }}">
<link rel="alternate" hreflang="en" href="{{ url('/en') }}">
<link rel="alternate" hreflang="x-default" href="{{ url('/de') }}">
@endsection
@section('content')
<x-web.site-header />
<x-web.site-header :ticker="$tickerItems ?? []" />
<main class="bg-bg text-ink">
<x-web.focus-hero
@ -17,7 +23,7 @@
<section class="max-w-layout mx-auto px-8 mt-16">
<div class="grid gap-9 grid-cols-1 lg:grid-cols-[1.7fr_1fr]">
<livewire:web.press-release-feed :portal="\App\Enums\Portal::Presseecho->value" />
<livewire:web.press-release-feed :portal="\App\Enums\Portal::Presseecho->value" :language="app()->getLocale()" />
<aside class="flex flex-col gap-9">
<x-web.most-read :releases="$mostReadReleases ?? []" />
@ -27,9 +33,10 @@
</div>
</section>
<x-web.industry-spotlight />
<x-web.events-week />
<x-web.industry-spotlight
:industry="$spotlight['industry'] ?? null"
:stats="$spotlight['stats'] ?? null"
:releases="$spotlight['releases'] ?? []" />
<x-web.industry-index :industries="($industryIndex ?? collect())->isNotEmpty() ? $industryIndex->toArray() : null" />

View file

@ -1,342 +1,430 @@
@extends('web.layouts.web-master')
@section('title', 'KI-Revolution in Deutschland - Business Portal 24')
@section('meta_description', 'Neue Studie zeigt massive Investitionen deutscher Unternehmen in künstliche Intelligenz für 2025.')
@php
use Illuminate\Support\Str;
\Carbon\Carbon::setLocale('de');
$company = $release->company;
$categoryTranslation = $release->category?->translations?->firstWhere('locale', 'de')
?? $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')
?? $release->category?->parent?->translations?->first();
$parentName = $parentTranslation?->name;
$parentSlug = $parentTranslation?->slug;
$parentHref = $parentSlug ? route('kategorie', ['slug' => $parentSlug]) : route('web.home');
$publishedAt = $release->published_at;
$embargoAt = $release->embargoAtLocal();
$bodyHtml = $release->renderedText();
$wordCount = str_word_count(strip_tags((string) $release->text));
$readTime = max(1, (int) round($wordCount / 200));
$teaser = (string) Str::of(strip_tags((string) $release->text))->squish()->limit(180);
$lead = filled($release->subtitle)
? $release->subtitle
: $teaser;
$image = $release->images->first();
$imageUrl = $image?->variantUrl('detail') ?? $image?->variantUrl('card') ?? $image?->url();
$imageCaption = $image?->description ?: $image?->title;
$imageCredit = $image?->copyright ?: $image?->author;
$companyName = $company?->name;
$companyInitial = $companyName ? mb_strtoupper(mb_substr($companyName, 0, 1)) : '·';
$companyHref = $company?->slug ? route('newsrooms').'#'.$company->slug : route('newsrooms');
$countryNames = ['DE' => 'Deutschland', 'AT' => 'Österreich', 'CH' => 'Schweiz'];
$companyCountry = $company?->country_code ? ($countryNames[strtoupper($company->country_code)] ?? null) : null;
$boilerplate = filled($release->boilerplate_override) ? $release->boilerplate_override : $company?->boilerplate;
$keywords = collect(preg_split('/[,;\n]+/', (string) $release->keywords))
->map(fn ($keyword) => trim($keyword))
->filter()
->unique()
->take(8);
$contact = $release->contacts->first();
$contactName = $contact ? trim(implode(' ', array_filter([$contact->title, $contact->first_name, $contact->last_name]))) : null;
$publisherVerified = (bool) ($company?->is_active && filled($company?->website));
$isRecommended = $release->content_score !== null && $release->content_score >= 80;
$shareUrl = route('release.detail', ['slug' => $release->slug]);
$brandLabel = config('app.theme') === 'presseecho' ? 'presseecho' : 'businessportal24';
@endphp
@section('title', $release->title.' '.$brandLabel)
@section('meta_description', $lead)
@section('og_type', 'article')
@section('canonical', route('release.detail', ['slug' => $releaseSlug]))
@if ($imageUrl)
@section('og_image', $imageUrl)
@endif
@section('canonical', $shareUrl)
@section('content')
{{-- Lese-Fortschrittsbalken --}}
<div class="fixed top-0 left-0 right-0 h-0.5 bg-bg-rule z-[60]"
x-data="{ progress: 0 }"
x-init="const update = () => { const h = document.body.scrollHeight - window.innerHeight; progress = h > 0 ? Math.min(100, (window.scrollY / h) * 100) : 0; }; update(); window.addEventListener('scroll', update, { passive: true }); window.addEventListener('resize', update);"
aria-hidden="true">
<div class="h-full bg-brand transition-[width] duration-150 ease-out" :style="`width:${progress}%`"></div>
</div>
<main class="min-h-screen flex flex-col bg-white dark:bg-zinc-950 transition-colors duration-200">
<!-- Burger Menu Component -->
<livewire:web.burger-menu />
<x-web.site-header />
<!-- Header Component -->
<livewire:web.header />
<!-- Article Content -->
<article class="container mx-auto px-4 py-8 max-w-4xl">
<!-- Breadcrumbs -->
<nav class="text-sm text-zinc-600 dark:text-zinc-400 mb-6">
<a href="/" class="hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors">Start</a>
<span class="mx-2"></span>
<a href="/kategorie/it" class="hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors">IT & Software</a>
<span class="mx-2"></span>
<span class="text-zinc-900 dark:text-zinc-100">KI-Revolution in Deutschland</span>
</nav>
<!-- Title -->
<h1 class="text-3xl md:text-4xl font-bold text-zinc-900 dark:text-zinc-100 mb-4 leading-tight">
KI-Revolution: Deutsche Unternehmen investieren Milliarden in Automatisierung
</h1>
<!-- Subtitle -->
<p class="text-xl text-zinc-600 dark:text-zinc-400 mb-6">
Neue Studie zeigt massive Investitionen in künstliche Intelligenz für 2025
</p>
<!-- Meta Info -->
<div class="flex flex-wrap items-center gap-4 mb-8 pb-8 border-b border-zinc-200 dark:border-zinc-800">
<div class="flex items-center gap-2 text-sm text-zinc-900 dark:text-zinc-100">
<svg class="h-4 w-4 text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span class="font-medium">Berlin</span>
</div>
<div class="flex items-center gap-2 text-sm text-zinc-900 dark:text-zinc-100">
<svg class="h-4 w-4 text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
<span>6. Oktober 2025</span>
</div>
<div class="flex items-center gap-2 text-sm text-zinc-900 dark:text-zinc-100">
<svg class="h-4 w-4 text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>14:30 Uhr</span>
</div>
<div class="flex items-center gap-2 text-sm text-zinc-600 dark:text-zinc-400">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>5 Min. Lesezeit</span>
</div>
<main class="bg-bg text-ink">
<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>
<span aria-hidden="true">/</span>
@if ($parentName)
<a href="{{ $parentHref }}" class="cursor-pointer hover:text-ink transition-colors">{{ $parentName }}</a>
<span aria-hidden="true">/</span>
@endif
@if ($categoryName)
<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>
</nav>
</div>
<!-- Key Facts Box - Desktop Side -->
<div class="lg:float-right lg:ml-8 lg:w-80 mb-8">
<div class="bg-gradient-to-br from-[var(--color-primary)]/5 to-[var(--color-secondary)]/5 border border-[var(--color-primary)]/20 dark:border-[var(--color-primary)]/30 rounded-xl p-6 transition-colors duration-200">
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100 mb-4 flex items-center gap-2">
<span class="w-1 h-5 bg-gradient-to-b from-[var(--color-primary)] to-[var(--color-secondary)] rounded-full"></span>
Key Facts
</h3>
<ul class="space-y-3 text-sm text-zinc-900 dark:text-zinc-100">
<li class="flex items-start gap-2">
<span class="text-[var(--color-primary)] mt-0.5"></span>
<span>15 Milliarden Euro Investitionsvolumen geplant</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[var(--color-primary)] mt-0.5"></span>
<span>78% der Unternehmen setzen auf KI-Automatisierung</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[var(--color-primary)] mt-0.5"></span>
<span>Produktivitätssteigerung um durchschnittlich 35%</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[var(--color-primary)] mt-0.5"></span>
<span>Neue Arbeitsplätze im KI-Sektor erwartet</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[var(--color-primary)] mt-0.5"></span>
<span>Fokus auf ethische KI-Entwicklung</span>
</li>
</ul>
</div>
</div>
<!-- Article Content -->
<div class="prose prose-lg max-w-none">
<p class="text-zinc-900 dark:text-zinc-100 leading-relaxed mb-6">
<strong>Berlin</strong> Eine umfassende Studie des TechVision Analytics Instituts zeigt einen beispiellosen Wandel in der deutschen Unternehmenslandschaft: Für das Jahr 2025 planen Unternehmen im DACH-Raum Investitionen in Höhe von über 15 Milliarden Euro in KI-gestützte Automatisierungslösungen.
</p>
<h2 class="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mt-8 mb-4">
Paradigmenwechsel in der Digitalisierung
</h2>
<p class="text-zinc-900 dark:text-zinc-100 leading-relaxed mb-6">
Die Ergebnisse der Studie, für die über 1.200 Unternehmen verschiedener Größen und Branchen befragt wurden, zeichnen ein klares Bild: Künstliche Intelligenz hat sich vom Experimentierfeld zur strategischen Priorität entwickelt. 78% der befragten Unternehmen geben an, dass KI-gestützte Automatisierung fester Bestandteil ihrer Digitalisierungsstrategie ist.
</p>
<blockquote class="border-l-4 border-[var(--color-primary)] pl-6 my-8 italic text-lg text-zinc-600 dark:text-zinc-400">
"Wir erleben einen fundamentalen Wandel. KI ist nicht länger eine Zukunftsvision, sondern wird zur Grundlage moderner Geschäftsmodelle. Die Investitionen der kommenden Jahre werden die Wettbewerbsfähigkeit für die nächste Dekade bestimmen."
<footer class="text-sm font-medium text-zinc-900 dark:text-zinc-100 mt-2 not-italic">
Dr. Sarah Müller, Geschäftsführerin TechVision Analytics
</footer>
</blockquote>
<h2 class="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mt-8 mb-4">
Schwerpunkte der Investitionen
</h2>
<p class="text-zinc-900 dark:text-zinc-100 leading-relaxed mb-4">
Die geplanten Investitionen konzentrieren sich auf mehrere Kernbereiche:
</p>
<ul class="list-disc pl-6 space-y-2 mb-6 text-zinc-900 dark:text-zinc-100">
<li>Prozessautomatisierung und intelligente Workflow-Systeme</li>
<li>Predictive Analytics und datengesteuerte Entscheidungsfindung</li>
<li>Kundenservice-Automatisierung durch Chatbots und virtuelle Assistenten</li>
<li>Qualitätskontrolle mittels Computer Vision</li>
<li>Personalisierte Marketing- und Vertriebsautomatisierung</li>
</ul>
<h2 class="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mt-8 mb-4">
Produktivitätssteigerung als Hauptmotivation
</h2>
<p class="text-zinc-900 dark:text-zinc-100 leading-relaxed mb-6">
Die Studie zeigt, dass Unternehmen, die bereits KI-Lösungen implementiert haben, eine durchschnittliche Produktivitätssteigerung von 35% verzeichnen. Gleichzeitig berichten 68% der Befragten von einer verbesserten Mitarbeiterzufriedenheit, da repetitive Aufgaben automatisiert werden und mehr Zeit für kreative und strategische Tätigkeiten bleibt.
</p>
</div>
<!-- Media Section -->
<div class="mt-12 mb-8">
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-4">Downloads & Medien</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-4 hover:shadow-lg transition-all cursor-pointer">
<div class="flex items-center gap-3">
<div class="w-12 h-12 bg-[var(--color-primary)]/10 rounded-lg flex items-center justify-center">
<svg class="h-6 w-6 text-[var(--color-primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</div>
<div class="flex-1">
<p class="font-medium text-zinc-900 dark:text-zinc-100">Vollständige Studie (PDF)</p>
<p class="text-sm text-zinc-600 dark:text-zinc-400">2.4 MB</p>
</div>
{{-- Hero Meta Block --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8">
<div class="pt-[18px] pb-8 border-b border-bg-rule">
@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
</span>
<span class="text-[11px] text-ink-3 font-mono">{{ $embargoAt->translatedFormat('j. F Y, H:i') }}</span>
</div>
@endif
<div class="flex items-center gap-3.5 mb-4 flex-wrap">
@if ($categoryName)
<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>
</div>
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-4 hover:shadow-lg transition-all cursor-pointer">
<div class="flex items-center gap-3">
<div class="w-12 h-12 bg-[var(--color-secondary)]/10 rounded-lg flex items-center justify-center">
<svg class="h-6 w-6 text-[var(--color-secondary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
<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;">
{{ $release->title }}
</h1>
@if (filled($lead))
<p class="font-serif text-[18px] sm:text-[21px] leading-[1.45] m-0 mb-7 text-ink-2 max-w-[880px] font-normal" style="text-wrap:pretty;">
{{ $lead }}
</p>
@endif
<div class="flex flex-wrap items-center text-[12.5px] text-ink-2" style="gap:10px 22px;">
@if ($companyName)
<span class="inline-flex items-center gap-2">
<span class="w-7 h-7 bg-black text-white inline-flex items-center justify-center text-[13px] font-bold font-serif">{{ $companyInitial }}</span>
<span class="font-semibold">{{ $companyName }}</span>
</span>
<span class="w-px h-[14px] bg-bg-rule-strong" aria-hidden="true"></span>
@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
</time>
@endif
<span class="font-mono text-[11.5px] text-ink-3">{{ $readTime }} Min. Lesezeit · {{ number_format($wordCount, 0, ',', '.') }} Wörter</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>
{{ $companyCountry }}
</span>
@endif
@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.">
<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>
</div>
<div class="flex-1">
<p class="font-medium text-zinc-900 dark:text-zinc-100">Infografiken (ZIP)</p>
<p class="text-sm text-zinc-600 dark:text-zinc-400">8.7 MB</p>
</div>
</div>
Redaktionsempfehlung
</span>
@endif
</div>
</div>
</div>
</section>
<!-- Contact Box -->
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6 mb-8 transition-colors duration-200">
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-4">Pressekontakt</h3>
<div class="space-y-4">
{{-- Hero Image --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-8">
<div class="relative flex items-end p-[18px] bg-bg-dark hatch-dark overflow-hidden" style="aspect-ratio:21/9;">
@if ($imageUrl)
<img src="{{ $imageUrl }}" alt="{{ $imageCaption ?: $release->title }}"
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)
</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
</div>
@endif
</section>
{{-- Body + Sidebar --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-10 grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-10 lg:gap-14">
{{-- Body --}}
<div>
<div class="pm-body max-w-[680px]">
{!! $bodyHtml !!}
</div>
@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>
<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="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">
<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" />
</svg>
</span>
@endif
</div>
@if ($contact->responsibility || $companyName)
<div class="text-[12.5px] text-ink-3 mb-3.5 leading-[1.5]">
@if ($contact->responsibility){{ $contact->responsibility }}<br />@endif
{{ $companyName }}
</div>
@endif
<div class="flex flex-col gap-2 text-[12.5px] font-mono">
@if ($company?->address)
<span class="flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><path d="M7 1a5 5 0 015 5c0 4-5 7-5 7s-5-3-5-7a5 5 0 015-5zm0 3.5a1.5 1.5 0 100 3 1.5 1.5 0 000-3z" fill="currentColor" /></svg>
<span class="text-ink-2">{{ $company->address }}</span>
</span>
@endif
@if ($contact->phone)
<span class="flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><path d="M2 3l3-1 1.5 3-1.5 1c.5 2 2 3.5 4 4l1-1.5L13 10l-1 3c-5 0-10-5-10-10z" fill="currentColor" /></svg>
<a href="tel:{{ preg_replace('/[^0-9+]/', '', $contact->phone) }}" class="text-ink-2 hover:text-ink transition-colors">{{ $contact->phone }}</a>
</span>
@endif
@if ($contact->email)
<span class="flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><path d="M1 3h12v8H1zM1 3l6 4 6-4" stroke="currentColor" stroke-width="1.2" fill="none" /></svg>
<a href="mailto:{{ $contact->email }}" class="text-ink underline underline-offset-2 hover:text-brand transition-colors">{{ $contact->email }}</a>
</span>
@endif
@if ($company?->website)
<span class="flex items-center gap-2">
<svg width="14" height="14" 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.2" fill="none" /><path d="M1.5 7h11M7 1.5c2 1.5 2 9.5 0 11M7 1.5c-2 1.5-2 9.5 0 11" stroke="currentColor" stroke-width="1" fill="none" /></svg>
<a href="{{ Str::startsWith($company->website, ['http://', 'https://']) ? $company->website : 'https://'.$company->website }}" rel="nofollow noopener" target="_blank" class="text-ink underline underline-offset-2 hover:text-brand transition-colors">{{ Str::of($company->website)->after('//')->rtrim('/') }}</a>
</span>
@endif
</div>
</div>
@endif
</div>
{{-- Sidebar --}}
<aside class="flex flex-col gap-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="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">
<strong class="block font-serif text-[14px] font-semibold leading-[1.25] tracking-[-0.1px] mb-0.5">{{ $companyName }}</strong>
@if ($companyCountry)
<div class="text-[10.5px] text-ink-4 mt-1 font-mono">{{ $companyCountry }}</div>
@endif
</div>
</div>
@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">
<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>
</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>
</div>
</div>
@endif
{{-- Teilen --}}
<div x-data="{
url: @js($shareUrl),
title: @js($release->title),
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="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>
LinkedIn
</a>
<a :href="`https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`" 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="M2 2l5.5 7.2L2 14h1.6l4.7-3.7L11.5 14H14L8.2 6.4 13.6 2H12L7.5 5.5 4.5 2H2z" fill="currentColor" /></svg>
X
</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
</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>
</button>
</div>
</div>
{{-- Schlagwörter --}}
@if ($keywords->isNotEmpty())
<div>
<div class="eyebrow muted text-[9.5px] mb-2.5">Schlagwörter</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>
@endforeach
</div>
</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>
{{-- Weiterführend --}}
<div>
<p class="font-medium text-zinc-900 dark:text-zinc-100">TechVision Analytics GmbH</p>
<p class="text-sm text-zinc-600 dark:text-zinc-400">Ansprechpartnerin: Dr. Sarah Müller</p>
</div>
<div class="space-y-2">
<div class="flex items-center gap-2 text-sm">
<svg class="h-4 w-4 text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
<a href="mailto:presse@techvision.de" class="text-[var(--color-primary)] hover:underline">
presse@techvision.de
<div class="eyebrow muted text-[9.5px] mb-2.5">Weiterführend</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>
</div>
<div class="flex items-center gap-2 text-sm text-zinc-900 dark:text-zinc-100">
<svg class="h-4 w-4 text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
</svg>
<a href="tel:+493012345678" class="hover:text-[var(--color-primary)] transition-colors">
+49 30 1234 5678
<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">
<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>
<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' => '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>
<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>
<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>
</div>
<a href="#" class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-lg transition-all">
vCard herunterladen
</a>
</div>
</div>
</aside>
</section>
<!-- Company Teaser -->
<div class="bg-zinc-50/50 dark:bg-zinc-900/50 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6 mb-8 transition-colors duration-200">
<div class="flex items-start gap-4">
<div class="w-16 h-16 bg-white dark:bg-zinc-800 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="h-8 w-8 text-zinc-500 dark: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>
</div>
<div class="flex-1">
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100 mb-2">TechVision Analytics</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
TechVision Analytics ist ein führendes Forschungsinstitut für Technologie- und Marktanalysen mit Sitz in Berlin. Seit 2015 beraten wir Unternehmen bei strategischen Technologie-Entscheidungen.
</p>
<a href="#" class="inline-flex items-center text-[var(--color-primary)] hover:text-[var(--color-secondary)] transition-colors text-sm font-medium">
Zum Newsroom
</a>
</div>
</div>
</div>
{{-- Empfehlungen --}}
@if ($companyReleases->isNotEmpty() || $relatedReleases->isNotEmpty())
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-14">
<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>
<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>
</div>
@endif
<!-- Utility Bar -->
<div class="flex items-center gap-3 py-6 border-y border-zinc-200 dark:border-zinc-800">
<a href="#" class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-lg transition-all">
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path>
</svg>
Teilen
</a>
<a href="#" class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-lg transition-all">
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"></path>
</svg>
Drucken
</a>
<a href="#" class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors ml-auto">
<svg class="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9"></path>
</svg>
Melden
</a>
</div>
@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>
<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>
@endif
</div>
@endif
</div>
</section>
@endif
</article>
<!-- Related Articles -->
<section class="bg-zinc-50/50 dark:bg-zinc-900/50 py-12 mt-8 transition-colors duration-200">
<div class="container mx-auto px-4 max-w-6xl">
<h2 class="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-6">Ähnliche Meldungen</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Related Article 1 -->
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-5 hover:shadow-lg transition-all">
<div class="flex items-center gap-2 mb-3">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
IT & Software
</span>
</div>
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100 mb-2 line-clamp-2">
Cloud-Migration: 78% der deutschen Unternehmen setzen auf Hybrid-Cloud
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-3 line-clamp-2">
Neue Umfrage zeigt: Hybrid-Cloud wird zum Standard für maximale Flexibilität und Sicherheit.
</p>
<a href="#" class="inline-flex items-center text-sm text-[var(--color-primary)] hover:text-[var(--color-secondary)] transition-colors">
Lesen
</a>
</div>
<!-- Related Article 2 -->
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-5 hover:shadow-lg transition-all">
<div class="flex items-center gap-2 mb-3">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
IT & Software
</span>
</div>
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100 mb-2 line-clamp-2">
Cybersecurity: Unternehmen verstärken Schutzmaßnahmen gegen Hackerangriffe
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-3 line-clamp-2">
Investitionen in IT-Sicherheit steigen um durchschnittlich 35%. Neue Standards werden eingeführt.
</p>
<a href="#" class="inline-flex items-center text-sm text-[var(--color-primary)] hover:text-[var(--color-secondary)] transition-colors">
Lesen
</a>
</div>
<!-- Related Article 3 -->
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-5 hover:shadow-lg transition-all">
<div class="flex items-center gap-2 mb-3">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-[var(--color-primary)]/10 text-[var(--color-primary)]">
IT & Software
</span>
</div>
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100 mb-2 line-clamp-2">
5G-Rollout beschleunigt: Netzabdeckung erreicht 85% in Deutschland
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-3 line-clamp-2">
Mobilfunkanbieter berichten von schnellerem Ausbau. IoT-Anwendungen profitieren enorm.
</p>
<a href="#" class="inline-flex items-center text-sm text-[var(--color-primary)] hover:text-[var(--color-secondary)] transition-colors">
Lesen
</a>
</div>
</div>
</div>
</section>
<!-- Footer Component -->
<livewire:web.footer />
</main>
<x-web.site-footer />
@endsection
@push('styles')
<style>
/* Domain-spezifische Color-Variablen für Inline-Styles */
:root {
--color-primary: {{ $domainConfig['color_scheme']['primary'] ?? '#cf3628' }};
--color-secondary: {{ $domainConfig['color_scheme']['secondary'] ?? '#f0834a' }};
}
/* Alpine.js Cloak */
[x-cloak] {
display: none !important;
}
/* Line clamp utilities */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
@endpush

View file

@ -1,268 +1,59 @@
@extends('web.layouts.web-master')
@section('title', 'Suche - Business Portal 24')
@php
$brandConfig = config('domains.domains.'.config('app.theme').'.brand', []);
$brandName = $brandConfig['name'] ?? 'businessportal';
$brandAccent = $brandConfig['accent'] ?? '24';
@endphp
@section('content')
<main class="min-h-screen flex flex-col bg-white dark:bg-zinc-950 transition-colors duration-200">
<livewire:web.burger-menu />
<livewire:web.header />
<!-- Hero Section with Search -->
<section class="relative overflow-hidden text-white py-20 animate-fade-in"
style="background: var(--gradient-hero);">
<div class="container mx-auto px-4">
<div class="max-w-3xl mx-auto text-center">
<h1 class="text-4xl md:text-5xl font-bold mb-6 animate-fade-in-up">
Pressemitteilungen durchsuchen
</h1>
<p class="text-xl text-white/90 mb-8 animate-fade-in-up animation-delay-200">
Durchsuchen Sie über 10.000 Pressemitteilungen aus allen Branchen
</p>
<!-- Search Form -->
<form class="animate-fade-in-up animation-delay-300" x-data="{ query: '' }">
<div class="relative">
<svg class="absolute left-6 top-1/2 -translate-y-1/2 h-6 w-6 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<input
type="search"
x-model="query"
placeholder="Suchbegriff eingeben, z.B. 'Künstliche Intelligenz' oder 'Produktlaunch'..."
class="w-full pl-16 pr-6 py-5 rounded-xl border-2 border-white/20 bg-white/10 backdrop-blur-sm text-white placeholder:text-white/70 text-lg focus:outline-none focus:ring-4 focus:ring-white/30 focus:border-white/40 transition-all"
/>
<button
type="submit"
class="absolute right-2 top-1/2 -translate-y-1/2 px-8 py-3 bg-white text-[var(--color-primary)] font-medium rounded-lg hover:bg-zinc-100 transition-all shadow-lg"
>
Suchen
</button>
</div>
</form>
</div>
</div>
</section>
<!-- Search Filters Section -->
<section class="py-8 bg-white dark:bg-zinc-950 border-b border-zinc-200 dark:border-zinc-800 transition-colors duration-200">
<div class="container mx-auto px-4">
<div class="max-w-6xl mx-auto">
<div class="flex flex-wrap gap-3">
<button class="px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg text-sm font-medium transition-all">
Alle Kategorien
</button>
<button class="px-4 py-2 bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-lg text-sm font-medium transition-all">
Wirtschaft
</button>
<button class="px-4 py-2 bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-lg text-sm font-medium transition-all">
Technologie
</button>
<button class="px-4 py-2 bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-lg text-sm font-medium transition-all">
Gesundheit
</button>
<button class="px-4 py-2 bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-lg text-sm font-medium transition-all">
Mehr...
</button>
</div>
</div>
</div>
</section>
<!-- Search Results / Placeholder Section -->
<section class="py-16 bg-white dark:bg-zinc-950 transition-colors duration-200">
<div class="container mx-auto px-4">
<div class="max-w-6xl mx-auto">
<!-- No Search Yet State -->
<div class="text-center py-16">
<div class="w-24 h-24 bg-gradient-to-br from-[var(--color-primary)]/10 to-[var(--color-secondary)]/10 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="h-12 w-12 text-[var(--color-primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<h2 class="text-2xl font-bold text-zinc-900 dark:text-zinc-100 mb-4">
Geben Sie einen Suchbegriff ein
</h2>
<p class="text-zinc-600 dark:text-zinc-400 mb-8 max-w-lg mx-auto">
Durchsuchen Sie unsere Datenbank mit über 10.000 Pressemitteilungen nach Stichworten, Unternehmen oder Themen
</p>
<!-- Popular Searches -->
<div class="max-w-2xl mx-auto">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100 mb-4">Beliebte Suchen:</h3>
<div class="flex flex-wrap gap-2 justify-center">
<a href="#" class="px-4 py-2 bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full text-sm transition-all">
KI & Machine Learning
</a>
<a href="#" class="px-4 py-2 bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full text-sm transition-all">
Nachhaltigkeit
</a>
<a href="#" class="px-4 py-2 bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full text-sm transition-all">
E-Mobilität
</a>
<a href="#" class="px-4 py-2 bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full text-sm transition-all">
Digitalisierung
</a>
<a href="#" class="px-4 py-2 bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full text-sm transition-all">
Start-ups
</a>
<a href="#" class="px-4 py-2 bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full text-sm transition-all">
Fusionen & Übernahmen
</a>
<a href="#" class="px-4 py-2 bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full text-sm transition-all">
Produktneuheiten
</a>
<a href="#" class="px-4 py-2 bg-zinc-100 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full text-sm transition-all">
Personalmeldungen
</a>
</div>
</div>
</div>
<!-- Example Results (Hidden by default, shown when search is active) -->
<div class="hidden">
<div class="flex justify-between items-center mb-8">
<div>
<h2 class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">Suchergebnisse</h2>
<p class="text-zinc-600 dark:text-zinc-400 mt-1">127 Ergebnisse für "Künstliche Intelligenz"</p>
</div>
<div class="flex gap-2">
<select class="px-4 py-2 border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 rounded-lg text-sm">
<option>Neueste zuerst</option>
<option>Älteste zuerst</option>
<option>Relevanz</option>
</select>
</div>
</div>
<!-- Sample Result Items -->
<div class="space-y-6">
<!-- Result 1 -->
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6 hover:shadow-lg transition-all">
<div class="flex gap-4">
<div class="flex-shrink-0 w-32 h-32 bg-zinc-200 dark:bg-zinc-800 rounded-lg"></div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 text-xs rounded-full">Technologie</span>
<span class="text-sm text-zinc-500 dark:text-zinc-500">15. Januar 2025</span>
</div>
<h3 class="text-xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2 hover:text-[var(--color-primary)] transition-colors">
<a href="#">KI-Revolution: Neues Framework für maschinelles Lernen vorgestellt</a>
</h3>
<p class="text-zinc-600 dark:text-zinc-400 mb-3">
Das innovative Unternehmen TechCorp präsentiert ein bahnbrechendes Framework für maschinelles Lernen, das die Entwicklung von KI-Anwendungen um 70% beschleunigt...
</p>
<div class="flex items-center gap-4 text-sm text-zinc-500 dark:text-zinc-500">
<span>TechCorp AG</span>
<span></span>
<span>2 Min. Lesezeit</span>
</div>
</div>
</div>
</div>
<!-- Result 2 -->
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6 hover:shadow-lg transition-all">
<div class="flex gap-4">
<div class="flex-shrink-0 w-32 h-32 bg-zinc-200 dark:bg-zinc-800 rounded-lg"></div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="px-2 py-1 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 text-xs rounded-full">Gesundheit</span>
<span class="text-sm text-zinc-500 dark:text-zinc-500">14. Januar 2025</span>
</div>
<h3 class="text-xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2 hover:text-[var(--color-primary)] transition-colors">
<a href="#">KI-gestützte Diagnostik erreicht 98% Genauigkeit</a>
</h3>
<p class="text-zinc-600 dark:text-zinc-400 mb-3">
Medizinisches Start-up entwickelt KI-System zur Früherkennung von Krankheiten mit beeindruckender Präzision. Klinische Studien zeigen vielversprechende Ergebnisse...
</p>
<div class="flex items-center gap-4 text-sm text-zinc-500 dark:text-zinc-500">
<span>MediAI Solutions</span>
<span></span>
<span>3 Min. Lesezeit</span>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div class="mt-12 flex justify-center gap-2">
<button class="px-4 py-2 border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-all">
Zurück
</button>
<button class="px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg">1</button>
<button class="px-4 py-2 border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-all">2</button>
<button class="px-4 py-2 border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-all">3</button>
<button class="px-4 py-2 border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-zinc-300 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-all">
Weiter
</button>
</div>
</div>
</div>
</div>
</section>
<!-- Search Tips Section -->
<section class="py-16 bg-zinc-50/50 dark:bg-zinc-900/50 transition-colors duration-200">
<div class="container mx-auto px-4">
<div class="max-w-4xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-4">
Such-Tipps
</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6">
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100 mb-3">Erweiterte Suche</h3>
<ul class="space-y-2 text-sm text-zinc-600 dark:text-zinc-400">
<li class="flex items-start gap-2">
<span class="text-[var(--color-primary)] font-mono">"Begriff"</span>
<span>- Exakte Suche</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[var(--color-primary)] font-mono">A OR B</span>
<span>- Entweder A oder B</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[var(--color-primary)] font-mono">A AND B</span>
<span>- Sowohl A als auch B</span>
</li>
<li class="flex items-start gap-2">
<span class="text-[var(--color-primary)] font-mono">-Begriff</span>
<span>- Begriff ausschließen</span>
</li>
</ul>
</div>
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6">
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100 mb-3">Filter nutzen</h3>
<ul class="space-y-2 text-sm text-zinc-600 dark:text-zinc-400">
<li> Nach Kategorie filtern</li>
<li> Zeitraum eingrenzen</li>
<li> Nach Unternehmen suchen</li>
<li> Relevanz oder Datum sortieren</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<livewire:web.footer />
</main>
@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('hreflang')
<link rel="alternate" hreflang="de" href="{{ url('/de/suche') }}">
<link rel="alternate" hreflang="en" href="{{ url('/en/suche') }}">
<link rel="alternate" hreflang="x-default" href="{{ url('/de/suche') }}">
@endsection
@push('styles')
<style>
:root {
--color-primary: {{ $domainConfig['color_scheme']['primary'] ?? '#cf3628' }};
--color-secondary: {{ $domainConfig['color_scheme']['secondary'] ?? '#f0834a' }};
}
[x-cloak] { display: none !important; }
</style>
@endpush
@section('content')
<x-web.site-header />
<main class="bg-bg text-ink">
{{-- 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">{{ $brandName }}{{ $brandAccent }}</a>
<span class="text-ink-4" aria-hidden="true"></span>
<span class="text-ink font-semibold">Suche</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>
<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
</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.
</p>
</div>
<hr class="rule-strong mt-8">
</section>
{{-- Suche + Sidebar --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-16 grid gap-9 lg:grid-cols-[1.7fr_1fr]">
<div>
<livewire:web.search :portal="$portal" :language="$language" />
</div>
<aside class="flex flex-col gap-9">
<x-web.most-read :releases="$mostReadReleases ?? []" />
<x-web.publisher-cta />
</aside>
</section>
</main>
<x-web.site-footer />
@endsection

View file

@ -1,355 +1,434 @@
@extends('web.layouts.web-master')
@section('title', 'Pressemitteilung veröffentlichen - Business Portal 24')
@php
$brandConfig = config('domains.domains.'.config('app.theme').'.brand', []);
$brandName = $brandConfig['name'] ?? 'businessportal';
$brandAccent = $brandConfig['accent'] ?? '24';
$publisherUrl = rtrim((string) config('domains.domain_portal_url', config('app.url')), '/');
@section('content')
$sinceYear = $oldestYear ?? 2012;
$archiveLabel = ($archiveTotal ?? 0) > 0 ? number_format((int) $archiveTotal, 0, ',', '.').'+' : '100.000+';
$newsroomNames = $activeNewsroomNames ?? [];
$newsroomTotalLabel = max(0, (int) ($activeNewsroomsTotal ?? 0) - count($newsroomNames));
<main class="min-h-screen flex flex-col bg-white dark:bg-zinc-950 transition-colors duration-200">
<livewire:web.burger-menu />
<livewire:web.header />
$example = $exampleRelease ?? null;
$exampleCategory = $example?->category?->translations?->first()?->name;
$exampleCompany = $example?->company?->name;
$examplePublishedAt = $example?->published_at;
$exampleReadMinutes = $example
? max(1, (int) ceil(str_word_count(strip_tags((string) $example->text)) / 200))
: 3;
@endphp
<!-- Hero Section -->
<section class="relative overflow-hidden text-white py-20 animate-fade-in"
style="background: var(--gradient-hero);">
<div class="container mx-auto px-4">
<div class="max-w-3xl mx-auto text-center">
<h1 class="text-4xl md:text-5xl font-bold mb-6 animate-fade-in-up">
Pressemitteilung veröffentlichen
</h1>
<p class="text-xl text-white/90 leading-relaxed animate-fade-in-up animation-delay-200">
In nur 5 Schritten zur professionellen Presseveröffentlichung
</p>
</div>
</div>
</section>
<!-- Quick Access Section -->
<section class="py-16 bg-white dark:bg-zinc-950 transition-colors duration-200">
<div class="container mx-auto px-4">
<div class="max-w-4xl mx-auto">
<div class="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] rounded-xl p-8 text-center text-white mb-16">
<h2 class="text-2xl font-bold mb-4">Bereit zum Veröffentlichen?</h2>
<p class="text-white/90 mb-6">
Melden Sie sich im Backend-Portal an, um Ihre Pressemitteilung zu erstellen
</p>
<div class="flex flex-wrap gap-4 justify-center">
<a href="{{ config('domains.domain_portal_url') }}/login" class="inline-flex items-center justify-center px-8 py-4 text-sm font-medium bg-white text-[var(--color-primary)] hover:bg-zinc-100 rounded-lg shadow-md hover:shadow-lg transition-all">
Zum Backend-Portal ({{ config('app.name') }})
</a>
<a href="{{ config('domains.domain_portal_url') }}/register" class="inline-flex items-center justify-center px-8 py-4 text-sm font-medium text-white border-2 border-white hover:bg-white/10 rounded-lg transition-all">
Kostenlos registrieren
</a>
</div>
</div>
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-4">
So funktioniert's
</h2>
<p class="text-zinc-600 dark:text-zinc-400">
Folgen Sie unserem einfachen 5-Schritte-Prozess
</p>
</div>
</div>
</div>
</section>
<!-- Steps Section -->
<section class="py-16 bg-zinc-50/50 dark:bg-zinc-900/50 transition-colors duration-200">
<div class="container mx-auto px-4">
<div class="max-w-4xl mx-auto space-y-8">
<!-- Step 1 -->
<div class="flex gap-6">
<div class="flex-shrink-0">
<div class="w-16 h-16 rounded-full flex items-center justify-center text-white font-bold text-2xl" style="background: var(--gradient-hero);">
1
</div>
</div>
<div class="flex-1 bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6">
<h3 class="text-xl font-bold text-zinc-900 dark:text-zinc-100 mb-3">
Account erstellen & anmelden
</h3>
<p class="text-zinc-600 dark:text-zinc-400 mb-4">
Registrieren Sie sich kostenlos im Backend-Portal (<strong>{{ config('domains.domain_portal') }}</strong>) oder melden Sie sich mit Ihrem bestehenden Account an.
</p>
<div class="bg-zinc-50 dark:bg-zinc-950 rounded-lg p-4">
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-2">
<strong>Tipp:</strong> Die Registrierung dauert nur 2 Minuten und Sie können sofort loslegen.
</p>
</div>
</div>
</div>
<!-- Step 2 -->
<div class="flex gap-6">
<div class="flex-shrink-0">
<div class="w-16 h-16 rounded-full flex items-center justify-center text-white font-bold text-2xl" style="background: var(--gradient-hero);">
2
</div>
</div>
<div class="flex-1 bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6">
<h3 class="text-xl font-bold text-zinc-900 dark:text-zinc-100 mb-3">
Grunddaten eingeben
</h3>
<p class="text-zinc-600 dark:text-zinc-400 mb-4">
Geben Sie die wichtigsten Informationen zu Ihrer Pressemitteilung ein:
</p>
<ul class="space-y-2 mb-4">
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">Aussagekräftiger Titel (max. 100 Zeichen)</span>
</li>
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">Kurze Zusammenfassung (Teaser)</span>
</li>
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">Passende Kategorie auswählen</span>
</li>
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">Veröffentlichungsdatum festlegen</span>
</li>
</ul>
</div>
</div>
<!-- Step 3 -->
<div class="flex gap-6">
<div class="flex-shrink-0">
<div class="w-16 h-16 rounded-full flex items-center justify-center text-white font-bold text-2xl" style="background: var(--gradient-hero);">
3
</div>
</div>
<div class="flex-1 bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6">
<h3 class="text-xl font-bold text-zinc-900 dark:text-zinc-100 mb-3">
Content erstellen
</h3>
<p class="text-zinc-600 dark:text-zinc-400 mb-4">
Verfassen Sie Ihre Pressemitteilung mit unserem intuitiven Editor:
</p>
<ul class="space-y-2 mb-4">
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">WYSIWYG-Editor mit Formatierungsoptionen</span>
</li>
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">Überschriften, Listen und Formatierungen</span>
</li>
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">SEO-Score in Echtzeit</span>
</li>
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">Automatische Speicherung als Entwurf</span>
</li>
</ul>
</div>
</div>
<!-- Step 4 -->
<div class="flex gap-6">
<div class="flex-shrink-0">
<div class="w-16 h-16 rounded-full flex items-center justify-center text-white font-bold text-2xl" style="background: var(--gradient-hero);">
4
</div>
</div>
<div class="flex-1 bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6">
<h3 class="text-xl font-bold text-zinc-900 dark:text-zinc-100 mb-3">
Medien hochladen
</h3>
<p class="text-zinc-600 dark:text-zinc-400 mb-4">
Ergänzen Sie Ihre Pressemitteilung mit aussagekräftigen Medien:
</p>
<ul class="space-y-2 mb-4">
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">Bilder (JPG, PNG, WebP, max. 10 MB)</span>
</li>
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">PDF-Dokumente (max. 25 MB)</span>
</li>
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">Video-Einbettung (YouTube, Vimeo)</span>
</li>
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">Automatische Bildoptimierung</span>
</li>
</ul>
<div class="bg-zinc-50 dark:bg-zinc-950 rounded-lg p-4">
<p class="text-sm text-zinc-600 dark:text-zinc-400">
<strong>Tipp:</strong> Hochauflösende Bilder erhöhen die Aufmerksamkeit um bis zu 80%
</p>
</div>
</div>
</div>
<!-- Step 5 -->
<div class="flex gap-6">
<div class="flex-shrink-0">
<div class="w-16 h-16 rounded-full flex items-center justify-center text-white font-bold text-2xl" style="background: var(--gradient-hero);">
5
</div>
</div>
<div class="flex-1 bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6">
<h3 class="text-xl font-bold text-zinc-900 dark:text-zinc-100 mb-3">
Veröffentlichen
</h3>
<p class="text-zinc-600 dark:text-zinc-400 mb-4">
Überprüfen Sie Ihre Pressemitteilung und veröffentlichen Sie sie:
</p>
<ul class="space-y-2 mb-4">
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">Live-Vorschau vor Veröffentlichung</span>
</li>
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">Freischaltung innerhalb von 2-4 Stunden</span>
</li>
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">E-Mail-Benachrichtigung bei Freischaltung</span>
</li>
<li class="flex items-start gap-2">
<svg class="h-5 w-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<span class="text-sm text-zinc-600 dark:text-zinc-400">Sofortige Sichtbarkeit nach Freischaltung</span>
</li>
</ul>
<div class="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950 dark:to-emerald-950 border border-green-200 dark:border-green-800 rounded-lg p-4">
<p class="text-sm text-green-800 dark:text-green-200">
<strong>Professional & Enterprise Kunden:</strong> Express-Freischaltung in unter 1 Stunde!
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Benefits Section -->
<section class="py-16 bg-white dark:bg-zinc-950 transition-colors duration-200">
<div class="container mx-auto px-4">
<div class="max-w-4xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-4">
Ihre Vorteile
</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6 text-center">
<div class="w-16 h-16 bg-gradient-to-br from-[var(--color-primary)]/10 to-[var(--color-secondary)]/10 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="h-8 w-8 text-[var(--color-primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100 mb-2">
Schnell & Einfach
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400">
In unter 10 Minuten zur fertigen Pressemitteilung
</p>
</div>
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6 text-center">
<div class="w-16 h-16 bg-gradient-to-br from-[var(--color-primary)]/10 to-[var(--color-secondary)]/10 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="h-8 w-8 text-[var(--color-primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path>
</svg>
</div>
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100 mb-2">
SEO-Optimiert
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400">
Maximale Sichtbarkeit in Suchmaschinen
</p>
</div>
<div class="bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200 dark:border-zinc-800 p-6 text-center">
<div class="w-16 h-16 bg-gradient-to-br from-[var(--color-primary)]/10 to-[var(--color-secondary)]/10 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="h-8 w-8 text-[var(--color-primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
</div>
<h3 class="font-semibold text-zinc-900 dark:text-zinc-100 mb-2">
Qualitätssicherung
</h3>
<p class="text-sm text-zinc-600 dark:text-zinc-400">
Redaktionelle Prüfung vor Veröffentlichung
</p>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="py-16 text-white" style="background: var(--gradient-hero);">
<div class="container mx-auto px-4 text-center">
<h2 class="text-3xl font-bold mb-4">
Jetzt loslegen!
</h2>
<p class="text-xl text-white/90 mb-8 max-w-2xl mx-auto">
Melden Sie sich im Backend-Portal an und veröffentlichen Sie Ihre erste Pressemitteilung
</p>
<a href="{{ config('domains.domain_portal_url') }}/login" class="inline-flex items-center justify-center px-8 py-4 text-sm font-medium bg-white text-[var(--color-primary)] hover:bg-zinc-100 rounded-lg shadow-md hover:shadow-lg transition-all">
Zum Backend-Portal
</a>
</div>
</section>
<livewire:web.footer />
</main>
@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.')
@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
@push('styles')
<style>
:root {
--color-primary: {{ $domainConfig['color_scheme']['primary'] ?? '#cf3628' }};
--color-secondary: {{ $domainConfig['color_scheme']['secondary'] ?? '#f0834a' }};
}
[x-cloak] { display: none !important; }
</style>
@endpush
@section('content')
<x-web.site-header />
<main class="bg-bg text-ink">
{{-- Breadcrumb / Subnav --}}
<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 justify-between gap-3 text-[12px] text-ink-3">
<div class="flex items-center gap-2">
<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">Veröffentlichen</span>
</div>
<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>
<span class="text-ink-4">·</span>
<a href="{{ route('hilfe') }}" class="text-ink-2 hover:text-ink transition-colors">Hilfe &amp; Support</a>
<a href="{{ route('preise') }}" class="text-ink-2 hover:text-ink transition-colors">Preise</a>
</div>
</div>
</div>
{{-- ============== HERO ============== --}}
<section class="bg-bg">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-10 lg:pt-14 pb-12 grid items-start gap-12 lg:gap-[72px] grid-cols-1 lg:grid-cols-[1.35fr_1fr]">
{{-- LEFT editorial copy --}}
<div>
<div class="flex items-center gap-3 mb-7">
<span class="w-7 h-px bg-brand"></span>
<span class="eyebrow text-[11px]">Pressestelle · Veröffentlichen</span>
</div>
<h1 class="font-serif text-[40px] sm:text-[50px] lg:text-[58px] font-semibold m-0 text-ink leading-[1.05] tracking-[-1.2px] text-balance">
Pressemitteilung veröffentlichen.<br class="hidden sm:block" />
Geprüft. Dauerhaft auffindbar.
</h1>
<p class="font-serif mt-7 text-[18px] lg:text-[19px] leading-[1.5] text-ink-2 max-w-[560px]">
{{ $brandName }}{{ $brandAccent }} ist die Presseplattform für mittelständische Unternehmen,
Selbstständige und PR-Agenturen aus Deutschland, Österreich und der Schweiz. Jede Mitteilung
wird auf Qualität geprüft und bleibt dauerhaft im Archiv auffindbar.
</p>
{{-- Störer Hub-CTA --}}
<div class="relative mt-9 grid items-center overflow-hidden text-ink-on-dark bg-topbar-grad gap-6 lg:gap-7 grid-cols-1 lg:grid-cols-[1.4fr_auto] px-7 py-7 lg:px-8">
<span class="absolute left-0 top-0 bottom-0 w-[3px] bg-brand" aria-hidden="true"></span>
<div>
<div class="eyebrow mb-2.5 text-[10.5px] text-brand-soft">Einreichen im Publisher-Bereich</div>
<h2 class="font-serif m-0 text-[21px] lg:text-[23px] font-semibold leading-[1.25] tracking-[-0.3px] text-ink-on-dark">
Die Einreichung läuft über den zentralen Publisher-Bereich.
</h2>
<p class="mt-2.5 mb-0 text-[13.5px] leading-[1.55] text-ink-on-dark-2 max-w-[540px]">
Dort verwalten Sie Mitteilungen, Credits und Newsroom einmaliges Konto, beide Portale
nutzbar (businessportal24 &amp; presseecho.de).
</p>
</div>
<div class="flex flex-col items-start lg:items-end gap-2.5">
<a href="{{ $publisherUrl }}/register"
class="inline-flex items-center gap-2 px-[22px] py-3.5 text-[14px] font-semibold text-white bg-brand hover:bg-brand-deep transition-colors">
Zum Publisher-Bereich
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M4 8L8.5 3.5M8.5 3.5H5M8.5 3.5V7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</a>
<span class="text-[11.5px] text-ink-on-dark-2">Login per Magic-Link · ohne Vertragsbindung</span>
</div>
</div>
{{-- Faktenleiste --}}
<div class="grid grid-cols-2 lg:grid-cols-4 border-t border-bg-rule mt-9">
<div class="py-5 pr-5 border-r border-bg-rule">
<div class="eyebrow muted text-[10px] mb-2">Seit</div>
<div class="font-serif text-[26px] lg:text-[28px] font-semibold leading-none text-ink tracking-[-0.4px]">{{ $sinceYear }}</div>
<div class="mt-2 text-[12px] leading-[1.4] text-ink-3">im Betrieb · durchgehende Archivierung</div>
</div>
<div class="py-5 px-5 lg:border-r border-bg-rule">
<div class="eyebrow muted text-[10px] mb-2">Archiv</div>
<div class="font-serif text-[26px] lg:text-[28px] font-semibold leading-none text-ink tracking-[-0.4px]">{{ $archiveLabel }}</div>
<div class="mt-2 text-[12px] leading-[1.4] text-ink-3">Mitteilungen weiterhin abrufbar</div>
</div>
<div class="py-5 pr-5 lg:px-5 border-r border-t lg:border-t-0 border-bg-rule">
<div class="eyebrow muted text-[10px] mb-2">Reichweite</div>
<div class="font-serif text-[26px] lg:text-[28px] font-semibold leading-none text-ink tracking-[-0.4px]">DACH</div>
<div class="mt-2 text-[12px] leading-[1.4] text-ink-3">Deutschland · Österreich · Schweiz</div>
</div>
<div class="py-5 pl-5 border-t lg:border-t-0 border-bg-rule">
<div class="eyebrow muted text-[10px] mb-2 inline-flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-brand pulse-dot"></span>Heute
</div>
<div class="font-serif text-[26px] lg:text-[28px] font-semibold leading-none text-ink tracking-[-0.4px]">{{ (int) $publishedToday }} / {{ (int) $inReview }}</div>
<div class="mt-2 text-[12px] leading-[1.4] text-ink-3">veröffentlicht / in Prüfung Stand {{ $referenceNow->translatedFormat('j. M') }}</div>
</div>
</div>
</div>
{{-- RIGHT Beispiel-Mitteilung --}}
<aside class="lg:sticky lg:top-6">
<div class="text-[11px] tracking-[0.14em] uppercase text-ink-3 font-semibold mb-2.5">So sieht eine veröffentlichte Mitteilung aus</div>
<article class="bg-bg-elev border border-bg-rule px-6 py-6">
<div class="flex items-center justify-between mb-3.5">
<span class="bp-cat text-[10.5px]">{{ $exampleCategory ?? 'Industrie · Maschinenbau' }}</span>
<span class="inline-flex items-center gap-1.5 text-[10.5px] text-ok font-semibold">
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true"><path d="M2 5.5l2.2 2.2L9 3" stroke="currentColor" stroke-width="1.7" fill="none" stroke-linecap="round"/></svg>
Geprüft
</span>
</div>
<h3 class="font-serif m-0 text-[22px] leading-[1.2] font-semibold text-ink tracking-[-0.3px]">
@if ($example)
<a href="{{ $example->slug ? route('release.detail', ['slug' => $example->slug]) : '#' }}" class="hover:text-brand transition-colors">{{ $example->title }}</a>
@else
Mittelständischer Fertiger eröffnet Werk und schafft 84 Arbeitsplätze
@endif
</h3>
<p class="font-serif mt-3.5 mb-0 text-[14px] leading-[1.55] text-ink-2">
{{ $example?->subtitle
?? \Illuminate\Support\Str::limit(strip_tags((string) $example?->text), 180)
?: 'Coesfeld · Die Brinkmann Präzisionstechnik GmbH hat einen Erweiterungsbau in Betrieb genommen. Investitionsvolumen: 9,4 Mio. Euro.' }}
</p>
<hr class="rule my-4" />
<div class="flex flex-wrap text-[11.5px] text-ink-3 gap-y-2 gap-x-[18px]">
<span>{{ $exampleCompany ?? 'Brinkmann Präzisionstechnik GmbH' }}</span>
<span class="text-ink-4">·</span>
<span class="font-mono">{{ $examplePublishedAt ? $examplePublishedAt->translatedFormat('j. M Y, H:i') : '14. Mai 2026, 09:30' }}</span>
<span class="text-ink-4">·</span>
<span>{{ $exampleReadMinutes }} Min. Lesezeit</span>
</div>
</article>
<div class="mt-3.5 text-[11.5px] text-ink-3 leading-[1.5]">
Beispiel. Jede Mitteilung erhält eine permanente URL, ein strukturiertes Datenblatt und bleibt im durchsuchbaren Archiv.
</div>
</aside>
</div>
</section>
{{-- ============== AKTIVE NEWSROOMS ============== --}}
@if (! empty($newsroomNames))
<section class="bg-bg border-t border-bg-rule">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-6 grid items-baseline gap-4 lg:gap-7 grid-cols-1 lg:grid-cols-[auto_1fr_auto]">
<span class="eyebrow muted text-[10.5px] flex-shrink-0">Aktive Newsrooms zuletzt</span>
<p class="m-0 text-[13.5px] leading-[1.6] text-ink-2">
@foreach ($newsroomNames as $name)
<span class="text-ink font-medium">{{ $name }}</span>@if (! $loop->last)<span class="text-ink-4"> · </span>@endif
@endforeach
@if ($newsroomTotalLabel > 0)
<span class="text-ink-3"> und {{ number_format($newsroomTotalLabel, 0, ',', '.') }} weitere veröffentlichende Unternehmen.</span>
@endif
</p>
<a href="{{ route('newsrooms') }}" class="text-[12px] text-brand font-medium border-b border-brand pb-px flex-shrink-0 justify-self-start">Alle Newsrooms </a>
</div>
</section>
@endif
{{-- ============== TRUST vier Prinzipien ============== --}}
<section class="bg-bg border-t border-bg-rule-strong">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-14">
<div class="flex flex-col gap-3.5 mb-9">
<span class="font-mono text-[11px] text-ink-4 tracking-[0.1em]">§&nbsp;01</span>
<div>
<div class="eyebrow text-[10.5px] mb-2.5">Was uns ausmacht</div>
<h2 class="font-serif m-0 text-[28px] lg:text-[36px] font-semibold leading-[1.1] text-ink max-w-[720px] tracking-[-0.6px] text-balance">Vier Prinzipien, die diese Plattform tragen</h2>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 border-t border-bg-rule-strong">
@foreach ([
['k' => 'Qualität', 't' => 'Geprüfte Mitteilungen', 'd' => 'Jede Mitteilung durchläuft eine Qualitätsprüfung, bevor sie online geht. Keine SEO-Spam-Texte, keine reinen Werbeanzeigen.'],
['k' => 'Auffindbarkeit', 't' => 'Dauerhaft im Archiv', 'd' => 'Pressemitteilungen bleiben im Archiv auch nach Jahren. Mitteilungen aus mehr als einem Jahrzehnt sind weiterhin abrufbar.'],
['k' => 'Konditionen', 't' => 'Faire Preise, keine Bindung', 'd' => 'Transparente Preise, kein Abo-Zwang, keine Vertragsfallen. Sie zahlen, was Sie einreichen.'],
['k' => 'Umgang mit Fehlern', 't' => 'Korrektur statt Löschung', 'd' => 'Fehler in einer Mitteilung? Wir korrigieren statt zu löschen damit Verweise und Verlinkungen bestehen bleiben.'],
] as $i => $p)
<div class="py-7 {{ $i % 4 !== 3 ? 'lg:border-r' : '' }} {{ $i % 2 === 0 ? 'sm:border-r lg:border-r' : '' }} border-bg-rule {{ $i > 0 ? 'border-t sm:border-t-0' : '' }} {{ $i >= 2 ? 'lg:border-t-0 sm:border-t' : '' }} px-0 sm:px-6 first:sm:pl-0 lg:first:pl-0 lg:pr-6">
<div class="eyebrow muted text-[10px] mb-3.5">{{ $p['k'] }}</div>
<h3 class="font-serif m-0 text-[22px] leading-[1.2] font-semibold tracking-[-0.3px]">{{ $p['t'] }}</h3>
<p class="mt-3.5 mb-0 text-[14px] leading-[1.55] text-ink-2">{{ $p['d'] }}</p>
</div>
@endforeach
</div>
</div>
</section>
{{-- ============== ABLAUF ============== --}}
<section id="ablauf" class="bg-bg-elev border-t border-bg-rule">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-14 lg:py-16">
<div class="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6 mb-10">
<div class="flex flex-col gap-3.5">
<span class="font-mono text-[11px] text-ink-4 tracking-[0.1em]">§&nbsp;02</span>
<div>
<div class="eyebrow text-[10.5px] mb-2.5">Ablauf</div>
<h2 class="font-serif m-0 text-[28px] lg:text-[36px] font-semibold leading-[1.1] text-ink max-w-[720px] tracking-[-0.6px] text-balance">So funktioniert's drei Schritte</h2>
</div>
</div>
<div class="text-[12.5px] text-ink-3 max-w-[280px] lg:text-right leading-[1.55]">Keine Magic-Sprache. Was hier passiert, passiert sichtbar.</div>
</div>
<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-Adresse. Kein Passwort nötig — Login per Magic-Link. Konto und Abrechnung liegen im zentralen Publisher-Bereich.'],
['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' => '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">
<div class="flex items-baseline justify-between mb-[22px]">
<span class="font-mono text-[13px] text-brand font-semibold">{{ $step['n'] }}</span>
<span class="eyebrow muted text-[10px]">{{ $step['meta'] }}</span>
</div>
<h3 class="font-serif m-0 text-[24px] leading-[1.2] font-semibold tracking-[-0.3px]">{{ $step['t'] }}</h3>
<p class="mt-3.5 mb-0 text-[14px] leading-[1.6] text-ink-2">{{ $step['d'] }}</p>
</div>
@endforeach
</div>
<p class="mt-[18px] mb-0 text-[12px] text-ink-3">
Die Veröffentlichung erfolgt über den zentralen Publisher-Bereich. Cross-Publishing nach presseecho.de ist optional verfügbar.
</p>
</div>
</section>
{{-- ============== ANLÄSSE ============== --}}
<section class="bg-bg border-t border-bg-rule">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-14 lg:py-16">
<div class="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6 mb-10">
<div class="flex flex-col gap-3.5">
<span class="font-mono text-[11px] text-ink-4 tracking-[0.1em]">§&nbsp;03</span>
<div>
<div class="eyebrow text-[10.5px] mb-2.5">Für wen</div>
<h2 class="font-serif m-0 text-[28px] lg:text-[36px] font-semibold leading-[1.1] text-ink max-w-[720px] tracking-[-0.6px] text-balance">Typische Anlässe einer Veröffentlichung</h2>
</div>
</div>
<div class="text-[12.5px] text-ink-3 max-w-[280px] lg:text-right leading-[1.55]">Anhaltspunkt für die Eignung. Eine Aufzählung, keine abschließende Liste.</div>
</div>
<div class="grid items-start gap-12 lg:gap-16 grid-cols-1 lg:grid-cols-[1.05fr_1fr]">
<ol class="list-none p-0 m-0">
@foreach ([
['t' => 'Neue Produkte oder Dienstleistungen', 'd' => 'Markteinführungen, Produktupdates, neue Services.'],
['t' => 'Personalien und Geschäftsleitungs-Wechsel', 'd' => 'Berufungen, Bestellungen, Verabschiedungen.'],
['t' => 'Auszeichnungen und Zertifizierungen', 'd' => 'Awards, ISO- und Branchen-Zertifikate, Audits.'],
['t' => 'Standort-Eröffnungen, Expansionen, Aufträge', 'd' => 'Werke, Niederlassungen, Großaufträge, Investitionen.'],
['t' => 'Veranstaltungs-Ankündigungen', 'd' => 'Tage der offenen Tür, Fachtage, Hauptversammlungen.'],
['t' => 'Studien und Marktanalysen', 'd' => 'Eigene Erhebungen, Trendberichte, Branchenkennzahlen.'],
] as $i => $anlass)
<li class="grid items-start gap-4 py-[18px] {{ $loop->first ? 'border-t border-bg-rule-strong' : '' }} border-b border-bg-rule grid-cols-[44px_1fr]">
<span class="font-mono text-[12px] text-ink-4 pt-1">{{ str_pad((string) ($i + 1), 2, '0', STR_PAD_LEFT) }}</span>
<div>
<div class="font-serif text-[18px] font-semibold leading-[1.25] text-ink tracking-[-0.2px]">{{ $anlass['t'] }}</div>
<div class="mt-1.5 text-[13px] text-ink-3 leading-[1.5]">{{ $anlass['d'] }}</div>
</div>
</li>
@endforeach
</ol>
<aside class="border border-bg-rule border-l-[3px] border-l-brand bg-bg-card-warm px-7 py-7 lg:px-8">
<div class="eyebrow text-[10.5px] mb-3.5">Wichtig zur Eingrenzung</div>
<h3 class="font-serif m-0 text-[20px] lg:text-[22px] leading-[1.25] font-semibold tracking-[-0.3px] text-ink">
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
erreichen, werden mit einer kurzen Begründung zurückgegeben meist mit einem konkreten Korrekturvorschlag.
</p>
<hr class="rule my-5" />
<div class="grid gap-2.5 text-[13px]">
<div class="flex items-start gap-2.5">
<svg width="14" height="14" viewBox="0 0 14 14" class="mt-[3px] flex-shrink-0 text-ok" aria-hidden="true"><path d="M3 7l3 3 5-6" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="text-ink">Mitteilungen mit klarem Anlass und Quelle</span>
</div>
<div class="flex items-start gap-2.5">
<svg width="14" height="14" viewBox="0 0 14 14" class="mt-[3px] flex-shrink-0 text-ok" aria-hidden="true"><path d="M3 7l3 3 5-6" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="text-ink">Ansprechpartner mit Funktion und Kontakt</span>
</div>
<div class="flex items-start gap-2.5">
<svg width="14" height="14" viewBox="0 0 14 14" class="mt-[3px] flex-shrink-0 text-ink-3" aria-hidden="true"><path d="M3.5 3.5l7 7M10.5 3.5l-7 7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<span class="text-ink-3">Reine Verkaufs- und Rabatt-Texte</span>
</div>
<div class="flex items-start gap-2.5">
<svg width="14" height="14" viewBox="0 0 14 14" class="mt-[3px] flex-shrink-0 text-ink-3" aria-hidden="true"><path d="M3.5 3.5l7 7M10.5 3.5l-7 7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<span class="text-ink-3">Anonyme oder Quellen-lose Inhalte</span>
</div>
</div>
</aside>
</div>
</div>
</section>
{{-- ============== PREISE ============== --}}
<section class="bg-bg-elev border-t border-bg-rule">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-14">
<div class="flex flex-col gap-3.5 mb-9">
<span class="font-mono text-[11px] text-ink-4 tracking-[0.1em]">§&nbsp;04</span>
<div>
<div class="eyebrow text-[10.5px] mb-2.5">Preise</div>
<h2 class="font-serif m-0 text-[28px] lg:text-[36px] font-semibold leading-[1.1] text-ink max-w-[720px] tracking-[-0.6px] text-balance">Sie zahlen, was Sie einreichen.</h2>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-[1.3fr_1fr_1fr] border-y border-bg-rule-strong">
<div class="pt-7 pb-8 lg:pr-8 lg:border-r border-bg-rule">
<div class="eyebrow muted text-[10px] mb-3">Logik</div>
<p class="font-serif m-0 text-[18px] lg:text-[19px] leading-[1.5] text-ink tracking-[-0.1px]">
Sie kaufen Credits, die für Veröffentlichungen eingesetzt werden. Keine Mindestlaufzeit, keine versteckten Gebühren.
</p>
<ul class="list-none p-0 mt-5 text-[13px] text-ink-2 grid gap-2.5">
<li class="flex items-start gap-2.5"><span class="w-1.5 h-px bg-brand mt-2.5 flex-shrink-0"></span><span>Credits verfallen nicht.</span></li>
<li class="flex items-start gap-2.5"><span class="w-1.5 h-px bg-brand mt-2.5 flex-shrink-0"></span><span>Korrekturen sind kostenfrei.</span></li>
<li class="flex items-start gap-2.5"><span class="w-1.5 h-px bg-brand mt-2.5 flex-shrink-0"></span><span>Mengenrabatte ab dem zweiten Paket.</span></li>
</ul>
</div>
<div class="pt-7 pb-8 lg:px-8 border-t lg:border-t-0 lg:border-r border-bg-rule flex flex-col justify-between">
<div>
<div class="eyebrow muted text-[10px] mb-3">Anker</div>
<div class="font-serif text-[16px] leading-[1.4] text-ink-2">Eine Veröffentlichung</div>
<div class="mt-2 font-serif text-[40px] lg:text-[48px] font-semibold leading-none text-ink tracking-[-1.5px]">ab&nbsp;89&nbsp;</div>
<div class="mt-2.5 text-[12px] text-ink-3">netto · zzgl. USt. · Mengenrabatte verfügbar.</div>
</div>
<div class="mt-5 text-[11.5px] text-ink-3 leading-[1.5]">Beispiel-Pakete: 5er ab 79&nbsp; / Mitteilung · 25er ab 64&nbsp; / Mitteilung.</div>
</div>
<div class="pt-7 pb-8 lg:pl-8 border-t lg:border-t-0 border-bg-rule flex flex-col justify-between">
<div>
<div class="eyebrow muted text-[10px] mb-3">Vollständige Übersicht</div>
<p class="m-0 text-[14px] leading-[1.6] text-ink-2">Pakete, Cross-Publishing nach presseecho.de, Agentur-Konditionen und Rechnungseinstellungen finden Sie in der Preisübersicht.</p>
</div>
<a href="{{ route('preise') }}" class="mt-5 inline-flex items-center gap-2 text-[13.5px] font-semibold text-brand border-b border-brand self-start pb-0.5">
Zur Preisübersicht
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M3 6h6M6 3l3 3-3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</a>
</div>
</div>
</div>
</section>
{{-- ============== FAQ ============== --}}
<section class="bg-bg border-t border-bg-rule">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-14 lg:py-16">
<div class="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6 mb-9">
<div class="flex flex-col gap-3.5">
<span class="font-mono text-[11px] text-ink-4 tracking-[0.1em]">§&nbsp;05</span>
<div>
<div class="eyebrow text-[10.5px] mb-2.5">Häufige Fragen</div>
<h2 class="font-serif m-0 text-[28px] lg:text-[36px] font-semibold leading-[1.1] text-ink max-w-[720px] tracking-[-0.6px] text-balance">Kurz beantwortet</h2>
</div>
</div>
</div>
<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="{{ route('kontakt') }}" 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>
<div class="mt-3.5 text-[12px] text-ink-3 leading-[1.55]">MoFr · 09:0017:00 Uhr (MEZ)</div>
</div>
<div class="border-t border-bg-rule-strong">
@foreach ([
['q' => 'Wie schnell wird meine Mitteilung veröffentlicht?', 'a' => 'Werktags üblicherweise innerhalb von 24 Stunden nach Einreichung. Bei zeitkritischen Mitteilungen ist eine Vorab-Absprache möglich.'],
['q' => 'Bleibt meine Mitteilung dauerhaft online?', 'a' => 'Ja. Mitteilungen werden nicht gelöscht. Korrekturen sind jederzeit möglich — die URL bleibt erhalten.'],
['q' => 'Kann ich auch auf presseecho.de veröffentlichen?', 'a' => 'Ja, über den zentralen Publisher-Bereich. Cross-Publishing ist optional verfügbar und wird separat abgerechnet.'],
['q' => 'Brauche ich ein Abo?', 'a' => 'Nein. Sie kaufen Credits nach Bedarf, ohne Vertragsbindung. Nicht eingesetzte Credits verfallen nicht.'],
['q' => 'Was passiert bei einem Fehler in der Mitteilung?', 'a' => 'Korrektur statt Löschung: Inhalte werden aktualisiert, die URL bleibt erhalten, Verlinkungen und Verweise bestehen weiter.'],
] as $i => $faq)
<details class="group border-b border-bg-rule" {{ $i === 0 ? 'open' : '' }}>
<summary class="grid items-center gap-4 py-[22px] cursor-pointer list-none grid-cols-[44px_1fr_24px] [&::-webkit-details-marker]:hidden">
<span class="font-mono text-[12px] text-ink-4">{{ str_pad((string) ($i + 1), 2, '0', STR_PAD_LEFT) }}</span>
<span class="font-serif text-[17px] lg:text-[19px] font-semibold text-ink leading-[1.3] tracking-[-0.2px]">{{ $faq['q'] }}</span>
<span class="inline-flex items-center justify-center w-[22px] h-[22px] border border-bg-rule-strong text-ink justify-self-end" aria-hidden="true">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" class="transition-transform group-open:rotate-45"><path d="M6 2v8M2 6h8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
</summary>
<div class="text-[14px] leading-[1.6] text-ink-2 max-w-[640px] pb-[22px] pl-[60px]">{{ $faq['a'] }}</div>
</details>
@endforeach
</div>
</div>
</div>
</section>
{{-- ============== FINAL CTA ============== --}}
<section class="bg-bg border-t border-bg-rule-strong">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-14 pb-16 lg:pb-[72px]">
<div class="relative overflow-hidden grid items-end text-ink-on-dark bg-topbar-grad gap-8 lg:gap-14 grid-cols-1 lg:grid-cols-[1.3fr_1fr] px-7 py-10 lg:px-14 lg:py-14">
<span class="absolute left-0 top-0 bottom-0 w-1 bg-brand" aria-hidden="true"></span>
<div>
<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.
Einmaliges Konto beide Portale (businessportal24 &amp; presseecho.de) nutzbar.
</p>
</div>
<div class="flex flex-col items-start gap-3.5">
<a href="{{ $publisherUrl }}/register"
class="inline-flex items-center gap-2 px-7 py-4 text-[15px] font-semibold text-white bg-brand hover:bg-brand-deep transition-colors">
Zum Publisher-Bereich
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M4 8L8.5 3.5M8.5 3.5H5M8.5 3.5V7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</a>
<span class="text-[12px] text-ink-on-dark-2">Login per Magic-Link · ohne Vertragsbindung</span>
<a href="{{ route('web.home') }}" class="mt-1.5 text-[13px] text-ink-on-dark border-b border-white/35 pb-0.5">Oder zuerst Beispiele ansehen </a>
</div>
</div>
</div>
</section>
</main>
<x-web.site-footer />
@endsection

View file

@ -3,14 +3,18 @@
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Http\Controllers\PressReleasePreviewController;
use App\Http\Middleware\SetEdition;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use App\Scopes\PortalScope;
use App\Support\DomainAssetContext;
use App\Support\EditorialClock;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Str;
Route::get('/pm-vorschau/{token}', PressReleasePreviewController::class)
->where('token', '[A-Za-z0-9]{40,}')
@ -41,19 +45,24 @@ $applyWebDomainConfig = static function (string $domainKey): array {
return $domainConfig;
};
$webHomeData = static function (Portal $primaryPortal): array {
$webHomeData = static function (Portal $primaryPortal, string $language = 'de'): array {
$portalValues = [$primaryPortal->value, Portal::Both->value];
// Aktualitäts-Stichtag an den jüngsten Datensatz koppeln (historisches Archiv).
$referenceNow = EditorialClock::reference($portalValues, $language);
$recentSince = $referenceNow->copy()->subDays(7);
$previousSince = $referenceNow->copy()->subDays(14);
$publishedQuery = static fn (): Builder => PressRelease::query()
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', 'de')
->where('language', $language)
->whereNotNull('published_at')
->where('published_at', '<=', now());
$with = [
'company',
'category.translations' => fn ($query) => $query->where('locale', 'de'),
'category.translations' => fn ($query) => $query->where('locale', $language),
'images' => fn ($query) => $query
->orderByDesc('is_preview')
->orderBy('sort_order')
@ -85,29 +94,29 @@ $webHomeData = static function (Portal $primaryPortal): array {
$activeNewsrooms = Company::query()
->whereIn('portal', $portalValues)
->where('is_active', true)
->whereHas('pressReleases', function (Builder $query) use ($portalValues): void {
->whereHas('pressReleases', function (Builder $query) use ($portalValues, $recentSince, $language): void {
$query
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', 'de')
->where('language', $language)
->whereNotNull('published_at')
->where('published_at', '>=', now()->subDays(7));
->where('published_at', '>=', $recentSince);
})
->withCount([
'pressReleases as recent_releases_count' => function (Builder $query) use ($portalValues): void {
'pressReleases as recent_releases_count' => function (Builder $query) use ($portalValues, $recentSince, $language): void {
$query
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', 'de')
->where('language', $language)
->whereNotNull('published_at')
->where('published_at', '>=', now()->subDays(7));
->where('published_at', '>=', $recentSince);
},
'pressReleases as today_releases_count' => function (Builder $query) use ($portalValues): void {
'pressReleases as today_releases_count' => function (Builder $query) use ($portalValues, $referenceNow, $language): void {
$query
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', 'de')
->whereDate('published_at', today());
->where('language', $language)
->whereDate('published_at', $referenceNow->toDateString());
},
])
->orderByDesc('today_releases_count')
@ -123,23 +132,23 @@ $webHomeData = static function (Portal $primaryPortal): array {
]);
$industryIndex = Category::query()
->with(['translations' => fn ($query) => $query->where('locale', 'de')])
->with(['translations' => fn ($query) => $query->where('locale', $language)])
->withCount([
'pressReleases as recent_count' => function (Builder $query) use ($portalValues): void {
'pressReleases as recent_count' => function (Builder $query) use ($portalValues, $recentSince, $language): void {
$query
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', 'de')
->where('published_at', '>=', now()->subDays(7))
->where('language', $language)
->where('published_at', '>=', $recentSince)
->whereNotNull('published_at');
},
'pressReleases as previous_count' => function (Builder $query) use ($portalValues): void {
'pressReleases as previous_count' => function (Builder $query) use ($portalValues, $recentSince, $previousSince, $language): void {
$query
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', 'de')
->where('published_at', '>=', now()->subDays(14))
->where('published_at', '<', now()->subDays(7));
->where('language', $language)
->where('published_at', '>=', $previousSince)
->where('published_at', '<', $recentSince);
},
])
->whereIn('portal', $portalValues)
@ -157,7 +166,7 @@ $webHomeData = static function (Portal $primaryPortal): array {
return [
'name' => $translation->name,
'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('kategorien'),
'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('web.home'),
'count' => (int) ($category->recent_count ?? 0),
'delta' => (int) ($category->recent_count ?? 0) - (int) ($category->previous_count ?? 0),
];
@ -165,50 +174,619 @@ $webHomeData = static function (Portal $primaryPortal): array {
->filter()
->values();
// "Heute im Fokus · Branche": aktivste Top-Rubrik im Stichtag-Fenster mit echten Meldungen.
$spotlightCategory = Category::query()
->with(['translations' => fn ($query) => $query->where('locale', $language)])
->withCount([
'pressReleases as window_count' => function (Builder $query) use ($portalValues, $recentSince, $referenceNow, $language): void {
$query
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', $language)
->whereNotNull('published_at')
->where('published_at', '>=', $recentSince)
->where('published_at', '<=', $referenceNow);
},
])
->whereIn('portal', $portalValues)
->where('is_active', true)
->whereNull('parent_id')
->orderByDesc('window_count')
->first();
$spotlight = null;
if ($spotlightCategory && $spotlightTranslation = $spotlightCategory->translations->first()) {
$spotlightBase = static fn (): Builder => PressRelease::query()
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', $language)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->where('category_id', $spotlightCategory->id);
$spotlightReleases = $spotlightBase()
->with(['company'])
->orderByDesc('published_at')
->limit(3)
->get()
->map(static function (PressRelease $release) use ($spotlightTranslation): array {
$words = str_word_count(strip_tags((string) $release->text));
$publishedAt = $release->published_at;
return [
'time' => $publishedAt?->format('H:i') ?? '',
'date' => $publishedAt?->translatedFormat('j. M') ?? '',
'category' => $spotlightTranslation->name,
'title' => $release->title,
'company' => $release->company?->name ?? '',
'city' => null,
'minutes' => max(1, (int) ceil($words / 200)),
'href' => $release->slug ? route('release.detail', ['slug' => $release->slug]) : '#',
];
});
if ($spotlightReleases->isNotEmpty()) {
$windowCount = (int) ($spotlightCategory->window_count ?? 0);
$archiveCount = $spotlightBase()->count();
$activeCompanies = (clone $spotlightBase())
->where('published_at', '>=', $recentSince)
->where('published_at', '<=', $referenceNow)
->distinct()
->count('company_id');
$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'],
],
'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,
'mostReadReleases' => $mostReadReleases,
'activeNewsrooms' => $activeNewsrooms,
'industryIndex' => $industryIndex,
'spotlight' => $spotlight,
'tickerItems' => $tickerItems,
'homeStats' => [
'publishedCount' => $publishedQuery()->count(),
'publishedToday' => $publishedQuery()->whereDate('published_at', today())->count(),
'publishedToday' => $publishedQuery()->whereDate('published_at', $referenceNow->toDateString())->count(),
'archiveSince' => $oldestPublishedAt ? (int) Carbon::parse($oldestPublishedAt)->format('Y') : null,
],
];
};
// Hauptseite - dynamisch basierend auf Domain
Route::get('/', function () use ($applyWebDomainConfig, $webHomeData) {
// Kategorie-/Branchenseite je Ausgabe.
$categoryPage = static function (string $edition, string $slug) use ($applyWebDomainConfig) {
$domain = request()->getHost();
$language = app()->getLocale();
[$domainKey, $portal] = match (true) {
str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho],
default => ['businessportal24', Portal::Businessportal24],
};
$applyWebDomainConfig($domainKey);
$portalValues = [$portal->value, Portal::Both->value];
$category = Category::query()
->whereIn('portal', $portalValues)
->where('is_active', true)
->whereHas('translations', fn (Builder $query) => $query->where('locale', $language)->where('slug', $slug))
->with([
'translations' => fn ($query) => $query->where('locale', $language),
'parent.translations' => fn ($query) => $query->where('locale', $language),
])
->first();
if (! $category) {
abort(404);
}
$childCategories = Category::query()
->where('parent_id', $category->id)
->where('is_active', true)
->with(['translations' => fn ($query) => $query->where('locale', $language)])
->get();
$categoryIds = collect([$category->id])->merge($childCategories->pluck('id'))->unique()->values()->all();
$base = static fn (): Builder => PressRelease::query()
->withoutGlobalScope(PortalScope::class)
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', $language)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->whereIn('category_id', $categoryIds);
// Kategoriespezifischer Stichtag: jüngste Meldung dieser Branche.
$latest = $base()->max('published_at');
$referenceNow = $latest ? Carbon::parse($latest) : now();
if ($referenceNow->gt(now())) {
$referenceNow = now();
}
$recentSince = $referenceNow->copy()->subDays(7);
$previousSince = $referenceNow->copy()->subDays(14);
$monthSince = $referenceNow->copy()->subDays(30);
$with = [
'company',
'category.translations' => fn ($query) => $query->where('locale', $language),
'images' => fn ($query) => $query->orderByDesc('is_preview')->orderBy('sort_order')->limit(1),
];
$leadRelease = $base()->with($with)->orderByDesc('published_at')->first();
$topReleases = $base()
->with($with)
->when($leadRelease, fn (Builder $query) => $query->whereKeyNot($leadRelease->getKey()))
->orderByDesc('published_at')
->limit(2)
->get();
$excludeIds = collect([$leadRelease?->getKey()])->merge($topReleases->modelKeys())->filter()->all();
$feedReleases = $base()
->with($with)
->when($excludeIds !== [], fn (Builder $query) => $query->whereNotIn('id', $excludeIds))
->orderByDesc('published_at')
->limit(12)
->get();
$mostReadReleases = $base()
->orderByDesc('hits')
->orderByDesc('published_at')
->limit(5)
->get(['id', 'slug', 'title', 'hits', 'portal', 'language']);
$stats = [
'today' => $base()->whereDate('published_at', $referenceNow->toDateString())->count(),
'week' => $base()->where('published_at', '>=', $recentSince)->count(),
'previousWeek' => $base()->where('published_at', '>=', $previousSince)->where('published_at', '<', $recentSince)->count(),
'month' => $base()->where('published_at', '>=', $monthSince)->count(),
'total' => $base()->count(),
'activeCompanies' => $base()->where('published_at', '>=', $recentSince)->distinct()->count('company_id'),
'totalCompanies' => $base()->distinct()->count('company_id'),
];
$newsroomFilter = static function (Builder $query) use ($portalValues, $categoryIds, $language): void {
$query
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', $language)
->whereNotNull('published_at')
->whereIn('category_id', $categoryIds);
};
$topNewsrooms = Company::query()
->withoutGlobalScope(PortalScope::class)
->whereIn('portal', $portalValues)
->where('is_active', true)
->whereHas('pressReleases', function (Builder $query) use ($newsroomFilter, $recentSince): void {
$newsroomFilter($query);
$query->where('published_at', '>=', $recentSince);
})
->withCount([
'pressReleases as recent_releases_count' => function (Builder $query) use ($newsroomFilter, $recentSince): void {
$newsroomFilter($query);
$query->where('published_at', '>=', $recentSince);
},
'pressReleases as today_releases_count' => function (Builder $query) use ($newsroomFilter, $referenceNow): void {
$newsroomFilter($query);
$query->whereDate('published_at', $referenceNow->toDateString());
},
])
->orderByDesc('recent_releases_count')
->limit(6)
->get()
->map(fn (Company $company): array => [
'name' => $company->name,
'slug' => $company->slug,
'initial' => mb_strtoupper(mb_substr((string) $company->name, 0, 1)),
'count' => (int) $company->recent_releases_count,
'today' => (int) $company->today_releases_count > 0,
]);
$publishedCount = static fn (Builder $query) => $query
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', $language)
->whereNotNull('published_at');
$subCategories = $childCategories
->map(function (Category $child) use ($recentSince, $previousSince, $publishedCount): ?array {
$translation = $child->translations->first();
if (! $translation) {
return null;
}
$total = PressRelease::query()->withoutGlobalScope(PortalScope::class)
->where('category_id', $child->id)->tap($publishedCount)->count();
$recent = PressRelease::query()->withoutGlobalScope(PortalScope::class)
->where('category_id', $child->id)->tap($publishedCount)->where('published_at', '>=', $recentSince)->count();
$previous = PressRelease::query()->withoutGlobalScope(PortalScope::class)
->where('category_id', $child->id)->tap($publishedCount)
->where('published_at', '>=', $previousSince)->where('published_at', '<', $recentSince)->count();
return [
'name' => $translation->name,
'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('web.home'),
'total' => $total,
'recent' => $recent,
'delta' => $recent - $previous,
];
})
->filter()
->sortByDesc('total')
->values();
$relatedCategories = Category::query()
->whereIn('portal', $portalValues)
->where('is_active', true)
->whereNull('parent_id')
->whereKeyNot($category->id)
->when($category->parent_id, fn (Builder $query) => $query->whereKeyNot($category->parent_id))
->with(['translations' => fn ($query) => $query->where('locale', $language)])
->withCount([
'pressReleases as total_count' => fn (Builder $query) => $publishedCount($query),
])
->orderByDesc('total_count')
->limit(8)
->get()
->map(function (Category $related): ?array {
$translation = $related->translations->first();
if (! $translation) {
return null;
}
return [
'name' => $translation->name,
'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('web.home'),
'count' => (int) ($related->total_count ?? 0),
];
})
->filter()
->values();
return view('web.kategorie', [
'category' => $category,
'referenceNow' => $referenceNow,
'leadRelease' => $leadRelease,
'topReleases' => $topReleases,
'feedReleases' => $feedReleases,
'mostReadReleases' => $mostReadReleases,
'categoryStats' => $stats,
'topNewsrooms' => $topNewsrooms,
'subCategories' => $subCategories,
'relatedCategories' => $relatedCategories,
]);
};
// Detailseite einer Pressemitteilung je Ausgabe.
$releaseDetailPage = static function (string $edition, string $slug) use ($applyWebDomainConfig) {
$domain = request()->getHost();
$language = app()->getLocale();
[$domainKey, $portal] = match (true) {
str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho],
default => ['businessportal24', Portal::Businessportal24],
};
$applyWebDomainConfig($domainKey);
$portalValues = [$portal->value, Portal::Both->value];
$publishedScope = static fn (Builder $query): Builder => $query
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', $language)
->whereNotNull('published_at')
->where('published_at', '<=', now());
$release = PressRelease::query()
->withoutGlobalScope(PortalScope::class)
->tap($publishedScope)
->where('slug', $slug)
->with([
'company',
'category.translations' => fn ($query) => $query->where('locale', $language),
'category.parent.translations' => fn ($query) => $query->where('locale', $language),
'images' => fn ($query) => $query->orderByDesc('is_preview')->orderBy('sort_order'),
'contacts',
])
->first();
if (! $release) {
abort(404);
}
PressRelease::withoutGlobalScope(PortalScope::class)
->whereKey($release->getKey())
->update(['hits' => ($release->hits ?? 0) + 1]);
$with = [
'company',
'category.translations' => fn ($query) => $query->where('locale', $language),
'images' => fn ($query) => $query->orderByDesc('is_preview')->orderBy('sort_order')->limit(1),
];
$companyReleases = $release->company_id
? PressRelease::query()
->withoutGlobalScope(PortalScope::class)
->tap($publishedScope)
->where('company_id', $release->company_id)
->whereKeyNot($release->getKey())
->with($with)
->orderByDesc('published_at')
->limit(3)
->get()
: collect();
$relatedReleases = $release->category_id
? PressRelease::query()
->withoutGlobalScope(PortalScope::class)
->tap($publishedScope)
->where('category_id', $release->category_id)
->whereKeyNot($release->getKey())
->when($companyReleases->isNotEmpty(), fn (Builder $query) => $query->whereNotIn('id', $companyReleases->modelKeys()))
->with($with)
->orderByDesc('published_at')
->limit(3)
->get()
: collect();
return view('web.release-detail', [
'release' => $release,
'companyReleases' => $companyReleases,
'relatedReleases' => $relatedReleases,
]);
};
// Veröffentlichen-Marketingseite je Ausgabe (Einreichung läuft im Publisher-Hub).
$veroeffentlichenPage = static function (string $edition) use ($applyWebDomainConfig) {
$domain = request()->getHost();
$language = app()->getLocale();
[$domainKey, $portal] = match (true) {
str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho],
default => ['businessportal24', Portal::Businessportal24],
};
$applyWebDomainConfig($domainKey);
$portalValues = [$portal->value, Portal::Both->value];
$referenceNow = EditorialClock::reference($portalValues, $language);
$recentSince = $referenceNow->copy()->subDays(30);
$publishedBase = static fn (): Builder => PressRelease::query()
->withoutGlobalScope(PortalScope::class)
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', $language)
->whereNotNull('published_at')
->where('published_at', '<=', now());
$archiveTotal = $publishedBase()->count();
$publishedToday = $publishedBase()->whereDate('published_at', $referenceNow->toDateString())->count();
$oldest = $publishedBase()->min('published_at');
$oldestYear = $oldest ? (int) Carbon::parse($oldest)->format('Y') : null;
$inReview = PressRelease::query()
->withoutGlobalScope(PortalScope::class)
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Review)
->where('language', $language)
->count();
$exampleRelease = $publishedBase()
->with([
'company',
'category.translations' => fn ($query) => $query->where('locale', $language),
])
->orderByDesc('published_at')
->first();
$newsroomFilter = static function (Builder $query) use ($portalValues, $language, $recentSince): void {
$query
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', $language)
->whereNotNull('published_at')
->where('published_at', '>=', $recentSince);
};
$newsroomQuery = Company::query()
->withoutGlobalScope(PortalScope::class)
->whereIn('portal', $portalValues)
->where('is_active', true)
->whereHas('pressReleases', fn (Builder $query) => $newsroomFilter($query));
$activeNewsroomsTotal = (clone $newsroomQuery)->count();
$activeNewsroomNames = (clone $newsroomQuery)
->withCount(['pressReleases as recent_count' => fn (Builder $query) => $newsroomFilter($query)])
->orderByDesc('recent_count')
->limit(10)
->pluck('name')
->all();
return view('web.veroeffentlichen', [
'referenceNow' => $referenceNow,
'archiveTotal' => $archiveTotal,
'publishedToday' => $publishedToday,
'inReview' => $inReview,
'oldestYear' => $oldestYear,
'exampleRelease' => $exampleRelease,
'activeNewsroomNames' => $activeNewsroomNames,
'activeNewsroomsTotal' => $activeNewsroomsTotal,
]);
};
// Archiv-Suchseite je Ausgabe (interaktive Suche läuft in der Volt-Komponente).
$suchePage = static function (string $edition) use ($applyWebDomainConfig) {
$domain = request()->getHost();
$language = app()->getLocale();
[$domainKey, $portal] = match (true) {
str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho],
default => ['businessportal24', Portal::Businessportal24],
};
$applyWebDomainConfig($domainKey);
$portalValues = [$portal->value, Portal::Both->value];
$mostReadReleases = PressRelease::query()
->withoutGlobalScope(PortalScope::class)
->whereIn('portal', $portalValues)
->where('status', PressReleaseStatus::Published)
->where('language', $language)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->orderByDesc('hits')
->orderByDesc('published_at')
->limit(5)
->get(['id', 'slug', 'title', 'hits', 'portal', 'language']);
return view('web.suche', [
'portal' => $portal->value,
'language' => $language,
'mostReadReleases' => $mostReadReleases,
]);
};
// ---------------------------------------------------------------------------
// Bare Root: Publisher-Hub (pressekonto) oder Redirect auf die Default-Ausgabe.
// ---------------------------------------------------------------------------
Route::get('/', function () use ($applyWebDomainConfig) {
$domain = request()->getHost();
if (str_contains($domain, 'presseecho')) {
$applyWebDomainConfig('presseecho');
return view('web.presseecho', $webHomeData(Portal::Presseecho));
} elseif (str_contains($domain, 'businessportal24')) {
$applyWebDomainConfig('businessportal24');
return view('web.businessportal24', $webHomeData(Portal::Businessportal24));
} elseif (str_contains($domain, 'pressekonto')) {
if (str_contains($domain, 'pressekonto')) {
$applyWebDomainConfig('pressekonto');
return view('web.pressekonto');
}
$applyWebDomainConfig('businessportal24');
return view('web.businessportal24', $webHomeData(Portal::Businessportal24));
return redirect(request()->getSchemeAndHttpHost().'/'.SetEdition::DEFAULT_EDITION, 301);
})->name('home');
// ---------------------------------------------------------------------------
// Legacy .html-URLs -> saubere 301-Weiterleitung (vor der Edition-Gruppe, damit
// `category/x.html` nicht als Slug `x.html` in die Kategorie-Route läuft).
// ---------------------------------------------------------------------------
$htmlRedirect = static function (string $edition, string $path) {
$clean = trim((string) preg_replace('/\.html$/', '', $path), '/');
$staticPages = [
'preise', 'faq', 'kontakt', 'ueber-uns', 'veroeffentlichen',
'suche', 'newsrooms', 'api', 'team', 'partner', 'karriere', 'presse',
'hilfe', 'impressum', 'datenschutz', 'agb', 'cookies',
];
// Entfallene Rubriken-Übersicht -> Startseite der Ausgabe.
if ($clean === 'kategorien') {
return redirect(request()->getSchemeAndHttpHost().'/'.$edition, 301);
}
if (str_starts_with($clean, 'category/') || str_starts_with($clean, 'press-release/')) {
$target = $clean;
} elseif ($clean === '' || str_contains($clean, '/')) {
$target = $clean;
} elseif (in_array($clean, $staticPages, true)) {
$target = $clean;
} else {
// Flache Detail-URL der Altseite: /{edition}/{slug}.html
$target = 'press-release/'.$clean;
}
return redirect(request()->getSchemeAndHttpHost().rtrim('/'.$edition.'/'.$target, '/'), 301);
};
Route::get('{edition}/{path}', function (string $edition, string $path) use ($htmlRedirect) {
return $htmlRedirect($edition, $path);
})->where('path', '.*\.html')->whereIn('edition', SetEdition::EDITIONS);
// Top-Level Legacy ohne Ausgabe-Präfix -> Default-Ausgabe.
Route::get('{path}', function (string $path) use ($htmlRedirect) {
return $htmlRedirect(SetEdition::DEFAULT_EDITION, $path);
})->where('path', '.*\.html');
// ---------------------------------------------------------------------------
// Öffentliche Inhalte je Ausgabe (Sprache): /de/... und /en/...
// ---------------------------------------------------------------------------
Route::prefix('{edition}')->whereIn('edition', SetEdition::EDITIONS)->group(function () use ($applyWebDomainConfig, $webHomeData, $categoryPage, $releaseDetailPage, $veroeffentlichenPage, $suchePage) {
// Startseite je Ausgabe
Route::get('/', function (string $edition) use ($applyWebDomainConfig, $webHomeData) {
$domain = request()->getHost();
if (str_contains($domain, 'pressekonto')) {
return redirect(request()->getSchemeAndHttpHost().'/', 301);
}
[$domainKey, $portal, $view] = match (true) {
str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho, 'web.presseecho'],
default => ['businessportal24', Portal::Businessportal24, 'web.businessportal24'],
};
$applyWebDomainConfig($domainKey);
return view($view, $webHomeData($portal, app()->getLocale()));
})->name('web.home');
// Kategorie / Branchenseite (z.B. /de/category/energie-klima)
Route::get('category/{slug}', $categoryPage)->name('kategorie');
// Detailseite einer Pressemitteilung (z.B. /de/press-release/ki-revolution)
Route::get('press-release/{slug}', $releaseDetailPage)->name('release.detail');
// Statische Seiten
Route::view('preise', 'web.preise')->name('preise');
Route::view('faq', 'web.faq')->name('faq');
Route::view('kontakt', 'web.kontakt')->name('kontakt');
Route::view('ueber-uns', 'web.ueber-uns')->name('ueber-uns');
Route::get('veroeffentlichen', $veroeffentlichenPage)->name('veroeffentlichen');
Route::get('suche', $suchePage)->name('suche');
Route::view('newsrooms', 'web.newsrooms')->name('newsrooms');
Route::view('api', 'web.api')->name('api');
Route::view('team', 'web.team')->name('team');
Route::view('partner', 'web.partner')->name('partner');
Route::view('karriere', 'web.karriere')->name('karriere');
Route::view('presse', 'web.presse')->name('presse');
Route::view('hilfe', 'web.hilfe')->name('hilfe');
Route::view('impressum', 'web.impressum')->name('impressum');
Route::view('datenschutz', 'web.datenschutz')->name('datenschutz');
Route::view('agb', 'web.agb')->name('agb');
Route::view('cookies', 'web.cookies')->name('cookies');
});
// ---------------------------------------------------------------------------
// Sprach-/Edition-unabhängige Routen (Dev-Varianten, API-Doku).
// ---------------------------------------------------------------------------
Route::get('/variant-1', function () {
$domain = request()->getHost();
// Domain-basierte View-Auswahl
if (str_contains($domain, 'presseecho')) {
return view('web.presseecho');
} elseif (str_contains($domain, 'businessportal24')) {
return view('web.businessportal24-variant-float-glow');
}
return view('web.businessportal24-variant-float-glow');
@ -216,108 +794,15 @@ Route::get('/variant-1', function () {
Route::get('/variant-2', function () {
$domain = request()->getHost();
// Domain-basierte View-Auswahl
if (str_contains($domain, 'presseecho')) {
return view('web.presseecho');
} elseif (str_contains($domain, 'businessportal24')) {
return view('web.businessportal24-variant-glass-gradient');
}
return view('web.businessportal24-variant-glass-gradient');
});
// Preise & Leistungen
Route::get('/preise', function () {
return view('web.preise');
})->name('preise');
// FAQ - Häufig gestellte Fragen
Route::get('/faq', function () {
return view('web.faq');
})->name('faq');
// Kontakt
Route::get('/kontakt', function () {
return view('web.kontakt');
})->name('kontakt');
// Über uns
Route::get('/ueber-uns', function () {
return view('web.ueber-uns');
})->name('ueber-uns');
// Veröffentlichen
Route::get('/veroeffentlichen', function () {
return view('web.veroeffentlichen');
})->name('veroeffentlichen');
// Kategorien
Route::get('/kategorien', function () {
return view('web.kategorien');
})->name('kategorien');
// Suche
Route::get('/suche', function () {
return view('web.suche');
})->name('suche');
// Services
Route::get('/newsrooms', function () {
return view('web.newsrooms');
})->name('newsrooms');
Route::get('/api', function () {
return view('web.api');
})->name('api');
Route::get('/docs/api/v1', function () {
return response((string) file_get_contents(base_path('docs/api/v1.yml')), 200, [
'Content-Type' => 'application/yaml; charset=UTF-8',
]);
})->name('docs.api.v1');
// Über uns - Unterseiten
Route::get('/team', function () {
return view('web.team');
})->name('team');
Route::get('/partner', function () {
return view('web.partner');
})->name('partner');
Route::get('/karriere', function () {
return view('web.karriere');
})->name('karriere');
Route::get('/presse', function () {
return view('web.presse');
})->name('presse');
// Hilfe & Support
Route::get('/hilfe', function () {
return view('web.hilfe');
})->name('hilfe');
// Rechtliches
Route::get('/impressum', function () {
return view('web.impressum');
})->name('impressum');
Route::get('/datenschutz', function () {
return view('web.datenschutz');
})->name('datenschutz');
Route::get('/agb', function () {
return view('web.agb');
})->name('agb');
Route::get('/cookies', function () {
return view('web.cookies');
})->name('cookies');
// Kategorie Seite (z.B. IT & Software)
Route::get('/kategorie/{slug}', function ($slug) {
return view('web.kategorie', ['categorySlug' => $slug]);
})->name('kategorie');
// Release Detail Seite
Route::get('/release/{slug}', function ($slug) {
return view('web.release-detail', ['releaseSlug' => $slug]);
})->name('release.detail');

View file

@ -9,7 +9,7 @@ use Tests\TestCase;
test('businessportal24 homepage renders the editorial shell', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/')
$this->get('https://businessportal24.test/de')
->assertSuccessful()
->assertSee('businessportal', false)
->assertSeeText('Pressemitteilungen · DACH')
@ -20,7 +20,6 @@ test('businessportal24 homepage renders the editorial shell', function () {
->assertSeeText('Veröffentlichen Sie Ihre Pressemitteilung')
->assertSeeText('Redaktioneller Qualitätsstandard')
->assertSeeText('AD-HOC')
->assertSeeText('Termine & Events')
->assertSeeText('Branchen-Index')
->assertSeeText('Heute im Fokus')
->assertSee('businessportal', false)
@ -89,7 +88,7 @@ test('businessportal24 homepage feed only shows published businessportal content
'published_at' => now()->subDay(),
]);
$this->get('https://businessportal24.test/')
$this->get('https://businessportal24.test/de')
->assertSuccessful()
->assertSeeText('BusinessPortal24 sichtbare Meldung')
->assertSeeText('Gemeinsame sichtbare Meldung')
@ -136,7 +135,7 @@ test('businessportal24 homepage shows most read releases in the sidebar', functi
'published_at' => now()->subDays(2),
]);
$this->get('https://businessportal24.test/')
$this->get('https://businessportal24.test/de')
->assertSuccessful()
->assertSeeText('Meistgelesen')
->assertSeeText('Meistgelesene BP24-Meldung')

View file

@ -1,20 +1,36 @@
<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use Tests\TestCase;
test('the release detail page emits an article canonical from its slug', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/release/ki-revolution')
$category = Category::factory()->create(['portal' => Portal::Businessportal24]);
$category->translations()->create(['locale' => 'de', 'name' => 'Technologie', 'slug' => 'technologie']);
$company = Company::factory()->businessportal24()->create();
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create(['slug' => 'ki-revolution', 'published_at' => now()->subDay()]);
$this->get('https://businessportal24.test/de/press-release/ki-revolution')
->assertSuccessful()
->assertSee('<link rel="canonical" href="', false)
->assertSee('/release/ki-revolution', false)
->assertSee('/de/press-release/ki-revolution', false)
->assertSee('<meta property="og:type" content="article">', false)
->assertSee('<meta property="og:url" content="', false);
});
test('a portal homepage emits a self-referencing canonical for its own domain', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/')
$this->get('https://businessportal24.test/de')
->assertSuccessful()
->assertSee('<link rel="canonical" href="https://businessportal24.test', false)
->assertSee('<meta property="og:site_name" content="', false);
@ -22,7 +38,7 @@ test('a portal homepage emits a self-referencing canonical for its own domain',
test('portals stay self-canonical on their own host, never cross-portal', function () {
/** @var TestCase $this */
$this->get('https://presseecho.test/')
$this->get('https://presseecho.test/de')
->assertSuccessful()
->assertSee('<link rel="canonical" href="https://presseecho.test', false)
->assertDontSee('businessportal24.test', false);

View file

@ -0,0 +1,53 @@
<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\PressRelease;
use Tests\TestCase;
function seedNavigationCategory(string $name, string $slug): Category
{
$category = Category::factory()->create(['portal' => Portal::Both, 'is_active' => true]);
$category->translations()->create(['locale' => 'de', 'name' => $name, 'slug' => $slug]);
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->create(['language' => 'de', 'published_at' => now()->subDay()]);
return $category;
}
test('the header navigation lists real categories that link to their category page', function () {
/** @var TestCase $this */
seedNavigationCategory('Energie & Klima', 'energie-klima');
$this->get('https://businessportal24.test/de')
->assertSuccessful()
->assertSee('/de/category/energie-klima', false)
->assertSeeText('Energie & Klima');
});
test('there is no standalone category overview page anymore', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/kategorien')->assertNotFound();
});
test('a legacy category overview url redirects to the edition home', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/kategorien.html')
->assertRedirect('https://businessportal24.test/de');
$this->get('https://businessportal24.test/kategorien.html')
->assertRedirect('https://businessportal24.test/de');
});
test('the navigation marks the active category on its own page', function () {
/** @var TestCase $this */
seedNavigationCategory('Mobilität', 'mobilitaet');
$this->get('https://businessportal24.test/de/category/mobilitaet')
->assertSuccessful()
->assertSee('/de/category/mobilitaet', false);
});

View file

@ -0,0 +1,79 @@
<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use Tests\TestCase;
function makeCategory(Portal $portal, string $name, string $slug): Category
{
$category = Category::factory()->create(['portal' => $portal]);
$category->translations()->create([
'locale' => 'de',
'name' => $name,
'slug' => $slug,
'description' => 'Testbeschreibung für '.$name.'.',
]);
return $category;
}
test('businessportal24 category page renders the editorial shell with real data', function () {
/** @var TestCase $this */
$category = makeCategory(Portal::Both, 'Energie & Klima', 'energie-klima');
$company = Company::factory()->businessportal24()->create([
'name' => 'Nordwind Energie AG',
]);
foreach (range(0, 3) as $i) {
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create([
'title' => "Energie Branchenmeldung {$i}",
'slug' => "energie-branchenmeldung-{$i}",
'hits' => 100 * ($i + 1),
'published_at' => now()->subDays($i + 1),
]);
}
$this->get('https://businessportal24.test/de/category/energie-klima')
->assertSuccessful()
->assertSeeText('Energie & Klima')
->assertSeeText('Aktuelle Meldungen')
->assertSeeText('Energie Branchenmeldung 0')
->assertSeeText('Nordwind Energie AG')
->assertSeeText('Meistgelesen');
});
test('category page returns 404 for unknown and inactive categories', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/category/gibt-es-nicht')->assertNotFound();
$inactive = Category::factory()->create(['portal' => Portal::Both, 'is_active' => false]);
$inactive->translations()->create(['locale' => 'de', 'name' => 'Versteckt', 'slug' => 'versteckt']);
$this->get('https://businessportal24.test/de/category/versteckt')->assertNotFound();
});
test('category page does not expose categories from another portal', function () {
/** @var TestCase $this */
$category = makeCategory(Portal::Presseecho, 'Nur Presseecho', 'nur-presseecho');
PressRelease::factory()
->published()
->forPortal(Portal::Presseecho)
->for($category)
->for(Company::factory()->presseecho())
->create(['slug' => 'pe-branchenmeldung', 'published_at' => now()->subDay()]);
$this->get('https://businessportal24.test/de/category/nur-presseecho')->assertNotFound();
$this->get('https://presseecho.test/de/category/nur-presseecho')
->assertSuccessful()
->assertSeeText('Nur Presseecho');
});

View file

@ -0,0 +1,115 @@
<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use Tests\TestCase;
test('the bare root redirects to the default edition with a 301', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/')
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de');
$this->get('https://presseecho.test/')
->assertStatus(301)
->assertRedirect('https://presseecho.test/de');
});
test('both editions render the homepage', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de')->assertSuccessful();
$this->get('https://businessportal24.test/en')->assertSuccessful();
});
test('an unsupported edition prefix returns 404', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/fr')->assertNotFound();
});
test('legacy .html urls redirect to clean canonical urls with a 301', function () {
/** @var TestCase $this */
// Flache Detail-URL der Altseite -> /press-release/{slug}
$this->get('https://businessportal24.test/de/steuerlast-spitzenverdiener.html')
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de/press-release/steuerlast-spitzenverdiener');
// Kategorie behält ihren Pfad, nur .html fällt weg.
$this->get('https://businessportal24.test/de/category/politik.html')
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de/category/politik');
// Statische Seite behält ihren Pfad.
$this->get('https://businessportal24.test/de/preise.html')
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de/preise');
// Englische Ausgabe ebenso.
$this->get('https://businessportal24.test/en/some-news.html')
->assertStatus(301)
->assertRedirect('https://businessportal24.test/en/press-release/some-news');
});
test('a legacy .html url without an edition prefix lands on the default edition', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/category/handel.html')
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de/category/handel');
});
test('the english edition only serves english content', function () {
/** @var TestCase $this */
$category = Category::factory()->create(['portal' => Portal::Both]);
$category->translations()->create(['locale' => 'de', 'name' => 'Politik', 'slug' => 'politik']);
$category->translations()->create(['locale' => 'en', 'name' => 'Politics', 'slug' => 'politics']);
$company = Company::factory()->businessportal24()->create(['name' => 'Global News AG']);
$german = PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create([
'title' => 'Deutsche Meldung',
'slug' => 'deutsche-meldung',
'language' => 'de',
'published_at' => now()->subDay(),
]);
$english = PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create([
'title' => 'English Story',
'slug' => 'english-story',
'language' => 'en',
'published_at' => now()->subDay(),
]);
// Englische Detailseite zeigt nur englische Meldungen.
$this->get('https://businessportal24.test/en/press-release/'.$english->slug)
->assertSuccessful()
->assertSeeText('English Story');
$this->get('https://businessportal24.test/en/press-release/'.$german->slug)
->assertNotFound();
// Deutsche Detailseite umgekehrt.
$this->get('https://businessportal24.test/de/press-release/'.$german->slug)
->assertSuccessful()
->assertSeeText('Deutsche Meldung');
$this->get('https://businessportal24.test/de/press-release/'.$english->slug)
->assertNotFound();
// Kategorie wird über den sprachspezifischen Slug aufgelöst.
$this->get('https://businessportal24.test/en/category/politics')
->assertSuccessful();
$this->get('https://businessportal24.test/en/category/politik')
->assertNotFound();
});

View file

@ -0,0 +1,52 @@
<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use Tests\TestCase;
test('homepage anchors recency to the latest release so spotlight, newsrooms and ticker use real data', function () {
/** @var TestCase $this */
$category = Category::factory()->create([
'portal' => Portal::Both,
]);
$category->translations()->create([
'locale' => 'de',
'name' => 'Energie & Klima',
'slug' => 'energie-klima',
]);
$company = Company::factory()->businessportal24()->create([
'name' => 'Nordwind Energie AG',
]);
// Historisches Archiv: jüngster Datensatz liegt 40 Tage in der Vergangenheit.
$anchor = now()->subDays(40);
foreach (range(0, 2) as $i) {
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create([
'title' => "Echte Spotlight Meldung {$i}",
'slug' => "echte-spotlight-meldung-{$i}",
'published_at' => $anchor->copy()->subDays($i),
]);
}
$this->get('https://businessportal24.test/de')
->assertSuccessful()
// Aktive Newsrooms zeigt echte Firma statt Mock.
->assertSeeText('Nordwind Energie AG')
->assertDontSeeText('Siemens AG')
// Heute im Fokus + Branchen-Index nutzen echte Rubrik.
->assertSeeText('Energie & Klima')
// Spotlight + Feed zeigen echte Meldungen statt Mock.
->assertSeeText('Echte Spotlight Meldung 0')
->assertDontSeeText('RWE AG')
// Ad-Hoc-Ticker zeigt keine Mock-Meldungen mehr.
->assertDontSeeText('Siemens Energy hebt Jahresprognose an');
});

View file

@ -9,7 +9,7 @@ use Tests\TestCase;
test('presseecho homepage renders the editorial shell', function () {
/** @var TestCase $this */
$this->get('https://presseecho.test/')
$this->get('https://presseecho.test/de')
->assertSuccessful()
->assertSee('presseecho', false)
->assertSeeText('Pressemitteilungen · DACH')
@ -20,7 +20,6 @@ test('presseecho homepage renders the editorial shell', function () {
->assertSeeText('Veröffentlichen Sie Ihre Pressemitteilung')
->assertSeeText('Redaktioneller Qualitätsstandard')
->assertSeeText('AD-HOC')
->assertSeeText('Termine & Events')
->assertSeeText('Branchen-Index')
->assertSeeText('Heute im Fokus');
});
@ -84,7 +83,7 @@ test('presseecho homepage feed only shows published presseecho content', functio
'published_at' => now()->subDay(),
]);
$this->get('https://presseecho.test/')
$this->get('https://presseecho.test/de')
->assertSuccessful()
->assertSeeText('Presseecho sichtbare Meldung')
->assertSeeText('Gemeinsame sichtbare Meldung')
@ -131,7 +130,7 @@ test('presseecho homepage shows most read releases in the sidebar', function ()
'published_at' => now()->subDays(2),
]);
$this->get('https://presseecho.test/')
$this->get('https://presseecho.test/de')
->assertSuccessful()
->assertSeeText('Meistgelesen')
->assertSeeText('Meistgelesene Presseecho-Meldung')

View file

@ -0,0 +1,169 @@
<?php
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Models\Category;
use App\Models\Company;
use App\Models\Contact;
use App\Models\PressRelease;
use Tests\TestCase;
/**
* @return array{0: Category, 1: Company}
*/
function detailCategoryAndCompany(Portal $portal): array
{
$category = Category::factory()->create(['portal' => $portal]);
$category->translations()->create([
'locale' => 'de',
'name' => 'Energie & Klima',
'slug' => 'energie-klima',
]);
$company = Company::factory()->create([
'portal' => $portal,
'name' => 'Muster Energie GmbH',
'is_active' => true,
'website' => 'https://muster-energie.de',
'boilerplate' => 'Die Muster Energie GmbH ist ein Spezialist für Industrie-Dekarbonisierung.',
]);
return [$category, $company];
}
test('businessportal24 release detail renders the editorial shell with real data', function () {
/** @var TestCase $this */
[$category, $company] = detailCategoryAndCompany(Portal::Businessportal24);
$release = PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create([
'title' => 'Energiewende 2030: 12-Milliarden-Programm angekündigt',
'subtitle' => 'Erste Tranche fließt ab dem dritten Quartal 2026.',
'slug' => 'energiewende-2030-programm',
'text' => '<p>Berlin. Das Programm beschleunigt die Dekarbonisierung der Schwerindustrie.</p>',
'keywords' => 'Energiewende, Dekarbonisierung, Wasserstoff',
'published_at' => now()->subDay(),
]);
$contact = Contact::factory()->create([
'portal' => Portal::Businessportal24,
'company_id' => $company->id,
'first_name' => 'Annika',
'last_name' => 'Werthmann',
'email' => 'presse@muster-energie.de',
]);
$release->contacts()->attach($contact);
$this->get('https://businessportal24.test/de/press-release/'.$release->slug)
->assertSuccessful()
->assertSee('businessportal', false)
->assertSeeText('Energiewende 2030: 12-Milliarden-Programm angekündigt')
->assertSeeText('Erste Tranche fließt ab dem dritten Quartal 2026.')
->assertSeeText('Muster Energie GmbH')
->assertSeeText('Energie & Klima')
->assertSeeText('Pressekontakt')
->assertSeeText('Annika Werthmann')
->assertSeeText('Dekarbonisierung der Schwerindustrie')
->assertSeeText('Über das Unternehmen')
->assertSeeText('Wasserstoff');
});
test('release detail counts a hit', function () {
/** @var TestCase $this */
[$category, $company] = detailCategoryAndCompany(Portal::Businessportal24);
$release = PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create(['slug' => 'hit-zaehler', 'hits' => 4, 'published_at' => now()->subDay()]);
$this->get('https://businessportal24.test/de/press-release/'.$release->slug)->assertSuccessful();
expect(PressRelease::withoutGlobalScopes()->find($release->id)->hits)->toBe(5);
});
test('release detail returns 404 for drafts and unknown slugs', function () {
/** @var TestCase $this */
[$category, $company] = detailCategoryAndCompany(Portal::Businessportal24);
$draft = PressRelease::factory()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create([
'status' => PressReleaseStatus::Draft,
'slug' => 'noch-ein-entwurf',
'published_at' => now()->subDay(),
]);
$this->get('https://businessportal24.test/de/press-release/'.$draft->slug)->assertNotFound();
$this->get('https://businessportal24.test/de/press-release/gibt-es-nicht')->assertNotFound();
});
test('release detail does not expose releases from another portal', function () {
/** @var TestCase $this */
[$category, $company] = detailCategoryAndCompany(Portal::Presseecho);
$release = PressRelease::factory()
->published()
->forPortal(Portal::Presseecho)
->for($category)
->for($company)
->create([
'title' => 'Nur für presseecho sichtbar',
'slug' => 'nur-presseecho',
'published_at' => now()->subDay(),
]);
$this->get('https://businessportal24.test/de/press-release/'.$release->slug)->assertNotFound();
$this->get('https://presseecho.test/de/press-release/'.$release->slug)
->assertSuccessful()
->assertSee('presse', false)
->assertSeeText('Nur für presseecho sichtbar');
});
test('release detail shows more from newsroom and related releases', function () {
/** @var TestCase $this */
[$category, $company] = detailCategoryAndCompany(Portal::Businessportal24);
$main = PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create(['title' => 'Hauptmeldung Energie', 'slug' => 'hauptmeldung-energie', 'published_at' => now()->subDay()]);
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create(['title' => 'Weitere Meldung desselben Newsrooms', 'slug' => 'weitere-newsroom-meldung', 'published_at' => now()->subDays(2)]);
$otherCompany = Company::factory()->create([
'portal' => Portal::Businessportal24,
'name' => 'Andere Industrie AG',
'is_active' => true,
]);
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($otherCompany)
->create(['title' => 'Verwandte Branchenmeldung', 'slug' => 'verwandte-branchenmeldung', 'published_at' => now()->subDays(3)]);
$this->get('https://businessportal24.test/de/press-release/'.$main->slug)
->assertSuccessful()
->assertSeeText('Mehr von Muster Energie GmbH')
->assertSeeText('Weitere Meldung desselben Newsrooms')
->assertSeeText('Verwandte Meldungen')
->assertSeeText('Verwandte Branchenmeldung');
});

View file

@ -0,0 +1,85 @@
<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use Illuminate\Support\Str;
use Livewire\Volt\Volt;
use Tests\TestCase;
function createSearchableRelease(string $title, array $attributes = [], ?Category $category = null, ?Company $company = null): PressRelease
{
$company ??= Company::factory()->businessportal24()->create();
$category ??= Category::factory()->create(['portal' => Portal::Both]);
return PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create(array_merge([
'title' => $title,
'slug' => Str::slug($title).'-'.fake()->unique()->numberBetween(1000, 9999),
'language' => 'de',
'published_at' => now()->subDay(),
], $attributes));
}
test('the search page renders the editorial shell and discovery state', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/suche')
->assertSuccessful()
->assertSee('businessportal', false)
->assertSeeText('Pressemitteilungen durchsuchen')
->assertSeeText('Durchsuchen Sie das Archiv');
});
test('the search page returns matching press releases for a query', function () {
/** @var TestCase $this */
createSearchableRelease('Mittelständischer Fertiger eröffnet neues Werk');
$this->get('https://businessportal24.test/de/suche?q=Werk')
->assertSuccessful()
->assertSeeText('Mittelständischer Fertiger eröffnet neues Werk')
->assertSeeText('Ergebnis');
});
test('the search component filters by query and resets', function () {
/** @var TestCase $this */
createSearchableRelease('Solarpark geht ans Netz');
createSearchableRelease('Neue Geschäftsführung berufen');
Volt::test('web.search', ['portal' => Portal::Businessportal24->value, 'language' => 'de'])
->set('q', 'Solarpark')
->assertSee('Solarpark geht ans Netz')
->assertDontSee('Neue Geschäftsführung berufen')
->call('clearFilters')
->assertSet('q', '')
->assertSee('Durchsuchen Sie das Archiv');
});
test('the search component filters by category', function () {
/** @var TestCase $this */
$energie = Category::factory()->create(['portal' => Portal::Both]);
$energie->translations()->create(['locale' => 'de', 'name' => 'Energie', 'slug' => 'energie']);
$finanzen = Category::factory()->create(['portal' => Portal::Both]);
$finanzen->translations()->create(['locale' => 'de', 'name' => 'Finanzen', 'slug' => 'finanzen']);
createSearchableRelease('Windkraft-Ausbau beschleunigt', category: $energie);
createSearchableRelease('Quartalszahlen veröffentlicht', category: $finanzen);
Volt::test('web.search', ['portal' => Portal::Businessportal24->value, 'language' => 'de'])
->call('selectCategory', 'energie')
->assertSee('Windkraft-Ausbau beschleunigt')
->assertDontSee('Quartalszahlen veröffentlicht');
});
test('the search page is available per edition and portal', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/en/suche')->assertSuccessful();
$this->get('https://presseecho.test/de/suche')
->assertSuccessful()
->assertSee('presse', false);
});

View file

@ -0,0 +1,54 @@
<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use Tests\TestCase;
test('the veroeffentlichen page renders the editorial shell', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/veroeffentlichen')
->assertSuccessful()
->assertSee('businessportal', false)
->assertSeeText('Pressemitteilung veröffentlichen.')
->assertSeeText('Vier Prinzipien, die diese Plattform tragen')
->assertSeeText('drei Schritte')
->assertSeeText('Typische Anlässe einer Veröffentlichung')
->assertSeeText('Sie zahlen, was Sie einreichen.')
->assertSeeText('Kurz beantwortet')
->assertSeeText('Bereit, Ihre Mitteilung einzureichen?');
});
test('the veroeffentlichen page surfaces real newsrooms and an example release', function () {
/** @var TestCase $this */
$category = Category::factory()->create(['portal' => Portal::Both]);
$category->translations()->create(['locale' => 'de', 'name' => 'Maschinenbau', 'slug' => 'maschinenbau']);
$company = Company::factory()->businessportal24()->create(['name' => 'Brinkmann Präzisionstechnik GmbH']);
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create([
'title' => 'Fertiger eröffnet neues Werk im Westmünsterland',
'slug' => 'fertiger-eroeffnet-werk',
'published_at' => now()->subDay(),
]);
$this->get('https://businessportal24.test/de/veroeffentlichen')
->assertSuccessful()
->assertSeeText('Brinkmann Präzisionstechnik GmbH')
->assertSeeText('Fertiger eröffnet neues Werk im Westmünsterland')
->assertSeeText('Maschinenbau');
});
test('the veroeffentlichen page is available per edition and portal', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/en/veroeffentlichen')->assertSuccessful();
$this->get('https://presseecho.test/de/veroeffentlichen')
->assertSuccessful()
->assertSee('presse', false);
});