Startseite und Heroes - Hero-Slideshow (11 Motive inkl. Febrü) mit Infokarte, Laufzeit-Linie, größeren Klickflächen, Goldrahmen und mobiler Karte unter dem Bild; Zeile über der Überschrift, neue Vertrauenspunkte - Hero-Stil auf Marken, Über uns und Kontakt übertragen, Breadcrumb entfernt - Gründer-Leiste, Vision-Box auf Ocker, Showroom-Display in Box mit „Größer ansehen“/Vollbild - Akzentfarbe #a8741f, größere Serif-Überschriften in der Interior-Welt - Marken: Bildpause mit Zitat; Vereine aktualisiert und verlinkt Inhalte - Lektorat aller angezeigten Texte, /about-Korrekturen, Rechtstexte (DDG, TDDDG, Hosting Hetzner) - Ablauf Maßanfertigung überall einheitlich, Magazin-Beitrag neu, alte Beiträge offline, deutsches Datum/Lesezeit, FAQ Projektausstattung, Otterpohl - Referenzen-Seite per CMS-Schalter ausblendbar CMS und Technik - Tote Sektionen, Felder, Seiten und Komponenten entfernt und archiviert (dev/2026-08-28/archiv-*) - Entwurf /entwurf, Farb-Schalter und /welcome entfernt; Test-Routen nur noch lokal - Seitenbaum-Beschreibungen geprüft, fehlende CMS-Felder ergänzt, Markenliste aus „Kernmarken“ - Admin-Bildvorschauen repariert, verwendete Bilder in die Mediathek übernommen - Seeder für das Deployment siehe dev/2026-08-28/entwicklungsplan.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
5.8 KiB
PHP
208 lines
5.8 KiB
PHP
<?php
|
||
|
||
namespace App\Models;
|
||
|
||
use Illuminate\Database\Eloquent\Builder;
|
||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use Spatie\Translatable\HasTranslations;
|
||
|
||
class CmsArticle extends Model
|
||
{
|
||
use HasFactory, HasTranslations;
|
||
|
||
protected $table = 'cms_articles';
|
||
|
||
/**
|
||
* Alte numerische Artikel-IDs (resources/lang/{locale}/b2in.php) → Slug.
|
||
*
|
||
* Wird an drei Stellen gebraucht: beim Anlegen durch den CmsArticleSeeder, für den
|
||
* Lang-Fallback und für die 301-Weiterleitung der alten /magazin/{id}-URLs.
|
||
*
|
||
* @var array<int, string>
|
||
*/
|
||
public const LEGACY_ID_TO_SLUG = [
|
||
1 => 'escrow-system-dubai-investoren',
|
||
2 => 'spotlight-al-jaddaf-hotspot',
|
||
3 => 'turnkey-investments-moeblierung-rendite',
|
||
4 => 'supply-chain-management-vertragssicherheit',
|
||
5 => 'local-for-local-regionaler-moebelhandel',
|
||
];
|
||
|
||
public const WORLD_INTERIOR = 'interior';
|
||
|
||
public const WORLD_IMMOBILIEN = 'immobilien';
|
||
|
||
/** @var array<int, string> */
|
||
public const WORLDS = [self::WORLD_INTERIOR, self::WORLD_IMMOBILIEN];
|
||
|
||
/**
|
||
* Beiträge aus dem Bestand, die zur Immobilien-Welt gehören (Dubai-Themen).
|
||
*
|
||
* @var array<int, string>
|
||
*/
|
||
public const LEGACY_IMMOBILIEN_SLUGS = [
|
||
'escrow-system-dubai-investoren',
|
||
'spotlight-al-jaddaf-hotspot',
|
||
'turnkey-investments-moeblierung-rendite',
|
||
];
|
||
|
||
protected $fillable = [
|
||
'slug',
|
||
'title',
|
||
'subtitle',
|
||
'image',
|
||
'category',
|
||
'world',
|
||
'date_label',
|
||
'read_time',
|
||
'author',
|
||
'content',
|
||
'is_published',
|
||
'order',
|
||
];
|
||
|
||
/** @var array<string> */
|
||
public array $translatable = [
|
||
'title',
|
||
'subtitle',
|
||
'content',
|
||
];
|
||
|
||
protected function casts(): array
|
||
{
|
||
return [
|
||
'author' => 'array',
|
||
'is_published' => 'boolean',
|
||
'order' => 'integer',
|
||
];
|
||
}
|
||
|
||
public function scopePublished(Builder $query): Builder
|
||
{
|
||
return $query->where('is_published', true);
|
||
}
|
||
|
||
public function scopeOrdered(Builder $query): Builder
|
||
{
|
||
return $query->orderBy('order')->orderByDesc('created_at');
|
||
}
|
||
|
||
public function scopeForWorld(Builder $query, string $world): Builder
|
||
{
|
||
return $query->where('world', $world);
|
||
}
|
||
|
||
/**
|
||
* Veröffentlichte Artikel fürs Frontend, nach Slug indiziert.
|
||
*
|
||
* Das CMS gewinnt. Solange keine Artikel gepflegt sind, greift die Lang-Datei als Fallback –
|
||
* dieselbe Reihenfolge wie bei cms_theme_section(), damit das Magazin auf einer frischen
|
||
* Installation nicht leer ist.
|
||
*
|
||
* Mit `$world` nur die Beiträge dieser Farbwelt (Interior oder Immobilien) – die beiden
|
||
* Magazinlisten sind getrennt, die Detailseite rendert in der Welt ihres Beitrags.
|
||
*
|
||
* @return array<string, array<string, mixed>>
|
||
*/
|
||
public static function forFrontend(?string $world = null): array
|
||
{
|
||
$maintained = static::query()->published()->exists();
|
||
|
||
if ($maintained) {
|
||
return static::query()
|
||
->published()
|
||
->when($world !== null, fn (Builder $query) => $query->forWorld($world))
|
||
->ordered()
|
||
->get()
|
||
->mapWithKeys(fn (self $article) => [$article->slug => $article->toFrontendArray()])
|
||
->all();
|
||
}
|
||
|
||
$articles = static::fallbackFromLang();
|
||
|
||
if ($world === null) {
|
||
return $articles;
|
||
}
|
||
|
||
return array_filter($articles, fn (array $article) => ($article['world'] ?? self::WORLD_INTERIOR) === $world);
|
||
}
|
||
|
||
/**
|
||
* Slug zu einer alten numerischen Magazin-ID, falls bekannt.
|
||
*/
|
||
public static function slugForLegacyId(int|string $id): ?string
|
||
{
|
||
return self::LEGACY_ID_TO_SLUG[(int) $id] ?? null;
|
||
}
|
||
|
||
/**
|
||
* Slug eines Artikels aus der Lang-Datei.
|
||
*
|
||
* Neue Artikel bringen ihren Slug selbst mit (`'slug' => …`). Die fünf ursprünglichen
|
||
* Artikel hatten keinen und werden weiter über {@see LEGACY_ID_TO_SLUG} aufgelöst – die
|
||
* Konstante bleibt damit das, was sie ist: die Karte für die alten `/magazin/{id}`-URLs.
|
||
*
|
||
* @param array<string, mixed> $article
|
||
*/
|
||
public static function slugForLangArticle(int|string $id, array $article): ?string
|
||
{
|
||
$slug = $article['slug'] ?? null;
|
||
|
||
if (is_string($slug) && $slug !== '') {
|
||
return $slug;
|
||
}
|
||
|
||
return self::slugForLegacyId($id);
|
||
}
|
||
|
||
/**
|
||
* @return array<string, array<string, mixed>>
|
||
*/
|
||
private static function fallbackFromLang(): array
|
||
{
|
||
$articles = trans('b2in.articles');
|
||
|
||
if (! is_array($articles)) {
|
||
return [];
|
||
}
|
||
|
||
$out = [];
|
||
|
||
foreach ($articles as $id => $article) {
|
||
if (! is_array($article) || ($article['published'] ?? true) === false) {
|
||
continue;
|
||
}
|
||
|
||
$slug = self::slugForLangArticle($id, $article);
|
||
|
||
if ($slug === null) {
|
||
continue;
|
||
}
|
||
|
||
$out[$slug] = $article + ['slug' => $slug, 'world' => self::WORLD_INTERIOR];
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* @return array<string, mixed>
|
||
*/
|
||
public function toFrontendArray(): array
|
||
{
|
||
return [
|
||
'id' => $this->id,
|
||
'slug' => $this->slug,
|
||
'title' => $this->title,
|
||
'subtitle' => $this->subtitle,
|
||
'image' => $this->image,
|
||
'category' => $this->category,
|
||
'world' => $this->world ?? self::WORLD_INTERIOR,
|
||
'date' => $this->date_label,
|
||
'readTime' => $this->read_time,
|
||
'author' => $this->author ?? [],
|
||
'content' => $this->content ?? [],
|
||
];
|
||
}
|
||
}
|