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>
287 lines
20 KiB
PHP
287 lines
20 KiB
PHP
<?php
|
||
|
||
namespace Database\Seeders;
|
||
|
||
use App\Models\CmsArticle;
|
||
use Database\Seeders\Concerns\ReplacesCmsTexts;
|
||
use FluxCms\Core\Models\CmsContent;
|
||
use FluxCms\Core\Services\CmsContentService;
|
||
use Illuminate\Database\Seeder;
|
||
|
||
/**
|
||
* Korrekturen vom 08.10.2026 (dev/2026-08-28/Korrekturen-2026-10-08.md):
|
||
*
|
||
* - Hero-Slideshow: zwei Febrü-Motive an Position 4 und 9.
|
||
* - „Regional verbunden“: die tatsächlich unterstützten Vereine (Arminia, TC Bad Salzuflen,
|
||
* TC Hiddenhausen, TC Blau Weiss Halle) mit einheitlichen Kurztexten.
|
||
* - Ablauf der Maßanfertigung überall gleich: grobe Maße für Beratung, Planung und Angebot –
|
||
* das professionelle Aufmaß erst nach Angebotsannahme und Anzahlung (CABINET, Herkunft,
|
||
* Netzwerk, Über uns, FAQ).
|
||
* - Magazin: Maßanfertigungs-Beitrag neu (aus der Lang-Datei), allgemeiner Abschluss statt
|
||
* Immobilien-Hinweis, „Supply-Chain“ und „Local for Local“ offline, Datum und Lesezeit deutsch.
|
||
* - FAQ Projektausstattung ohne Garantie-/Eskalationssprache, Otterpohl „erhöht die
|
||
* Planungssicherheit deutlich“.
|
||
*
|
||
* Die neuen Texte stehen auch in den Lang-Dateien und Quell-Seedern (frische Installation).
|
||
* Ersetzt wird nur der bisherige Wortlaut – im CMS selbst Umformuliertes bleibt.
|
||
* Mehrfaches Ausführen ist unbedenklich.
|
||
*
|
||
* php artisan db:seed --class=ContentCorrections20261008Seeder
|
||
*/
|
||
class ContentCorrections20261008Seeder extends Seeder
|
||
{
|
||
use ReplacesCmsTexts;
|
||
|
||
/** @var array<string, string> Teiltexte, die ersetzt werden (Deutsch und Englisch) */
|
||
public const REPLACEMENTS = [
|
||
// CABINET: „Ein Bild und ein grobes Maß reichen für den Anfang“
|
||
'So läuft’s: Foto + grobe Maße → Beratung → Planung & Angebot → Aufmaß vor Ort → Feinplanung → Bestellung' => 'So läuft’s: Foto / Grundriss + grobe Maße → Beratung → Planung & Angebot → Angebotsannahme → Anzahlung → professionelles Aufmaß → Feinplanung / Fertigungsfreigabe → Bestellung / Fertigung',
|
||
'Für den Anfang reichen uns ein Bild und ein grobes Maß völlig aus.' => 'Am Anfang reicht das grobe Maß. Am Ende entscheidet der Millimeter.',
|
||
'Sie müssen also nicht erst alles millimetergenau ausmessen. Ein paar Fotos von der Raumsituation und die ungefähren Maße reichen, damit wir schon eine gute Planung machen und Ihnen ein konkretes Angebot erstellen können.' => 'Sie müssen also nicht erst alles millimetergenau ausmessen, bevor Sie zu uns kommen. Ein paar Fotos, ein Grundriss und die ungefähren Maße reichen, damit wir Sie beraten, planen und Ihnen ein konkretes Angebot erstellen können.',
|
||
'Wenn alles grundsätzlich passt, kommen wir zum professionellen Aufmaß bei Ihnen vor Ort. Danach machen wir die Feinplanung fertig und geben den Schrank zur Bestellung frei.' => 'Wenn Sie das Angebot annehmen und die Anzahlung erfolgt ist, übernehmen wir das professionelle Aufmaß bei Ihnen vor Ort. Ab jetzt zählt jeder Millimeter: Auf Grundlage dieser Maße machen wir die Feinplanung fertig, geben die Fertigung frei und bestellen Ihren Schrank.',
|
||
'Wir messen millimetergenau in Ihren Räumen – Grundlage jeder Planung.' => 'Nach Angebotsannahme und Anzahlung erfolgt das professionelle Aufmaß bei Ihnen vor Ort. Es bildet die Grundlage für die finale Feinplanung und Fertigungsfreigabe.',
|
||
|
||
// „Aus der Region gefertigt“ – Ablauf am Beispiel CABINET
|
||
'Beraten, aufgemessen und geplant wird bei uns im CABINET Store Bielefeld.' => 'Beraten und geplant wird bei uns im CABINET Store Bielefeld, aufgemessen nach Ihrer Zusage bei Ihnen vor Ort.',
|
||
'Im CABINET Store nehmen wir Maß, planen bis ins Detail und stimmen jede Front und jede Innenaufteilung mit Ihnen ab.' => 'Im CABINET Store planen wir mit Ihnen bis ins Detail und stimmen jede Front und jede Innenaufteilung ab – für den Anfang reichen Fotos und grobe Maße. Nach Ihrer Zusage nehmen wir vor Ort exakt Maß.',
|
||
'CABINET fertigt Ihren Schrank in der eigenen Manufaktur – nach den Maßen aus der Planung.' => 'CABINET fertigt Ihren Schrank in der eigenen Manufaktur – nach den exakten Maßen aus dem Aufmaß vor Ort.',
|
||
'Consulting, measuring and planning happen with us at the CABINET Store Bielefeld. Assembly takes place on site – with our joinery,' => 'Consulting and planning happen with us at the CABINET Store Bielefeld; the exact measurements are taken at your home once you give the go-ahead. Assembly takes place on site – with the Otterpohl joinery,',
|
||
'At the CABINET Store we take measurements, plan down to the last detail and agree every front and interior layout with you.' => 'At the CABINET Store we plan with you down to the last detail and agree every front and interior layout – photos and rough measurements are enough to start. Once you give the go-ahead, we take exact measurements on site.',
|
||
'CABINET builds your wardrobe in its own manufactory – to the measurements from the planning.' => 'CABINET builds your wardrobe in its own manufactory – to the exact measurements taken on site.',
|
||
|
||
// Marken: „Mehr als Marken: unser Netzwerk“
|
||
'Wir kommen zu Ihnen, nehmen Maß und planen mit Ihnen gemeinsam.' => 'Wir planen mit Ihnen gemeinsam – zunächst nach Fotos und groben Maßen. Das exakte Aufmaß vor Ort übernehmen wir nach Ihrer Zusage.',
|
||
'We come to you, measure up and plan together with you.' => 'We plan together with you – first from photos and rough measurements. We take the exact measurements on site once you give the go-ahead.',
|
||
|
||
// Über uns: Zitat im Seitenkopf
|
||
'ein Ansprechpartner, vom ersten Aufmaß bis nach der Montage.' => 'ein Ansprechpartner, vom ersten Gespräch bis nach der Montage.',
|
||
'one point of contact, from the first measurement to after installation.' => 'one point of contact, from the first conversation to after installation.',
|
||
|
||
// Magazin: Abschluss jeder Beitragsseite
|
||
'Entdecken Sie mehr über <span class="text-white font-medium">Immobilien-Investments</span> und das <span class="text-white font-medium">B2in-Ökosystem</span>.' => 'Weitere Einblicke rund um Einrichtung, Planung und Projekte finden Sie im <span class="text-white font-medium">B2in Magazin</span>.',
|
||
'Discover more about <span class="text-white font-medium">real estate investments</span> and the <span class="text-white font-medium">B2in ecosystem</span>.' => 'Further insights into interiors, planning and projects can be found in the <span class="text-white font-medium">B2in Magazine</span>.',
|
||
|
||
// Otterpohl: Montage
|
||
'Das garantiert Ihnen absolute Planungssicherheit.' => 'Das erhöht die Planungssicherheit deutlich.',
|
||
];
|
||
|
||
/** @var array<string, string> Ganze Werte, die nur als Ganzes getauscht werden */
|
||
public const EXACT = [
|
||
'Aufmaß & Planung' => 'Planung & Aufmaß',
|
||
'Measuring & planning' => 'Planning & measuring',
|
||
|
||
// FAQ „Wie läuft eine Beratung bei Ihnen ab?“
|
||
'Am Anfang steht ein Gespräch – im Showroom, bei Ihnen zu Hause oder telefonisch. Wir schauen uns an, welche Räume es betrifft, wie Sie sie nutzen und was Ihnen wichtig ist. Danach folgt das Aufmaß vor Ort, anschließend die Planung. Sie sehen Ihre Lösung, bevor etwas gefertigt wird. Termine vereinbaren Sie über das Formular auf dieser Seite oder direkt telefonisch.' => self::FAQ_CONSULTATION_DE,
|
||
'It starts with a conversation – in the showroom, at your home or by phone. We look at the rooms involved, how you use them and what matters to you. Measuring up on site follows, then the planning. You see your solution before anything is made. Appointments can be arranged through the form on this page or by phone.' => self::FAQ_CONSULTATION_EN,
|
||
|
||
// FAQ „Was kostet eine Maßanfertigung …?“
|
||
'Das hängt vom Raum, den Materialien und der Ausstattung ab – eine seriöse Zahl lässt sich erst nach dem Aufmaß nennen. Was sich sagen lässt: Maßanfertigung nutzt Flächen, die ein Standardmöbel liegen lässt – Schrägen, Nischen, ungewöhnliche Höhen. Sie zahlen für den Raum, den Sie sonst verschenken. Das Angebot bekommen Sie vor der Fertigung, verbindlich und aufgeschlüsselt.' => self::FAQ_COST_DE,
|
||
'That depends on the room, the materials and the fittings – a serious figure is only possible after measuring up. What can be said: bespoke uses the space standard furniture leaves behind – slopes, niches, unusual heights. You pay for the space you would otherwise give away. You receive the quote before anything is made, binding and itemised.' => self::FAQ_COST_EN,
|
||
|
||
// FAQ „Was bedeutet Projektausstattung bei B2in?“
|
||
'Für Objekte, Büros und Projektentwicklungen übernehmen wir die vollständige Ausstattung – koordiniert aus einer Hand. Wir steuern die Beschaffung von Möbeln und Innenausstattung operativ und strategisch. Das bedeutet: Wir sichern Lieferverträge ab, überwachen Meilensteine direkt bei den Herstellern und eskalieren bei Abweichungen sofort auf Managementebene. Unser Ziel ist absolute Vertragssicherheit und termingerechte Lieferung ohne Reibungsverluste.' => self::FAQ_PROJECT_DE,
|
||
'For properties, offices and developments we take on the complete fit-out, coordinated from a single source. We manage the procurement of furniture and interiors both operationally and strategically: we secure supply contracts, monitor milestones directly with the manufacturers and escalate deviations immediately. The goal is contractual reliability and on-time delivery without friction.' => self::FAQ_PROJECT_EN,
|
||
];
|
||
|
||
public const FAQ_CONSULTATION_DE = 'Am Anfang steht ein Gespräch – im Showroom, telefonisch oder auf Wunsch bei Ihnen vor Ort. Für eine erste Planung reichen uns in der Regel Fotos, ein Grundriss und grobe Maße. Damit können wir bereits Ideen entwickeln, die Planung konkretisieren und Ihnen ein entsprechendes Angebot erstellen. Wenn Ihnen die Planung gefällt und Sie unser Angebot annehmen, erfolgt nach der Anzahlung das professionelle Aufmaß bei Ihnen vor Ort. Auf Grundlage dieser exakten Maße erstellen wir die Feinplanung und geben die Möbel anschließend zur Fertigung frei.';
|
||
|
||
public const FAQ_CONSULTATION_EN = 'It starts with a conversation – in the showroom, by phone or, if you like, at your home. For a first plan, photos, a floor plan and rough measurements are usually all we need. With these we can develop ideas, refine the planning and prepare a quote. If you like the plan and accept our quote, we take the professional measurements on site once the deposit has been paid. Based on these exact measurements we complete the detailed planning and then release the furniture for production.';
|
||
|
||
public const FAQ_COST_DE = 'Das hängt vom Raum, den Materialien und der Ausstattung ab. Bereits auf Basis von Fotos, Grundriss und groben Maßen können wir eine konkrete Planung und ein entsprechendes Angebot erstellen. Das professionelle Aufmaß erfolgt nach Angebotsannahme und Anzahlung und dient anschließend der finalen Feinplanung und Fertigungsfreigabe; zeigen sich dabei vor Ort relevante Abweichungen, stimmen wir Anpassungen mit Ihnen ab. Maßanfertigung nutzt Flächen, die ein Standardmöbel liegen lässt – Schrägen, Nischen, ungewöhnliche Höhen. Sie zahlen für den Raum, den Sie sonst verschenken.';
|
||
|
||
public const FAQ_COST_EN = 'That depends on the room, the materials and the fittings. Based on photos, a floor plan and rough measurements we can already prepare a concrete plan and a matching quote. The professional measurement on site follows once the quote is accepted and the deposit paid, and serves the final detailed planning and production release; if relevant deviations show up on site, we agree any adjustments with you. Bespoke uses the space standard furniture leaves behind – slopes, niches, unusual heights. You pay for the space you would otherwise give away.';
|
||
|
||
public const FAQ_PROJECT_DE = 'Für Objekte, Büros und Projektentwicklungen übernehmen wir die vollständige Einrichtung – koordiniert aus einer Hand, von der Küche bis zur letzten Blume. Sie haben einen zentralen deutschen Vertragspartner und einen festen Ansprechpartner, der Planung, Hersteller und Gewerke zusammenhält. Dahinter steht ein eingespieltes Herstellernetzwerk, dessen Abläufe wir kennen – so bleibt auch ein großes Projekt übersichtlich.';
|
||
|
||
public const FAQ_PROJECT_EN = 'For properties, offices and developments we take on the complete fit-out – coordinated from a single source, from the kitchen to the last flower. You have one central German contracting partner and a fixed point of contact who keeps planning, manufacturers and trades together. Behind this is a well-established network of manufacturers whose processes we know – so even a large project stays manageable.';
|
||
|
||
/** @var list<string> Beiträge aus der früheren Ausrichtung – offline, bis es neue gibt */
|
||
public const UNPUBLISHED_ARTICLES = [
|
||
'supply-chain-management-vertragssicherheit',
|
||
'local-for-local-regionaler-moebelhandel',
|
||
];
|
||
|
||
public const REWRITTEN_ARTICLE = 'massanfertigung-warum-der-millimeter-zaehlt';
|
||
|
||
/** @var list<string> Bisherige Vereinsnamen – solange einer davon drinsteht, kommt die neue Liste */
|
||
public const FORMER_SPONSORS = ['TC Bad Waldliesborn-Horn', 'TC Hövelhof'];
|
||
|
||
private const MONTHS = [
|
||
'januar' => 'Januar', 'january' => 'Januar', 'februar' => 'Februar', 'february' => 'Februar',
|
||
'märz' => 'März', 'march' => 'März', 'april' => 'April', 'mai' => 'Mai', 'may' => 'Mai',
|
||
'juni' => 'Juni', 'june' => 'Juni', 'juli' => 'Juli', 'july' => 'Juli', 'august' => 'August',
|
||
'september' => 'September', 'oktober' => 'Oktober', 'october' => 'Oktober', 'november' => 'November',
|
||
'dezember' => 'Dezember', 'december' => 'Dezember',
|
||
];
|
||
|
||
public function run(): void
|
||
{
|
||
$texts = $this->replaceCmsTexts(self::REPLACEMENTS, self::EXACT);
|
||
$slides = $this->addSlides();
|
||
$sponsors = $this->replaceSponsors();
|
||
$articles = $this->updateArticles();
|
||
|
||
$this->command?->info(sprintf(
|
||
'Korrekturen 08.10.: %d Textstelle(n), %d Slide(s), Vereine %s, %d Beitrag/Beiträge aktualisiert.',
|
||
$texts, $slides, $sponsors ? 'neu' : 'unverändert', $articles,
|
||
));
|
||
}
|
||
|
||
/**
|
||
* Fügt die Slides der Lang-Datei, die im CMS fehlen, an derselben Stelle ein (hinter dem Bild,
|
||
* das in der Lang-Datei davor steht). Fehlt dieses Bild, bleibt die Liste unangetastet.
|
||
*/
|
||
private function addSlides(): int
|
||
{
|
||
$row = CmsContent::query()->where('group', 'home')->where('key', 'hero')->first();
|
||
|
||
if (! $row) {
|
||
return 0;
|
||
}
|
||
|
||
$value = $row->getTranslations('value');
|
||
$added = 0;
|
||
|
||
foreach ($value as $locale => $content) {
|
||
$defaults = trans('b2in.themes.b2in.hero.slides', [], $locale);
|
||
|
||
if (! is_array($content) || ! is_array($content['slides'] ?? null) || ! is_array($defaults)) {
|
||
continue;
|
||
}
|
||
|
||
foreach ($defaults as $index => $slide) {
|
||
$images = array_column($content['slides'], 'image');
|
||
|
||
if (in_array($slide['image'], $images, true)) {
|
||
continue;
|
||
}
|
||
|
||
// Nur wenn das Nachbarbild aus der Lang-Datei in der Liste steht – eine ganz eigene
|
||
// Liste im CMS bleibt so, wie sie ist.
|
||
$previous = $index > 0 ? array_search($defaults[$index - 1]['image'], $images, true) : false;
|
||
|
||
if ($previous === false) {
|
||
continue;
|
||
}
|
||
|
||
array_splice($content['slides'], $previous + 1, 0, [$slide]);
|
||
$added++;
|
||
}
|
||
|
||
$value[$locale] = $content;
|
||
}
|
||
|
||
if ($added > 0) {
|
||
$row->value = $value;
|
||
$row->save();
|
||
app(CmsContentService::class)->clearCache('home');
|
||
}
|
||
|
||
return $added;
|
||
}
|
||
|
||
private function replaceSponsors(): bool
|
||
{
|
||
$row = CmsContent::query()->where('group', 'home')->where('key', 'sponsoring')->first();
|
||
|
||
if (! $row) {
|
||
return false;
|
||
}
|
||
|
||
$value = $row->getTranslations('value');
|
||
$changed = false;
|
||
|
||
foreach ($value as $locale => $content) {
|
||
$names = array_column((array) ($content['logos'] ?? []), 'name');
|
||
$defaults = trans('b2in.themes.b2in.sponsoring.logos', [], $locale);
|
||
|
||
if (! is_array($content) || ! is_array($defaults)) {
|
||
continue;
|
||
}
|
||
|
||
if (array_intersect($names, self::FORMER_SPONSORS) !== []) {
|
||
$value[$locale]['logos'] = $defaults;
|
||
$changed = true;
|
||
|
||
continue;
|
||
}
|
||
|
||
// Vereinsseiten nachtragen, wo der Verein schon steht, aber noch keine URL hat.
|
||
$urls = array_column($defaults, 'url', 'name');
|
||
|
||
foreach ((array) ($content['logos'] ?? []) as $index => $logo) {
|
||
$url = $urls[$logo['name'] ?? ''] ?? null;
|
||
|
||
if ($url !== null && blank($logo['url'] ?? null)) {
|
||
$value[$locale]['logos'][$index]['url'] = $url;
|
||
$changed = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($changed) {
|
||
$row->value = $value;
|
||
$row->save();
|
||
app(CmsContentService::class)->clearCache('home');
|
||
}
|
||
|
||
return $changed;
|
||
}
|
||
|
||
/**
|
||
* Maßanfertigungs-Beitrag aus der Lang-Datei, zwei Beiträge offline, Datum und Lesezeit deutsch.
|
||
*/
|
||
private function updateArticles(): int
|
||
{
|
||
$count = 0;
|
||
$source = collect(trans('b2in.articles', [], 'de'))->firstWhere('slug', self::REWRITTEN_ARTICLE);
|
||
$sourceEn = collect(trans('b2in.articles', [], 'en'))->firstWhere('slug', self::REWRITTEN_ARTICLE);
|
||
|
||
foreach (CmsArticle::query()->get() as $article) {
|
||
if (in_array($article->slug, self::UNPUBLISHED_ARTICLES, true)) {
|
||
$article->is_published = false;
|
||
}
|
||
|
||
if ($article->slug === self::REWRITTEN_ARTICLE && is_array($source)
|
||
&& str_contains((string) $article->getTranslation('title', 'de'), 'Warum der Millimeter über den Raum entscheidet')) {
|
||
foreach (['title', 'subtitle', 'content'] as $field) {
|
||
$article->setTranslation($field, 'de', $source[$field]);
|
||
|
||
if (is_array($sourceEn)) {
|
||
$article->setTranslation($field, 'en', $sourceEn[$field]);
|
||
}
|
||
}
|
||
}
|
||
|
||
$article->date_label = self::germanDate((string) $article->date_label);
|
||
$article->read_time = self::germanReadTime((string) $article->read_time);
|
||
|
||
if ($article->isDirty()) {
|
||
$article->save();
|
||
$count++;
|
||
}
|
||
}
|
||
|
||
return $count;
|
||
}
|
||
|
||
/** „August 26, 2026“ / „März 10, 2026“ → „26. August 2026“; schon Deutsches bleibt. */
|
||
public static function germanDate(string $value): string
|
||
{
|
||
if (! preg_match('/^\s*(\p{L}+)\s+(\d{1,2}),\s*(\d{4})\s*$/u', $value, $match)) {
|
||
return $value;
|
||
}
|
||
|
||
$month = self::MONTHS[mb_strtolower($match[1])] ?? null;
|
||
|
||
return $month === null ? $value : "{$match[2]}. {$month} {$match[3]}";
|
||
}
|
||
|
||
/** „5 min read“ → „5 Min. Lesezeit“; schon Deutsches bleibt. */
|
||
public static function germanReadTime(string $value): string
|
||
{
|
||
return preg_match('/^\s*(\d+)\s*min(?:\.|utes?)?\s*read\s*$/i', $value, $match)
|
||
? "{$match[1]} Min. Lesezeit"
|
||
: $value;
|
||
}
|
||
}
|